构建AI代理:仍然很难
构建代理仍然很混乱。一旦你遇到真正的工具使用,SDK 抽象就会失效。自己管理缓存效果更好,但不同模型之间有所不同。
我觉得现在写一些我最近学到的新东西可能是个好时机。大部分内容将涉及构建代理,同时也会提到一些使用代理编码工具的内容。
TL;DR:构建代理仍然很混乱。一旦你遇到真正的工具使用,SDK 抽象就会失效。自己管理缓存效果更好,但不同模型之间有所不同。强化学习最终承担了比预期更多的工作量,而且失败需要严格的隔离以避免破坏循环。通过文件系统层共享状态是一个重要的构建模块。输出工具出乎意料地棘手,模型选择仍然取决于任务。
1、应该针对哪个代理 SDK?
当你构建自己的代理时,你可以选择针对底层 SDK,如 OpenAI SDK 或 Anthropic SDK,或者可以选择更高级别的抽象,如 Vercel AI SDK 或 Pydantic。我们之前的选择是采用 Vercel AI SDK,但只使用提供者抽象,并基本上自己驱动代理循环。目前我们不会再做这个选择。Vercel AI SDK 没有任何问题,但当你试图构建一个代理时,会发生两件我们最初没有预料到的事情:
第一件事是模型之间的差异足够大,以至于你需要构建自己的代理抽象。我们还没有发现这些 SDK 中的任何解决方案能构建出适合代理的正确抽象。我认为这部分是因为尽管基本的代理设计只是一个循环,但根据你提供的工具,会有一些细微的差异。这些差异会影响找到正确抽象的难易程度(缓存控制、不同的强化要求、工具提示、提供者端工具等)。由于正确的抽象尚不明确,使用来自专门平台的原始 SDK 会让你完全掌控。在一些更高层次的 SDK 上,你必须在其现有抽象之上构建,而这可能不是你最终想要的抽象。
我们也发现,在处理提供者端工具时,与 Vercel SDK 合作非常具有挑战性。尝试统一消息格式并不完全奏效。例如,Anthropic 的网络搜索工具经常破坏与 Vercel SDK 的消息历史,而我们尚未完全弄清楚原因。此外,在 Anthropic 的情况下,直接针对他们的 SDK 而不是 Vercel 的 SDK 进行缓存管理要容易得多。当你犯错时,错误信息也更加清晰。
这可能会改变,但现在我们可能不会在构建代理时使用抽象,至少在事情稳定下来之前不会。对我们来说,好处还不足以超过成本。
其他人可能已经解决了这个问题。如果你读到这里并认为我错了,请给我发邮件。我想学习。
2、缓存经验
不同的平台对缓存有非常不同的方法。已经有很多人讨论过这一点,但 Anthropic 要求你为缓存付费。它要求你显式管理缓存点,这从代理工程的角度来看确实改变了你与它的交互方式。我最初觉得手动管理非常愚蠢。为什么平台不能替我做这件事?但我现在已经完全接受了显式缓存管理。它使成本和缓存利用率更加可预测。
显式缓存允许你做一些其他方式很难做到的事情。例如,你可以拆分对话并让其同时向两个方向发展。你还有机会进行上下文编辑。最佳策略尚不明确,但显然你拥有更多的控制权,我真的很喜欢这种控制权。它也使理解底层代理的成本变得更加容易。你可以对缓存的利用情况做出更多假设,而在其他平台上,我们发现缓存效果参差不齐。
我们在代理中使用 Anthropic 进行缓存的方式相当简单。一个缓存点是在系统提示之后。两个缓存点放在对话开始处,最后一个缓存点随着对话尾部移动。然后在过程中还可以进行一些优化。
由于系统提示和工具选择现在必须大部分保持静态,我们稍后会输入动态消息来提供诸如当前时间之类的信息。否则,这会破坏缓存。我们还更多地利用循环中的强化。
3、在代理循环中使用强化
每次代理运行一个工具时,你有机会不仅返回工具产生的数据,还可以将更多信息反馈到循环中。例如,你可以提醒代理总体目标和单个任务的状态。你也可以在工具调用失败时提供有关如何成功调用工具的提示。强化的另一个用途是通知系统在后台发生的状态变化。如果你有一个使用并行处理的代理,你可以在每次工具调用后注入信息,当状态发生变化且对完成任务相关时。
有时,代理自我强化就足够了。例如,在 Claude Code 中,todo 写入工具就是一个自我强化工具。它只是从代理那里获取它认为应该执行的任务列表并回显进来的内容。它基本上只是一个回显工具;它真的不做其他事情。但这足以让代理比仅在上下文开始时给出任务和子任务时表现得更好,因为中间发生了太多事情。
我们还使用强化来通知系统如果在执行期间环境发生了对代理有害的变化。例如,如果我们代理失败并从某个步骤向前重试,但恢复操作基于损坏的数据,我们会注入一条消息,告知它可能需要退后几步并重新执行早期的步骤。
4、隔离失败
如果你预计代码执行过程中会有大量失败,那么有机会将这些失败隐藏在上下文中。这可以通过两种方式发生。一种是单独运行可能需要迭代的任务。你会在子代理中运行它们,直到成功,然后只报告成功,以及可能对未成功的方法的简要总结。对于代理来说,了解子任务中哪些没有成功是有帮助的,因为它可以将这些信息输入到下一个任务中,希望避免这些失败。
第二种方法在所有代理或基础模型中都不存在,但在 Anthropic 中你可以进行上下文编辑。到目前为止,我们在这方面并没有取得太多成功,但我们相信这是值得探索的一个有趣点。我们也想了解人们是否在这方面取得了成功。上下文编辑有趣之处在于,你应该能够保留令牌以便在后续的迭代循环中使用。你可以从上下文中移除某些失败,这些失败并未推动循环的成功完成,但仅在执行期间对某些尝试产生了负面影响。但正如我之前提到的:对于代理来说,了解什么没有成功也是有用的,但可能不需要全部状态和所有失败的完整输出。
不幸的是,上下文编辑会自动使缓存失效。真的没有其他办法。因此,很难判断这样做是否补偿了清除缓存的额外成本。
5、子代理 / 子推理
正如我在本博客上多次提到的那样,我们的大多数代理都是基于代码执行和代码生成的。这真的需要一个代理存储数据的共同地方。我们的选择是文件系统——在我们的情况下是一个虚拟文件系统——但这需要不同的工具来访问它。这对于像子代理或子推理这样的东西尤其重要。
你应该尝试构建一个没有死胡同的代理。死胡同是指任务只能在你构建的子工具内部继续执行。例如,你可能构建了一个生成图像的工具,但只能将该图像反馈给另一个工具。这是一个问题,因为你可能想使用代码执行工具将这些图像放入 zip 归档文件中。因此,需要一个系统,允许图像生成工具将图像写入代码执行工具可以读取的同一位置。本质上,这就是文件系统。
显然,它也必须反向工作。你可能想使用代码执行工具解压 zip 归档文件,然后回到推理中描述所有图像,以便下一步可以回到代码执行等等。文件系统就是我们为此使用的机制。但它确实需要工具以某种方式构建,以便它们可以使用虚拟文件系统的文件路径来工作。
所以基本上,ExecuteCode 工具将拥有与 RunInference 工具相同的文件系统,后者可以接受该虚拟文件系统上的文件路径。
6、使用输出工具
我们构建代理的一种有趣方式是,它不表示聊天会话。它最终会与用户或外部世界交流,但其中间发送的所有消息通常不会被揭示。问题是:它是如何创建这条消息的?我们有一个工具,即输出工具。代理会显式使用它来与人类沟通。然后我们使用一个提示来指导它何时使用该工具。在我们的案例中,输出工具会发送电子邮件。
但这也带来了一些其他挑战。一个是,与直接使用主代理循环的文本输出作为与用户交流的机制相比,调整该输出工具的措辞和语气令人惊讶地困难。我无法说明为什么,但我觉得这可能与这些模型是如何训练的有关。
一次尝试没有很好地发挥作用,即让输出工具运行另一个快速 LLM 如 Gemini 2.5 Flash 来调整语气以符合我们的偏好。但这增加了延迟并实际上降低了输出质量。部分原因是模型本身措辞不当,而子工具缺乏足够的上下文。提供更多的主代理上下文片段到子工具会使它变得昂贵,而且也没有完全解决问题。它有时还会在最终输出中透露我们不想出现的信息,比如导致最终结果的步骤。
输出工具的另一个问题是,有时它根本不调用该工具。我们强制这一点的一种方式是记住输出工具是否被调用。如果循环结束时没有调用输出工具,我们会注入一条强化信息,鼓励它使用输出工具。
7、模型选择
总的来说,我们目前的模型选择并没有发生太大变化。我认为 Haiku 和 Sonnet 仍然是最好的工具调用者,因此它们在代理循环中是出色的选择。它们在 RL 方面也相对透明。其他明显的选择是 Gemini 模型。到目前为止,我们还没有在主循环中发现 GPT 系列模型的太多成功。
对于需要推理的个别子工具,我们目前的选择是 Gemini 2.5,如果你需要总结大文档或处理 PDF 等内容的话。这也是一个很好的模型,用于从图像中提取信息,特别是因为 Sonnet 系列模型经常会遇到安全过滤器,这很烦人。
还有一个非常明显的认识是,仅凭令牌成本并不能真正定义代理的费用。一个更好的工具调用者会在更少的令牌中完成工作。目前有一些比 Sonnet 更便宜的模型,但它们在循环中并不一定更便宜。
但总的来说,过去几周内并没有发生太多变化。
8、测试与评估
我们发现测试和评估是这里最困难的问题。这并不令人意外,但代理的特性使它变得更难。与提示不同,你不能仅仅在某个外部系统中进行评估,因为需要喂入太多内容。这意味着你想基于可观测性数据或实际测试运行来执行评估。到目前为止,我们尝试过的所有解决方案都没有让我们确信它们找到了正确的方法。不幸的是,我必须报告说,目前我们还没有找到真正让我们满意的东西。我希望我们能找到解决这个问题的方法,因为这正成为构建代理时越来越令人沮丧的部分。
9、编码代理更新
至于我使用编码代理的经验,其实并没有发生太多变化。主要的新发展是我正在试验 Amp。如果你好奇为什么:并不是因为它在客观上比我现在使用的更好,而是我真的很喜欢他们关于代理的思维方式,从他们发布的帖子中可以看出。不同子代理(如 Oracle)与主循环的互动做得非常好,而今天很少有其他框架能做到这一点。这也是让我验证不同代理设计如何工作的良好方式。Amp 与 Claude Code 类似,感觉像是由也使用自己工具的人构建的产品。我不觉得行业内的每个代理都是这样做的。
10、我读到并发现的一些快速内容
这只是我感觉可能也值得分享的一些随机内容:
- 如果你根本不需要 MCP 呢?:马里奥认为许多 MCP 服务器过于复杂,包含消耗大量上下文的大工具集。他提出了一种极简主义方法,用于浏览器代理的使用场景,通过 Bash 执行简单的 CLI 工具(例如,启动、导航、评估 JS、截图),从而保持令牌使用量小且工作流灵活。我用它构建了一个 Claude/Amp 技能。
- “小型”开源项目的命运:作者认为,微型、单一用途的开源库的时代即将结束,主要是因为内置平台 API 和 AI 工具现在可以按需生成简单的实用程序。感谢......。
- Tmux 是爱。没有文章与之相关,但 TLDR 是 Tmux 很棒。如果你有任何看起来像交互式系统的东西,代理应该与其配合,你应该给它一些 Tmux 技能。
- LLM APIs 是一个同步问题。这是一个独立的发现,太长了,不适合这篇博文,所以我写了另一篇。
原文连接:Agent Design Is Still Hard
汇智网翻译整理,转载请标明出处