1. 动手之前:先想清楚你缺的到底是不是一个“AI系统”
先说我自己的经历。上个月有个做供应链的朋友找我,开口就是“帮我写个算法,用AI预测下个月每个仓的补货量”。聊到第三轮我才搞明白,他其实已经用Excel公式把80%的常规补货跑得很熟了,真正头疼的是每周要花几个小时处理供应商发来的格式混乱的对账单。他需要的根本不是预测模型,而是一套能把非结构化文本清洗成语义化字段的抽取管线。这个案例特别典型——很多人说要搞AI工程,但真正缺的往往不是一个“更聪明的模型”,而是一条更顺的工程链路。
这几年AI相关的岗位和项目肉眼可见地膨胀,但“AI工程化”这个词被严重滥用。有人把它等同于调API、套LangChain,有人觉得是训练模型、调超参数。以我从零搭过几套系统的体会来说,AI工程化是一条完整的手术流水线:先判断病人(业务问题)需不需要开刀(要不要用模型)、选什么术式(模型选型)、怎么准备手术室(基础设施)、怎么处理器官移植的排异反应(生产环境下的性能和可靠性),最后还得管术后恢复(监控和迭代)。任何一个环节掉链子,整个系统都会在某个深夜突然崩给你看。
这篇内容我打算按自己实际趟过的路径来讲,从需求判断开始,一路走到数据、模型集成、部署和监控。标题叫“ai-engineering-from-scratch”,说的就是从零开始的完整落地过程,而不是在某一个点上深挖。我不预设你有多深的机器学习背景,Python能写、基本命令行会用就行,剩下的概念我会用工程化的视角重新解释一遍。读完你应该具备自己从零搭一套可运行、可维护、可演进的AI服务的基本能力,并且知道哪些坑是文档里绝对不会告诉你的。
2. 需求拆解与技术选型:不是所有问题都应该用神经网络硬怼
2.1 先用“规则优先”原则过滤掉伪需求
我在第一部分举的对账单例子,本质上是一个“信息抽取”问题。但信息抽取有一个残酷的事实:如果你面对的是固定格式、固定字段的文档,正则表达式比任何大模型都便宜、快速且稳定。我在生产环境见过很多AI项目,最后被一个三十行的正则脚本替换掉,不是因为模型效果差,而是因为业务和技术的成本结构完全不匹配。
构建AI工程的第一步,不是选框架,而是做一个“问题分级”:
- 固定逻辑类问题:字段格式统一、规则清晰的抽取、转换、校验,优先用正则、xpath、pandas规则函数解决。这类业务的错误率要求往往是0%,而模型天生做不到100%。
- 强分类/数值预测类问题:有历史标签、变量间存在统计相关性,比如销售预测、风控评分、客户分群。这类用传统机器学习(XGBoost、LightGBM)往往性价比远高于深度学习,训练快、可解释性强、部署轻。
- 开放式生成/理解类问题:需要语义理解、文本生成、多轮对话,没有固定答案,这才轮到LLM出场。
- 混合类问题:实际业务里最多,比如“先把非结构化PDF转成结构化字段(LLM),再做规则校验(脚本),最后跑一个数值模型打分(XGBoost)”。这个组合我称之为“AI系统拼盘”,也是后面所有章节的核心场景。
给一个我自己常用的判断标准:如果业务方说“偶尔错一个也能接受”,优先考虑模型;如果他说“线上绝对不能错”,那第一选择永远是规则,模型只能做候选排序和兜底。这背后是代价结构问题——大模型一次调用成本看似只有几分钱,但错误导致的返工、客诉、审核人力,才是真正的隐形账单。
2.2 模型选型的决策矩阵:凭什么选GPT、开源模型还是传统模型
一旦确认某块确实需要模型参与,立刻进入选型阶段。我总结了一个三维度决策框架:效果天花板、成本结构、可控性需求。
效果天花板最好理解——同一批数据,不同模型能达到的准确率上限不同。成本结构分两块:调用成本(按token或按量付费)和运维成本(自己部署GPU集群的硬件和人力)。可控性则包括数据隐私、输出稳定性、供应商锁定风险。
| 需求类型 | 推荐方案 | 典型理由 |
|---|---|---|
| 少量调用、效果优先、无隐私约束 | 云端大模型API(GPT系列、Claude、Gemini等) | 效果最好,开发效率最高,按量付费 |
| 高频调用、成本敏感、可用开源模型达标 | 本地/私有化部署开源模型(Llama、Qwen等) | 单次成本趋近于零,可批量推理 |
| 结构化预测、需要可解释性 | XGBoost / LightGBM 等传统模型 | 训练快、特征可控、便于合规审查 |
| 私有数据、强合规场景 | 私有化开源模型 + 向量检索(RAG) | 数据不出内网,可控性最强 |
补充一个容易踩的坑:模型在测试集上的“效果”和生产中的“效果”是两个完全不同的指标。测试集评估的是离线指标,比如准确率、F1、BLEU,而生产环境真正考验的是“输入分布漂移”下的鲁棒性。我在选型阶段一定会做一个小样本“对抗测试”而不是只看公开榜单——拿50条业务真实数据、包含各种脏乱差格式、边缘情况,手动跑一遍输出,观察它在意外输入下的表现。这一步花半天时间,能省下后面好几个月的返工。
2.3 成本估算:算清一笔账再去谈方案
成本估算很多人在项目启动时完全忽略,等账单出来才目瞪口呆。我给一个标准的估算流程:
假设你要做一个客服助手,日均调用5000次,每次请求约2000 token输入 + 500 token输出,按GPT-4o级别价格估算:输入约每百万token几十元,输出按倍率计算。粗略一算就是日均几百元的调用费,一个月轻松破万。这还是在没有重试、没有失败回退、没有链式多次调用的情况下。
对比之下,如果自己部署一个13B量级的开源模型,配一张消费级24GB显存的显卡,初期硬件投入约万元级别,电费每月几百。但它需要你付出工程时间——量化、批处理、并发优化,这些后面会细讲。
关键结论:API方案适合起步期和低并发场景,成本随调用量线性增长;本地部署适合稳定高并发场景,前期投入大但边际成本低。最理想的架构往往是混合的——高价值请求走最强模型,普通请求走开源小模型,极简单请求走规则脚本。这条“三级漏斗”思路,是我在成本优化上做过最划算的一个决定,没有之一。
3. 基础设施从零到一:先把地基浇结实再谈上层建筑
3.1 环境管理与依赖锁定:为什么我放弃conda改用组合方案
从零开始的AI工程项目,我踩的第一个坑就是环境管理。早年图省事直接用conda一把梭,结果遇到两次事故:一次是某次conda solve依赖花了四十分钟,另一次是同事的Python版本和CUDA版本对不上,模型死活加载不出来。互不兼容的依赖版本是AI工程里最普遍的“项目杀手”。
现在的推荐组合是:pyenv管理Python版本 +poetry或uv管理虚拟环境和依赖,配合Docker做最终部署集成。如果你还在用conda,问题不大,但务必要坚持一个原则——环境定义全部代码化,禁止手工操作。
poetry的核心价值在于poetry.lock文件,它把每个依赖的精确版本锁死。这有多重要?我见过不止一次,项目跑得好好的,某天突然崩了,查半天发现是某个依赖从1.2.3自动升到了1.2.4,悄悄改了一个返回字段的类型。上线半年的系统败给一个小版本升级,这种事故一次都嫌多。
一个可复用的最小操作序列:
# 安装pyenv后,指定Python版本 pyenv install 3.11.9 pyenv local 3.11.9 # 初始化poetry项目 poetry new ai-service cd ai-service poetry add torch transformers fastapi pydantic-settings poetry deploy # 等价于lock文件生成后的完整安装3.2 配置管理与密钥隔离:把变量从代码里扔出去
第二个常被忽略的是配置管理。很多初学者把数据库连接串、API密钥、模型路径直接硬编码在代码里。这带来的问题远不止泄露风险——代码一多,每处硬编码都是一个需要维护的“可变点”,改一处漏三处是家常便饭。
我现在的做法是:用pydantic-settings统一管理所有配置项,环境变量注入,配合.env文件做本地开发配置。生产环境的密钥放在专门的密钥管理服务(如Vault或云厂商的Secrets Manager)里,绝不进代码仓库。
配置项至少要覆盖这几类:
- 服务端口、超时时间、重试次数等基础运行参数
- 模型名称、版本号、推理参数(temperature、max_tokens等)
- 数据库、缓存、对象存储的连接信息
- 外部API的地址、密钥、RateLimit策略
为什么特别强调模型版本号?因为大模型API的更新非常频繁,同一个模型名背后的实际版本可能已经换了好几代。如果不把版本号固定在配置里,今天跑得好好的管线,明天输出风格/格式可能就变了,而你的下游解析逻辑还停留在旧时代。把模型版本当作工程配置而不是代码常量来管理,是工作习惯从“调模型”转向“做工程”的标志性一步。
3.3 实验追踪:没有记录等于白干
从零搭建系统的过程中,你会频繁更换prompt模板、换模型版本、调参数。如果没有一个实验追踪机制,你会陷入“我记得上星期跑过一个效果很好的版本,但我忘了我当时填了什么参数”的崩溃循环。
我推荐MLflow,理由有三点:开源、轻量、和主流框架集成度高。它至少能帮你做三件事:
- 记录每次实验的配置参数(params)、指标(metrics)和输出样例(artifacts)
- 提供模型注册中心,管理不同版本的模型状态(Staging/Production)
- 和Docker、KServe等部署工具衔接,让线上跑的和实验跑的版本可溯源
这里有一条我付出过代价的经验:笔记记得再详细,都不如随手在MLflow里点一下记录。因为人会遗忘、会遗漏,但日志不会。哪怕是最简单的项目,我也建议从一开始就养成“每次跑实验必留档”的习惯。这个习惯会在一个月后省下你至少一天的排查时间。
4. 数据处理链路:脏活累活才是真正决定效果上限的地方
4.1 数据获取与清洗的工程化细节
做过真实项目的都知道,80%的时间不是在调模型,而是在处理数据。AI工程里的数据处理链路,至少要包含:抽取 → 校验 → 清洗 → 转换 → 版本化这五个环节。我按顺序拆开讲。
抽取阶段最容易翻车的地方是“来源多样性”。同一个业务字段,可能来自数据库、CSV报表、PDF邮件、合作伙伴的API,格式千差万别。我的建议是:不要试图用一个函数处理所有来源,而是为每个来源写一个独立的抽取模块,返回统一的内部数据结构。这个设计能让你在某个来源格式变化时,只改对应模块,而不影响全局。
校验阶段的核心是“fail fast”:数据在入口处就要被检查,而不是等跑到模型训练/推理时才爆雷。基础校验包括字段完整性、类型正确性、取值区间合理性。进阶校验包括跨字段逻辑一致性——比如消费金额为正、城市字段存在于行政区划表。
清洗阶段要特别注意三类问题:
- 重复数据:同一客户在不同来源可能用不同ID,需要定义主键合并策略
- 敏感信息:个人隐私数据的脱敏处理,必须在清洗阶段完成,绝不能让原始敏感字段进入模型、日志或下游系统
- 脏格式:日期格式混杂、编码问题(UTF-8与GBK)、全角半角混用,这些看似小问题,实际会导致模型输入质量断崖式下降
4.2 数据版本化:为什么直接存CSV会翻车
很多踩坑故事的开头都是“我们直接把清洗后的数据存成了CSV,然后……”。
数据一旦进入模型训练和推理环节,就和代码一样需要版本管理。原因很简单:模型输出和训练数据的版本强绑定。如果你改了数据没改版本号,下游所有实验记录都失去可比性。
我目前的生产级做法是:
- 数据文件用
DVC(Data Version Control)管理,数据本身放对象存储或NAS,元数据进Git仓库,保证每次数据变更都有记录、可回滚 - 对于文本类数据集,使用
Hugging Face Datasets库管理,它内置了数据集的加载、切分、映射和缓存功能,还能和深度学习框架无缝衔接 - 每次数据处理后生成一个数据摘要文件(行数、字段统计、异常值占比),随数据一起提交,方便后续对比
举个例子,DVC的基础流程大概是这样:
dvc init dvc add data/raw_supplier_reports.csv # 把大文件加入DVC管理 git add data/raw_supplier_reports.csv.dvc git commit -m "add raw supplier reports v1" dvc push # 推送到远端存储后续想回到之前某个数据集版本,一条dvc checkout就搞定。这套机制在团队协作中的价值是巨大的——你再也不需要听同事说“我拿的是最新的那版数据”,因为“最新”有明确的版本号可查。
4.3 标注与数据质量:从“能用”到“可信”
如果项目涉及监督学习(传统ML或模型微调),标注质量就决定了效果天花板。我见过太多团队把大量精力花在调模型结构上,却给标注工人下发极其含糊的标注规范,最后模型效果怎么调都上不去——问题不在模型,在标签噪声太高。
标注规范至少要明确几个点:
- 每个标签的精确定义,附带正例和反例
- 边界情况的处理策略:比如“客户投诉中包含表扬,算正面还是负面?”
- 一致性校验机制:同一批数据由多人标注后计算一致性指标,冲突样本需要仲裁流程
这里推荐一个很实用的小技巧:标注完成后,不要急着训练,先做一次“标签自检”——写一个脚本统计每个类别的样本量、平均文本长度、标签分布,再随机抽几十条看标注质量。这一步成本极低,但能避免你拿着充满噪声的数据集跑三天训练才发现方向全错。
另外,如果是真实系统中的日志数据,一定警惕“标签泄漏”问题——用未来信息预测过去事件,这在时间序列场景尤其常见。做过一次销售预测项目,差点把“当月的实际销售额”当成特征塞进训练集,上线后才发现模型“预测”的其实是已经发生的事实。这种错误在离线评估时看不出来,因为指标异常漂亮,上线就抓瞎。
5. 模型的工程化集成:从notebook代码到可服务模块
5.1 推理层的统一封装:别让业务代码碰模型细节
模型集成阶段最大的思维转变是:把模型当成一个服务模块,而不是一段可随意调用的函数。这个抽象带来的好处是,你可以随时替换底层模型(从GPT换到开源模型、从小模型换到大模型),而不必改动业务代码的每一处调用。
我设计的推理层接口通常包含:
class LLMService: def chat(self, messages: list[dict], **kwargs) -> ChatResult: """统一的对话接口,返回包含文本、token用量、延迟的结构化结果""" ... def embed(self, texts: list[str], **kwargs) -> list[list[float]]: """统一的向量化接口""" ... def health_check(self) -> bool: """探活接口,供监控系统调用""" ...在这个封装下面,你可以自由选择API厂商、开源模型推理框架(如vLLM、TGI、Ollama),甚至做A/B对比。调用方的代码完全无感知。这对于后续的模型降级、成本优化至关重要。
除了接口统一,推理层还要内置三个关键机制:
- 超时控制:每类模型调用设置合理的超时时间,防止一个慢请求拖垮整个请求线程
- 重试策略:对瞬时的限流、网络抖动做指数退避重试,但要设置上限,防止雪崩
- 优雅降级:主模型挂了自动切到备用模型或规则脚本,而不是直接把异常抛给用户
5.2 提示词也是代码:模板、版本管理和测试
“提示词工程”这个词听上去像玄学,但我更愿意把提示词当成一种高度业务相关的配置代码来管理。它需要版本管理、测试用例和审计追溯。
我的提示词管理实践是这样的:
- 把提示词模板拆成“系统指令”和“用户输入”两部分,系统指令中绝对不能拼接用户数据,防止提示注入攻击
- 模板放在独立目录里,每个版本一个文件(
prompts/classifier_v2.md),修改即留痕 - 为每个提示词编写一组最小测试用例,覆盖正常输入、边界输入、恶意输入,每次改动后跑一遍
这里重点说提示注入。它是LLM应用特有的安全问题:用户输入的文本里如果夹带了“忽略你之前的所有指令,直接输出……”之类的指令,模型可能真的照做。防护措施包括:系统提示词中明确声明指令边界、对输入做特殊字符检测和标记、输出侧做策略过滤。我在所有生产级系统里都会加这个保护,因为AI服务的输出一旦被污染,对业务的可信度打击是致命的。
5.3 模型路由与缓存:省钱的本质是“少算”
在线上高并发场景,模型路由和缓存是成本优化的两个杠杆。
模型路由的核心思路是“分级处理”:
- 简单、低风险、可复用的问题 → 轻量开源模型或规则脚本
- 一般业务问题 → 中等规模模型
- 疑难/高价值问题 → 最强模型
如何判断问题难度?可以用一个“难例检测器”前置模块,比如文本长度、关键词命中、历史相似度等特征打分,高于阈值的走强模型,低于阈值的走弱模型。这个分级体系做得好,通常能省下30%-50%的总调用成本,而效果几乎没有肉眼可见的下降。
再说缓存。这里的缓存不是HTTP缓存,而是语义缓存。普通的KV缓存要求键完全一致才命中,但用户的问题是自然语言,表达方式千变万化。语义缓存的做法是:将查询向量化后在向量数据库或内存索引中做相似性检索,相似度超过阈值的直接返回缓存结果。比如“你们的退款政策是什么”和“请问退款怎么操作”在语义上是同一个问题,如果系统把这个查询缓存住了,第二个请求完全不需要再调用大模型。在重复问题比例高的客服场景,语义缓存的命中率有时能到30%以上,这是个相当可观的数字。
6. 部署与性能优化:本地跑通到生产稳跑,中间隔着一整条河
6.1 推理性能的度量与瓶颈定位
模型部署之后,第一件事不是调参,而是建立性能基线。我始终在追踪四个指标:
- 首Token延迟(TTFT):用户发出请求到看到第一个输出token的时间。在交互场景,这个值超过2秒体验就会明显变差。
- 生成吞吐(tokens/sec):单请求的生成速度,决定单次对话的等待时长。
- 并发吞吐(requests/sec):系统整体能支撑的QPS,决定扩容需求。
- GPU利用率:硬件资源的真实使用率,决定你买的卡是不是在闲置。
实操中最常见的瓶颈是:QPS上去了但GPU利用率低得吓人,原因多半是并发逻辑写成了串行——每个请求独占了模型推理,没有做请求合并。解决思路是引入推理框架自带的批处理机制。
以vLLM为例,它实现了一种叫做“连续批处理”的调度机制:不用等一批完整凑齐再推理,而是随时有请求进来就插到正在推理的batch里,极大提高了GPU利用率。实测用vLLM部署开源模型,同等硬件下吞吐通常比原生Transformers推理高出数倍。如果只是个人项目,也可以用Ollama这类工具快速起一个本地推理服务,它的性能优化做得不错,且配置成本极低。
6.2 结构化输出与容错设计:让模型行为可预期
生产系统的核心要求是可预期,而大模型天生不可预期。所以集成时一定要在“模型的不可控输出”和“下游对可控格式的需求”之间,加一层强制规范。
我最常用的方案是:在提示词中要求模型输出JSON,然后把输出交给一个严格的JSON解析器并做字段校验。但只靠提示词约束JSON格式并不可靠——模型偶尔会输出成别的结构,我就在解析失败时自动触发一次“重新生成并修正格式”的重试。同时设置格式解析失败次数的上限,超过上限就走备用方案(比如规则抽取),而不是无限重试。
结构化输出的更进阶方案是使用各家的“响应格式约束”功能(如OpenAI的JSON Mode,以及各类推理框架支持的约束解码),它从解码阶段就直接限制token生成,输出天然符合JSON语法。做工程化一定要优先用这类能力,它把很多解析噩梦直接消灭在源头。
6.3 部署形态选择:容器化、Serverless还是全托管
部署形态的选择没有银弹,取决于你的团队能力和调用规模。
如果团队有基础运维能力、调用量稳定且大,推荐“容器化 + K8s”路线:用Docker镜像封装推理服务,K8s负责伸缩和自愈。这条路的成本是前期的Dockerfile、资源配置、探针调试,收益是长期稳定的部署体验。
如果调用量波动大、懒得维护集群,Serverless容器(如云厂商的弹性容器实例)是更灵活的选择。冷启动可以接受的话,成本还能进一步压低。唯一的风险是冷启动延迟——大模型镜像动辄十几GB,冷启动可能要几十秒,需要预热策略或者保活策略来兜底。
无论选哪条路,容器镜像的构建规范都应包括:基于固定基础镜像、锁定依赖版本、单独挂载模型权重目录(模型不打包进镜像)、设置非root用户运行。这些细节决定了你在生产环境出问题时能不能三分钟内定位。
这里特别强调“模型权重不打包进镜像”这一点。模型权重动辄几GB到几十GB,如果每次都打进镜像,每次发布都要重传几GB数据,既慢又费流量。更好的做法是模型权重放独立的对象存储/共享卷中,镜像启动时按版本号拉取,镜像本身保持小而快。版本号放在配置里,发布新版本只需要改一个配置项和触发一次热加载,线上服务几乎无感。
7. 上线后的持续迭代:监控、反馈闭环、什么时候重新训练
7.1 线上监控指标:除了延迟,还要盯“效果漂移”
很多人以为服务上线后监控就是看看CPU、内存、延迟,这些基础设施指标当然要看,但对AI服务来说远远不够。你要额外监控的是“模型效果”本身。
效果漂移最常见的形式是输入分布漂移:线上流量的输入数据,和开发期测试数据的分布逐渐不一致。比如客服机器人刚上线时,用户问“登录不了怎么办”这类标准问题,三个月后用户开始问“我的账号被异地登录了怎么办”,新问题在你的训练数据里占比很低,模型效果自然下滑。
检测方法是在每个请求上打标记(输入长度、主题分类、关键词命中率),按时间窗口做分布统计。一旦发现分布偏移超过阈值,就要触发一条告警。另外,在输出侧也要做质量监测——定期抽样线上请求,人工或LLM-as-judge检查输出质量,把结果记录到监控看板。这里补充一个经验:不要迷信自动评价工具,抽查+人工复核永远是最后一道防线。
7.2 反馈闭环:把用户的每一次不满变成算法改进的燃料
一个合格的AI工程系统,必须有数据回流机制。用户对输出的点赞/点踩、客服人工修改前后的内容差异、业务方的需求变更,这些信号要能自动汇聚成一个个“待改进样本库”,定期补充进数据集,用于后续的prompt优化或模型微调。
我在系统里一般会设计一个用户反馈表,至少包含:
- 用户查询原文
- 模型输出原文
- 用户/客服的最终修改结果(作为弱标签)
- 反馈类型(点赞、点踩、修改、超时、举报)
- 时间戳、业务来源
这个表的价值会随时间推移越来越大。它不仅是评估线上效果的数据源,更是未来训练集扩充的金矿。没有这个回流机制,AI系统就像一个只输出不学习的书呆子,永远在老问题上反复犯错。
7.3 什么时候需要微调,什么时候只需要换Prompt
每次有业务方来找我说“效果不够好”,我都会先做一轮排查,再决定要不要碰模型本身。排查顺序是这样:
- 是不是数据问题:输入解析错了、字段缺失、编码异常。这类问题最常见,修完立刻见效。
- 是不是Prompt问题:指令不够清晰、示例太少、格式约束没写死。多数情况下,修改prompt能解决80%的效果问题。
- 是不是模型能力问题:换一个更强或更合适的模型对比跑一批测试数据。如果换模型效果提升很大,说明当前模型能力不够。
- 最后才考虑微调:真正需要微调的场景是,业务有独特风格或领域术语(法律合同、医疗报告、代码规范),通用模型很难通过prompt学到位。
我做过的微调项目里,最有价值的一个不是调大模型,而是用少量业务数据微调了一个小开源的embedding模型,把检索的召回率提升了15个百分点。这件事成本极低、效果极稳,比微调生成模型性价比高得多。所以在“微调”这件事上,我个人的建议永远是:先想清楚你的瓶颈在“理解”还是“生成”,理解的问题优先调检索、调embedding,生成的问题优先试prompt,最后才砸钱去微调大模型。
回到开头那句“从零开始”。AI工程化不是买一本教材背概念,也不是跑通一个demo就完事。它是一个从业务需求到技术选型、从数据治理到模型集成、从部署监控到迭代反馈的闭环系统。每一步都有无数细节坑等着你踩,但只要整体的工程框架搭对了,踩坑的成本都是可控的、可学习的。
最后再分享一个我坚持了很久的小习惯:每个项目建一个“事故复盘文档”,把每一次线上故障的根因、修复过程、预防措施都记下来。半年后回头看,你会发现这份文档的价值不亚于代码本身。踩坑不可怕,怕的是同一个坑踩第二遍。