为什么不应该把 Agent 当作 Tool
很多人经常会问:"为什么 Agent 不能直接当 Tool 用?"这篇文章阐述了一个观点:为什么 Tool 和 Agent 应该被区别对待,以及为什么我们需要用不同的方式与它们交互。
当我们讨论 Agent 之间的互操作是否只是"在一个 Agent 中把另一个 Agent 当作 Tool 来调用"时,我们需要先定义不同类型的操作边界。为此,我们必须将思考的焦点转移到控制流决策的来源,以及决策所涉及的范围上。
Tool 的确定性本质
考虑一个可以使用多个 Tool 的 Agent。Tool 被用来执行操作——收集信息、做出变更,或者将信息从一种形式转换为另一种形式。在每种情况下,都存在一个时序关系:提供输入,返回某种输出。下图对此进行了说明,其中还包含了一个流式操作的示例。
Long Running Operations(LRO)在概念上与标准流程完全相同,只是时间轴更长。在实践中,其输出是一个指向 LRO 的引用,并通过单独的操作来查询其状态。
当我们谈论一个操作单元时,有一个隐含的预期:它遵循这样的时序结构——请求操作、执行操作、完成操作,以及在操作无法完成时的错误处理。在这个序列中,除了"进行中"、"已完成"或"错误"之外,不存在其他状态。这与传统的 API 结构一致,也是 Tool 概念的基础。Tool 是一种可以被要求执行操作、可以等待操作完成、并且能够报告错误的东西。
为了能解决问题,与另一个 Agent 的交互必须能够报告"需要更多信息"或"提出弥补缺陷的建议"。有人可能会尝试将这些信息塞进错误码处理或扩展 Tool 定义中的返回 payload,但这会将 Tool 从一个操作单元变成一个问题求解的参与者。如果用户(人类或 Agent)在问题求解过程中改变了目标,情况会变得更加复杂。本文的立场是:我们应该将这两种场景建模为不同的交互模式。
Agent 是问题求解的协作者
Agent 被期望具有自主性。这意味着它们有权根据可用信息和所处环境做出决策。它们能够处理不断变化的需求("我改主意了,不要 A 了,我想要 B")。举个例子,一个 Agent 被要求*"将用户的地址更新为 X"*。这听起来像是一个操作请求,应该简单地以成功或失败的结果完成。但实际上,根据 Agent 的环境和知识,情况可能要复杂得多。例如:
- 如果 Agent 追踪的唯一地址是电子邮件地址,而提供的却是实际邮寄地址,该怎么办?
- 如果 Agent 使用的系统在做此类变更前需要某种验证,比如提供地址证明,该怎么办?
- 如果 Agent 所对接的记录系统有特定要求,比如所有带地址的用户账户还必须有电话号码,该怎么办?
在这种场景下,Agent 不再是一个 Tool,而是一个问题求解者。作为问题求解者,它需要与调用方进行交互,以确定最佳行动方案。下图展示了 Agent 接口的多轮交互特性,其中 Agent 使用了 Tool 接口。
通过这种接口,Agent 可以向调用方返回信息,协同解决问题。例如:
- "抱歉,更新用户资料需要电子邮件地址,但您提供的是邮寄地址。"
- "可以更新地址。但首先需要通过 verify.my.address/ 完成地址验证。"
- "很乐意更新地址。但该账户目前没有电话号码,我需要电话号码才能完成地址更新。"
此时,调用方可以自行决定如何继续。也许它已经有了所需的信息可以直接提供,也许需要将问题上报给人类来处理等等。这与 Tool 流程的关键区别在于:操作在返回时不保证已经完成。操作可能处于未完成或中断状态——它已经开始了,也许某些子操作已经完成,但原始操作尚未完成。更进一步,操作有可能永远不会完成。例如,如果用户没有验证地址,那么更改地址的操作就永远不会完成。
结构化交互 vs 开放式交互
更深入地看,Tool 和 Agent 在输入输出的形态上存在本质区别。如前所述,Tool 定义了一个操作单元,这个定义可以通过高度结构化的输入和输出 Schema 来表达。这种结构限定了输入域 Ω 和输出域 ℝ,使得操作空间被严格约束。这种严格约束使得错误空间有了清晰的界定:任何不符合输入域的内容都是错误,任何无法用输出域表示的内容也是错误。错误如何处理取决于调用方,但错误状态的表示至少由一个错误码来定义。
因此,我们将 Tool 定义为一个有时间边界的操作,它使用结构化的输入和输出来定义函数映射:
ƒ(x ∈ Ω) → y ∈ ℝ,当 x ∉ Ω 或函数产生 y ∉ ℝ 时进入错误状态。
这个定义使得 Tool 的使用可以遵循结构化的预期,并支持模型对 Tool 使用进行推理——即确保模型能够生成合适的 x ∈ Ω,且目标输出为 y ∈ ℝ。如果返回了错误,模型可以推断出这两个条件中有一个未满足。它还可以假设操作完成时,要么返回错误,要么返回 y ∈ ℝ。操作的错误状态意味着操作失败了。再次调用时不会恢复之前的操作,而是用新的 x ∈ Ω 执行一个全新的操作。这是 Tool 和 Agent 之间的关键区别。
然而,在 Agent 交互中,Ω 和 ℝ 实际上是无界的。此外,当返回 y ∈ ℝ 时操作可能尚未完成;相反,y 包含的是某些增量信息,调用方应当消费、转换这些信息,然后传回给 Agent 以继续执行。这种朝着操作完成方向的迭代过程引入了一系列新的挑战,这也是我们提出这个接口不同于 Tool 接口的原因——因此 Agent 不应该被当作 Tool 来对待。
Tool 接口是 Agent 接口的退化情形。只有在你只需要支持退化情形的场景下——即 Agent 可以执行一个操作并看到它完成或报错,而不会进入需要恢复的中断状态——才应该将 Agent 当作 Tool 使用。
示例:旅行规划器
我们用一个更具体的例子来说明问题求解能力对于达成目标的关键作用。这个例子基于 a2a-samples 中的示例。问题场景是帮助用户规划一次旅行。这个旅行规划方案有多个参与者:一个编排器(Orchestrator)、一个规划器(Planner)、一个机票预订 Agent、一个酒店预订 Agent 和一个租车预订 Agent。该系统旨在帮助用户完成这样的任务:
"我想在七月去伦敦待 5 天,预算不超过 4000 美元。"
在这里,规划器必须制定搜索条件——例如预算分配以及关于地点和时间的参数。这个计划被用来与 3 个不同的 Agent 交互以制定方案。计划的每个部分可能需要特定信息,而这些信息可能需要编排器向用户询问。例如:
- "您从哪个机场出发?"
- "您想住在伦敦哪个地铁站附近?"
- "大多数人在伦敦不租车,您确定要租吗?"
关键在于,这是一个问题求解式的交互。什么样的参数组合才能满足问题空间的整体约束?这些约束甚至不是事先已知的,而是通过多次交互逐步明确的。
一旦完整的约束条件确定下来(包括预算、时间范围、更具体的个人偏好如舱位等级或更精确的位置等),问题就变得更容易识别出一组最优选项。用户可以从这些选项中选择,然后进行预订。搜索和预订都通过 Tool 完成,而在约束和搜索空间中的导航则通过开放式的、多轮的 Agent 交互来完成。
GOTO 与未来方向
这种框架可以类比传统编程中 GOTO 的使用。Agent 接口引入了一种类似 GOTO 的情形——控制流可能被迫离开预期的执行上下文,并且可能永远不会返回。它还可能被推入另一个 GOTO,跳转到另一个上下文。这种情形更难管理和设计。
正如围绕 GOTO 在编程中使用的争论一样——建议将其使用限制在少数几种结构中(如跳出循环)——我们同样建议应该将其限制在 Agent 与 Agent 的边界上。我们不应该将其推到 Tool 的边界,因为在 Tool 的边界上,可以强制执行结构化编程、提高可读性和可解释性,以及可调试性的要求。
如果我们遵循这些原则,每个独立的 Agent 都可以在其操作空间中以更可控、更受约束的方式运行,从而提高解决方案的可扩展性。
本文翻译自:Agents are not Tools