做AI工程化这一年多,我最大的感触是:从零做一个AI系统不难,真正难的是把它“工程化”。所谓from scratch,不是让你手写Transformer,而是在大模型能力之上,搭一套属于自己的、可维护、可评估、可观测的AI应用体系——从提示词管理、RAG链路、Agent编排,到测试夹具、回归集、成本监控,全都得有。这篇内容就围绕“AI工程从零开始(ai-engineering-from-scratch)”这条主线,把我实际趟过的路径、踩过的坑、验证过的方案完整梳理一遍。适合正在从后端转AI应用开发的人、刚立项要带AI项目的技术负责人,以及被MCP和多Agent协作搞得一头雾水的开发同学。不管你现在用哪家大模型,这套方法论基本都能直接套用。
1. 先想明白:AI工程从零开始到底在工程什么
1.1 AI工程不是“调API”,而是三条流水线
很多人一上来就问选哪个模型,我真觉得这是最不该先问的问题。AI工程落到业务里,从来不是某个模型单点能力的问题,而是三套流水线能不能转起来的问题。
第一是数据流水线,包括文档采集、清洗、分块、向量化、入库、增量更新。第二是模型流水线,包括模型选型、部署、路由、提示词管理、调用链路的稳定性治理。第三是评估流水线,包括测试集建设、指标计算、回归检测、线上表现监控。传统后端开发习惯把这三块拆成若干独立系统,但AI应用不行,它们是一个闭环:数据变了,检索效果就变;提示词变了,回答质量就变;模型换了,一切都要重新验证。
我做第一个RAG项目时,把80%精力花在调提示词上,结果换了一个embedding模型后,线上回答质量明显下降,提示词怎么调都救不回来。后来才意识到问题出在数据侧——文档分块粒度跟新模型的语义空间不匹配。那次教训让我彻底改变了做AI工程的顺序:先定评估,再定数据,最后才谈模型和提示词。
1.2 和传统软件工程的三个关键差异
如果你带过纯后端团队,会发现AI工程在很多地方是反直觉的。最核心的三个差异,我建议团队每个人都要记熟。
不确定性。传统代码是确定性逻辑,同样的输入必然有同样的输出;大模型是概率分布,同样的提示词每次输出都可能不同。所以测试目标要从“对不对”变成“稳不稳”,需要通过约束输出结构、限定答案范围、增加校验层来压缩不确定性。我常用的思路是让模型按JSON schema输出,再用Pydantic做运行时校验,不合格就重试一次,而不是放任模型自由发挥。
成本结构。传统应用边际成本趋近于零,AI应用每次调用都是钱,token就是真金白银。这意味着要在代码里做预算管理、缓存、模型路由,长对话要做摘要压缩,检索要控制送入上下文的块数。这些在设计阶段就要想清楚。
可观测性。以前看日志、看RPS就够了,现在还要看提示词版本、上下文窗口占用、token消耗、置信度、用户反馈信号。没有一套LLMOps工具链,线上出了问题你连“当时模型看到了什么”都不知道,排查会非常痛苦。
1.3 从零起步的能力图谱
如果现在让我带一个团队从零做AI工程,我会先把能力图谱贴在墙上,让每个人知道自己负责的是哪一块。
| 能力域 | 要解决的问题 | 核心产出 |
|---|---|---|
| Prompt工程 | 输出稳定性、指令跟随、低成本复用 | 模板仓库、版本管理、评测集 |
| RAG(检索增强) | 知识更新、引用溯源、降低幻觉 | 向量库、切块策略、召回与重排链路 |
| Agent编排 | 多步骤任务自动执行 | 工具注册、循环控制、人工审批(HITL) |
| 评估体系 | 可回归、可验收、可灰度 | 黄金数据集、指标计算、CI闸门 |
| 可观测性 | 线上问题定位、成本核算、反馈闭环 | 链路追踪、token计量、看板 |
这张表里的每一项,在下面的章节我都会展开。这里只提一个原则:不要试图一口气全做。最稳的路径是选一个窄场景,比如“售后知识库问答助手”,把五条流水线都跑通,再横向扩展到其他场景。我见过太多团队一上来就做超级Agent,结果连最简单的检索质量都没保障,最后沦为大家都不愿意用的玩具。
2. 核心细节实操要点:Prompt、RAG、Agent逐个击破
2.1 Prompt工程化落地:模板、版本和上下文预算
Prompt不是一个魔法咒语,它就是一段不断演进的代码。我的团队把Prompt按Jinja2模板管理,system、few-shot示例、工具定义分开存放,每个模板带版本号,发布前必须过评估集。这点看起来笨,但能帮你少掉很多头发——我吃过亏:线上偷偷改了Prompt没记录,一周后回答风格突变,评估集里的分数却显示一切正常,因为评估脚本用的是旧模板。
一个基本的模板长这样:
{% set sys_template = "你是{{ product_name }}的智能客服。只根据【知识库】内容回答,不要编造。若知识库无答案,请直接说'抱歉,我还没有学会回答这个问题'。回答不超过{{ max_tokens }}字。" %} 你叫{{ user_name }},请问{{ question }}上下文窗口是有限资源,我习惯在代码里做“上下文预算”,而不是拍脑袋决定传多少块文档。以32K上下文模型为例,你可以这么算:
context_limit = 32768 tokens system + tools = 2200 tokens # 常驻系统提示词和工具定义 history = 2400 tokens # 最近5轮对话,超出则摘要压缩 output_reserve = 1024 tokens # 给模型生成留够冗余 budget_docs = 32768 - 2200 - 2400 - 1024 = 27144 tokens # 单个chunk约800 tokens,最多可以塞34块 # 但为了延迟和注意力质量,实际检索TopK建议控制在5~8块这套计算逻辑比“凭感觉调retriever”靠谱得多。另外关于生成参数,给你一份我常用的参考表:
| 参数 | 取值范围 | 适用场景 | 备注 |
|---|---|---|---|
| temperature | 0.1~0.3 | 代码生成、分类、结构化抽取 | 越低越稳定 |
| temperature | 0.5~0.8 | 文案改写、头脑风暴 | 太高容易废话连篇 |
| top_p | 0.1~0.9 | 核采样阈值 | 一般不与temperature同时大幅调整 |
| max_tokens | 按需 | 保护上下文不被撑爆 | 宁可截断,不要失控 |
调参的经验法则:先固定temperature为0.2,把Promot和检索调好,最后再微调生成参数。一上来就玩随机性,你会分不清效果波动是来自Prompt还是来自采样。
2.2 RAG链路:切块、召回、重排的坑与参数计算
RAG说简单也简单,说深也深。很多人直接pip install一个向量库就开干,结果线上召回率惨不忍睹,问题多半出在切块和重排上。
切块我按“先语义、再长度”的策略:优先按Markdown标题、段落边界切,块过大就再切一刀,单块控制在200~500个汉字,相邻块重叠80~120字,避免语义断层。块太大,检索出来的文档包含大量无关内容,模型容易被带偏;块太小,语义不完整,embedding质量也会下降。
召回阶段,embedding模型建议用bge-m3或text-embedding-3-small这类常见中文场景验证过的模型。向量库我推荐先上PostgreSQL+pgvector,原因很朴素:从零起步时不想多维护一套中间件,而且pgvector 0.5之后性能对百万级向量够用了。召回量级上,我习惯先取Top20候选,再用bge-reranker重排到Top5。不带重排的RAG只能叫向量搜索,这句话是我花两星期踩坑换来的:纯向量召回的前5条,往往有2条以上在语义上是重复或偏离主问题的,重排能把这些噪声压下去。
还有一个隐藏瓶颈是query理解。多轮对话里,用户说“那这个怎么退?”——你直接拿这句话去检索,库里根本找不到对应内容。正确做法是先做指代消解和改写,把这句话改成“上一轮提到的商品如何申请退货”,再去检索。这一步可以用轻量模型来做,成本很低但效果提升立竿见影。
2.3 模型选型、路由与Token预算
模型不是越强越好,而是越适合越好。我习惯把模型分层,让不同难度的请求走不同的模型,这就是模型路由。
| 模型类型 | 适用场景 | 典型成本系数 |
|---|---|---|
| 旗舰大模型 | 复杂推理、代码生成、罕见问题 | 10x |
| 轻量模型 | FAQ、分类、改写、结构化抽取 | 0.2x |
| 专用小模型 | 重排、意图判断 | 0.05x |
路由判断不需要太花哨,一个最简实现就是把历史请求按难度打标,然后做规则分类:命中FAQ关键词、问题模板明确的走轻量模型;包含“为什么”“对比”“帮我设计”这类开放词的走旗舰。有个很直观的账可以算一下:假设日请求30万次,其中60%是简单问题,如果不做任何路由,全部走旗舰模型,单次输入2000 token、输出300 token、按演示价格0.02元/千token输入、0.08元/千token输出算,单次成本是0.064元,一天1.92万元;加了路由后,简单问题走轻量模型(价格约为旗舰的十分之一),日成本降到不到9000元,一个月省出的小三十万,够养一整个AI团队了。价格会随市场波动,但这个账的思路不会变。
Token预算管理也是同样的逻辑。我每次调用前都会打印“本次请求预估消耗是多少 token,其中历史占多少、检索块占多少”,一旦发现history膨胀,就启动分段压缩:前面对话抽summary继续保留,最近的原始消息完整保留。有朋友问我为什么他的长对话越聊越贵,多半就是history无上限累加造成的。
2.4 Agent编排:从单轮到多轮的工程约束
聊完纯问答,进入现在最热的Agent。Agent的本质就四样:大模型做大脑、工具当手脚、记忆存状态、循环控制保证不跑飞。真正难的不是让它跑起来,是让它停下来、可追踪、不产生不可逆副作用。
我在Agent里加入了HITL(人在回路),业务上所有写操作,比如发邮件、改数据库、下单,都必须经过人工确认。技术上给每个Agent定义YAML配置,把工具、循环上限、超时都事先声明:
agent: name: code_review_agent model: qwen-plus tools: - fetch_repo - run_linter - diff_parser max_iters: 5 hitl: on_failure: true on_write: true timeout_ms: 15000循环上限是防跑飞的最重要防线。之前测过一个自动修Bug的Agent,它修完一个Bug后又引入两个新Bug,然后继续修,整整跑了47轮才被我们停掉。从那以后我规定:默认max_iters=5,每个工具调用必须有明确的成功/失败信号,失败就停止而不是重试到底。这就是热词里讲的loop engineering,循环工程,它关注的不是怎么让Agent多绕几圈,而是如何让每一圈都有意义、有边界、能收敛。
多Agent协作上,我踩过的坑是角色边界模糊。两个Agent共享同一份状态文件,A写入、B覆盖,最后完全乱掉。后来改成“一个任务,一个Leader Agent,n个Worker Agent”,状态变更通过事件通知,而不是共享可变全局量。如果你刚接触Agent,建议先从单Agent+多个工具开始,等工具调用稳定了再上多Agent。另外,MCP这层协议确实让工具接入标准化了不少,但也不要迷信,底层控制逻辑仍然要自己写好。
3. 从零搭起一套可评估的AI工程基线:完整实操过程
3.1 项目骨架与最小闭环
说一千道一万,不如直接把一套能跑的基线摆出来。我的项目结构一般长这样:
ai-engineering-from-scratch/ ├── app/ │ ├── core/ # 配置、提示词模板仓库 │ ├── services/ # RAG、Agent、重排等核心服务 │ ├── api/ # FastAPI 路由 │ └── metrics/ # 自定义观测打点 ├── tests/ │ ├── golden_set/ # 黄金回归数据集 │ └── harness/ # 测试夹具与评估器 ├── deploy/ # 部署编排 └── pyproject.toml用FastAPI做API层是个人偏好,社区生态成熟、异步支持好;数据库上先用PostgreSQL+pgvector,数据量到了千万级再换专用向量库;embedding服务用bge-m3;模型调用先锁一个供应商的接口,后续通过适配层替换。最小闭环的意思是:能完成“传问题-检索-拼Prompt-调大模型-返回答案-记录trace”这一整条链路,中间任何一环都不要用未经验证的花哨组件。
3.2 用60行代码实现一个带评估的RAG服务
下面这个简化版代码已经能说明问题,主要展示链路骨架,生产环境请加鉴权、限流、链路追踪和错误重试。
# app/services/rag.py from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class AskRequest(BaseModel): question: str history: list[str] = [] class AskResponse(BaseModel): answer: str sources: list[str] def recall(question: str, top_k: int = 5) -> list[dict]: # 1. 对question做embedding # 2. 在pgvector里做余弦相似度检索 # 3. 返回 [{doc_id, chunk_text, score}, ...] # 简化示意,实际代码在src/services/recall.py ... def call_llm(system: str, user: str) -> str: # 统一封装模型调用,记录token用量和延迟 ... def build_user_prompt(question: str, chunks: list[dict]) -> str: docs = "\n\n---\n\n".join( f"[文档{d['doc_id']}]: {d['chunk_text']}" for d in chunks ) return f"【知识库内容】\n{docs}\n\n【问题】{question}" @app.post("/ask", response_model=AskResponse) def ask(req: AskRequest): chunks = recall(req.question, top_k=5) system_prompt = load_prompt("default_system") user_prompt = build_user_prompt(req.question, chunks) answer = call_llm(system_prompt, user_prompt) return AskResponse( answer=answer, sources=[c["doc_id"] for c in chunks], )配套的评估脚本才是整套体系的关键,它决定了你改Prompt、换模型之后有没有底气上线:
# tests/test_rag_accuracy.py import pytest from app.services.evaluators import semantic_similarity, faithfulness @pytest.mark.rag def test_golden_set_accuracy(): golden = load_golden_set("tests/golden_set/v3.jsonl") failures = [] for case in golden: resp = client.post("/ask", json={"question": case["question"]}) score = semantic_similarity(resp["answer"], case["golden_answer"]) if score < 0.75: failures.append((case["question"], score)) assert not failures, f"未达标的用例: {failures}"这里的语义相似度不一定要用复杂模型,bge重排模型顺手就能当相似度计算器用。阈值0.75是我个人经验值,具体看业务对准确率容忍度,调低容易放过问题,调高容易误伤迭代。
3.3 harness engineering落地:把Capability装进测试夹具
“harness engineering”这个词,说白了就是给AI能力套一个可测试、可约束的“夹具”。打个比方,给气球充气时你先套一个网兜,就算充爆了也是可控地炸,不会伤到人。我在团队里要求所有新增能力必须同步提供harness——能力是充气的气球,夹具是那个网兜。
落地的第一步是建黄金问题集。不用贪多,从线上日志抽几百条真实query,人工写好参考答案,覆盖常见问题、边界问题、无答案问题三类。第二步是定义质量指标,我常用三件套:
| 指标 | 计算方式 | 我的及格线 |
|---|---|---|
| 忠实度 | 检查回答是否超出知识库内容范围 | 90%以上 |
| 覆盖率 | 参考回答中的关键信息是否都出现 | 85%以上 |
| 无关性 | 检索出的chunk与问题是否主题一致 | 95%以上 |
第三步是把harness接入CI。每次合并代码前自动跑一遍黄金集,任一指标下降超过3个百分点就阻断合并。这一步的意义在于让AI工程“可回归”——以后不管谁偷偷改Prompt、换模型、调切块参数,只要跑一次测试就知道有没有退步。
现在有些AI编程工具已经把这套思想做到了开发环境里,比如CodeBuddy在生成代码时也会同步搭测试夹具,让我这类强迫症选手省了不少事。关于自身开发中使用这类工具,后面第5部分再展开。
3.4 数据标注与回归集建设
回归集是AI工程的地基,地基不稳,上面全是危楼。我见过团队准备了上万条测试数据,但90%都是模板化的重复问题,导致评估分数很好看,线上体验却很差。我的经验是少量但有效,200条高质量用例就够撑起一个初版基线,关键要把这三类录进去:高频真实用户问题、历史上导致回答翻车的失败case、故意来挑刺的恶意/边界问题。
标注环节别偷懒,每条case要带上原始上下文和“为什么这么答”,这样后续别人接手才能看懂。新case的来源主要靠用户反馈闭环——线上加一个“这个回答有用吗”按钮,觉得没用的记录自动进未标注池子,定期抽人补标。数据版本也要管理起来,golden_set目录下的每个版本都不能删除,否则旧模型回归对比就没参考了。
3.5 上线后的灰度策略与指标监控
上线不是终点,是另一个起点。我用的灰度策略简称为“影子模式”:新模型或新提示词先跑在影子环境里,把线上真实流量复制一份进去,让新旧两个答案同时生成,只把旧答案返回给用户,后台人工抽样对比。等影子模式下评测指标稳定超过旧版,再放量5%、10%、50%、100%。
监控指标上,除了传统的延迟、成功率、token消耗,一定要看“对话轮次终止方式”:用户主动结束、得到答案后结束、还是因为不耐烦流失。这个信号最能反映AI产品到底有没有解决问题。可观测性工具可以用Langfuse这类开源方案,便宜且社区活跃,没有预算也可以自建打点系统,把每次请求的prompt版本、检索chunk、token数、模型输出全部落库。
4. 上线后常见问题与排查技巧实录
4.1 高频问题速查表
真实上线后你遇到的坑,95%都逃不出下面这张表:
| 现象 | 可能原因 | 排查路径 | 解决套路 |
|---|---|---|---|
| 回答胡编乱造 | 检索没召回内容,模型在裸答 | 看trace中检索chunk列表是否为空 | 增加“无答案拒答”约束,降temperature |
| 检索质量差 | 切块粒度与问题语义不匹配 | 打印TopK召回的相似度分数分布 | 调整切块大小、增加重排层 |
| Agent死循环 | 工具副作用让状态无法收敛 | 看循环计数与工具执行记录 | 设max_iters、加状态快照、人工中断 |
| 上下文Token爆掉 | history无限增长 | 看输入token曲线 | 旧消息摘要压缩,只留最近N轮原文 |
| 延迟高 | 检索慢、重排慢、调用太重 | 分步打点每一环节耗时 | 缓存高频问题、缩减召回数量、升级基础设施 |
每一条我都真实遇到过。最典型的要数检索为空导致的裸答问题——模型接不到知识库内容时,并不会乖乖认错,而是会用训练时的知识硬编一个答案,看起来还挺像回事。这种幻觉是RAG类应用的头号风险,排查时要盯着检索环节,而不是急着调Prompt。
4.2 实测中反复踩过的几个坑
第一个坑是提示词“热更”后忘了同步评估集。有次同事直接改了线上Prompt,效果看着不错,但黄金集的语义相似度阈值已经旧版本早就绑定了,测试一直通过,直到客服反馈话术风格突然变了,才追到是Prompt版本没跟评估版本绑定。现在系统里统一靠版本号对齐:代码层使用哪个Prompt版本,评估集就使用哪个。
第二个坑是知识库越加越杂,回答质量反而下降。我做知识库时候总有一种囤积癖,看到什么都想塞进去,结果检索出来的内容一半跟问题无关。后来砍掉一半低质量文档,单问答案质量明显提升。信息太多时,高质量的信息才会浮出水面,这个教训在AI场景比在数据库里更放大了。
第三个坑是对测试用例不加区分。有些easy case永远能过,真正需要靠它拦住回归的困难问题反而被淹没。现在我在黄金集里给每个case打标签(easy/hard/edge),pytest配置里hard类必须单独过一遍,且hard类指标权重更高。
4.3 长上下文与幻觉的取舍
还有一个很常见的思维误区:既然模型窗口越来越大,干脆把知识库文档全塞进一次调用里,RAG都不用做了。我跟你说,实际效果往往不行。窗口越大,模型对中后部信息的注意力越容易衰减,而且长上下文会显著增加延迟和成本。
即使窗口有128K,我也坚持限定每次送入的文档块数。宁可多一轮检索、多一次重排,也不要把整篇几十万字直接甩给模型。另外长对话要定期做语义断点:历史超过一定轮次,先把前面压缩成摘要,只保留用户最近的完整意图。这里的核心原则是“为模型减负”,而不是“给模型堆料”。
5. 工具链选型与一个让AI参与自身构建的飞轮
5.1 框架、平台和观测工具的取舍
工具圈更新太快,我的原则是只选长期维护、社区活跃、能被公司DT(数据技术)体系接纳的组件。框架上,LangChain适合快速验证原型,LlamaIndex在RAG场景更顺手,Dify这类平台适合非深度定制团队快速搭应用。但从零做AI工程、尤其是要长期维护的项目,我更推荐“轻框架+自研业务层”:框架管编排、协议、集成,你自己的代码管Prompt版本、评估逻辑、业务规则。
| 层面 | 我的推荐 | 替代选项 | 选型理由 |
|---|---|---|---|
| Web框架 | FastAPI | Flask、Spring | 异步支持好,类型提示友好 |
| 向量库 | pgvector | Qdrant、Milvus | 起步简单,一套PostgreSQL顺带搞定 |
| 编排框架 | 自研 + 必要库 | LangChain、MCP | 可控性优先,框架只做胶水层 |
| 可观测 | Langfuse | 自建打点 | 开源,自带trace和评估能力 |
| 测试 | pytest + 自研评估器 | 自定义CI脚本 | 生态稳定,黄金集直接变成测试用例 |
不推荐一开始上太重的东西。我见过团队把K8s、监控大屏、复杂评估平台全搭好,结果业务还没跑起来就被基础设施拖垮了。先把最朴素的组件跑通,等用户量和问题复杂度上来了再逐步升级。
5.2 让AI参与自己的构建
最后讲一点我个人觉得很有后劲的实践:当你把AI工程基线搭好后,完全可以开一个让AI辅助自身构建的飞轮。
我现在让开发团队的每个人把CodeBuddy这类AI编程工具当成结对伙伴:生成代码时要求它同步补单测和测试夹具,我们则在code review时检查AI生成的用例质量。这套流程跑起来之后,黄金集和初版测试代码的产出速度明显加快,团队对自动生成代码的信任度也在提高。要注意的是,不要让AI直接改生产规则,所有它生成的东西都必须走人审和回归闸门。这其实就是前面讲的harness engineering思想在开发流程上的延伸:给AI装个网兜,然后放心让它干活。
从我个人经验看,从零开始做AI工程,最值钱的能力不是会调几行参数,而是建立“任何改动都可度量、可回归、可回滚”的工程化心智。第一个版本用点心把评估、trace和成本这三根桩打下去,后面不管是换模型、扩场景还是上Agent,都会从容很多。所以如果你正准备启动AI项目,别急着写业务代码,先把黄金集和几条trace定义出来,这会是你整个项目里最值得的一笔投资。