1. 为什么“学习顺序”比“学什么”更致命
我见过太多人一头扎进 Agent 开发,上来就装框架、拉仓库、跑 Demo,结果两周后卡在一个400报错上动弹不得,连问题出在哪一层都说不清。Agent 这个领域最大的坑不是知识点难,而是知识点的依赖关系被严重低估。你让一个还没搞懂模型调用的人去调工具链,就像让一个不会加减法的人去解方程——不是学不会,是根本不知道自己在学什么。
先把结论摆出来:从零构建 Agent,正确的学习顺序应该是模型调用 → 工具调用 → 上下文管理 → 会话恢复 → 编排与架构 → 安全与工程化。这个顺序不是拍脑袋定的,它对应的是 Agent 运行时最底层的依赖链。每一层都建立在前一层的能力之上,跳过任何一层,后面都会以“莫名其妙的报错”形式还债。
为什么模型调用必须排第一?因为 Agent 的本质是一个“会做决策的循环”,而循环里每一次决策都依赖模型输出。你连一次干净的模型调用都跑不通,谈何循环?为什么工具调用排第二?因为 Agent 区别于普通聊天机器人的核心,就是它能“动手”——调用外部能力改变世界。为什么上下文管理排第三?因为一旦工具开始返回结果,上下文就会爆炸式增长,不会压缩和裁剪,你的 Agent 跑三轮就撑爆窗口。
这套顺序背后有一个朴素的判断标准:每一层都是下一层的必要条件,且每一层都能独立验证。你学完模型调用,能写一个单轮问答脚本;学完工具调用,能写一个“查天气再回答”的两步流程;学完上下文管理,能让这个流程跑二十轮不崩。每一步都有可交付的成果,而不是悬在空中的概念。
我特别想强调一点:现在网上充斥着“三天从零到 Agent 上线”的内容,这些内容最大的问题是把编排框架当成了起点。LangChain、Dify、CrewAI 这些框架确实好用,但它们封装掉的恰恰是你最该亲手写一遍的东西。你跳过模型调用直接上框架,遇到agent rpc error (-1): empty sid and service name这种报错时,你连它是框架层的问题还是模型层的问题都判断不了。所以我的建议很直接:前三个阶段,手写,不要用框架。框架是加速器,不是教科书。
下面这张表是我总结的六层学习路径和每层的验证标准,你可以拿它当路线图:
| 阶段 | 核心能力 | 验证标准 | 常见误区 |
|---|---|---|---|
| 模型调用 | 稳定拿到模型输出 | 单轮问答脚本可复现 | 直接上框架 |
| 工具调用 | 让模型触发外部能力 | 两步流程跑通 | 工具描述写太随意 |
| 上下文管理 | 控制 token 不爆炸 | 二十轮对话不崩 | 从不做压缩 |
| 会话恢复 | 状态可持久化 | 重启后继续对话 | 只存消息不存状态 |
| 编排架构 | 多步骤多工具协同 | 复杂任务可拆解 | 过早引入多 Agent |
| 安全工程化 | 可控可观测可兜底 | 有日志有熔断 | 忽略权限边界 |
这张表你先存着,后面每一节我都会展开讲怎么落地。
2. 第一阶段:模型调用,把“一次干净的请求”做到极致
2.1 模型调用到底在调什么
很多人以为模型调用就是发个 HTTP 请求拿个字符串回来,这个理解在单轮场景下没错,但在 Agent 场景下会害死你。Agent 里的模型调用有三个特殊性:第一,它需要结构化输出,因为下游代码要解析模型的决策;第二,它需要可控的停止条件,因为 Agent 是循环,不能无限调;第三,它需要错误可分类,因为不同错误对应不同的重试策略。
先说结构化输出。普通聊天你拿个字符串就行,但 Agent 需要模型告诉你“我要调用哪个工具、参数是什么”。这时候如果你让模型自由发挥,它可能返回一段自然语言,你还得写正则去抠,极其脆弱。正确做法是让模型输出 JSON,并且用 schema 约束它。比如你要模型决定是否调用工具,可以约定输出格式:
{ "action": "tool_call", "tool_name": "get_weather", "arguments": {"city": "北京"}, "reasoning": "用户询问天气" }这个格式看起来简单,但它解决了 Agent 最核心的问题:把模型的“想法”变成代码能执行的“指令”。我实测下来,只要在 prompt 里把这个 schema 写清楚,并给一两个示例,主流模型的遵循率能到 95% 以上。剩下 5% 怎么办?后面讲容错时再说。
再说停止条件。Agent 循环最怕的就是模型陷入“我再想想”的死循环。你必须在调用层就设好边界:最大轮次、最大 token、超时时间。这三个参数不是可选项,是必选项。我一般设最大轮次 10、单次超时 30 秒、总 token 预算按任务复杂度给 8000 到 32000 不等。超过就强制中断并返回当前状态,而不是让它继续烧钱。
最后说错误分类。模型调用会遇到的错误大致分四类:网络类(超时、连接失败)、限流类(429)、模型类(400 参数错误、内容被拒)、服务类(500、503)。这四类的处理策略完全不同:网络类可以立即重试,限流类要退避重试,模型类要修参数不能盲目重试,服务类要切换备用模型。我见过有人把所有错误都写成retry 3 times,结果 400 错误重试三次还是 400,白白浪费时间和额度。
提示:
400错误里有一类特别隐蔽,就是模型名称写错或该模型不支持当前参数。热词里提到的“请切换模型或重试”往往就是这类。遇到 400 先别重试,先把请求体和模型文档对一遍。
2.2 手写一个最小可用的模型调用封装
我不建议你在这个阶段用任何框架,就手写一个函数。这个函数要包含:请求构造、超时控制、错误分类、重试逻辑、响应解析。下面是一个 Python 的骨架,你可以直接参考:
import time import json import requests class ModelClient: def __init__(self, base_url, api_key, model, timeout=30): self.base_url = base_url self.api_key = api_key self.model = model self.timeout = timeout def call(self, messages, max_retries=3): for attempt in range(max_retries): try: resp = requests.post( f"{self.base_url}/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, json={"model": self.model, "messages": messages}, timeout=self.timeout ) if resp.status_code == 200: return resp.json()["choices"][0]["message"]["content"] elif resp.status_code == 429: time.sleep(2 ** attempt) continue elif 400 <= resp.status_code < 500: raise ValueError(f"客户端错误 {resp.status_code}: {resp.text}") else: time.sleep(1) continue except requests.Timeout: if attempt == max_retries - 1: raise time.sleep(1) raise RuntimeError("重试耗尽")这段代码的价值不在于它多完美,而在于它逼你想清楚每个分支。比如 429 用指数退避,4xx 直接抛出不重试,5xx 和超时才重试。你亲手写过一遍,后面用框架时才知道框架帮你做了什么、没帮你做什么。
2.3 这个阶段最容易踩的三个坑
第一个坑是把 API Key 硬编码在代码里。我知道你是练手,但习惯要从第一天养起。用环境变量,用配置文件,别提交到仓库。第二个坑是不记录原始响应。模型返回不符合预期时,你第一反应应该是看原始响应,而不是猜。我习惯在调试期把每次请求和响应都落盘,出问题直接翻日志。第三个坑是忽略 token 计数。你不算 token,就不知道什么时候会超窗口,上下文管理阶段会吃大亏。建议这个阶段就引入一个 token 计数工具,每次调用后打印消耗。
这个阶段的目标很明确:你能稳定地、可观测地、可容错地拿到模型输出。做到了,再往下走。
3. 第二阶段:工具调用,让 Agent 真正“能动手”
3.1 工具调用的本质是“协议设计”
工具调用看起来是技术问题,其实是协议设计问题。你要设计一套模型和代码之间的“合同”:模型说要调什么、传什么参数,代码负责执行并返回结果。这套合同设计得好,Agent 就稳;设计得烂,模型天天给你传错参数。
工具描述是这套合同的核心。我见过太多人把工具描述写成一句话,比如“查询天气”,然后抱怨模型老是传错城市名。问题出在描述太模糊。一个好的工具描述应该包含:功能说明、参数说明、参数格式、边界条件、示例。对比一下:
差的描述:
get_weather: 查询天气好的描述:
get_weather: 查询指定城市的当前天气。 参数: - city (string, 必填): 城市名称,使用中文,如"北京"、"上海"。 - unit (string, 可选): 温度单位,可选"celsius"或"fahrenheit",默认"celsius"。 返回:包含温度和天气状况的 JSON。 示例:get_weather({"city": "北京", "unit": "celsius"})后者看起来啰嗦,但模型遵循率能提升一大截。这不是玄学,是因为模型在生成参数时依赖描述里的信息,你给的信息越全,它猜错的空间越小。
3.2 工具调用的完整流程拆解
一次完整的工具调用包含六个环节:模型决策、参数解析、参数校验、工具执行、结果封装、结果回传。每个环节都有坑。
模型决策环节,你要处理模型“不调用工具直接回答”和“调用不存在的工具”两种情况。前者正常,后者要拦截并给模型反馈。参数解析环节,模型返回的 JSON 可能不合法,要有兜底解析。参数校验环节,类型、必填、范围都要查,别指望模型每次都传对。工具执行环节,要有超时和异常捕获,工具挂了不能让 Agent 挂。结果封装环节,工具返回的原始数据往往很长,要裁剪成模型能消化的形式。结果回传环节,要把工具结果作为一条消息塞回上下文,格式要统一。
我画不出图,但你可以这样理解这个循环:模型输出决策 → 代码执行 → 结果回填 → 模型再决策,直到模型输出最终答案。这个循环就是 Agent 的心跳。
3.3 工具设计的三个原则
原则一:工具要原子化。一个工具只做一件事。不要设计“查询天气并发送邮件”这种复合工具,因为模型无法在中间插入决策。原子化工具让模型有更多组合空间。
原则二:工具要幂等。同一个调用重复执行结果应该一致。查询类天然幂等,写入类要小心。如果工具不幂等,重试就会出问题。
原则三:工具要可观测。每次调用都要记录:谁调的、传了什么、返回什么、耗时多少。出问题时这些日志就是救命稻草。
注意:工具数量不是越多越好。我实测下来,单个 Agent 暴露给模型的工具控制在 5 到 15 个比较合适。超过 20 个,模型选择困难,错误率明显上升。工具多了就分组,或者用路由先筛一轮。
3.4 这个阶段的验证方法
写一个“查天气 → 根据天气建议穿什么”的两步流程。如果模型能正确调用天气工具、拿到结果、再基于结果给出建议,说明你的工具调用链路是通的。这个流程跑通,你就具备了 Agent 最核心的“感知-行动”能力。
4. 第三阶段:上下文管理,决定 Agent 能跑多远
4.1 上下文为什么会爆炸
Agent 和聊天机器人最大的区别是:聊天机器人一轮对话就几条消息,Agent 一个任务可能产生几十条消息。每条工具调用都产生请求和响应,响应里可能还有大段数据。跑几轮下来,上下文轻松破万 token。你不管理,要么超窗口报错,要么成本飙升。
上下文管理要解决三个问题:裁剪、压缩、检索。裁剪是删掉不重要的消息,压缩是把长消息变短,检索是按需加载而不是全量塞入。这三个手段配合使用,才能让 Agent 长期运行。
4.2 裁剪策略:什么该留,什么该扔
裁剪的核心判断是“这条消息对后续决策还有没有用”。我的经验是:系统提示永远留,最近 N 轮对话永远留,工具调用的原始大结果可以扔,但结论要留。比如天气工具返回了一大段 JSON,你只需要留“北京今天晴,25 度”,原始 JSON 可以删。
具体实现上,我一般保留最近 6 到 10 轮完整对话,更早的做摘要。摘要不是简单截断,而是让模型自己总结:“把以下对话压缩成三句话,保留关键决策和结论。”这样压缩后的信息密度更高。
4.3 压缩策略:让模型帮你瘦身
压缩有两种做法:一种是规则压缩,比如超过 500 字的消息截断;一种是模型压缩,让模型总结。规则压缩快但可能丢信息,模型压缩慢但保留好。我一般混合用:工具返回的原始数据用规则压缩,对话历史用模型压缩。
模型压缩的 prompt 很关键。我常用的模板是:
请将以下对话历史压缩为简洁摘要,保留: 1. 用户的核心目标和约束 2. 已确认的关键事实 3. 已执行的操作和结果 4. 待解决的问题 删除:寒暄、重复内容、中间过程的细节。这个模板的好处是它明确了“保留什么、删除什么”,压缩质量比一句“总结一下”高很多。
4.4 检索策略:按需加载
当你的 Agent 需要访问大量知识时,全量塞入上下文不现实。这时候要用检索:把知识存起来,根据当前问题检索相关片段。这就是常说的 RAG。RAG 的关键是检索质量,检索不准,塞进去的都是噪音。
我踩过的坑是:一开始用向量检索,发现对精确匹配不友好。后来改成混合检索——向量加关键词,效果好很多。另外,检索回来的片段要排序和去重,别一股脑塞进去。
4.5 上下文管理的验证标准
写一个能跑二十轮的对话 Agent,中间穿插工具调用。如果二十轮后它还能记住第一轮的关键信息,且 token 消耗在预算内,说明你的上下文管理是合格的。这个测试很残酷,但很有效。
5. 第四阶段:会话恢复,让 Agent 有“记忆”
5.1 会话恢复不只是存消息
很多人以为会话恢复就是把消息列表存数据库,重启后读出来。这个理解太浅。真正的会话恢复要存三类东西:消息历史、Agent 状态、外部资源引用。
消息历史好理解,就是对话记录。Agent 状态指的是当前任务进行到哪一步、有哪些中间变量。比如一个多步骤任务,跑到第三步崩了,恢复后要从第三步继续,而不是从头来。外部资源引用指的是工具调用产生的文件、数据库记录等,恢复后要能重新关联上。
5.2 状态持久化的设计
我一般用一个 session 对象来承载状态,包含:session_id、messages、state、created_at、updated_at。state 是个字典,存任务相关的中间数据。每次 Agent 循环结束,把 session 序列化存起来。恢复时反序列化,继续跑。
存储介质看场景:本地开发用 SQLite 或 JSON 文件,生产环境用 Redis 或数据库。关键是序列化要稳定,别存了读不出来。我建议用 JSON,可读性好,调试方便。
5.3 恢复时的坑
第一个坑是消息格式不一致。你存的时候是一种格式,读出来喂给模型时格式变了,模型就懵了。解决办法是定义统一的消息 schema,存取都走这个 schema。第二个坑是状态过期。任务状态存太久,外部资源可能已经失效。要加过期时间和有效性检查。第三个坑是并发冲突。同一个 session 被两个请求同时恢复,状态会乱。要加锁或者用乐观并发控制。
提示:会话恢复的测试方法是“杀进程重启”。你写完恢复逻辑,直接 kill 掉进程,重启后看能不能接着聊。能,才算过关。
6. 第五阶段:编排与架构,从单 Agent 到多 Agent
6.1 什么时候需要编排
单 Agent 能搞定的事,别上编排。编排是有成本的:复杂度上升、调试变难、延迟增加。我判断的标准是:当一个任务需要多个不同领域的专业能力,且这些能力之间需要协调时,才考虑编排。比如一个电商 Agent,既要查库存、又要算价格、又要生成推荐,这三个能力差异大,可以拆成多个 Agent 协作。
6.2 主流编排模式
编排模式大致分三种:流水线、路由、协作。流水线是把任务拆成固定步骤,一步步走。路由是先判断任务类型,分发给对应的 Agent。协作是多个 Agent 互相通信,共同完成。流水线最简单,协作最复杂。我建议从流水线开始,需要了再升级。
框架方面,LangChain、Dify、CrewAI 各有侧重。LangChain 灵活但抽象多,Dify 可视化但定制受限,CrewAI 专注多 Agent 协作。选哪个看你的需求:要快速验证用 Dify,要深度定制用 LangChain,要做多 Agent 协作用 CrewAI。但记住,框架是工具,底层能力你还是得有。
6.3 多 Agent 通信的坑
多 Agent 最大的坑是通信开销。Agent 之间传消息,消息本身也消耗 token。如果通信设计不好,token 消耗会翻倍。我的经验是:Agent 之间传结构化数据,别传自然语言。结构化数据紧凑,解析也稳定。
另一个坑是死锁。Agent A 等 Agent B 的结果,Agent B 等 Agent A 的结果,就卡住了。要设超时和降级策略。还有责任不清,多个 Agent 都能处理某个任务,结果互相推诿或者重复处理。要明确每个 Agent 的职责边界。
7. 第六阶段:安全与工程化,让 Agent 能上生产
7.1 Agent 安全的三个层面
Agent 安全和传统应用安全不一样,因为它有“自主决策”能力。风险分三层:输入层、决策层、执行层。输入层要防提示注入,用户可能通过精心构造的输入让 Agent 做不该做的事。决策层要防越权,Agent 可能调用它不该调用的工具。执行层要防副作用,工具执行可能造成不可逆的后果。
提示注入的防御,核心是隔离用户输入和系统指令。不要把用户输入直接拼进系统提示,要用明确的分隔符。另外,对敏感操作要加二次确认,别让 Agent 自己决定就执行了。
7.2 可观测性建设
Agent 上生产,可观测性是底线。你要能回答:这次请求经过了哪些步骤、每步耗时多少、调用了哪些工具、消耗了多少 token、有没有报错。这些信息要能串起来,用一个 trace_id 关联。热词里提到的trace id就是这个用途。
日志要分级:DEBUG 记详细过程,INFO 记关键节点,ERROR 记异常。生产环境别开 DEBUG,日志量太大。另外,敏感信息要脱敏,别把用户隐私写进日志。
7.3 熔断与降级
Agent 依赖外部模型和工具,这些都可能挂。要有熔断机制:连续失败 N 次就暂停调用,给系统喘息时间。要有降级策略:主模型挂了切备用模型,工具挂了返回缓存结果或友好提示。这些不是可选项,是生产必备。
7.4 成本控制
Agent 的成本主要是 token。控制成本的手段有:上下文压缩、工具结果裁剪、缓存常见请求、设置 token 预算。我一般给每个任务设预算,超了就中断并告知用户。别让一个任务悄悄烧掉几百块。
8. 常见问题速查与避坑清单
8.1 报错排查速查表
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 400 参数错误 | 模型名错、参数不支持 | 对照模型文档检查请求体 |
| 429 限流 | 请求频率过高 | 加退避重试,降低并发 |
| 上下文超限 | 消息太多太长 | 检查压缩和裁剪逻辑 |
| 工具调用失败 | 参数格式错、工具超时 | 看工具日志和参数校验 |
| 会话恢复异常 | 序列化格式不一致 | 检查存取 schema |
| rpc error | 服务名或会话 id 为空 | 检查初始化参数 |
8.2 我踩过的坑
第一个坑是过早引入框架。我一开始用 LangChain,遇到问题不知道是框架的还是我的,排查效率极低。后来手写了一遍,再回头看框架源码,豁然开朗。第二个坑是忽略 token 计数。有次跑一个长任务,跑到一半超窗口,前面的工作全白费。从那以后我每次调用都算 token。第三个坑是工具描述太随意。模型老是传错参数,我以为是模型笨,后来把描述写详细,问题就没了。第四个坑是不做会话恢复测试。上线后进程重启,用户对话全丢,被投诉。后来加了 kill 测试,才放心。
8.3 学习节奏建议
这套路径走下来,我的建议是:模型调用和工具调用各花一周,上下文管理和会话恢复各花一周,编排和安全各花两周。总共六到八周,能到能上手的程度。别贪快,每一层都要有可运行的代码产出。没有产出的学习都是假学习。
最后分享一个我自己的习惯:每学完一层,写一个最小 Demo 存起来。这些 Demo 后面会变成你的“积木”,做新项目时直接拼。Agent 开发本质上就是搭积木,积木越全,搭得越快。