我一直觉得"ai-engineering-from-scratch"这个标题挺有意思的,乍看像一门课程的名字,其实它最能概括我最近做实操项目时的状态:把一个业务问题,从头到尾用AI技术解决掉,中间没有任何人帮我兜底。去年组里接了个任务,客服工单要自动分类、自动摘要,还要给出建议处理方案。我当时以为这事很简单,写一段prompt调一下模型API就完事了。真正动手才发现,AI工程和传统开发完全是两种玩法:你要面对的不确定性太多了,模型输出会漂、格式会裂、上下文会爆,你需要的不是"调通一次",而是设计一个即使模型表现不稳定、结果依然可用的系统。
这篇文章我打算把这套东西完整拆一遍:从怎么判断需求适不适合用AI,到模型选型、上下文构建、提示词工程,再到评估、上线、成本控制,完整讲清楚一个AI功能从零到落地的全过程。不管你是在做AI测试、AI编程辅助,还是想搞Agent、自动化工单,这套思路都可以直接套用。
1. 必须先把"AI工程"这个概念掰开
1.1 AI工程不是"调模型",也不是"写提示词"
很多人把AI工程理解成"写一段能调用模型的代码"。这属于还没入门的状态。跑通一次调用,就像你点燃了一个打火机,但你没法用它稳稳地烧完一顿饭。AI工程真正要解决的是:在模型输出带有随机性、甚至偶尔完全跑偏的情况下,依然把产品功能做得稳定、可度量、可维护。
我自己最大的体会是,AI工程的核心资产不是模型,而是"可控性"。模型是别人的,平台随时可能调整版本;提示词是你写的,但同一个提示词每次输出可能不同;上下文是你拼的,但拼多了模型就不听你的话。你得在这么多不确定因素里,找出一个确定性的框架,让业务结果整体在可接受范围内。这就是AI工程和普通后端开发最本质的区别:传统代码是"输入确定、逻辑确定、输出确定",AI工程是"输入带噪声、模型带概率、输出带误差",而你要为这套概率系统设计质量护栏。
1.2 AI工程和传统软件工程的差异到底在哪
举个具体的例子。你写一个普通的工单查询接口,用户传一个工单号,你查数据库,返回结果,这个结果你用单元测试就能锁定。但AI工单分类不一样:用户传入的工单文本千奇百怪,有错别字、有口语、有夹杂截图说明;模型可能今天把"手机进水"分到"硬件故障",明天分到"售后服务";而且就算模型分错了,程序也不会报错,它只会安安静静地给你一个错误结果。
所以传统工程可以依赖"断言"和"异常"来保证质量,AI工程必须换成"评估"和"兜底"。你没法断言模型一定输出合法分类,你只能通过校验和重试来降低非法输出的概率;你没法保证摘要100%覆盖所有关键信息,你只能通过评估集定期验证,并让业务方接受一个可容忍的错误率。这种思路的转变,对很多老开发来说是最难的一关。代码写错了可以修bug,模型输出错了你连bug都没法定位,只能从"输入-输出-上下文"三者关系里一点点排查。
1.3 AI工程的核心组成:五个环节缺一不可
我习惯把一个AI工程拆成五块:模型、上下文、提示词、流程编排、评估反馈。这五个词听起来各自独立,实际上是一个闭环。
- 模型:负责真正"理解"和"生成"的部分,它决定能力上限。
- 上下文:你提供给模型的背景信息、历史数据、知识库片段,它决定模型能不能答到点子上。
- 提示词:让模型知道怎么组织输出,它决定结果的形式和可用性。
- 流程编排:包括前置条件判断、后置校验、多步调用顺序、工具调用,它决定系统的复杂度和稳定性。
- 评估反馈:包括评估集、指标、日志、人工反馈回流,它决定你能不能持续优化。
这五块里最容易忽略的是最后一个。我见过太多团队把精力花在调提示词上,调了一周,准确率看着还行,一上线就崩。原因很简单:没有评估集,你的"看着还行"只是猜;有了评估集,你才知道每次修改到底是变好还是变差。所以我的习惯是:评估先行,提示词后调。先花时间攒一批评估数据,再开始调,效率翻倍。
2. 从零到一的核心设计:先解决什么问题
2.1 需求判断:你的问题适不适合用AI解决
做AI工程最容易犯的错就是"拿着锤子找钉子"。不是所有问题都需要大模型,也不是所有问题用传统规则就做不好。我自己的判断标准有三个:
- 语义理解是否占主导。工单分类、情感判断、文本摘要、意图识别,这些都是"读一段话、理解意思"的任务,适合AI。反过来,如果你需要精确计算库存、判断金额是否超限,用规则和代码就够了。
- 输入是否高度可变。如果输入完全可以通过枚举穷举,比如固定下拉框,那你不需要AI;如果输入是自由文本、语音转文字、图片OCR结果,形态千变万化,那才需要考虑AI。
- 业务对错误率的容忍度。AI永远做不到100%准确。如果你的业务不允许任何误判,比如医疗诊断、金融风控的最终决策,那AI只能做辅助,必须有人工审核兜底。能接受百分之一到百分之几的错误率,且可以通过流程弥补的,才适合直接上AI。
拿我自己做的工单系统来说,分类错误可以靠人工复核兜底,摘要不完整不影响后续处理,所以这个场景适合AI。如果你要做一个"绝对不许算错"的财务系统,就别硬塞大模型进去,用公式和数据库约束更靠谱。
2.2 模型选型:别一上来就冲参数最大的
模型选型是个成本工程。很多人一说AI就往最强模型上想,但实际项目里没必要。大模型确实聪明,但贵、慢、还有可能过度发散,输出自说自话。小模型便宜、快,但理解能力弱,复杂任务容易翻车。
我用过一个比较实用的分层策略:
- 简单分类任务(比如工单一级分类,类别数量不超过十个):可以先用轻量级模型试,比如量化过的小参数模型,成本低、延迟低,够用就好。我们实测下来,大部分工单分类只要类别定义清晰,中小模型的表现足够好。
- 中等任务(多级分类、信息抽取、轻量摘要):选中等规模的模型,加上好的few-shot示例,效果能追上大模型的大部分场景。
- 复杂推理任务(多步骤规划、代码生成、长文档综合理解):这个才需要顶级大模型,不要省这个钱。
选模型还有一个隐藏点:同一个任务,你可以用一个便宜模型做"粗筛",判断是否真的需要上大模型。比如先让规则或小模型把明显简单的工单处理掉,只有复杂的才转发给大模型。这叫"前置路由",后面成本控制部分我也会细讲。模型选型看的是性价比,不是刷榜分数,这一点千万记住。
2.3 形态选择:单次调用、工作流还是Agent
同样是AI功能,实现形态差别很大。选型逻辑是:任务的确定性越高,用的形态越简单。
- 单次调用:一个输入,一次模型调用,一个输出。适合摘要、翻译、单分类。结构最简单,问题最容易排查。
- 工作流(Workflow):多个步骤,按固定顺序编排,每步一个明确任务。比如先判断工单类型,再抽取关键实体,最后生成摘要。每一步都单独校验,失败可以单独重试。适合流程很清晰的任务。
- Agent:由模型自己决定下一步干什么、调用什么工具,路径不可预知。适合开放任务,比如"帮我排查这个系统故障原因",模型可能需要读日志、查文档、执行命令。
我在工单项目里用的就是工作流形态,没有硬上Agent。原因很简单:分类和摘要的顺序是固定的,你不需要模型自己决定步骤。很多人把"用Agent"当成工程高级感,但Agent意味着你放弃了部分控制权,排查难度指数上升。能用工作流解决的,不要硬上Agent,这一点在工程上真的非常重要。
2.4 上下文工程:决定模型能不能答对的隐藏变量
同样的模型、同样的提示词,上下文里塞的东西不同,输出效果天差地别。上下文工程说白了就是两件事:什么东西该塞进去,什么东西不该塞。
该塞的是什么?用户的核心诉求、必要的背景数据、相关的历史记录、业务知识库里命中片段。不该塞的是什么?无关的闲聊、大段的原文引用、已经过期的信息、重复的历史对话。上下文窗口是有限的,塞满了,模型注意力会被稀释,还可能"顾头不顾尾",完全忽略你放在前面或后面的关键指令。
我在做工单摘要时,最初的做法是把整条工单的长文本和全部历史对话都丢进去,结果模型生成的摘要总是带着对话记录的废话,"客户说""客户说"反复出现,关键问题反而没提炼出来。后来改成预处理:先用规则把工单里的日志记录、无用格式符清理掉,再用一个轻量模型把历史对话压成三句话的"过程摘要",最后再传给主力模型做最终摘要。效果立刻上来了,token消耗还降了一半。上下文工程不是"多给信息模型就更聪明",而是"给对信息模型才靠谱"。
3. 实操:一个工单助手从需求到上线的完整链路
3.1 业务需求拆解与技术方案设计
工单助手的业务需求其实不复杂,但拆解完会发现,每一步都有讲究:
- 第一步,对工单做一级分类:咨询、故障、投诉、其他;
- 第二步,从工单里抽取关键信息:产品型号、报修时间、用户期望、联系方式;
- 第三步,生成一段简明摘要,给客服或技术员快速浏览;
- 第四步,给出建议处理方案。
对应到技术上,我拆成四个模块:文本清洗、分类模型调用、信息抽取调用、摘要与建议生成调用。这四个模块是串行的工作流,每一步的输出都作为下一步的输入。为了让每一环都能独立排查问题,我没有做成一个大prompt全包,而是每步单独一个函数、单独一套提示词、单独的日志记录。这样哪个环节出了问题,看日志就能定位,不用把整条链路翻个底朝天。
3.2 评估集建设:最枯燥但最值得花时间的一步
没有评估集,后面所有"优化"都等于开车不看路。我的做法是这样:
- 从历史工单里随机抽了200条,覆盖五个业务大类,每条单独做人工标注,标注内容包括:正确分类、关键信息点、标准摘要。
- 标注完,把数据按8:2分成调试集和测试集。调试集是我平时改提示词用的,测试集是动都不敢动的,用来做最终验证。
- 指标上,分类任务看准确率和混淆矩阵,摘要和信息抽取任务看"信息召回率"和"格式通过率",也就是模型输出能否被程序正确解析。
这个数据集看起来只有200条,但对我们的实际场景已经够了,因为工单类型相对集中。如果你要做的是开放领域文本处理,建议至少500到1000条起步。数据少有一个问题,就是你很容易在调试集上过拟合——调提示词调到在调试集上准确率95%,一上真实数据就掉到80%。原因就是调试集覆盖不到真实世界的多样写法。所以后来我又专门加了一条规则:每周从生产环境抽20条未标注工单,先人工标注入库,再跑一次离线评估,用新数据检验模型在真实分布上的表现。
3.3 提示词工程的关键设计:从"写话术"到"写接口文档"
很多人写提示词像是在跟模型聊天,写一大段自然语言让模型"好好做"。我的经验是,提示词应该写得像接口文档一样严谨。我常用的结构是:
- 角色定义:一句话说明模型扮演什么角色,回答问题的范围和边界是什么。
- 任务说明:明确要完成的具体任务,分类还是抽取还是摘要。
- 输入格式:给模型定义好输入内容的JSON结构说明。
- 输出格式:明确要求输出JSON,并给出完整的JSON Schema,包含每个字段的含义和可选项。
- 示例:放2到3个输入输出对作为few-shot示例。注意,示例不是越多越好,我实测下来,示例太多了,模型容易从示例里学习"照抄"而不是理解任务,而且会消耗大量token。3到5个高质量的示例足够覆盖绝大多数场景。
- 兜底规则:告诉模型遇到不确定的情况怎么处理,比如"无法分类时输出其他,并说明原因"。
我最看重的是输出格式约束。工单助手工序有四个模块要串联,最怕的就是模型输出一堆长文本加个序号,导致下游程序解析不了。所以每个模块我都明确要求结构化输出,并要求后置校验。有一个很典型的例子:第一版分类模块,我在提示词里写"输出格式:JSON,包含category字段"。结果模型经常输出一大堆解释,所谓JSON还是用Markdown块包起来的。后来我改成了:明确写"只输出JSON对象,不要输出任何解释、不要使用Markdown代码块";然后在代码里再加一道"JSON区域提取"的预处理,双重保险,才算彻底稳定下来。
3.4 工程实现细节:解析、校验、重试与缓存
提示词写得再好,工程实现不严谨一样会炸。我把工单助手的核心链路拆成几段,最小可运行版本大概是这样的结构:
import json import re from tenacity import retry, stop_after_attempt, wait_exponential def extract_json(text: str) -> dict: # 模型偶尔会在JSON外裹代码块或解释文字,先把JSON块抠出来 match = re.search(r"\{.*\}", text, re.DOTALL) if not match: raise ValueError("没有找到 JSON 对象") return json.loads(match.group()) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10)) def call_model_with_retry(messages, model_name, temperature=0): # 实际项目里这里替换成你用的模型SDK调用 response = model_client.chat(messages=messages, model=model_name, temperature=temperature) content = response["choices"][0]["message"]["content"] data = extract_json(content) validate_schema(data) # 用JSON Schema校验必填字段和枚举值 return data # 分类模块调用 def classify_ticket(cleaned_text: str) -> dict: messages = build_classification_messages(cleaned_text) return call_model_with_retry(messages, model_name="cheap-fast-model")有几个细节我必须单独拎出来讲:
- temperature设为0:分类和抽取这类有明确答案的任务,不需要模型发挥创造性,temperature开得越低越稳定。闲聊才需要高temperature。
- 重试必须有:模型服务有概率返回超时或异常,也可能返回格式错误。如果解析JSON失败,使用指数退避重试,一般重试两三次就能拿到合法输出。
- 必须做Schema校验:程序里不仅要有JSON解析,还要校验category是否在允许的枚举值内、summary字段是否非空。校验不过就按"模型输出异常"处理,打进异常工单队列,不要硬着头皮往下游传。
- 缓存机制:如果工单内容高度相似,比如同一个用户反复反馈同类问题,可以在调用模型前先算文本哈希,命中缓存就直接返回历史结果,能省不少成本。这个优化我上线后第二周加的,直接省了大概三成的模型调用费用。
我这里用的回调函数风格是示意,实际项目里你可能用函数调用(Function Calling)方式来做结构化输出,原理一样:让模型生成一个结构化的参数对象,比你让它自由写JSON要稳定得多。
3.5 上线观测与反馈闭环:工程能不能活下去的关键
上线不是结束,而是评估工作的开始。我在工单助手里放了三层观测:
- 第一层是日志层:每次模型调用,记录输入文本摘要、模型名称、输出结果、耗时、token数。这层日志的成本很低,但对排查问题价值巨大。
- 第二层是抽样层:从生产流量里随机抽5%到10%的工单,做人工复检,复检结果会回流到评估集里,用来持续更新标注数据。
- 第三层是反馈层:客服看到摘要后可以点"有帮助/没帮助",这个反馈数据用来发现高频错误类别,后续针对性地调模型或加规则。
有了这三层,你才真正有资格说自己在做AI工程。否则你只是写了一段调用代码,产品和业务根本不知道它在线上表现如何,也不知道下个版本要不要改、怎么改。
4. 问题排查、成本控制与避坑经验
4.1 常见故障现象与排查方法速查表
做到现在,我遇到的坑基本都能归类到下面这张表里:
| 故障现象 | 常见原因 | 排查步骤 |
|---|---|---|
| 分类结果不稳定,同样的输入两次结果不同 | temperature设置过高;提示词歧义 | 先降到0;再检查类别定义是否互相重叠 |
| JSON解析经常失败 | 模型能力弱;上下文太多影响指令遵循 | 检查是否有无关内容压缩了指令;换用结构化输出方式 |
| 输出内容被截断 | 上下文超长;max_tokens设置过小 | 检查token统计;压缩上下文;调大输出token上限 |
| 摘要信息不完整,漏重点 | 上下文被无关内容淹没;提示词没有强调关键字段 | 精简输入内容;用规则前置清洗;在提示词中显式列出摘要需覆盖的字段 |
| 响应延迟过长 | 模型模型大;上下文长;重试次数多 | 加缓存;用前置路由把简单请求分给小模型;降低重试上限 |
| 成本快速上涨 | 把大量无关数据塞进上下文;没有缓存 | 监控token消耗;做上下文压缩;引入缓存 |
排查的第一步永远是看日志里的"输入-输出-耗时-模型版本"四件套。没有这四样,你就是在盲人摸象。
4.2 几个让我印象很深的实战坑
第一个坑是输出格式约束"失效"。有一版分类模块我自信满满,提示词里要求"必须输出JSON,不要输出额外解释",技术栈里也做了JSON解析。结果发现模型偶尔还是会输出一段类似"好的,我来为您分类"的话再接JSON。原因不是提示词写得不好,而是模型有时"自作主张"。最终解决方法是两层:提示词强调,代码里先做"抠JSON块"预处理,再解析。这个"先抠再解"的套路,直接消灭了80%的解析异常。
第二个坑是上下文被"工单全文"撑爆。历史工单动辄几百字,加上对话记录可以达到上千token。一开始我把全文直接塞给摘要模块,模型输出时明显"失焦"。后来我加了一步预处理:先按规则把无意义的重复句子、系统日志、时间戳清理掉,再用小模型把长对话压缩成一句"事态进展"。这一步做完,摘要准确率提升,token消耗还下降了。这就是上下文工程的典型价值。
第三个坑是评估集过拟合。我前面说过,在200条调试集上把准确率调到95%,上线后真实分布上只有80%。原因很简单:真实工单有大量调试集里没见过的说法、错别字和情绪化表达。解决思路就是前面提过的"生产环境抽样回流"机制。从那之后,我再也不信"调试集上的数字",只看"抽样回流后的数字"。
4.3 成本与延迟的平衡技巧
做一个AI功能,如果成本失控,业务一样撑不住。我总结了几条实用的平衡策略:
- 能不用大模型就不用大模型。一级分类用中小模型,复杂摘要才用大模型,这叫分级调用。
- 能缓存就缓存。相同或相似输入的请求,命中缓存直接返回。工单这类业务尤其适合,因为反复反馈同一个问题的情况非常多。
- 能做前置路由就做前置路由。先用规则或文本特征判断复杂度,简单任务直接走一条轻量路径,复杂任务才走完整模型链路。我在工单助手里加了一条规则:工单字数少于20且关键词命中"装不了、打不开、闪退"的,直接分类为"故障",不用调模型。这类规则不一定覆盖很多场景,但能省掉一部分高成本调用。
- 合理设置超时与重试。重试不是越多越好,每次重试都增加延迟和成本。我的习惯是重试上限设3次,间隔按指数递增。
这个平衡说白了就是:让小模型和规则承担确定性高的部分,把大模型留给真正需要理解力的部分。这是AI工程成本优化的核心思路,比任何"调参优化prompt"都见效。
5. 我踩坑之后的一些个人体会
做这个工单助手项目,我最深的感受是:AI工程其实是一门"用成本换确定性"的生意。你不可能让模型输出100%稳定,但可以通过上下文工程、提示词约束、后置校验、人工兜底,让系统整体达到业务能接受的准确率。工程的意义不是消灭不确定性,而是把不确定性管理在一个可控范围内。
最后分享一个我现在一定会遵守的习惯:提示词必须像代码一样做版本管理。每次修改提示词,我都带上版本号、模型版本、评估结果记录,使用统一的文件保存。因为AI工程里最常见的情况,不是模型代码出bug,而是有人在某个版本里改了一句话,效果莫名其妙变差了,又找不出是什么时候改的。有了版本管理,这种情况可以少掉一大半。AI工程从零到一,技术上不难,难的是把每个细节都当成工程问题来对待。这也是"ai-engineering-from-scratch"这个标题最打动我的地方:从零开始,但每一步都走扎实。