
AI engineering from scratch 这串英文最开始只是我 Git 仓库里的一个目录名。后来这个目录越写越大成了我半年跌撞实践后的浓缩——从零开始把一个只会生成文字的模型变成一套能稳定产出结果的工程系统。这篇文章就是把整个过程里的设计取舍、代码细节、踩坑记录一次讲透。适合哪些人看已经会调大模型 API、写过几个 prompt但还没把 AI 功能做出产品感的开发者以及想系统理解 AI 工程化到底在做什么准备上手 agent 或 AI 工作流的团队。如果你以为 from scratch 是要从训练模型开始那会绕很远的路我的实践结论恰好相反真正的工程瓶颈不在模型而在模型外面那套约束、评估和协作机制。1. 从零开始做 AI 工程先想清楚要解决什么问题1.1 我踩过的“非工程化”弯路第一次做 AI 需求时我的方案很原始写一大段 prompt 塞给模型让它直接输出结果。demo 演示很惊艳一到真实数据上就原形毕露。格式偶尔崩、内容偶尔偏、同一个问题问两次答案不一致更别说超时和成本波动。当时我以为是 prompt 写得不够好反复调词、加示例、改语气折腾两周效果依旧不可控。后来我意识到问题根本不在 prompt 写得好不好而是我把“调用模型”当成了“做工程”。让模型稳定干活需要的不是一段更长的咒语而是一整套围绕模型建立起来的工程框架。这也是项目名叫 from scratch 的原因不是从零训练模型而是从零搭建一个能让模型稳定工作的系统。1.2 我理解的 AI 工程四层结构做了几个项目后我把 AI 应用工程化拆成四层后面所有实践都围绕这四层展开模型接入层选哪个模型、怎么调用、超时重试、成本控制、版本切换。上下文与提示词层怎么组织 prompt、怎么管理上下文窗口、怎么给模型提供必要信息。工具执行层模型决定要做什么系统真正去执行什么包括函数调用、文件读写、命令执行、结果回传。观测评估层怎么判断模型干得好不好怎么在它犯错时及时发现并修正。用一个生活化类比把大模型想象成一个能力很强但经验不足的实习生。你不可能只丢一句“把这事搞定”就期待高质量交付。你需要给他工作手册、明确交付格式、限定能动的工具、设置检查节点还要有一套验收标准。AI 工程化做的就是这样的事。这四层里面很多人把注意力全放在第 2 层prompt忽略第 3 层和第 4 层。但真正让 AI 功能从“能用”变成“好用”的恰好是工具执行和观测评估。后面我会用一个完整案例把这四层全部串起来。1.3 工程目标与边界划定做 from-scratch 项目最重要的一件事是先划定边界。我做的是“面向代码生成与执行的 AI 工程工作流”所以明确不碰模型训练、不碰大规模分布式推理只聚焦“基于现成大模型构建可复现、可评估、可维护的 AI 应用系统”。把边界写清楚能避免很多无效劳动。比如我不会花时间去微调模型来让输出格式变稳而是用结构化约束和后处理校验解决我不会追求让模型“一次答对”而是设计一个“生成—执行—反馈—修复”的闭环让系统自己迭代到正确结果。这个思路也是我理解的 harness engineering 的核心。2. 核心设计prompt engineering 与 harness engineering2.1 提示词工程真不是写作文很多人把 prompt engineering 理解为“把话说漂亮”实际不是。一段可维护、可复用的 prompt应该像代码一样有结构。我自己常用的模板包含六个部分角色定义让模型明确自己是谁。任务目标一句话说清要做什么。背景上下文必要的信息输入。约束条件明确禁止项和边界。输出格式指定结构化格式比如 JSON、代码块。示例样本给一两个 few-shot 例子。实际的模板例子长这样你是一名资深测试工程师。给定仓库结构、目标文件路径和需求说明请完成以下任务 1. 分析目标模块的输入输出和核心逻辑 2. 设计覆盖正常路径、边界条件和异常路径的测试用例 3. 输出可直接执行的 pytest 代码。 约束 - 只依赖 pytest不引入额外测试框架 - 所有 mock 必须写在测试文件内 - 输出只需包含 python 代码块不要任何解释性文字 - 代码必须能够在当前工作目录下直接运行。 目标文件src/order.py 仓库结构见上下文中的 JSON 列表这个模板看起来简单但每个字段都有存在的理由。角色和任务目标降低理解偏差“输出只需包含 python 代码块”配合后续的解析逻辑能减少格式解析的失败率few-shot 示例我通常会附在模板下方让模型知道“成品的具体长什么样”。关于采样参数也要根据任务调整。生成测试用例这类偏逻辑的任务我会把 temperature 设在 0.2 以下减少随机性如果是创意文案类任务才把 temperature 调高。很多人全程用默认参数遇到输出不稳定就怪模型其实参数也是工程的一部分。2.2 harness engineering 到底是什么harness engineering 这个词听起来很玄其实可以直译为“约束工程”或“夹具工程”。它的思路是不要指望模型自觉而是在模型外面套一层限制装置让它可以发挥能力但无法跑偏。我在实际项目中把它拆成三层约束第一层是输出约束。模型输出必须通过 JSON schema 校验或者正则匹配不符合就直接判定失败重来。第二层是行为约束。给模型配置工具白名单它不能随意调用任何函数只能使用我们注册过的、有权限边界的工具。第三层是流程约束。把任务拆成固定状态机模型每一步做完后系统检查状态是否合法再决定下一步给什么指令。打个比方这就像给赛车修赛道。赛车性能再强没有护栏和赛道边界它也只能跑成事故。模型是赛车harness 就是护栏和赛道标识。你是在创造一个“它怎么跑都不会出大问题”的环境而不是赌它每次都跑对。我在 CodeBuddy 里落地这个理念时会把每个工具函数写成一个带描述和参数 schema 的注册项然后由执行器统一调度。模型不能直接执行代码它只能“请求”执行真正执行和结果校验都交给系统。这样一来即使模型产生了不合理的请求也会在执行层被拦截。2.3 上下文管理与函数调用设计上下文窗口是 AI 工程里最容易被忽略的瓶颈。很多人把一整个项目的文件内容全塞进 prompt很快就撞上 token 上限或者让模型被海量信息淹没。我的策略是三层管理滑动窗口只保留最近 N 轮对话内容更早的做截断。摘要压缩当上下文快满时调用模型对中间过程做摘要把压缩后的结果放回上下文。检索增强不让模型看整个仓库而是先通过工具拿到目录结构再按需读取目标文件。函数调用function calling是工具执行层的核心。我通常这样定义一个工具{ name: list_files, description: 列出指定目录下的文件和子目录, parameters: { type: object, properties: { path: { type: string, description: 要列出的目录路径 } }, required: [path] } }定义工具时description 写清楚比参数设计更重要。模型靠描述来决定什么时候调用、传什么参数如果描述含糊它就会在错误的时候调用。参数类型和必填项也要严格减少模型传错参数的概率。此外每次工具调用都必须在系统侧记录日志方便后续排查。3. 实操用 CodeBuddy 搭一个“自动生成测试用例”的 Agent 工作流3.1 场景定义与工程规划纸上谈兵够多了我拿一个真实跑通过的项目做例子做一个 AI Agent输入一个仓库地址自动分析核心模块并生成 pytest 测试用例然后执行测试、收集结果、修复失败用例直到通过或达到迭代上限。这个场景非常适合展示 AI 工程化因为它同时涉及代码理解、生成、执行、反馈闭环几乎覆盖了前面讲的所有层。项目结构我按下面这样组织ai-test-agent/ ├── agent/ │ ├── planner.py # 任务拆解与规划 │ ├── executor.py # 工具调度与执行 │ ├── reviewer.py # 结果评审与决策 │ └── prompts.py # 所有提示词模板 ├── tools/ │ ├── file_tools.py # 文件读取与列表 │ ├── run_tests.py # 执行 pytest │ └── registry.py # 工具注册表 ├── core/ │ ├── context.py # 上下文管理 │ └── schema.py # JSON schema 校验 └── main.py # 工作流入口这个结构我是刻意设计的。prompts 单独放因为提示词会频繁改版tools 单独放因为工具是模型能力的边界core 里放上下文管理和校验逻辑这是 harness 的关键。一切按职责分开后续换模型、加工具、调提示词都不会牵一发动全身。3.2 从“手动调模型”到 Agent 模式刚开始我是在 PyCharm 里手动写脚本调用模型后来换了更高效的方式直接使用 CodeBuddy 的 Agent 模式来搭建和调试这个工作流。CodeBuddy 这类支持 AI 编程和 agent 编排的工具本身就能让你把提示词、工具函数、执行流程串起来省掉很多基础设施代码。我在 PyCharm 里也装了 AI 插件但说实话插件更适合写代码片段和解释报错真正要跑一个完整的“生成—执行—反馈”循环还是需要把流程写成脚本。实操中我的做法是用 PyCharm 写 agent 代码用 CodeBuddy 的 Agent 模式跑通端到端流程两边互相配合。工作流的状态机我设计成四态PLAN - GENERATE - EXECUTE - REVIEWPLAN模型分析仓库结构确定要测的目标函数。GENERATE基于目标文件生成 pytest 测试代码。EXECUTE系统实际运行 pytest捕获结果。REVIEW模型看失败报告判断是测试写错还是被测代码有 bug决定是否修复重试。每次 REVIEW 后如果判定需要重试系统回到 GENERATE但会带上上一次的执行结果。最多重试三次超过就终止并把中间过程写入日志。3.3 关键代码逻辑与反馈闭环主循环核心逻辑并不复杂重点是“执行结果必须原样回传给模型”这个反馈闭环是整个 agent 能收敛的关键。for attempt in range(MAX_ATTEMPTS): if state GENERATE: test_code generate_test(target_info, previous_feedback) if not validate_code_shape(test_code): previous_feedback 输出格式不合法请只输出代码块 continue write_test_file(test_code) state EXECUTE if state EXECUTE: result run_pytest(test_file) state REVIEW if state REVIEW: decision review_result(result) if decision pass: break previous_feedback build_feedback(result, decision) state GENERATE这里最容易犯错的地方是把“模型认为的结果”当成“真实结果”。比如模型生成代码后自信地说“测试应该会通过”可我们绝不能就此为止。真实行情是必须让系统实际执行 pytest拿到 stdout 和退出码再把这些真实反馈塞给模型。没有执行层的反馈闭环一切都是模型在自说自话。另一个容易踩坑的点是 validate_code_shape。模型有时会在 python 代码块外夹带“以下是测试代码”这样的说明如果直接写文件pytest 会因 Markdown 标记报语法错。我的做法是用正则提取代码块内容再做一次 Python 编译检查编译通过才写入文件。3.4 评估机制怎么证明 Agent 靠谱Agent 能跑通只是第一步还得证明它稳定。我建了两层评估。第一层是过程评估。每次运行都记录生成轮数、pytest 通过率、代码覆盖率、失败原因分类。连续跑 30 次后汇总如果通过率低于 90%就去检查是哪类问题导致的。第二层是回归集评估。我准备了一个包含 5 个不同模块的测试仓库每次改完提示词或工具代码就全量跑一遍防止“修好一个案例、弄坏另一个案例”。评估还可以引入 LLM-as-judge让另一个模型来评审判定结果质量。比如 reviewer 角色判断失败原因是“测试逻辑写错”还是“被测代码缺陷”这个分类结果直接影响下一步动作。但要注意模型做判断也不一定准所以关键路径上的判定需要人工抽检。我的经验是评估机制本身也要做回归和抽检否则会变成“用模型相信模型”的循环。4. 常见问题与排查技巧实录4.1 输出格式飘忽不定模型有时会输出 JSON 前后夹带解释文字、代码块标记不闭合、甚至把工具调用参数拼接错误。我实测下来最有效的三层方案是在提示词里明确输出格式在解析层做容错能提取的尽量提取在架构层用 schema 校验校验不过就重试。三层都做了之后格式问题基本从高频降为偶发。有个小技巧如果解析失败不要直接把原始输出吞掉要把“期望格式 vs 实际输出”的差异作为反馈回传给模型。比如“你上一个输出没有闭合代码块请重新生成这次只输出代码”。这种反馈往往比修改提示词更有效因为它是在具体错误上下文里的即时纠正。4.2 上下文窗口不够用项目仓库一大文件名列表就可能几百行更别说读取文件内容。刚开始我直接把整个目录树塞进去很快触发上下文上限而且模型开始忽略后面的信息。后来我改成两阶段先调用 list_files 拿到目录结构由模型自己决定读哪些文件并且限制每次只能读一个文件避免一次读太多导致注意力分散。对于必须要读的大文件我会做分段读取加摘要压缩。先用滑动窗口读前 100 行让模型生成摘要再根据摘要决定是否继续读后面部分。虽然多了几次模型调用但效果比一口气全塞进去好得多。4.3 工具调用陷入死循环模型有时会在同一类工具上调来调去始终无法推进任务。比如反复 list_files 同一个目录或者反复读取同一个文件。我的解决办法是加最大调用次数限制默认 15 次超过就自动终止同时记录“相同参数调用次数”如果同一个工具在同一个参数上出现了 3 次就打断循环并把历史记录回传给模型要求更换策略。这个问题的根因往往不是模型傻而是它缺少足够的信息来做决策。所以打断后要给的信息不是“你错了”而是“你已经看过这些内容当前真正缺失的是另一部分信息”。用有效反馈而不是简单报错能大幅减少再次进入死循环的概率。现象根因处理方案输出夹带解释文字格式约束不足解析层提取 schema 校验同一个目录反复 list缺少信息做决策打断循环 回传历史记录生成测试文件写错路径模型凭空猜测先 list_files 再让模型选真实路径测试全红但不知原因反馈信息太粗把完整 pytest stdout 回传给模型每次运行结果差异大采样参数过大调低 temperature固定 prompt 顺序token 成本飙升无条件重试限制最大轮次 中间结果缓存4.4 成本与性能的权衡AI 工程绕不开算账。一次完整的生成测试用例流程我实测大约消耗 8 万到 15 万 token。如果每次代码变更都全量跑评估成本很快失控。我的做法是对所有不依赖模型输出的中间状态做缓存比如目录树、摘要、历史工具调用记录命中缓存就直接复用只有真正变化的部分才重新调用模型。另外一个性价比很高的技巧是模型分层。简单任务比如格式转换、摘要生成用便宜的小模型复杂任务比如生成测试用例、审查失败原因才用更强的大模型。这个思路在工程界叫 prompt routing做 AI 应用时值得优先考虑能省掉一大部分成本。5. 从单个 Agent 到多 Agent 协作5.1 为什么需要多个角色分工单个 Agent 能力再强承载的任务一多就容易顾此失彼。我在实践中慢慢把单 Agent 拆成三个角色planner 负责分析和拆解任务executor 负责具体工具调用和代码生成reviewer 负责检查执行结果并判断是否返工。这其实是参考了真实团队的协作方式也是我理解的多 AI 协作的基本形态。拆分的核心价值不只是“多模型并行干活”而是“每个模型的任务边界更清晰”。planner 只需要做决策不需要生成完整代码executor 只需要聚焦生成不需要纠结策略reviewer 只需要对照目标做判断不需要自己动手修复。边界清晰之后提示词可以更短输出质量也更稳定因为每个模型都在做自己最擅长的那部分。5.2 多 Agent 协作的消息协议多 Agent 协作最怕没有约定各说各话。我定义了一套简单的消息协议每个 agent 之间传递的都是结构化消息{ task_id: test-gen-001, actor: planner, action: plan_delivered, payload: { target: src/order.py, strategy: 功能测试边界测试 }, status: done }这条消息其实就是“数据和状态流转”。planner 输出 planexecutor 拿到 plan 后干活并回传结果reviewer 把审核结果再传回 executor。所有消息汇总统一日志每次流程结束后都能回放“谁在什么时间做了什么判断”排查问题的时候特别有用。多 Agent 协作的正确姿势是先把单 Agent 和工具流程跑通再把流程拆成角色。一上来就铺六个 agent 互相聊天大概率会得到一本谁也读不懂的流水账。我见过太多团队把多 Agent 做成了一台烧钱的大型复读机问题不在于概念不好而是还没有为拆分做好基础约束和消息设计。5.3 robot engineering 给我带来的启发做这些工作流时我一直参考 robot engineering机器人工程的思路。机器人领域有一个常识机械臂的每个动作都受限于关节角度、力矩范围和环境感知系统必须为这些限制做规划。把 AI 模型当作机械臂把 prompt 当作控制指令把工具当作执行器很多设计就豁然开朗了。比如机器人不会因为“意识”而突然偏离规划路径是因为控制器实时采样并校正。我们的 AI 工作流也应该如此每次模型输出都要经过校验发现偏差立即校正。这个类比提醒我工程化的核心不是让系统看起来智能而是让系统在每一个不智能的瞬间都有兜底方案。这套思路就是 harness engineering 和 robot engineering 最相通的地方。6. 写在最后做完这个 from-scratch 项目我最深的体会是AI 工程化不是把 prompt 写得更长更花哨而是给系统加上可观测、可回滚、可评估的机制。模型是一个高速引擎我们要做的是为它安装仪表盘、方向盘和安全气囊。判断一个 AI 项目是不是工程化最简单的标准就是当模型输出错误时你的系统是能自动发现、自动修正还是只能在用户反馈后手足无措最后分享一个非常实用的小建议任何一个 AI 工作流都先做一个最小的完整闭环——一段输入、一次模型调用、一次工具执行、一个结果判断。跑通这个四步闭环比设计任何复杂架构都重要。这半年实践下来我所有稳定好用的功能都是从这样的小闭环长出来的所有翻车严重的功能也都是因为跳过了这个小闭环直接铺了大摊子。如果你正打算开始一个 AI 工程老实从闭环做起先把反馈转起来再去装饰你的控制面板。