这篇文章不是 Agent 的入门科普,而是我在实际开发和翻源码之后,关于”怎么把 Agent 做扎实”的十条理解。它们大多不是”能不能做”的问题,而是”怎么取舍”的问题——工具怎么设计、上下文怎么省、模型怎么选、缓存怎么不踩坑。
如果只记一句话,我希望是这句:Agent 的工程质量,很大程度上是在有限的上下文预算里做取舍。
0. 什么是 AI Agent?
一句话概括:Agent = LLM + 工具 + 循环。
LLM 本身只会”说”——你问它问题,它给你回答。但 Agent 不只是聊天,它还能”做”:调用工具、读取信息、修改文件、发请求……然后根据工具返回的结果继续推理,再决定下一步做什么,如此循环,直到任务完成。
和普通的 LLM 对话相比,Agent 多了三个关键能力:
| 能力 | 说明 | 举例 |
|---|---|---|
| 工具调用(Tool Use) | LLM 调用外部工具来感知和操作环境 | 读文件、跑命令、查数据库、调 API |
| 自主决策(Autonomy) | LLM 自己决定下一步做什么,而不是人一步步指挥 | 不是”先读 A 再读 B”,而是它自己判断该读什么 |
| 多轮循环(Agentic Loop) | 不是一次调用就结束,而是”思考→行动→观察→再思考” | 改完代码跑测试,测试挂了自动排查修复 |
这就是 Agent 和普通 ChatBot 最大的区别:ChatBot 主要是”回答”,Agent 还要”行动”。
1. 先 Workflow,再 Agent
不是所有问题都需要一个全自主的 Agent。能用确定性 workflow 解决的问题,优先用 workflow。
- Workflow:LLM 和工具按预定义的代码路径编排,流程是写死的
- Agent:LLM 自己决定走哪条路、用什么工具
判断标准也很简单:这条流程你能不能提前穷举出来?能,就用 workflow。只有当”下一步该做什么”本身就需要模型动态判断时,才值得上 Agent。
全自主是有代价的——更难调试、更难评测、更贵、更容易跑偏。先用 workflow 把能确定的都确定下来,只把真正需要判断的那一小段交给 Agent,通常比一上来就全自主更稳。
2. Tool 也是上下文,别忽视它的成本
工具定义是要进模型输入的,所以工具不是”零成本的能力扩展”。它至少带来两个问题:占用上下文,以及稀释 LLM 的注意力。
许多 LLM API 提供商对一次请求能带的工具数量有限制。更麻烦的是,工具一多,模型选错工具、或者被一堆不相关的 schema 干扰的概率都会上升。
所以主流工具都做了”延迟加载”:不再一次性把所有工具的完整定义塞进 prompt,而是先给每个工具一个轻量 stub(只有名字,标记为延迟加载),完整 schema 等模型真正选中它时再加载。Claude Code / Claude API 里对应的能力叫 MCP tool search(Claude Code 侧)/ tool search tool(API 侧),在支持的新模型上默认开启(可以用 ENABLE_TOOL_SEARCH=false 关掉)。
这里有个常见误解:以为它”只暴露一个 tool、把工具的说明放进系统提示词”。更准确的说法是:它为每个被延迟的工具发送一个只有名字的 stub,顺序固定,完整定义等到需要时才拉取——而且这些被延迟的工具体并不占系统提示词的前缀。
这一点给我们的启示是:工具的”数量”和”体积”本身就是要管理的成本,不是免费的能力扩展。 如果你自己写 Agent,工具一多,就该考虑类似的按需加载,而不是无脑全量塞进去。
3. 上下文不是”越大越好”,LLM 的注意力是稀缺资源
长上下文 ≠ 高质量上下文。而且模型对上下文的注意力并不是均匀的——首尾的信息注意力最高,中间最容易丢(这就是常说的 lost in the middle)。
换句话说,你塞进去的每一条信息都在和别的信息竞争模型的注意力。无效信息越多,关键信息被忽略的概率越高。
几条被验证有效的做法:
- 把探索过程隔离到子 Agent 去执行。 让子 Agent 去做大量的搜索、浏览、试错,只把结论交回主 Agent。Claude Code、Codex 这类编程工具基本都是这么做的——主 Agent 的上下文里不应该堆满”我翻过哪些文件”的过程细节。
- 工具返回摘要,别全部回灌。 比如 web search 工具,不需要把原文整篇返回,只返回总结性信息就够了。工具的异常报错也不该返回全量堆栈——那会占用大量无效上下文。
- 完成阶段性任务要及时摘要。 别让原始细节一直占着上下文,工具调用的结果要在合适的时机压缩掉。
再往前一步,是分清三种”记忆”手段:上下文编辑(清掉过期的工具结果和思考块)、压缩(接近上限时把早期内容摘要掉)、外部记忆(跨会话持久化)。它们解决的不是一回事,长跑任务通常三者都用。
4. Tool 描述要写”调用策略”,不是写”功能说明”
工具描述是给模型做路由决策用的。写描述时,可以想想你会怎么给团队新来的同事介绍这个工具——把那些隐含的领域知识、专业术语、资源之间的关系都写出来。
一份好的工具描述,至少要回答四个问题:
- 做什么
- 什么时候用
- 什么时候不用
- 和相似工具的边界在哪
最后一条最容易被忽略,也最关键。如果你有两个功能相近的工具,描述里不写清楚”什么情况该选谁”,模型就只能靠猜。
SKILL 也是同理。 光写”这个 skill 能干什么”不够,description 里最好也写清楚什么时候该调用、什么时候不该用——因为它和工具描述一样,本质上都是给模型看的”路由表”。
5. Tool 要面向 Agent 设计,别照搬后端 API
面向 Agent 的工具,不应该只是后端 API 的一层薄包装。
后端 API 是按”资源”和”职责”拆分的,一个完整业务动作往往要串好几个接口。但让 Agent 自己一步步拼这些中间步骤,既费 token 又容易出错。把常见的链式调用合并成一个工具,是更贴合 Agent 的设计。
另一个更进一步的思路是 programmatic tool calling:让模型不直接一个接一个地调工具,而是在代码执行环境里写一段脚本,把多次工具调用编排进去。工具的返回值回到脚本里处理(循环、过滤、分支),只有脚本的最终输出才会回到模型的上下文。这样一来,中间那堆数据根本不会污染上下文,往返次数也大幅减少。
设计工具时,多问一句:这个工具是给”人”用的封装,还是给”模型”用的能力? 两者的最优形态往往不一样。
6. 模型要分层路由,别什么都上最强
不是所有任务都值得上最强的模型。搜索、查找这类任务,用小模型又快又便宜,质量差距可以忽略;只有架构决策、复杂规划这种真正需要深度推理的任务,才值得上最强的模型。
| 任务类型 | 推荐层级 | 为什么 |
|---|---|---|
| 文件搜索、简单查找 | 小模型(如 Haiku) | 便宜快,质量差别可忽略 |
| 代码编写、常规推理 | 中档模型(如 Sonnet) | 性能和成本的最佳平衡 |
| 架构决策、复杂规划 | 最强模型(如 Opus) | 需要深度推理 |
不同模型的价差是数量级的,一句”什么都上最强”可能让你多花十倍的钱。
成熟的 Agent 系统几乎都会做分层路由。Claude Code 的子 Agent 就支持按模型解析(按”单次调用指定 → 子 Agent 配置 → 环境变量 → 主会话模型”的顺序决定用哪个模型),内置的一些辅助 Agent 也是分层的。
顺带纠正一个流传很广的说法:“Explore 子 Agent 固定跑 Haiku”已经过时了。 从 v2.1.198 起,Explore 会继承主会话的模型,而不是固定用 Haiku。分层路由的思路没错,但别把某个具体的实现细节当成不变的结论。
有的用户量大的 Agent,甚至会专门加一个路由层,先判断用户问题的复杂度,再决定路由到哪个级别的模型。
还有一个和缓存相关的取舍:如果主循环正在用某个模型,中途切模型会让缓存全部失效。要省钱,正确做法是”派一个用更便宜模型的子 Agent 去做子任务”,而不是把主循环的模型换掉——子任务是独立的前缀,主循环的缓存可以保住。
7. 内容顺序很重要:缓存是前缀匹配,改一个字后面全废
LLM API 是无状态的,每次调用都要把上下文全部重新传一遍:System Prompt、工具定义、历史消息……为了省钱,各家都提供了 prompt caching。
但缓存是按前缀匹配的:模型看到的顺序是 工具定义 → System → 消息,从这个顺序的第一个 token 开始必须完全一致,中间任何一个 token 变了,它后面的所有缓存全部失效。
由此得到一条核心原则:
变化频率低的东西放前面,变化频率高的东西放后面。
坑 1:在 System Prompt 里塞动态内容。 有人在 System Prompt 顶部写了 Today is ${new Date().toISOString()}。结果每次请求前缀都不一样,缓存永远命中不了。解决办法:时间戳这类动态信息不要放在系统提示词里,放到后面的 messages 里——在支持的模型上,用一条”中途插入的 system 消息”是更好的选择,既不影响缓存,又保留操作者指令的语义。
坑 2:对话中途换工具集或换模型。 工具定义位于前缀最前面,一旦增删工具、或者切换模型(缓存是按模型隔离的),前缀就变了,之前的缓存全部失效。所以尽量让工具集和模型在整个会话内保持稳定。
这里也要修正一个常见说法。并不是”装个新 skill / 新工具就必须新开会话”——skill 和命令是以用户消息的形式注入的,对缓存是安全的;插件带的 skills / commands / agents 也不会让缓存失效,
/reload-plugins就能生效。真正需要重启会话的,通常是改动了 MCP server 的配置。
另外两个实用细节:能缓存的最小前缀长度和模型有关(大致在 512~4096 token 之间),太短的前缀根本不会缓存,加了标记也没用;想确认缓存到底有没有命中,看响应里的 usage 字段——如果 cache_read_input_tokens 一直是 0,那多半有个”隐形杀手”在偷偷改动前缀。
8. 上下文不是数据库
我们很容易习惯性地把所有信息都塞进上下文里传来传去。但 LLM 的上下文应该只保留当前推理所需的关键信息,不该承担数据存储的职责。
典型的”不该塞进上下文”的数据:
- 图表数据
- 大批量查询结果
- 中间计算结果
- 当前的状态信息
这些东西完全可以放在数据库、Redis 或对象存储里,上下文里只保留 ID、摘要或关键结果。Agent 需要时,再通过工具按 ID 或 session 信息动态取回。
举个具体的例子:一些图表数据,可以存进数据库,只把数据 ID 返回给模型;业务页面通过 ID 去查全量数据。再比如一些中间变量,不必全靠上下文传递,可以绑定到 sessionId 之类的关键信息上,需要时再取。
这样做既降低 token 消耗,也减少无关信息对模型推理的干扰。核心思想是:
让 LLM 负责思考,让存储系统负责记忆,让工具负责获取数据。
Agent 不是简单的 ChatBot——它可以借助外部存储,而不是完全依赖上下文来传递全部信息。
9. AGENTS.md / CLAUDE.md 别写太长,做索引就好
项目级的指导文件(AGENTS.md、CLAUDE.md 这类)不适合无限膨胀。
它们一旦变长,就会同时带来两个问题:一是每次都占用大量上下文,二是把真正重要的规则淹没在细节里。
更合适的方式是把它们当成入口 + 索引:只写最关键的约定和指针,具体内容让 Agent 按需加载。这就是所谓的渐进式披露——和 skill 的机制是同一个思路:skill 的描述常驻在上下文里,完整内容等真正用到时才读。
同样的道理,也适用于工具描述、参考文档、规则文件。常驻上下文的应该是”目录”,不是”正文”。
结语
回头看这十条,它们其实是同一个约束的不同侧面:上下文预算是有限的,而 Agent 的每一处设计都在决定”什么东西值得占用它”。
工具要延迟加载,是因为工具定义也占上下文;子 Agent 要隔离,是为了不让过程细节污染主上下文;工具要返回摘要,是为了别把原文硬塞回来;数据要外置,是因为上下文不该当数据库;缓存要按前缀排布,是因为稳定的前缀才能被复用;指导文件要做索引,是因为常驻的应该是目录而不是正文。
想清楚”这条信息,此刻真的需要出现在模型面前吗”——很多设计问题就有了答案。
少即是多。