ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零构建AI Agent:掌握对话引擎、工具调用与工程化落地的完整路线

从零构建AI Agent:掌握对话引擎、工具调用与工程化落地的完整路线 很多人跑来问我Agent 到底该怎么学为什么看了一堆教程还是做不出能用的东西。说实话我当年也经历过这个阶段——架构图看得很明白概念都能背等到真要自己写一个能自动查资料、整理结果、再按时通知人的 Agent 时脑子一下就空了。后来我慢慢想清楚一件事问题不是“学得不够”而是学习顺序错了。Agent 本质上不是一个模型也不是一组 API 调用而是一套把大模型组织起来完成目标的系统。这篇文章不打算推某一家框架也不铺开讲所有概念而是把从零构建 Agent 必经的几个阶段按顺序拆开每一段配一个可验收的成果。你跟着这条路径走每一步都能真实落地。1. 为什么大多数人学 Agent 会半途而废先认清它是一套系统先说一个我观察到的普遍现象。很多开发者最初接触 Agent 时习惯性把它当成“大模型的高级用法”于是先去学某个流行框架照着模板跑通一个 demo再改几个参数然后就以为学会了。但真到了自己的业务场景面对多步任务、多工具、多轮对话时框架里的默认行为根本撑不住最后只能放弃。问题出在认知上Agent 不是一个模型而是一个系统。大模型只是这个系统里的“判断器官”负责理解、推理、生成Agent 还得有任务规划的手、调用工具的脚、记录信息的记忆以及从外部反馈中修正方向的能力。你如果一上来就研究某个框架怎么用等于直接学一套成品器官的连接方式却不知道每个器官为什么长这样、它要解决什么问题。所以我给学习顺序定了几条原则你可以拿来自查依赖关系优先先学那些后面所有环节都会依赖的地基。模型调用、提示词组织、输出解析这些不掌握后面做循环、做工具、做记忆全都悬空。使用频率优先Agent 日常运行中最高频的动作是什么调用模型、执行函数、记录结果。高频动作相关的知识往前放低频的调优、部署往后放。每个阶段可验收每学完一段必须能交付一个能运行、能测试的小东西。不是“我懂了”而是“它跑通了”。没有验收的学习基本等于没学。基于这三条原则完整的路线是这样的阶段核心问题关键能力可验收成果对话引擎怎么跟模型高质量对话Prompt 组织、参数控制、输出解析一个能稳定返回 JSON 的对话脚本执行循环怎么让模型多步思考ReAct / Plan-Execute 模式一个能顺序执行两步任务的极简 Agent工具调用怎么让模型使用外部能力Function Calling、工具定义一个能调 2~3 个工具的 Agent记忆系统怎么让 Agent 记住上下文短期总结、长期存储、检索一个能记住上次对话信息的 Agent工程化怎么让它稳定、可评估、省钱评测集、日志追踪、成本控制一个带完整 trace 和统计信息的可演示闭环这张表在后面每一章都会反复用到。你不需要一口气全学会但需要清楚自己正处在哪一格。2. 第一步不是学框架而是练好“对话引擎的手感”框架学多了最大的副作用是你离模型本身越来越远。Agent 的每一个动作本质上都是一次“带明确目标的对话”。你如果连一次高质量对话都组织不好后面所有复杂环节都是在沙地上建塔。2.1 先把一次调用的基本功打牢我建议你第一阶段只用原生接口不要碰任何框架。先做好这几件事搞清楚 system prompt 和 user prompt 的分工。system prompt 负责定义角色、规则、输出格式是稳定行为的锚user prompt 是具体的任务输入。很多人把规则和任务混在一个 prompt 里模型就容易分不清优先级。理解温度、最大 token 长度、停止符号这些参数的实际影响。温度高适合头脑风暴低适合写代码、做数据抽取max_tokens 不只是“能输出多长”它决定了模型会不会在关键结论还没出来就被截断。学会“格式约束”。不要让模型自由发挥而是明确要求它输出 JSON并给出示例。格式一旦可控后面所有解析、循环、工具调用才有基础。这里给一个最小可运行的 Python 示例用的占位 API 地址你可以替换成任何主流大模型平台的接入点import requests import json API_URL https://api.llm.example.com/v1/chat/completions API_KEY your_api_key def chat(system_prompt, user_prompt, temperature0.2): resp requests.post( API_URL, headers{Authorization: fBearer {API_KEY}}, json{ model: your_model_name, messages: [ {role: system, content: system_prompt}, {role: user, content: user_prompt} ], temperature: temperature }, timeout30 ) resp.raise_for_status() return resp.json()[choices][0][message][content] system 你是一个信息抽取助手。只输出 JSON不要输出任何解释。格式: {\names\: [\...\], \dates\: [\...\]}。 user 帮我提取这句话里的人名和日期张三在6月15日参加了发布会而李四则在7月2日提交了报告。 print(chat(system, user))这段代码的核心不在于调用本身而在于你通过 system prompt 把输出约束成了固定结构。你反复调这种方式直到不写代码也能预判模型会怎么回答手感就出来了。2.2 新手最常见的三个对话误区第一个误区是不给模型“决策空间”的边界。比如你让模型判断一段文本的情感却不告诉它类别集合也不给示例结果它发明出各种奇奇怪怪的标签。正确做法是把标签枚举出来每个给一句话定义。第二个误区是忽视模型的输出一致性。模型不是数据库同样的输入在不同温度下可能给出不同回答。做 Agent 的时候凡是用作逻辑判断的输出温度尽量压低凡是用于生成文案的输出可以适度升高。这个分寸只能靠大量实验来找。第三个误区是忽略幻觉。模型会根据你的 prompt 风格“脑补”内容尤其当你问它某个具体数据时它可能编造一个看似合理的答案。这就是为什么后面一定要接工具、接检索、接验证——模型的记忆不可靠至少不能把关键事实放在它脑子里。这一阶段的验收标准很简单你能写一段脚本让它完成一次“抽取 格式化 本地保存”的任务全程不报错。如果这个都做不到先别往下走。3. 把“一句问答”升级成“闭环思考”任务拆解与执行循环对话引擎解决的是“单次反应”但 Agent 的价值在于“多步执行”。它不是一个问答机器人而是一个能自己规划、自己行动、从环境反馈中修正方向的主体。这一步是整个学习路线里最容易卡住的地方因为要转变思维方式。3.1 先理解 ReAct 循环观察、思考、行动、再观察ReAct 模式的思路非常朴素让模型先看当前状态Observation想清楚下一步该干嘛Thought然后执行动作Action拿到新状态之后继续循环。这不是什么高深算法本质就是把“人在做复杂任务时的思考方式”显式编码成循环。我见过很多初学者反复读这个概念却写不出代码原因是他们把它想得太抽象了。实际上一个最小的 ReAct 循环就是这么简单def run_agent(task): context f任务: {task} while True: response chat( system_prompt你是一个任务执行代理。每一步输出思考过程Thought和要执行的动作Action。, user_promptcontext, temperature0 ) action parse_action(response) if action[type] finish: return action[result] observation execute_step(action) context f\n行动结果: {observation}这段代码里最重要的不是函数本身而是context 这一行。它把每轮的行动结果拼接回上下文供模型下一轮决策。你可能会问为什么不直接让模型一次把步骤都想好因为真实环境里动作执行结果往往和预期不一致——比如你让模型去查某个接口接口返回的格式可能跟它预想的不同它必须根据真实结果调整下一步。3.2 ReAct 和 Plan-Execute 怎么选看任务的确定性很多教程只讲 ReAct但实际工程里 Plan-Execute 更常见。它的思路是先让模型把大目标拆成步骤清单然后逐项执行执行完再统一检查。我把两者的选择逻辑总结成了一张判断表场景推荐模式原因开放型任务目标模糊需要试错ReAct每一步都在根据真实反馈调整流程固定步骤明确像标准作业程序Plan-Execute更省 token更稳定不需要反复推理长链路任务中间结果依赖外部系统Plan-Execute 阶段检查方便监控每个阶段的执行质量任务中途大概率出现意外分支ReAct灵活性高不容易死板地卡在预规划里一个管理建议是能用 Plan-Execute 解决的尽量不要打成开放式 ReAct。不是因为 ReAct 不好而是它的推理开销更高、更不稳定。你要把模型的“智能”花在真正需要判断的地方而不是每一步都让它重新思考。这一阶段的验收标准你能用原生代码写出一个极简 Agent让它顺序执行两个有依赖关系的步骤。比如先生成一份关键词清单再用清单去一个模拟数据源里筛选结果。跑通之后你对 Agent 的认知就和只看教程的人完全不一样了。4. 给 Agent 装上手脚函数调用与工具层的工程细节有了循环Agent 还只是“空想家”。它得真的能操作外部世界去查数据库、调接口、发通知、写文件。这就是工具调用的环节。我把它放在循环之后讲是因为工具必须嵌套在“观察—行动”循环里才有意义反过来先学工具再学循环你会做出一堆互不关联的独立函数拼不成一个整体。4.1 从 tools 参数开始理解“模型只会填空代码负责执行”现代大模型平台普遍支持一种方式你在请求里传入一组“工具描述”模型根据用户请求返回“该调用哪个工具、参数填什么”而不是直接替你执行工具。粗看好像多此一举但这是安全边界上很关键的设计——模型只负责决定调用意图真正执行代码的是你校验、权限、异常处理都在你自己手里。一个工具描述长这样伪结构示例{ type: function, function: { name: search_documents, description: 在内部知识库中检索文档返回匹配的标题和摘要, parameters: { type: object, properties: { query: {type: string, description: 检索关键词}, limit: {type: integer, description: 最多返回条数, default: 5} }, required: [query] } } }模型读完这个描述会返回类似“我要调用 search_documents参数 query退款流程, limit3”的结构。你的代码拿到这个指令后执行真实的检索函数再把结果塞回对话上下文让模型基于结果继续组织回答。实现调度逻辑时记住一个原则模型的话只是“建议”代码才是“决定”。返回的工具调用信息必须经过白名单校验、参数类型校验、执行超时控制之后才能放行。我之前带过一位朋友写 Agent省掉了参数校验结果模型在一次任务里把 limit 参数传成了负数检索模块直接崩了。后来养成了习惯所有外部参数统一走一套 validate 函数宁可多写几行也不让脏数据流进生产环境。4.2 工具描述的质量直接决定 Agent 的智力上限很多人以为工具描述写得越详细越好实际恰恰相反。描述不是给模型“学习”的而是给模型“选择”的。它需要的是“什么时候用这个工具”“这个工具和相邻工具的区别”而不是长篇累牍的底层实现逻辑。维度Bad 描述Good 描述触发场景“执行搜索”“当用户询问产品价格或库存时使用本地数据库优先”边界“传入查询词”“query 必须是经过分词的关键词不要含完整句子”冲突区分“查订单信息”“查单个订单请用此工具查订单列表用 list_orders”工具数量变多之后模型选错工具的几率会上升。这不是模型变笨了而是你的描述没有帮它做区分。这时候要回头逐条审视 description而不是急着换模型。我在实际项目里的经验是工具超过 10 个时一定要有层级结构先让模型选择“工具类别”再进类别内选具体函数否则选择准确率会明显下滑。这个阶段的验收标准很简单实现 2~3 个工具比如搜索、计算、发通知让 Agent 能根据用户请求正确选择并调用它们并且对非法参数能优雅地返回错误而不是崩掉整个任务。达到这个程度你就可以说自己的 Agent 有手脚了。5. 让 Agent 记住事情短期记忆、长期记忆与检索增强工具让 Agent 能干活但没有记忆的 Agent每轮任务都像失忆一样从零开始。你可以把上下文窗口想象成一张工作台桌上能摆的东西有限做完一步就得收盘子腾地方。想让 Agent 在长任务里保持连贯必须设计记忆系统。5.1 三层记忆模型窗口、摘要、档案柜我会把记忆拆成三层来理解短期记忆上下文窗口内保留的信息。最常见的问题是窗口撑爆。解决方案不是无限扩窗口而是控制塞进去的内容——只保留与当前目标相关的信息。中间层记忆把前面对话压缩成摘要。每次上下文快满时调用模型生成一份“到目前为止做了什么、剩余目标是什么”的总结替代原始对话继续推进。这种“总结蒸馏”的思路能显著降低 token 消耗同时保留关键决策链。长期记忆超出单次任务的信息比如用户偏好、历史结论、项目背景。这层必须外置存储通常是向量数据库加关键词索引的组合。大多数人第一次做 Agent只考虑短期记忆结果任务稍微长一点就乱套。我的建议至少把中间层摘要做出来再谈长期记忆。这就像你先学会整理桌面再考虑建档案室。5.2 一个最小可用的检索增强流程长期记忆最常用的实现方式就是检索增强生成RAG。名字很吓人原理却简单把文档切成小段每段转成向量embedding查询时把问题也转成向量找最相似的小段拼到 prompt 里让模型参考。它的意义在于模型不需要“背下”文档只需要在回答时能“查出来”。一个最小流程如下def retrieve_documents(query, docs, top_k3): # docs 是已经切分好并向量化的文本片段列表 q_vec embed(query) scored [(cosine_similarity(q_vec, doc[vector]), doc[text]) for doc in docs] scored.sort(reverseTrue, keylambda x: x[0]) return [text for _, text in scored[:top_k]] def answer_with_context(question, docs): context \n.join(retrieve_documents(question, docs)) return chat( system_prompt你是一个客服助手。必须基于提供的资料回答资料不足时直接说不知道。, user_promptf资料:\n{context}\n\n问题: {question} )需要留意的不是 embedding 本身而是切分策略。切得太细语义被拆碎切得太粗检索噪声大。比较稳妥的方式是按语义段落切配合重叠策略让相邻片段保留一部分边界信息。这块没有绝对最优我通常的做法是先按常见长度切再根据检索命中效果反向调整。这个阶段的验收标准给 Agent 输入一批资料让它回答问题时主动引用资料里的信息然后隔几天再来问同一个问题它还能用长期存储回答出正确内容。能稳定做到这一点你手里的 Agent 就不一样了。6. 最后一块拼图评测、可观测性与成本控制决定能不能上线很多人的 Agent 停在“能跑通”的阶段但这离“能交付”还很远。我自己第一次把 Agent 部署到真实环境时最大的感受是没有评测和日志你根本不知道它到底行不行出了问题也不知道是模型判断错了还是工具执行错了。所以最后一个阶段必须补上工程化三件套。6.1 先建一小组评测集别信“感觉还行”评测集不需要特别大但需要覆盖典型场景和边界场景。先把高频正常任务放进去再加几个故意刁难的用例没资料的提问、参数缺失的调用、模糊需求。给每一条用例定好评分标准完成、未完成、完成但返工。运行时统计任务完成率和平均返工次数这两个指标比任何逻辑代码都更能说明 Agent 的健康度。评测维度具体指标我们的经验基线任务完成率成功完成的用例 / 总用例90% 以上才能进真实场景工具调用正确率正确工具 正确参数占比95% 以上返工次数一次任务平均触发几次修正越低越好超过 2 次要查链路延迟单任务从开始到结束的时间10 秒以内体验可接受单次成本token 消耗折算价格必须有预算上限6.2 把每一步都记下来trace 才是调试 Agent 的命根子Agent 的特性是“不可完全复现”同样的输入模型答案可能不同。没有 trace你无法定位失败步骤。我会建议至少记录以下几类信息当前任务的完整输入、模型每一轮输出的 Thought、选择的工具与参数、工具返回结果、是否发生异常、每轮 token 消耗。trace [] trace.append({step: 1, type: thought, content: response}) trace.append({step: 1, type: action, tool: search_documents, params: {query: 退款流程}}) trace.append({step: 1, type: observation, content: 返回 5 条文档})这些 trace 要落盘成可查询的结构化数据。调试时打开一条失败的 trace你能一眼看出是模型在思考环节跑偏了还是工具结果不符合预期还是上下文拼接出了问题。没有这套日志你只能反复“调到它碰巧能跑”。6.3 成本控制不是省 API 的钱而是优化“浪费的思考”关于成本我的经验是与其抠单个请求的价格不如减少“不必要的模型决策”。能固定执行的流程用代码写死不要每次都让模型思考简单分类任务用轻量小模型先分流只有在真正需要复杂推理时才调大模型对于反复出现的查询加一层缓存命中直接返回历史结果。这个阶段的验收标准选一个真实任务跑通并保存完整 trace统计出单次任务的成功率、延迟、成本和平均返工次数。如果你能把这些数字填进一张表里你的 Agent 已经从“玩具”变成“系统”了。最后再分享一个小技巧。我自己的学习节奏是每个阶段花一周到十天卡住了不硬撑回到上一阶段补课。比如发现工具调用老是出错问题往往不在工具本身而在 prompt 没有把触发条件说清楚。另外不要急于用框架封装所有步骤先裸写一遍理解了每行代码要干嘛再上框架才不会迷茫。真正拉开人和人差距的不是谁学会了更多工具而是谁能把一个系统拆成可验证的模块再一块块拼回来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进