
ai-engineering-from-scratch 这个项目是我给自己布置的一门野课。起因特别简单天天在社区看别人晒AI项目什么AI Agent、多智能体协作、提示工程体系看得心痒痒但真到自己做又不知道从哪下手。于是我决定从零开始不依赖任何封装好的低代码平台自己在Python里搭一套AI工程的最小闭环。这篇文章就是这门课的笔记。我的目标很明确做一个能用、能迭代、能交给别人的AI应用而不是一个只会回答你好的聊天Demo。跑完之后我把这套链路拆成了五个模块——模型接入、上下文管理、Agent编排、评估反馈、协作规范——每一步都有踩坑记录。如果你也是被教程淹没、想亲手从零搭一个AI工程的人这篇文章可以直接照着抄作业。1. 内容整体设计与思路拆解1.1 为什么from scratch不是重复造轮子在开始动工前我先给自己泼了一盆冷水。现在外面现成的AI应用框架已经多到用不完有可视化拖拽平台也有LangChain这类编排库甚至很多低代码平台你只需要连几个节点就能跑出一个聊天机器人。那还有必要搞所谓的from scratch吗我的回答是有必要但不是为了造轮子而是为了建立手感。我见过太多人拖出一个AI应用跑通了就发群里炫耀一旦要接真实业务数据、要加权限、要改提示词立刻抓瞎。原因很简单工具掩盖了细节而细节才是工程化的根基。所以这个项目我从最底层开始做三件事——直接调用模型接口、自己写prompt模板、自己实现工具调用循环。框架能替你省掉的时间很多年后也能替你省但理解力省不下来。把这三件事弄明白之后你再回头看那些框架会发现它们不过是把你走过的路封装好了而已。1.2 AI工程与传统工程的本质差异做这个项目之前我一直带着传统软件工程的经验入场结果第一个礼拜就被上了一课。传统工程是确定性系统接口的输入输出是约定死的单元测试一跑对就是对错就是错。AI工程恰恰相反它是概率性系统——同一个prompt同一个模型同一批参数换一次生成就可能给你来点小差异。这个差异直接改变了工程质量的定义。以前说一个功能做完了指的是代码通过测试现在说AI功能做完了只能指在一组评测集上达到了可接受的准确率。更麻烦的是prompt不再是一句文案它本质上是逻辑的一部分。请严格基于文档回答和请只根据上下文回答不要添加任何额外信息看着差不多实际效果可能差出五个百分点。用个生活化类比传统工程像修铁路轨道铺到哪火车就必须走哪AI工程像驯马方向大体能控但每一脚踩在哪都有随机性。所以做AI工程心态要先换不要追求这一条回答绝对正确而是追求一百条回答里有九十五条合格。1.3 从零开始的四阶段路线在实际动手前我把项目拆成了四个阶段每个阶段都有明确的出口标准避免被新出的模型或框架带走注意力。第一阶段是跑通最小Demo不做Agent不做复杂的记忆管理只实现用户提问接模型API返回答案核心目标是验证链路畅通。第二阶段建立评估集从真实场景收集几十条问题人工写好参考回答哪怕简陋也要先有一个裁判。第三阶段引入工具与Agent给模型配上检索、查日程等工具允许它在限定范围内自主决策。第四阶段做产品化优化加日志、监控、成本控制再考虑多Agent协作和团队协作流程。这条路线看起来不酷但它稳。我见过不少人一上来就奔着多智能体协作去连单Agent都还不稳定结果项目死在一个接一个的失控循环里。从Demo到评估再到Agent化每走一步都有据可依这个基础打好了后面跑得反而快。2. 核心技术与工具选型解析2.1 模型层云端API与本地模型怎么选第一个绕不开的选型问题是用哪一类模型。我当时同时试了三条路线云端商用API、国内云服务商的模型接口、本地开源模型。你项目里未必需要全上但至少在选型阶段要有判断依据。维度云端商用API本地开源模型响应速度受网络影响首字通常1~3秒取决于GPU7B量化在消费级显卡上约20~50 token/s显性成本按token计费用量大时线性上涨一次性硬件投入之后电费摊薄数据安全数据离开内网需脱敏与协议审查数据不出本机敏感场景更稳维护负担几乎零运维要部署、更新、异常处理与监控能力上限当前通用能力普遍更强受参数量和微调水平限制我自己的实践是分环境用的开发阶段用本地的小参数模型7B到8B级别跑通链路因为反复调试、试错成本低等到验证方案可行、需要对外交付时再切到商用API去跑精度验证。切换的时候要特别注意temperature和prompt风格。每个模型对同样的prompt反应完全不同切模型后一定要重跑一遍评估集不能看两个例子没问题就觉得万事大吉。另外要结合场景看数据要求。比如处理客户订单、员工信息这类敏感数据我基本不会让数据出内网但如果是给公开商品做文案生成直接用商用API反而省事。2.2 检索与记忆向量库与上下文管理让模型直接回答上限很快就到了。它只知道训练时的知识不知道你团队的内部制度、你们系统的接口文档。所以几乎所有真实AI工程都要接外部知识这就是检索增强生成RAG的用武之地。我在项目里没有一上来就接分布式向量库而是从最朴素的方案开始把文档切成块用模型算成embedding存到一个本地JSON文件里查询时暴力算余弦相似度。几百条文档时这个方案足够快代码也就一百行。规模上来了再把存储切换成FAISS或者pgvector索引逻辑基本不用变。这样做的最大好处是你先把RAG的链路逻辑搞明白了后面换存储只是换一个接口而不会把整个项目推倒重来。有一个参数值得反复调切片大小chunk_size。我常用的值是512到1024个token重叠100到200个token。切太大语义被稀释检索出来的片段不够精确切太小单块信息不完整模型拿着碎片拼不出完整答案。重叠部分是为了保证衔接处的信息不丢失。另外检索结果不要一股脑全塞给模型top_k设成3到5就够了多召回的往往是噪音。多轮对话后历史消息会越积越多。我的办法是维护一个滑动窗口最近5轮消息全量保留更早的用摘要压缩。或者干脆只保留用户核心意图和上一轮回复。否则上下文窗口很快就会被旧内容塞满模型要么报超出限制要么直接忘掉了你开头的指令。2.3 编排层从链式调用到Agent编排层是最容易被框架掩盖的部分也是AI工程真正体现工程的地方。很多人一听到Agent就兴奋其实先别急着让模型自主决策。一个更务实的顺序是先用确定性流程把场景串起来解决不了再交给Agent。什么是确定性流程比如用户提问先检索文档再把检索结果和问题一起交给模型总结这就是一条链。链式调用适合那种步骤固定、不需要跳转的场景优点是稳定、可预测、好调试。真正需要Agent的场景是步骤不固定、需要根据中间结果动态决策。比如用户说帮我查一下同事张三的时间并预约会议室模型得先查张三的日程日历发现某段时间可用再去调会议室API中间任何一步结果变了下一步也可能变。这种动态性才值得上Agent。但Agent不是让模型自由发挥而是要给它一个约束装置这也就是现在工程圈常说的harness engineering。具体来说要明确它能调用哪些工具、每个工具的参数格式是什么、最多执行几步、遇到错误怎么回退、超时怎么办。我见过一个Agent因为没有设置最大步数同一个工具调用反复执行了二十几次费用哗啦啦地流。把Agent关进围栏里听起来不酷但比模型本身的聪明程度更能决定项目死活。2.4 Prompt工程的核心套路Prompt是AI工程里最容易被低估的一环。我早先以为prompt就是把需求写得清楚一点后来才意识到它和代码一样需要设计、测试、版本管理。我在项目里沉淀了一套自己的模板套路不一定适合所有场景但可复用性很高。系统提示部分写清四件事角色你是一个企业内部知识库助手、任务根据检索内容回答员工关于制度的问题、约束不要编造引用来源无法回答时明确说出、输出格式JSON或markdown。之后再放两到三个few-shot示例示例要贴近真实问题让模型看到输入长什么样、输出长什么样。如果任务需要复杂推理我会在prompt里明确要求先列出推理步骤再给结论。这看着像一个心理暗示实测下来确实能降低低级错误。输出格式用结构化的JSON schema会更稳能直接用代码校验而不是靠肉眼检查。最近各种咒语式prompt技巧很流行比如在提示词里加一句你是某个领域的专家之类的。我的实测感觉是这类技巧短期内有一点效果但换模型、换任务后经常失效。真正持久有效的永远是任务边界清晰、约束明确、示例到位。3. 实操从零搭一个可运行的Agent工程3.1 立项与需求拆解文档问答助手纸上谈兵聊了一大堆现在进入实操。这个项目我选的场景是内部文档问答助手给团队用来查制度、问流程、约会议室。选它是因为它同时覆盖了四项关键能力文档检索RAG、工具调用查日程、约会议室、多轮对话追问与澄清、权限控制不同角色看到的内容不同。复杂度足够典型又不会大到失控。我把系统拆成了六个模块文档加载与切片、向量检索、模型调用封装、工具注册中心、会话管理、评估脚本。其中评估脚本是我后来才补上的但回头来看它应该是第一个写的模块。实际开发顺序建议按这条路走先把模型封装和对话跑通再插入检索然后是工具最后是评估。每一步都留一个可运行的小版本不要憋大招写几百行再联调。3.2 搭建最小骨架配置与模型封装先写配置。我习惯把所有可变参数放进一个dataclass而不是散落在各个文件里。# config.py from dataclasses import dataclass dataclass class ModelConfig: model_name: str gpt-4o-mini # 切换模型时只改这里 base_url: str # 云端API或本地Ollama/vLLM地址 api_key_env: str OPENAI_API_KEY temperature: float 0.2 # 文档问答场景建议低一点 max_tokens: int 2048 # 别太小JSON输出容易被截断 timeout: float 30.0为什么单独做一个配置层因为AI项目的变数太多了公司突然换模型、某模型API限流、本地部署要改服务地址……这些全是你意料之外的干扰。配置集中了出问题就不用到处翻代码。实测下来这个习惯帮我省了不少排查时间。接着封装模型客户端。下面这个wrapper看起来简单但为后面统一加入日志、重试、降级留了位置。# llm.py import os from openai import OpenAI def get_client(cfg: ModelConfig): return OpenAI( base_urlcfg.base_url, api_keyos.environ.get(cfg.api_key_env), timeoutcfg.timeout, ) def chat(cfg, messages, toolsNone): client get_client(cfg) resp client.chat.completions.create( modelcfg.model_name, messagesmessages, temperaturecfg.temperature, max_tokenscfg.max_tokens, toolstools, tool_choiceauto if tools else None, ) return resp.choices[0].message如果你不想在项目初期就引入OpenAI SDK也可以用requests直接调兼容接口SDK的好处是自动处理了流式、重试、类型解析。本地模型方面Ollama起了兼容服务之后把base_url改成http://localhost:11434/v1就能无缝对接这套封装两边通用。3.3 Agent化工具注册与执行循环骨架起来之后下一步让模型可以调用工具。工具的设计在AI工程里很讲究我采用注册中心模式每个工具就是一个函数加一个装饰器把自己的名字、描述、参数schema登记到一个全局字典里。# tools.py import json import datetime TOOL_REGISTRY {} def register_tool(name: str, description: str, parameters: dict): def decorator(func): TOOL_REGISTRY[name] { schema: { type: function, function: { name: name, description: description, parameters: parameters, } }, func: func, } return func return decorator register_tool( search_docs, 在内部文档库中检索与问题相关的片段返回命中的文档列表, { type: object, properties: { query: {type: string, description: 检索关键词}, top_k: {type: integer, description: 返回条数默认3} }, required: [query] } ) def search_docs(query: str, top_k: int 3): # 内部调用向量检索模块 return vector_search(query, top_ktop_k)有了工具接下来是Agent的核心循环。每一轮循环里把当前对话历史和工具列表一起发给模型模型如果决定调用工具会返回tool_calls我们执行工具把结果以roletool的消息回传再发给模型直到模型返回最终答案。# agent.py import json MAX_STEPS 6 def run_agent(cfg, user_query, historyNone): messages history or [] messages.append({role: user, content: user_query}) tools [info[schema] for info in TOOL_REGISTRY.values()] for step in range(MAX_STEPS): msg chat(cfg, messages, toolstools) if not getattr(msg, tool_calls, None): return msg.content # 把带工具调用的助手消息追加回历史 messages.append({ role: assistant, content: msg.content or , tool_calls: [ {id: c.id, type: function, function: c.function} for c in msg.tool_calls ] }) for call in msg.tool_calls: tool TOOL_REGISTRY.get(call.function.name) if not tool: result {error: unknown tool} else: try: args json.loads(call.function.arguments) result tool[func](**args) except Exception as e: result {error: str(e)} # 重要工具返回内容要做截断避免撑爆上下文 messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse)[:2000] }) return 已达到最大执行步数请简化问题或补充必要信息。这段代码有几个地方特别值得注意都是我调试时踩过坑后加上的。第一是MAX_STEPS没有它你会看到模型在同一个工具上反复横跳第二是工具返回结果截断到2000字符不然一次检索塞回一万个字符上下文立刻告急第三是每个工具调用都用try-except包住工具出错不应该让整个Agent崩溃应该把error信息还给模型让它换个方式再试。这就是前面说的harness engineering——不是限制Agent的能力而是保证它犯错之后还能回到正轨。3.4 评估闭环让质量可测量代码写完只是第一步更难的是让模型输出保持稳定。我在项目里用三样东西搭了一个极简评估闭环一个评测集、一个评委函数、一个回归脚本。评测集长这样每个case包含用户问题和参考答案里的关键点# evals.py EVAL_CASES [ { query: 今年的年假政策是什么, required_keywords: [15天, 必须提前一周申请], forbidden_keywords: [考试, 加班], }, { query: 找一下张三明天上午的空闲时间并约会议室, required_tools: [search_calendar, book_meeting_room], required_keywords: [会议室A], }, ]评委我选择用大模型打分因为纯靠人去看几十条回答实在太慢了。简单做法是让一个更强的模型当裁判给它用户问题、模型回答和参考答案让它分几个维度打分并返回JSONdef judge(cfg, query, answer, case): prompt f 你是评测员。请根据参考标准评判回答质量。 问题{query} 回答{answer} 要求 1. 是否包含这些关键信息{case[required_keywords]} 2. 是否出现不应该有内容{case[forbidden_keywords]} 仅返回JSON{{score: 0-1, reason: 简短说明}} result chat(cfg, [{role: user, content: prompt}]) data json.loads(result.content) return data然后用一个脚本把整个评测集跑一遍输出通过率。每次我改prompt、换模型、调工具都重跑一遍这个脚本。你会明显感觉到有了测得准才能改得稳——没有评估集时我改prompt全凭感觉有了评估集之后每次改动都有了客观反馈。评测集的case也要不断补充凡是线上用户问出过坏答案的问题都值得变成一条新的eval case。4. 常见问题与排查技巧实录4.1 五大失败模式速查表跑了几个月这个项目我整理了一份对自己最有用的速查表遇到问题先对照一下省得从头瞎猜。失败模式典型现象排查方向我的解法幻觉回答言之凿凿但事实错误检索内容是否覆盖、prompt是否强制引用要求引用来源检索不到时明确拒绝回答temperature降到0.2以下输出格式不稳定JSON解析失败、带多余markdownmax_tokens、schema约束、temperature调大max_tokens用JSON schema必要时用strict模式上下文溢出报token超限或模型忽略早期指令历史消息和工具结果累积太多滑动窗口旧消息摘要压缩工具结果截断工具死循环同一工具被反复调用缺少终止和错误重试机制MAX_STEPS上限异常结果返回给模型工具内部加超时响应太慢用户等5秒以上没反馈链路串行、检索慢、大上下文检索并行化优先流式输出简单任务交给小模型这张表不是理论推测几乎每一条都是我当时真实撞上的问题。4.2 定位问题的标准路径很多人遇到模型回答不对第一反应是改改prompt再试一次我一开始也这样结果越试越乱。后来我总结出一个标准路径先记录再定位最后修复。第一步是保证所有请求都有日志。每次调用模型前把系统提示、用户问题、检索结果、工具输出、模型回答全部落到结构化日志里。没有日志排查一个问题等于在一团迷雾里找一根针。第二步是构造最小复现把线上出问题的query单独拿出来不加额外的上下文看能不能稳定复现。如果复现不出来那可能是历史消息里的某条内容影响了模型就把历史消息逐条去掉做二分。第三步是针对问题改一处不要同时改prompt、temperature和工具逻辑否则你根本不知道哪个改动起作用了。这套流程听起来朴素但效率远高于瞎调prompt。我上过最大的当就是一次同时改了三个地方结果问题消失了但不知道为什么消失下次遇到同样的bug还是得重新查一遍。4.3 我踩过的几个真实坑第一个坑是JSON输出被截断。早期我把max_tokens设成1024模型一回答复杂问题JSON就断在半路解析怎么改都报错。后来把所有需要结构化输出的调用统一调大max_tokens同时让模型先输出好的我来逐步分析再输出JSON实测截断率大幅下降。第二个坑是temperature设太高。为了追求回答有创意我把问答Agent的温度调到0.8结果它写制度问答也发挥起了创作直接编出公司根本不存在的福利。给知识库问答这类对准确性要求高的场景温度建议0.2以下。想让它有温度去调prompt的语气描述而不是去调随机性。第三个坑是工具参数的schema写太复杂。一个会议室查询工具我一开始设计了七八个字段模型频繁调错参数干脆多轮重试。后来把参数精简到日期人数两个必填项错误率立刻降下来。工具设计的第一原则是让模型好调用而不是让函数看起来全面。第四个坑是Agent历史消息越滚越大。每轮工具返回了什么东西、模型说了什么全都囤在messages里跑十几轮后几个请求就把上下文窗口顶爆。现在的做法是会话超过N轮就做一次摘要关键事实压缩只保留压缩后的记忆效果稳定很多。5. 从Demo到可维护产品多Agent协作与团队规范5.1 多Agent协作的真相项目从Demo往产品走就会有人提要不要上多Agent。我的看法是先给这句话泼冷水多Agent不是性能提升器而是复杂度放大器。两个模型互相传话错误传播概率会叠加成本接近翻倍调试难度指数上升。设计多Agent协作时脑子里想的不该是让AI们开会而是如何把复杂任务拆成几个可验收的子任务每个子任务用一个模型去完成。我实践中比较稳的模式有两种。一种是主管-执行模式一个规划Agent把任务拆成步骤再把每个步骤交给一个执行Agent最后汇总结果。另一种是流水线模式上游Agent的输出是下游Agent的输入适合处理步骤固定的流程比如先理解需求、再生成草稿、最后做合规检查。相对不推荐的是辩论模式几个Agent互相讨论看着热闹实际上输出的提升非常不稳定成本却高得吓人。还有一点很重要Agent之间通信要用结构化数据而不是自然语言长文本。比如契约定义成JSON{action: search, args: {...}, deadline: ...}。模型读结构化的契约比读一大段人话可靠得多排查问题也方便——你只需要看它是否按契约执行而不是去读懂它到底在想什么。5.2 反馈闭环与Prompt版本管理产品化之后光靠开发阶段那几十条评测集是不够的线上真实用户的问题分布永远超出你的想象。所以我在应用里加了简单反馈按钮回答底部放有帮助/没帮助。没帮助的回答连同当时的输入和模型输出自动进一个bad-case池。每周我抽一批bad-case挑出高频问题改prompt或者补工具、补文档然后把它们新增为评测集case。Prompt本身也要像代码一样版本管理。我见过很多团队用prompt_v2_final_最终版3.docx这样的命名这是灾难。正确做法是把所有prompt做成模板文件放在git里每次改动走diff和评审。我自己习惯维护一个版本记录表版本变更摘要评估集通过率上线后反馈v1初始版本62%负面反馈集中在答非所问v2重写任务边界与约束74%答非所问减少格式偶尔乱v3增加JSON schema约束86%格式稳定仍有幻觉v4增加无法回答时明确拒绝89%幻觉率明显下降每次模型升级也一样新模型不是直接换上去就行的。我现在的流程是先拿评测集跑一遍把通过率和bad-case对比看一遍再决定要不要切换。没有评估集就去换模型基本等于赌运气。5.3 团队协作里的隐性资产如果你的项目不止一个人维护有几条潜规则越早定越好。第一条评估集是团队的公共资产每个成员都要往里加bad case不准删别人的case改动要说明理由。第二条prompt评审要关注任务边界与评估覆盖不是措辞润色。团队成员很容易为了一个prompt里的形容词争论不休而真正应该讨论的是这个任务在什么输入下允许什么、拒绝什么。第三条模型接入要统一走配置和封装任何人不能绕过封装直接在自己业务里调底层模型接口否则出了事没法统一处置。还有一条容易被忽略AI工程的线上监控指标和传统系统不一样。除了CPU、内存、延迟还要看答案得分工具调用成功率单次会话成本。我一般在日志里把这几项一起输出定时生成统计。很多模型问题在用户反馈之前就已经能从数据里看到苗头。把整个项目从零跑到这个阶段我最深的体会是AI工程真正的门槛不在懂AI而在懂工程。模型更新换代很快今天的最强模型两个月后就可能被超越但评估驱动、harness设计、日志先行、小步快跑这些工程方法不会过时它们才是项目能不能交付的关键。最后分享一个我觉得对新人最有用的小技巧不要一开始就给自己定我要做一个超级Agent的目标。先做一个只做一件事、而且做得足够稳的小工具——比如一个只看文档、只答制度、其余问题礼貌拒绝的助手。你会发现它在业务里的价值可能远高于一堆什么都能聊但什么都不靠谱的Agent。先把一件小事做扎实AI工程的大门才算真正打开。