编者按: 构建真正可靠的智能体(Agent)为什么依然如此困难?尽管大模型能力日新月异,工具调用、多步推理、状态管理等核心环节却仍充满"脏活累活" ------ 是抽象层不够好?平台差异太大?还是我们尚未找到正确的工程范式?
我们今天为大家带来的这篇文章,作者以一线实践者的视角明确指出:在智能体开发生态远未成熟的当下,直面底层复杂性而非依赖抽象层,通过精细的工程实践才能应对真实场景中的复杂性与不确定性。
作者 | Armin Ronacher
编译 | 岳扬
我觉得现在可能是时候写一写最近学到的一些新东西了。其中大部分内容会围绕构建智能体(agents)展开,也会稍微分享一点关于使用具有智能体特性的编码工具的经验。
TL;DR:构建智能体依然是个脏活累活。一旦涉及到真实的工具调用,SDK 抽象层就容易出问题。对于缓存,我们自己手动管理效果更好,但不同模型的缓存管理方式各不相同。强化机制最终承担了比预期更重的职责,而失败情况必须严格隔离,否则整个循环很容易跑偏。通过一个类似文件系统的结构来管理共享状态,是构建 Agent 系统中非常重要的一块基石。处理输出结果所需的工具链,出人意料地棘手。而模型的选择,依然取决于具体的任务。
01 该选用哪个智能体 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),缓存管理要简单得多,而且出错时返回的错误信息也清晰很多。
这种情况未来或许会改善,但在现阶段,我们在构建智能体时很可能不会再使用这类抽象层了 ------ 至少要等整个技术生态稍微稳定下来再说。对我们来说,现阶段抽象层带来的收益还不足以抵消成本。
02 对不同平台缓存机制的使用体会、经验与策略总结
不同的平台在缓存策略上有着非常不同的做法。关于这一点,技术社区其实已经有很多讨论了,但 Anthropic 的做法是:用户要为使用缓存付费。它要求我们显式地管理缓存点,而这从 Agent 工程的层面上改变了我们与其交互的方式。最初,我觉得手动管理很愚蠢,平台为什么不帮我做?但我现在已经完全转变了看法,现在更倾向于显式的缓存管理。这让成本和缓存利用率变得更加可预测。
由开发者主动、明确地启用缓存(Explicit caching)让我们能够实现某些原本极其困难的操作 。 例如,我们让一个对话同时朝两个不同的方向运行。我们还可以上下文编辑(context editing)。虽然最优策略尚不明确,但我们显然拥有了更多的控制权,而我非常享受这种掌控感,这也让我们更容易理解底层智能体的运行成本。我们也可以更准确地预估缓存的利用效果,而使用其他平台时,缓存的效果却时好时坏、难以预测。
我们在使用 Anthropic 的 Agent 时进行缓存的方式非常简单直接。一个缓存点位于系统提示词之后。两个缓存点放置在对话开头,其中最后一个缓存点会随着对话的尾部移动。在此过程中,我们还可以进行一些优化。
由于系统提示词和工具选择现在必须基本保持静态,我们会随后插入一条动态消息来提供诸如当前时间之类的信息,否则会破坏缓存。我们在智能体循环中也更多地利用了强化机制。
03 智能体循环中的强化机制
每当智能体调用工具时,我们不仅有机会返回该工具产生的数据,还可以向智能体循环中注入更多信息。例如,我们可以提醒智能体当前的总体目标以及各个子任务的进展状态。当某个工具调用失败时,我们也可以提供关于如何成功调用工具的提示词,帮助它下次更可能成功。此外,强化机制的另一个用途,是向系统通报在后台发生的状态变化。如果你的智能体使用了并行处理,那么每当后台状态发生变化,并且该变化与任务完成相关时,你就可以在每次工具调用之后注入相应的信息。
有时候,智能体进行自我强化就足够了。比如在 Claude Code 中,todo write 工具就是一个自我强化工具:它只是接收智能体认为自己应该执行的任务列表,然后原样回显出来。本质上它就是一个"回显工具",不做任何其他处理。但这种简单机制已经足够了 ------ 比起只在上下文开头一次性给出所有任务和子任务(而中间又发生了大量操作),这种方式能更好地推动智能体向前推进。
我们也利用强化机制,在执行过程中环境发生对智能体不利的变化时及时告知系统。例如,如果智能体在某一步运行失败并尝试重试,但重试所依赖的数据本身已损坏,我们就会注入一条消息,提示它可能需要回退几步,重新执行更早的步骤。
04 将失败的副作用限制在局部范围,不让它扩散到整个系统或干扰后续决策
如果我们预期在代码执行过程中会频繁出现失败,那么可以把这些失败从主上下文中隐藏起来。这可以通过两种方式来实现:
第一种是将可能需要进行多次迭代的任务单独运行。 可以在一个子智能体(subagent)中反复执行该任务,直到成功,然后只将成功结果(以及哪些方法未奏效的简要总结)汇报回主循环。让智能体了解子任务中哪些方法行不通是有帮助的,因为它可以把这些信息带入下一个任务,从而尽量避开类似的失败路径。
第二种方式并非所有智能体或基础模型都支持,但在 Anthropic 上你可以进行上下文编辑(context editing)。 到目前为止,我们在上下文编辑上还没有太多成功经验,但我们认为这是一个非常值得深入探索的方向。我们也非常希望了解是否有其他人在这方面取得了成效。上下文编辑的妙处在于,它理论上能为你节省一些 token,留到后续的迭代循环中使用:你可以将某些对完成任务没有推动作用、仅对执行过程中的某些尝试产生负面影响的失败记录,从上下文中移除。不过,正如我之前提到的 ------ 让智能体知道"什么方法行不通"仍然是有价值的,只是未必需要保留每一次失败的完整状态和完整输出。
不幸的是,上下文编辑会自动使缓存失效,且这一点几乎无法避免。 因此,很难判断在什么情况下,这种操作带来的收益能抵消因缓存被破坏而产生的额外成本。
05 子智能体 / 子推理过程
我们的大多数智能体都基于代码执行与代码生成,这就要求智能体必须有一个共享的数据存储位置。 我们选择的是文件系统 ------ 具体来说是一个虚拟文件系统,但这就需要不同的工具都能访问它。如果你使用了诸如子智能体(subagent)或子推理(subinference)之类的机制,这一点就尤为重要。
你应该尽量构建一个没有"死胡同"的智能体。所谓"死胡同",是指任务只能在你自己构建的特定子工具内继续执行。例如,你可能会构建一个生成图像的工具,但它只能把图像反馈给另一个工具。这就有问题,因为你可能之后会想用代码执行工具把这些图像打包进压缩文件。因此,需要一个系统能让图像生成工具将图像写入代码执行工具能够读取的位置。本质上,这就是文件系统。
显然,反过来也需要可行。你可能希望先用代码执行工具解压压缩包,然后回到推理环节,让模型描述解压出的所有图像,接着再回到代码执行环节进行下一步操作,如此往复。我们正是用这个文件系统来实现这种跨工具协作的。但这要求所有工具在设计时,都必须支持以文件路径作为输入/输出接口,且这些路径指向同一个共享的虚拟文件系统。
所以基本上,ExecuteCode 和 RunInference 这样的工具需要能访问同一个共享文件系统。这样,后者只需接收一个文件路径参数,就能直接处理前者在共享空间里生成的文件。
06 输出工具的使用
我们构建智能体的一个有趣之处在于:它并不代表一次聊天会话。它最终确实会向用户或外界传递某些信息,但中间产生的所有消息通常不会暴露给用户。那么问题来了:它如何生成最终要传递的消息?我们的做法是提供一个专门的输出工具(output tool),智能体会显式调用这个工具来与人类沟通。我们通过提示词明确告诉它何时该使用这个工具。在我们的场景中,这个输出工具会发送一封电子邮件。
但这样做也带来了一些意料之外的挑战。其中一个问题是:相比直接让主智能体循环输出文本给用户,通过输出工具来控制措辞和语气要困难得多。 我说不清具体原因,但很可能与这些模型的训练方式有关。
我们曾尝试过一种方法:让输出工具调用另一个轻量级 LLM(比如 Gemini 2.5 Flash)来将语气调整为我们喜欢的风格。但效果并不好 ------ 不仅增加了延迟,反而还降低了输出质量。部分原因我认为是模型本身的措辞就不够准确,而子工具也没有足够的上下文。向子工具提供更多主智能体上下文的片段会使成本升高,且未能完全解决问题。更糟的是,有时子工具还会在最终输出中意外泄露我们不希望用户看到的信息,比如得出最终结果前的步骤细节。
输出工具的另一个问题是,有时它根本不会被调用。 为了解决这个问题,我们加入了一个机制:记录输出工具是否已被调用。如果循环结束时仍未调用,我们就注入一条强化消息,明确鼓励(甚至强制)它使用输出工具来完成最终输出。
07 模型选择
总体而言,我们目前在模型选择上的核心思路并没有发生剧烈变化。我依然认为 Haiku 和 Sonnet 是目前(译者注:原文发表时间为 2025 年 11 月 21 日)市面上最出色的工具调用模型,因此它们是构建 Agent 主循环的绝佳选择。同时,它们在工具调用、多步推理等过程中表现出的策略性行为也更加可预测、可解释、可调试。另一个显而易见的选择是 Gemini 系列模型。至于 GPT 家族,目前我们在将其用于主循环任务时,尚未获得理想的效果。
对于那些可能涉及推理的子工具插件,如果你需要总结超长文档或处理 PDF 等任务,我们的首选是 Gemini 2.5。在处理图像信息提取任务时,它同样表现优异,而 Sonnet 系列模型经常会触发安全过滤机制,这在实际操作中相当令人头疼。
还有一个显而易见但常被忽视的事实:Token 的单价并不能完全决定一个智能体的运行成本。一个更擅长工具调用的模型,往往能用更少的 Token 完成任务。虽然目前市面上有一些比 Sonnet 更便宜的模型,但在智能体循环(Loop)中使用时,它们的综合成本未必更低。
总的来说,过去几周内,模型格局其实并没有太大变化。
08 测试与评估
我们发现,测试和评估(Evals)依然是目前最棘手的难题。 这其实并不令人意外,但智能体的特性让这一问题变得更加复杂。与简单的 Prompt 不同,我们无法直接在外部系统中进行评估,因为需要注入的上下文信息实在太多了。这意味着我们必须基于可观测数据(Observability data)或在运行过程中进行埋点(Instrumenting)来完成评估。
遗憾的是,目前我们尝试过的各种方案,还没有一个让我们觉得真正找到了正确方向。我必须承认,目前我们还没有找到令人满意的评估方法。我由衷希望未来能有更好的解决方案出现,因为这已经成为 Agent 开发过程中最令人沮丧的一环。
09 Coding Agent 的进展
关于编程智能体(Coding Agent)的使用体验,近期变动也不大。最主要的进展是,我最近在更多地试用 Amp。如果你好奇原因,倒不是因为它在客观指标上比我正在用的工具更强,而是我非常认可他们在社交媒体上分享的关于 Agent 的设计逻辑。Amp 中不同子智能体(如 Oracle)与主循环之间的交互设计得非常优雅,目前很少有其他框架能达到这种水平。
同时,通过 Amp 也可以很好地验证不同 Agent 设计方案的实际效果。Amp 和 Claude Code 类似,给人的感觉是真正由开发者为自己量身打造,自己真正在使用的产品。说实话,业内并不是所有的智能体产品都能给我这种感觉。
END
本期互动内容 🍻
❓在构建智能体时,你更倾向于直接使用原生 SDK(如 OpenAI、Anthropic),还是选择高层抽象框架(如 Vercel AI SDK)?
原文链接: