我最早想系统地写“ai-engineering-from-scratch”这件事,其实不是因为看了什么热门课程,而是踩了一次实实在在的坑。当时我们团队打算上一个AI Agent来做自动化测试,老板丢过来一句话:“把接口测试、回归测试都交给AI。”听起来很轻松对吧?结果我们连第一条提示词模板都没写对,模型一本正经地生成了不存在的测试数据,还给了绿色的通过报告。那一刻我才意识到,AI工程和“调一个API”之间的距离,远比大多数人想象的大。它需要一套从数据、模型、提示词到评估、部署、迭代的完整体系,任何一个环节掉链子,整个系统都会翻车。
这篇文章我想以“从零开始搭建AI工程体系”为主题,把这条路拆开讲透,重点回答三个问题:AI工程到底在工程化什么?真正的实操步骤是什么?最容易在哪些环节翻车?适合准备做AI应用、AI Agent、智能客服或自动化流程的开发者、架构师,也适合那些已经接过大模型API但总觉得“味道不对”的团队参考。内容会尽量口语化,但该上代码上代码,该给参数给参数,基本可以照着抄。
1. 内容整体设计与思路拆解:AI工程到底在工程化什么?
1.1 AI工程的三个层次
先想清楚一个基础问题:AI工程和传统软件工程差在哪儿?传统工程解决的是“确定性逻辑”,输入输出可以被穷举和测试;AI工程面对的是概率模型,同样的输入,模型可能给出不同答案,甚至给出错误但看起来很合理的答案。所以AI工程的核心不是“写代码调用模型”,而是围绕这种不确定性建立控制机制。
我习惯把AI工程拆成三个层次来看:
- 应用层:你最终交付的Agent、智能客服、内容生成工具、自动化测试系统,这是用户直接接触的东西。
- 能力层:大模型本身、提示词模板、函数调用、向量数据库、工具链编排,这是让AI“能做事”的部分。
- 基础设施层:数据准备、评估体系、监控告警、成本控制、版本回滚,这是决定AI系统能不能长期运行的底座。
很多人从应用层开始,直接写界面、接API,跑到一半发现模型质量不行,才回头补提示词、补数据、补评估,整个项目变得像个打补丁大赛。正确的思路是从基础设施层往上走:先定义“什么样的输出算合格”,再选模型、设计提示词,最后才是把功能包装给用户。
1.2 为什么从零开始反而更高效
这句话听起来有点反直觉,因为网上到处是现成的框架、模板、脚手架。拿过来用不香吗?香,但容易让你跳过关键决策。你用了别人封装好的Agent框架,却不知道它内部是怎么做工具调用的;你复制了一段很酷的提示词,却不清楚它为什么有效。等到线上出问题时,你对系统的理解仅限于“它能跑”,而不是“它为什么这样跑”。
从零开始,我指的是从最核心的调用逻辑开始,不用重型框架,先用原生SDK写一个能跑的最小闭环,然后逐步替换和优化。这个过程类似于自己从水电图开始装修,而不是直接买精装房。最早的那版代码可能很粗糙,但你会因此在每个环节建立感觉——模型输出质量、响应延迟、上下文占用、成本波动,这些体感是看文档永远无法替代的。
我自己的经验是,用最小的代码量跑通一次“输入→模型→输出→评估”之后,再回头看LangChain之类的框架,才能真正理解它们解决了什么问题、又带来了什么新的复杂度。否则你只是在一个黑盒上继续盖黑盒。
1.3 什么时候你已经进入“AI工程”的坑
有些信号很典型,出现两个以上,说明你已经需要体系化思路了:
- 提示词改了十几个版本,效果时好时坏,无法判断是词序问题还是模型抽风;
- 同一个问题,换了一种问法就崩,你开始怀疑是不是用户不会说话;
- Agent偶尔调用错工具、偶尔卡在死循环,团队里没有人能解释原因;
- 成本账单数字吓人,但你说不清钱花在了哪些请求上;
- 模型升级之后,原本正常的业务流程突然开始出错,没有任何代码变更。
如果这些情况你至少中了一半,那就别继续“打补丁”了,重新整理一下你的AI工程体系会更划算。
2. 模型选型与提示词工程的实操要点
2.1 模型选型:先别急着做选择题
很多团队一上来就问“用GPT-4还是Claude还是开源的某某”,这是典型的顺序错误。第一步先想清楚任务类型,再决定用哪类模型。
| 任务类型 | 推荐模型类别 | 主要考量点 |
|---|---|---|
| 开放域聊天、内容生成、头脑风暴 | 通用对话模型 | 表达质量、上下文长度、风格控制 |
| 数学推理、代码生成、逻辑判断 | 专用推理/代码模型 | 逻辑正确性、代码执行能力 |
| 知识库问答、相似度检索 | 嵌入模型+通用模型组合 | 向量维度、检索召回率、生成幻觉率 |
| 结构化数据抽取、分类、标注 | 小参数模型或微调模型 | 输出格式稳定性、成本 |
| 多模态(图片、音频、视频) | 多模态模型 | 输入限制、内容理解精度 |
核心逻辑很简单:不要用通用模型硬扛所有任务。拿分类和抽取这种结构性强、规则明确的任务来说,用大模型有点浪费,用小模型配合约束生成甚至正则,成本和延迟都可以大幅下降。而复杂推理任务,通用模型往往不如专门的推理模型靠谱。
另外要注意模型版本冻结问题。大模型厂商经常更新版本,看起来只是小迭代,实际效果可能有波动。如果你做的是正式业务系统,千万不要在代码里写“用最新模型”这种逻辑,必须显式锁定版本号,并建立升级前的回归测试机制。我自己就吃过这个亏——模型厂商把某个能力“悄悄增强了”,结果我们的输出格式多了一个字段,把解析模块干崩了。
2.2 提示词工程的三个习惯
提示词工程听起来很玄学,好像是在跟模型“说话”。其实它是一门工程化的内容设计学科,核心目标是降低输出的不确定性和解析成本。我总结了三个习惯,基本能覆盖大部分场景。
第一个习惯:永远明确角色、任务、约束、输出格式。不要只写“帮我总结这段文字”,而要写“你是技术文档整理助手,请将以下内容总结为三条要点,每条不超过30字,使用中文输出,不要出现‘首先/其次’之类的词,直接给要点列表”。每多明确一个约束,模型输出的可预测性就提升一截。
第二个习惯:用示例代替形容词。如果你告诉模型“要专业一点”,它会给出一堆含糊的官话;如果你给它两个“专业输出”的例子,它会照着例子的结构走。在提示词里嵌入Few-shot示例是成本最低、效果最明显的手段。示例不在于多,两三个高质量的反而比一堆凑数的更管用。
第三个习惯:把需要模型自由发挥的部分压缩到最小。能选项就选项,能JSON就JSON。比如判断用户情绪,不要问“情绪如何?”,而是给出枚举“positive / neutral / negative / mixed”,再配合输出格式约束。这样你的下游解析只需处理固定枚举值,不需要去做情感分析文本的二次解析,系统稳定性会高很多。
2.3 AI Agent:从函数调用开始
Agent的本质是让模型学会“使用工具”。目前最主流的实现路径是Function Calling(函数调用):模型在生成回复时,不是直接输出最终答案,而是输出一个工具调用指令,你的代码执行这个工具,再把结果反馈给模型,形成一轮新的推理循环。这就是教科书里常见的ReAct模式:Reason(思考)→ Act(行动)→ Observe(观察结果)→ 循环,直到得到结论。
我把一段最简可用的函数调用逻辑写成代码:
import openai from openai import OpenAI client = OpenAI() # 定义一个“查询订单状态”的工具 tools = [ { "type": "function", "function": { "name": "get_order_status", "description": "根据订单号查询订单的当前状态", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单号"} }, "required": ["order_id"] } } } ] def get_order_status(order_id: str) -> str: # 这里模拟查询数据库或调用内部接口 status_map = {"20240101": "已发货", "20240102": "待付款"} return status_map.get(order_id, "未找到订单") messages = [ {"role": "system", "content": "你是订单助手,必须通过查询工具回答订单问题。"}, {"role": "user", "content": "请帮我查一下订单 20240101 现在是什么状态?"} ] response = client.chat.completions.create( model="gpt-4o-mini", # 生产环境务必锁定具体版本快照 messages=messages, tools=tools, tool_choice="auto", ) # 如果模型决定调用工具 if response.choices[0].message.tool_calls: tool_call = response.choices[0].message.tool_calls[0] result = get_order_status(tool_call.function.arguments) # 实际执行工具 # 把工具结果反馈给模型 messages.append(response.choices[0].message) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) final_response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="auto", ) print(final_response.choices[0].message.content)这段代码看着很短,但包含了Agent系统的核心循环:模型决策调用工具、程序执行工具、结果回填模型、模型继续推理。实际项目中,工具数量可能几十个、几百个,还涉及工具调度的优先级、并发控制、超时处理、工具鉴权,复杂程度呈指数上升。但从最简实现开始,你能牢牢掌握“模型是决策器,代码是执行器”这一原则。
3. 从零搭建一个可用AI应用:一个完整实操案例
3.1 业务背景与功能拆解
理论讲再多,不如跑一个完整项目。我以一个“智能客服工单自动分类+回复助手”为例,这个项目足够典型:有结构化数据(工单编号、客户信息)、有非结构化文本(问题描述)、有决策动作(分类和回复),还涉及内部工具调用(查询历史订单、查询退款进度)。
核心功能拆成三步:
- 工单分类:把客户问题归入“订单查询 / 退款退货 / 技术咨询 / 投诉建议”四类;
- 信息提取:从问题文本中提取订单号、退款金额等结构化字段;
- 回复生成:基于当前状态生成给客户的回复草稿。
这个流程其实就是很多企业“AI客服”的最小原型。技术上并不复杂,但涉及了模型选型、提示词模板、结构化输出、工具调用和数据回流,足够完整地展示AI工程的骨架。
3.2 环境准备与一个小型实现
我习惯用Python + OpenAI SDK来实现,但思路完全适用于其他大模型服务。核心依赖只有两个:openai和pydantic,前者用来调用模型,后者用来做结构化输出校验。
先准备一个数据提取模块。这部分用结构化输出(JSON模式)而不是纯文本回复,目的是把结果变成可校验的Python对象:
from openai import OpenAI from pydantic import BaseModel, Field import json client = OpenAI() class TicketInfo(BaseModel): category: str = Field(description="工单分类:order_query / refund / tech_support / complaint") order_id: str = Field(description="从问题中提取的订单号,没有则为空字符串") amount: float = Field(description="涉及的金额,没有则为0") urgency: str = Field(description="紧急程度:low / medium / high") def extract_ticket_info(question: str) -> TicketInfo: response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是工单信息抽取助手。只输出规定字段的JSON。"}, {"role": "user", "content": f"请从以下用户问题中提取信息:{question}"} ], response_format={"type": "json_object"}, ) raw_json = json.loads(response.choices[0].message.content) return TicketInfo(**raw_json)这段代码的关键在于response_format={"type": "json_object"}。没有这个约束,模型可能把JSON包在Markdown代码块里,解析脚本会气得骂人;加了之后,输出直接就是干净的JSON,配合Pydantic的校验,基本杜绝了“字段缺失”“类型不对”这类问题。
接下来是工单分类提示词模板。我把模板设计在代码之外,单独放在配置文件里,这样后续优化提示词,不需要发版本,改配置就行。生产环境强烈建议这样做:
SYSTEM_PROMPT = """你是电商平台的智能客服预处理助手。 你的任务是对用户的工单问题做分类,并提取关键信息。 输出必须满足以下JSON结构: { "category": "order_query | refund | tech_support | complaint", "order_id": "字符串,如不存在则填空字符串", "confidence": "0到1之间的数字" } 注意: 1. 必须完整输出JSON,不要加任何解释文字。 2. 如果信息不足,字段填空值,不要猜测。 """3.3 工作流、评估与数据回流闭环
代码能跑通只是第一步,AI工程的重点在“闭环”。我搭建这套智能客服系统时,真正花时间的不是代码,而是评估体系和数据回流。
评估体系分三块:
- 单元评估:针对分类和抽取模块,准备一批人工标注的测试用例。每次修改提示词,先过一遍这组用例,计算准确率和字段完整率。
- 场景评估:模拟真实对话流程,比如“查订单→催退款→转人工”,观察Agent在长上下文中的行为是否符合预期。
- 回归评估:把线上出现过问题的case存成测试集,模型每次升级、提示词每次改动,都必须重跑一遍,防止“修好一个问题,引入三个问题”。
数据回流是很多团队忽略的部分。线上用户的提问是最宝贵的训练素材和评估素材,但直接拿用户数据去调整模型策略会涉及隐私合规问题,所以一般做法是脱敏后存入样本库。我习惯把所有失败的case(模型输出不合格的、Agent判断错误的)自动存下来,每周做一次复盘,把共性case补充进测试集。
这套闭环一开始感觉很麻烦,但它本质上是一个质量守门员。没有它,你根本分不清下一次效果变好是“提示词真的有效”,还是“这次模型运气好”。
4. 落地过程中的常见坑与排查技巧实录
4.1 提示词不稳定与模型幻觉
提示词不稳定的表现是:同样的输入,换成不同措辞,结果差距很大。这个问题只能通过结构化和约束来缓解,不要指望模型“理解你的深层意图”。具体做法是给模型提供确定的选项、预设的结构、明确的反例。有一次我们把系统提示词里的“不要输出多余内容”改成“只输出JSON对象”,错误率立刻下降了非常多。细节差别就是这么大。
模型幻觉则是另一类问题,集中在知识库问答里。模型不是搜索引擎,它不知道知识边界,容易一本正经地编造答案。我们的解决方案是“先检索后生成”(RAG):先通过向量数据库检索相关知识片段,再把这些片段作为上下文交给模型生成。如果检索不到相关片段,直接让模型回答“我不知道”,这样就切断了幻觉的主要来源。注意RAG只能降低幻觉概率,无法完全消除,关键场景仍需要人工审核或置信度阈值拦截。
4.2 上下文管理、记忆与成本失控
Agent类应用最容易被忽略的问题是多轮对话中的上下文膨胀。每轮都塞入完整历史,很快就把模型的上下文窗口撑爆。常用的策略是窗口截断、摘要压缩和关键信息提取三结合。窗口截断就是只保留最近几轮对话;摘要压缩是对早期对话做一轮总结,用摘要代替原文;关键信息提取是把订单号、用户名这类必须长期记住的信息单独存下来,每次拼进上下文。这三种策略可以在成本和质量之间找到一个相对合理的平衡。
成本失控和上下文膨胀是直系亲属。你的每轮请求都在为上下文长度付费,而且输入和输出的价格模型不同,很多团队看到账单时才发现,最烧钱的不是推理,而是那些无意义的历史记录。对策是给每条对话设置预算上限、为上下文做一个“引用链”,只携带与当前问题相关的片段。成本监控必须从一开始就做,不要等月底账单吓人再亡羊补牢。
4.3 典型问题速查表
| 问题现象 | 常见原因 | 排查方向 |
|---|---|---|
| 同样的提示词,今天好用明天不好用 | 模型版本浮动 / 上下文内容变化 | 锁定版本快照,对比线上上下文日志 |
| 输出格式频繁解析失败 | 提示词约束不足 / 模型版本变化 | 启用JSON模式,补强schema校验,增加重试逻辑 |
| Agent调用工具越来越频繁但无效 | 提示词鼓励“多用工具” / 工具描述不清晰 | 精简工具描述,增加工具使用条件约束 |
| 回复出现编造的订单信息 | RAG检索为空 / 上下文信息不足 | 空检索兜底逻辑,强制输出“无法确认” |
| 成本突然翻倍 | 上下文膨胀 / 重试次数过多 | 查看请求token均值,做上下文压缩和重试上限限制 |
| 模型分类判断越来越慢 | 上下文太长 / 工具列表过多 | 做分类检索,只保留相关工具子集 |
| 用户说“客服越来越笨” | 上下文被无关信息污染 | 增加关键字段记忆,弱化历史全量回传 |
这套排查思路不只是针对智能客服,放到任何AI应用上都通用。我建议每个AI项目从第一天就建一套线上日志系统,把每次请求的输入输出、token消耗、延迟、模型版本都记录下来。踩坑并不可怕,可怕的是你连踩的坑长什么样都看不清。
最后说一点个人体会。AI工程真正难的地方不是某个模型有多强,而是你要为不确定性搭建足够坚固的护栏。从零开始不是最省力的路,但绝对是你理解AI系统最快的一条路。把最小闭环跑通、把评估体系立起来、把失败case沉淀成资产,这些基本功做到位之后,你会发现任何新模型、新框架都只是工具箱里又多了一把好用的工具,而不是能让你偷懒的魔法。