跳到正文
堆栈茶社
返回

Agent 开发的十条实践心得:工具、上下文与缓存的取舍

—次阅读
编辑此页

这篇文章不是 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。只有当”下一步该做什么”本身就需要模型动态判断时,才值得上 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)。

换句话说,你塞进去的每一条信息都在和别的信息竞争模型的注意力。无效信息越多,关键信息被忽略的概率越高。

几条被验证有效的做法:

  1. 把探索过程隔离到子 Agent 去执行。 让子 Agent 去做大量的搜索、浏览、试错,只把结论交回主 Agent。Claude Code、Codex 这类编程工具基本都是这么做的——主 Agent 的上下文里不应该堆满”我翻过哪些文件”的过程细节。
  2. 工具返回摘要,别全部回灌。 比如 web search 工具,不需要把原文整篇返回,只返回总结性信息就够了。工具的异常报错也不该返回全量堆栈——那会占用大量无效上下文。
  3. 完成阶段性任务要及时摘要。 别让原始细节一直占着上下文,工具调用的结果要在合适的时机压缩掉。

再往前一步,是分清三种”记忆”手段:上下文编辑(清掉过期的工具结果和思考块)、压缩(接近上限时把早期内容摘要掉)、外部记忆(跨会话持久化)。它们解决的不是一回事,长跑任务通常三者都用。

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 要隔离,是为了不让过程细节污染主上下文;工具要返回摘要,是为了别把原文硬塞回来;数据要外置,是因为上下文不该当数据库;缓存要按前缀排布,是因为稳定的前缀才能被复用;指导文件要做索引,是因为常驻的应该是目录而不是正文。

想清楚”这条信息,此刻真的需要出现在模型面前吗”——很多设计问题就有了答案。

少即是多。


编辑此页
分享这篇文章:

上一篇
我为什么开始推荐 superpowers
下一篇
把海尔热水器接入米家,用小爱打开零冷水