接手"ai-engineering-from-scratch"这个项目时,我就知道这会是一条很长的路。它不是什么现成框架的快速集成,而是要把AI应用的每个环节——数据、模型、提示词、Agent编排、部署监控——都亲手过一遍。如果你和我一样,不满足于"调一个API然后祈祷它表现稳定",而是想在问题发生时知道该改哪里,那么这篇文章应该能给你一张相对完整的地图。
我会用自己搭建"企业文档问答AI"的过程来串起整条链路,分享哪些环节值得砸时间,哪些环节其实是过度设计,以及那些只有把手弄脏之后才能发现的坑。
1. 先弄清楚"从零构建AI工程"到底在构建什么
很多人听到"from scratch"会以为是要从反向传播开始手写Transformer。真不是这样。我做这个项目的初衷很简单:不想再当一个只会填Prompt的调包侠。遇到badcase时,如果我只知道改Prompt,那本质上是在和模型猜拳;但如果我能看懂数据、懂微调、懂检索、懂评估,我就能精准定位问题出在哪个环节。
1.1 为什么我给自己挖了"from scratch"这个坑
我见过太多项目,前期用现成大模型API跑得飞快,一周就上线Demo。可一旦进入真实业务,问题就来了:模型把A客户的文档答案回答给了B客户,或者面对一个稍微绕弯的问题就开始胡编。这时候团队能做的只有两件事:拼命改Prompt,或者换一个更大的模型。改Prompt十次有八次会引入新问题,换大模型则意味着成本翻倍、延迟变高。
我决定用"ai-engineering-from-scratch"这个项目把自己从这种困境里捞出来。目标不是不用任何现成工具,而是把AI应用拆成我能控制的模块:数据怎么构造、模型用什么底座、指令怎么设计、Agent怎么编排、上线后怎么评估。每一步都要能说出"为什么这么选"。
对想入行的朋友来说,这个项目的价值在于:它会逼你把"AI应用"从黑盒变成白盒。你会开始理解为什么同一个Prompt在不同模型上表现天差地别,也会理解为什么有时候问题根本不在模型身上。
1.2 AI工程的三层结构:模型层、能力层、应用层
我习惯把一个AI系统分成三层,这三层恰好对应了排查问题时的排查顺序。
| 层级 | 核心问题 | 典型工作 |
|---|---|---|
| 模型层 | 模型有没有能力做这件事 | 数据采集、预训练/微调、对齐、量化 |
| 能力层 | 模型能不能按你的方式完成任务 | Prompt设计、RAG检索、工具调用、记忆管理 |
| 应用层 | 系统能不能稳定跑在业务里 | 流程编排、权限控制、成本监控、评估反馈 |
大多数业务问题表面出在应用层,但根子往往在模型层或能力层。比如模型总是答非所问,你可能以为是Prompt写得不够好,实际是训练数据里根本没有这类问题的正例。又比如Agent执行任务经常中断,你可能以为是代码流程问题,实际是模型在规划时压根不理解工具的使用边界。
1.3 "from scratch"不等于重复造轮子
我得强调一点:from scratch不是让你从零去写CUDA核函数,也不是禁止用开源模型和框架。真正有价值的"从零",是不要跳过任何一个关键环节。
我当时的选择是:底座用开源的7B模型,但数据全部来自业务真实文档,微调、对齐、评估、Harness约束全部自己搭。框架可以用LangChain或LlamaIndex,但每个封装背后发生了什么,必须自己走一遍源码逻辑。用轮子,但要知道轮子为什么这么转,这样当轮子不转时你才能动手修。
2. 数据与推理模型:从零训练的最小闭环
如果让我给AI工程里的工作排一个性价比排序,数据一定排在模型前面。很多项目模型选来选去没有质的飞跃,就是因为训练数据的质量根本没有撑起来。这个阶段我做的是把"文档问答"这个场景的数据飞轮完整转起来。
2.1 先把数据飞轮转起来
我的业务场景是企业内部知识库问答。原始材料是几百份PDF和Word文档,它们的问题不是"缺数据",而是"数据太脏"。PDF里有过期内容、有扫描件、有表格被错误抽取后的乱码,Word文档里还有大量排版占位符。
处理流程大概是:先做文本抽取,再用规则去重、去广告页、去页眉页脚,然后按章节切分。切分这一步非常关键,切得太碎会让上下文丢失,切得太大又让检索精度下降。我当时的做法是按标题层级做语义切块,每一块控制在500到1000字之间。
接下来是构造训练数据。我有两个来源:一是人工标注,找业务同事整理了大约2000对问答;二是用大模型生成候选问答对,生成后必须经过规则过滤和人工抽检。为什么需要人?因为大模型生成的问答往往"看起来对",但会漏掉业务里的关键限制条件,比如"这个流程只适用于华南区",这种条件丢一条,上线就是事故。
2.2 选型:什么时候值得从零训练
我见过不少团队上来就说"我们要训练自己的大模型",但问他业务场景是什么,他说"做一个通用的企业助手"。这种场景根本不需要从零训练,直接调用商用API加一层RAG就能解决。
我整理了一个选型对比表,你可以直接参考:
| 方案 | 成本 | 可控性 | 适合场景 |
|---|---|---|---|
| 直接调用商用API | 按量付费,起步快 | 最低,只能靠Prompt调整 | 快速验证、通用场景、数据不敏感 |
| 微调开源模型 | 算力+数据标注成本 | 较高,能注入业务知识 | 垂直领域、专属术语、离线部署 |
| 从零预训练 | 极高,非一般团队可承担 | 最高 | 研究探索、特殊语言、特殊架构 |
我选的是微调开源模型。原因有两个:一是业务术语太特殊,通用模型经常把"结算单"理解成"报销单";二是数据敏感,客户不希望对话内容离开自有服务器。如果你也要微调,建议从一个指令微调过的底座开始,不要从基座模型开始,否则你需要花大量数据重新教它服从指令。
2.3 复现一个"从零构建推理模型"的最小实验
为了理解"推理能力"到底是怎么来的,我照着"build a reasoning model from scratch"的思路做了一个最小实验。这里不是真的训练一个能打败DeepSeek的模型,而是用一个小到能在单卡上跑起来的Transformer,训练它学会"先推理、再回答"。
核心思路很简单:普通的语言模型只学习"根据上文预测下一个词",而推理模型要多学一步,就是生成中间推理过程。我用一个微型Transformer做演示:
import torch import torch.nn as nn class TinyReasoner(nn.Module): def __init__(self, vocab_size, d_model=64, nhead=4, num_layers=2): super().__init__() self.emb = nn.Embedding(vocab_size, d_model) self.transformer = nn.TransformerEncoder( nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead), num_layers=num_layers ) self.fc = nn.Linear(d_model, vocab_size) def forward(self, x): emb = self.emb(x) out = self.transformer(emb) logits = self.fc(out) return logits训练数据用的是三元组:问题、推理过程、最终答案。损失函数用标准的交叉熵,但只在"推理过程"和"最终答案"部分计算loss,问题部分不计。更进阶的做法是用强化学习,让模型在探索中自己发现"什么样的推理路径能得到正确答案",DeepSeek公开的智能体训练方法里就有类似的思路,而且它把工具调用结果也放进了训练信号里。
这个小实验让我彻底理解了为什么推理模型需要专门的训练,而不是靠Prompt硬挤出来。也让我意识到:如果你的场景需要复杂推理,微调底座模型时,训练数据里必须带"思维链"样本,否则模型只会输出结论,不会输出过程,一出错你就没法定位。
3. Prompt Engineering与Harness Engineering:给模型戴上的两套缰绳
模型训练好之后,接下来是让它稳定地在业务里干活。这就要提到两个经常被混为一谈的概念:Prompt Engineering和Harness Engineering。简单说,一个是软约束,一个是硬约束。
3.1 Prompt Engineering的本质是接口设计
我见过太多人把Prompt当成"许愿咒语",同样的需求换个说法效果就天差地别。实际上,Prompt工程不是玄学,它是在设计模型行为的接口规范。一个行为稳定的Prompt,通常包含角色、任务、上下文、约束、输出格式五个部分。
下面是我在企业问答场景里沉淀出来的模板:
你是一名熟悉企业流程的客服专家。 任务:基于给定的文档片段,回答用户问题。 上下文:{检索到的相关文档片段} 约束: - 只能使用上下文中出现的信息,不要推测。 - 如果上下文中没有答案,直接回答"暂未查到相关信息"。 - 回答需要标注引用来源,格式为[文档名-章节]。 输出格式:{"answer": "回答内容", "source": "来源列表"}为什么要结构化?因为结构化Prompt便于测试。你可以针对每个字段做消融实验,比如去掉"约束"里的第二行,看模型是不是又开始胡编。同时它让Prompt在不同模型之间迁移时更加稳定,换底座模型时只需要微调格式说明,而不是把整个咒语重写一遍。
3.2 Harness Engineering:把"请模型自觉"变成"系统强制"
Prompt说得再好,模型也可能某一天突然不听话。这时候就需要Harness Engineering上场。我理解的Harness Engineering,是围绕模型搭建的一整套工程护栏,包含输出校验、工具沙箱、重试降级、审计日志等机制。
拿输出格式举例。Prompt里写了"输出JSON",模型偶尔还是会输出一个Markdown代码块,或者JSON里漏一个字段。如果你只在Prompt层面使劲,永远堵不完漏洞。正确做法是在代码层强制校验:
from pydantic import BaseModel, ValidationError class AnswerSchema(BaseModel): answer: str source: list[str] def parse_model_output(raw_output: str): # 去掉可能的Markdown代码块 cleaned = raw_output.strip().removeprefix("```json").removesuffix("```") data = json.loads(cleaned) return AnswerSchema(**data)校验失败就自动重试一次,让模型重新生成;重试仍失败就走兜底策略。这个过程完全不需要模型"自觉",而是系统强制。用一句大白话总结:Prompt是跟员工交代任务,Harness是给员工配上工位、流程、质检员和应急预案。
3.3 CodeBuddy实现Harness Engineering的完整案例
关于Harness Engineering,我最近一次比较完整的落地,是借助CodeBuddy这个AI编程助手来写的。说实话,Harness代码本身并不难,但很繁琐,要覆盖各种边界情况。我当时的做法是先把接口定义和几个典型badcase丢给CodeBuddy,让它生成校验逻辑和测试用例。
具体分三步。第一步,把上面那个AnswerSchema的定义和几个"模型可能犯的错"示例给CodeBuddy,让它生成一个更健壮的解析函数,至少覆盖JSON里混入额外字段、Unicode转义、字段缺失三类情况。第二步,让CodeBuddy用property-based testing的思路生成随机边界输入,比如超长字符串、空字符串、恶意JSON结构,然后跑一轮测试。第三步,让它把校验逻辑封装成FastAPI中间件,这样所有经过网关的模型输出都统一走校验。
这个案例给我最大的启发是:用AI编程工具来写"约束AI的代码",本身就是AI工程里很典型的闭环。CodeBuddy这类工具能加速Harness代码的编写,但前提是你得自己清楚Harness需要什么,不然生成的代码只会看起来完整,实际覆盖不到关键风险。
4. AI Agent的工程化落地:从单次调用到自主协作
如果你只想做一个"输入问题输出答案"的工具,那到第三章就够了。但一旦涉及复杂任务,比如"帮我对比近两年三份合同的付款条款差异",就需要Agent出场。
4.1 Agent不是"会调工具的ChatGPT",而是一个带状态的任务系统
很多团队对Agent的理解停留在"给模型加一个工具调用的API"。但真正的Agent是一个带状态的任务执行系统,至少包含四部分:规划器(Planner)、记忆(Memory)、工具集(Tools)、反思机制(Reflector)。
规划器负责把大任务拆成小步骤,记忆负责保存中间结果和对话历史,工具集是模型可以调用的外部能力,反思机制则负责检查自己的执行结果并对错误进行修正。没有反思的Agent,就像一个人埋头做一百道题但从不检查,错了就一路错到底。
在企业问答场景里,我的Agent结构是这样的:主规划Agent先判断用户问题需要几个子任务,然后派发检索任务给检索Agent,派发信息整合任务给写作Agent,最后让一个评审Agent检查答案是否引用完整、是否偏离问题。这个过程就是多AI协作。
4.2 多AI协作的编排模式:主从式与对等式
多Agent协作不是"把一堆Agent放在一起就会自动工作",编排模式设计不好,结果就是互相踢皮球。我常用的有三种模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 主从式 | 一个主Agent规划并调度多个子Agent | 任务可以明确拆分的场景,如报告生成 |
| 对等式 | 多个Agent互相评审、纠错 | 需要高准确率的场景,如合同审核 |
| 流水线式 | 上一个Agent的输出是下一个Agent的输入 | 流程固定的场景,如数据清洗 |
我做了主从式加一点对等式的混合:让规划Agent调度检索和写作,但最后一定有一个评审Agent来挑毛病。这个评审Agent不是摆设,它会检查答案里的每个事实是否能回溯到检索结果。如果有一处对不上,就退回给写作Agent重写。
4.3 一个可运行的多Agent工作流示例
下面是一个简化版的多Agent工作流,我用它跑通了一次"文档对比分析"任务:
def run_multi_agent(query): plan = planner.run(query) # 拆解任务 tasks = plan.subtasks search_results = [] for task in tasks: # 每个子任务只传必要信息,避免上下文爆炸 result = search_worker.run(task.query, task.time_range) search_results.append(result) draft = writer.run(query, search_results) # 反思机制:评审不过就打回重写 review = reviewer.run(draft) if review.score < 0.8: draft = writer.run(query, search_results, review.suggestions) return draft.answer注意看代码里的注释,我特意强调"每个子任务只传必要信息"。这是我在成本控制上学到的血泪教训:多Agent协作最容易翻车的不是逻辑,而是Token消耗。如果每个Agent都带着完整对话历史跑来跑去,一次任务的Token消耗会轻松放大十倍。
5. 部署、评估与迭代:真正决定项目生死的三件事
很多教程到"跑通Demo"就结束了,但真实的AI项目,上线才是开始。我整理了三件决定项目生死的事。
5.1 评估:别用"感觉回答得好"代替指标
没有评估体系的AI项目,就像一个没有质检员的工厂。我建的评估体系分两大部分:客观指标和模型打分。
| 指标 | 含义 | 测量方式 |
|---|---|---|
| 答案准确率 | 关键信息是否正确 | 人工抽检 + LLM打分 |
| 忠实度 | 答案是否忠于检索文档 | 检查每个论断是否有来源 |
| 格式合规率 | 输出是否符合JSON Schema | 程序自动判断 |
| 延迟 | 从请求到返回完整结果的时间 | 压测 |
| Token成本 | 平均一次请求消耗的Token数 | 日志统计 |
在模型打分部分,我用的是LLM-as-a-Judge方式,让一个更强的大模型充当裁判。但这里有个大坑:裁判模型本身也有偏好。我的缓解方案是让三个不同模型独立打分,取多数票,并且把答案顺序随机化,防止位置偏置。还有,裁判模型不能和生成模型是同一个,否则它会偏好自己风格的答案。
5.2 部署形态选型:边缘、Serverless、私有化
部署不是"能跑就行",要结合数据敏感性和流量特征。我这个项目因为客户数据不允许出私有网络,最终选择了私有化部署。如果你没有这个限制,Serverless会更省心,它可以自动扩缩容,很适合问答这类流量波动大的场景。
推理加速方面,我做了两件实事:一是把模型量化到INT8,延迟降了约40%,精度损失在可接受范围;二是用vLLM做推理服务,它通过PagedAttention和Continuous Batching大幅提升了吞吐量。如果你用的是开源模型,强烈建议不要直接用HuggingFace的原始pipeline上线,至少套一层vLLM或TGI。
5.3 线上监控与回归测试:让模型升级不翻车
模型升级是AI工程里最容易出事故的环节。我见过有人直接把新模型推到全量流量,结果badcase暴增,用户反馈炸了。正确姿势是金丝雀发布:先让新模型服务5%的流量,对比新旧模型的badcase数量和打分情况,确认没问题再逐步放量。
同时要维护一套独立的回归测试集。这套测试集一定要和训练数据隔离,且需要持续补充线上出现的高价值badcase。每次升级模型、改Prompt、改检索策略,都要先跑一遍回归,保证老问题不复发。我在这件事上吃过亏,当时为了快速上线,跳过了回归,结果修好了一个坏case,却把之前三个好case全弄坏了。
6. 踩坑实录:我在AI工程化路上反复踩过的五个坑
最后分享几个我用真金白银换来的教训。
6.1 数据集里的隐性偏置让模型答非所问
第一次做微调时,我的训练数据里90%都是"有答案"的问题,只有10%是"无答案"问题。结果模型上线后,面对没见过的知识疯狂编答案,因为它从数据里学到的模式就是"所有问题都必须回答"。后来我在训练数据里把无答案比例调到30%,并且单独加了一批"空文档检索结果"的样本,模型才学会说"暂未查到相关信息"。数据平衡这件事,优先级永远高于模型调参。
6.2 上下文窗口不是越大越好
我曾经觉得,模型上下文窗口有8K,那把整份合同塞进去不就好了?事实是,模型在超长文本里会"注意力涣散",尤其是跨章节的信息,它经常抓不住重点。后来我改用检索增强,每次只把与问题最相关的几个文档片段喂给模型,效果反而更好。记住:上下文窗口是上限,不是最优值。
6.3 盲目堆Agent导致token成本失控
有段时间我痴迷于"多Agent协作",一口气设计了5个Agent。结果跑一次任务要消耗几万Token,成本是单次调用的十倍以上。后来我给系统加了三个硬约束:全局Token预算、最大迭代轮次、子任务间只传摘要和关键结论。控制成本不是牺牲效果,而是倒逼你把流程设计得更精简。
6.4 测试集污染导致评估结果虚高
我在早期犯过一个经典错误:用大模型生成的同源数据既当训练集又当验证集,评估分数高达95%。结果上线真实流量,效果断崖式下跌。原因很简单,模型已经"背过"那些题目了。后来我单独维护了一套Golden Set,由人工编写,永远不进入训练管道,并且每两周补充新的线上badcase进去。这个Golden Set才是评估的锚点。
6.5 只调Prompt不调系统结构,天花板太低
最后一个坑最隐晦,也最要命。当我面对badcase时,第一反应永远是"把Prompt改一下"。但随着问题变多,Prompt越来越长,行为越来越不稳定。后来我意识到,一个badcase如果通过改Prompt能解决,那说明系统整体是健康的;如果同样的badcase反复出现,问题一定出在数据覆盖、检索质量、Harness约束或模型基础能力上。这也是"ai-engineering-from-scratch"这个项目带给我的最大转变:从"调词"到"调系统"。
最后再分享一个我至今受益的习惯:从项目第一天就建立badcase日志,记录原始输入、预期输出、实际输出、模型版本、Prompt版本、检索命中的内容。不要小看这个习惯,它会在你升级模型或调整策略时,帮你快速定位"这次改坏了什么"。AI工程没有银弹,但好的日志与评估机制,能让你每一次迭代都站在上一次的肩膀上。