Agent 设计依然很难
AI Agent, 翻译, Agent 设计

Agent 设计依然很难

翻译自 Armin Ronacher 的最新文章,讨论构建 Agent 时在 SDK 抽象、缓存、强化、失败隔离与输出工具等方面的实战经验。

0次点击1分钟阅读

作者:Armin Ronacher;原文发表于 2025 年 11 月 21 日。本译文保留原始结构与要点,方便中文读者参考。

TL;DR

  • 构建 Agent 依旧容易失控,一旦真正依赖工具,SDK 抽象就会暴露断层。
  • 缓存最好自己管,不同模型对缓存的支持差别巨大。
  • 强化反馈承担的工作远比预期多,失败必须严格隔离,避免拖垮循环。
  • 像文件系统一样的共享状态层是搭建子 Agent / 子推理的关键。
  • 输出工具难以调教,模型选择依旧得看任务场景。

选择哪种 Agent SDK?

自建 Agent 时可以直接使用 OpenAI、Anthropic 等官方 SDK,也可以套一层更高阶的抽象,例如 Vercel AI SDK 或 Pydantic。我们曾选择 Vercel AI SDK,但只用了 provider 层,其余 Agent 循环自己写。今天回看,那不是最佳决定。

Vercel AI SDK 本身没问题,真正的问题在于不同模型之间差异太大,迫使你最终还是得写自己的 Agent 抽象。虽然 Agent 结构就是一个循环,但每种工具都会在缓存控制、强化需求、提示设计、provider 侧工具等方面引入微妙差异。因为正确抽象还没定型,直接使用官方 SDK 反而能完全掌握细节;一旦站在高阶抽象之上,就不得不沿用它的设计,不一定是你最终想要的。

另一个大痛点是 provider 侧工具。Vercel 尝试统一消息格式,但在 Anthropic 的网页搜索工具上就会把消息历史搞坏,我们至今没查出原因。针对 Anthropic 时,缓存管理也明显更容易,错误信息更清晰。

也许未来情况会变,但现在我们宁可不加抽象,至少在生态稳定之前是如此。如果你认为我们想错了,请务必发邮件告诉我。

缓存的经验

不同平台的缓存策略差异巨大。Anthropic 会为缓存收费,而且需要你显式管理缓存点,这彻底改变了工程方式。一开始我觉得手动管理缓存很蠢——为什么平台不替我做?但现在我完全接受甚至更喜欢这种方式,因为成本和命中率都更可控。

显式缓存允许你做很多难以做到的事:比如切分对话,平行向两个方向推进;或在上下文中进行编辑。最佳策略尚不明确,但你确实有更多控制权,也更容易估算成本。相比其他平台的“碰运气”,Anthropic 的缓存预期更明确。

我们在 Agent 中的做法是:系统提示之后放一个缓存点;对话开头放两个缓存点,尾部那个会随着对话末尾向前移动,中途还能进一步优化。因为系统提示与工具选择需要基本静态,所以像“当前时间”这种动态信息会在后续消息中补充,避免污染缓存,并在循环中更多使用强化。

Agent 循环中的强化

每次工具调用都是强化的机会:除了返回数据,还能把更多信息写回循环,提醒 Agent 任务目标、各子任务状态,或者在失败时提示可能的成功路径。并行处理场景下,还可以在每次工具调用后注入状态变化,以便 Agent 理解背景。

有时 Agent 自我强化就够了。Claude Code 中的 todo write 工具就是自我强化工具:它只是接收 Agent 认为接下来要做的任务清单,再原样回传,像一个回声工具,却能比固定任务列表更好地推进循环。

我们还会通过强化告诉系统:如果执行过程中环境发生了问题,比如 Agent 重试时依赖了损坏的数据,就提醒它往回退几个步骤重新来。

隔离失败

当你预期代码执行会频繁失败,可以尝试把失败隔离出上下文。有两种方式:

  1. 将需要多次迭代的任务拆成子 Agent 独立执行,直到成功才向主循环报告结果,并附上一小段失败策略总结。Agent 了解失败路径仍然有价值,能帮助下一步避开坑。
  2. 如果模型支持,可进行上下文编辑,从对话里删掉那些对最终成功没有帮助、还消耗大量 token 的失败输出。Agent 仍然需要知道哪些方案失败过,但未必需要完整日志。

遗憾的是,一旦编辑上下文,缓存一定作废——没有办法避免。至于是否值得,为了节省 token 而丢掉缓存,需要仔细权衡。

子 Agent / 子推理

我们的 Agent 大多依赖代码执行与生成,因此需要一个共享的“存储地”。我们选择了虚拟文件系统,所有工具都围绕它设计,这对有子 Agent 或子推理的场景尤其重要。

理想的 Agent 不应该有死胡同。比如某个图像生成工具只能把结果交给唯一的另一个工具,那你就没法把图像打包进 zip 再交给代码执行工具。所以我们需要一个所有工具都能读写的文件系统,让 ExecuteCode 和 RunInference 工具共享路径,也让代码工具可以解压文件,再交给推理工具描述图像,如此往复。

输出工具的使用

我们的 Agent 并不是聊天产品,过程中发送的消息不会直接曝光给用户。它最终会对外输出,但需要靠一个“输出工具”来完成,我们用提示告诉它何时调用该工具。在我们的系统里,输出工具负责发送邮件。

问题在于:相比让主循环直接对话,与输出工具配合时很难控制语气。我们尝试过让输出工具调用一个更快的 LLM(例如 Gemini 2.5 Flash)来调Tone,但这样会增加延迟,还降低质量:辅助模型词不达意,缺少上下文。把更多上下文传给子工具又贵又没完全解决问题,有时还会泄露我们不想暴露的步骤。

另一个问题是 Agent 有时根本不调用输出工具。我们的做法是在循环里记录它是否调用过,如果循环结束仍未调用,就注入强化消息逼它补上。

模型选择

总体选型变化不大。Haiku 和 Sonnet 依旧是最佳工具调用模型,很适合充当主循环,它们对强化也比较透明。另一个显而易见的选择是 Gemini 系列。反而我们在主循环里并没有从 GPT 家族获得太多成功。

对于需要推理的子工具,我们会在处理长文档或 PDF、图像信息提取时使用 Gemini 2.5。Sonnet 系列容易被安全过滤器拦住,在这类任务上容易掉坑。

不要只盯着 token 单价:更好的工具调用模型会用更少 token 完成任务。即便有更便宜的模型,如果在循环里效率低,最终也不见得省钱。

测试与评估

测试与评估是目前我们觉得最难的问题。Agent 的性质决定了没法像提示那样在外部系统跑完评测,因为需要太多上下文,所以更可能依赖可观测数据或直接在真实测试跑 instrumentation。我们还没看到让人满意的解决方案,也希望能尽快找到,否则这个问题只会越来越令人抓狂。

代码 Agent 的近况

近期在编码 Agent 上没有激进变化,唯一大更新是开始试用 Amp。它未必客观上比现有工具强,但我很喜欢他们分享的 Agent 设计理念。Oracle 等子 Agent 与主循环的协作非常漂亮,其他框架很少做到这一点。这也是验证不同 Agent 设计的好方式。Amp 和 Claude Code 都让人感觉是“造给自己用的”产品,而很多同行的 Agent 工具并没有这种自用体验。

最近速读

  • 要是根本不需要 MCP 呢? Mario 认为很多 MCP 服务器过度设计,塞满工具导致上下文暴涨。他主张对浏览器 Agent 使用极简 CLI(start、navigate、evaluate JS、screenshot 等),保持 token 低而流程灵活。我已经做了一个 Claude/Amp Skill 来实现这个想法。
  • “小型”开源的命运: 作者认为微型单一用途的开源库时代即将结束,因为平台内建 API 与 AI 工具足够按需生成简单工具。谢天谢地。
  • Tmux is love: 没有配套文章,核心观点是:如果 Agent 需要和交互式系统打交道,就给它一些 Tmux 技能。
  • LLM APIs are a Synchronization Problem: 这是另一个想法,篇幅太长,因此我专门写了另一篇文章。

原文链接:https://lucumr.pocoo.org/2025/11/21/agents-are-hard/

相关文章