上一篇讲的是 Agent 的最小闭环。
一个 Agent 不只是接收输入再输出文本。它需要在上下文里做决策,需要在必要时行动,需要观察行动结果,再决定是继续行动还是给出最终回答。
这一篇进入闭环里的“行动”环节:工具调用。
很多 Agent 原型一开始会很自然地写成这样:模型想做什么,就调用某个函数。要记忆,就调用一个写入函数;要查时间,就调用一个时间函数;要读文件,就调用一个文件函数。
这个做法在 demo 阶段很快,但如果系统要继续长大,它会很快变成问题。
因为工具不是几个随手调用的函数。工具是 Agent 能力边界的一部分。它需要被描述、注册、调度、记录、限制,也需要把执行后的结果重新交还给模型。
这就是为什么我认为,Tool Registry 应该成为 Agent 系统里的正式协议层。
工具列表的问题
把工具当成函数列表,最开始看起来很省事。
模型需要什么能力,就写一个函数。Agent Loop 里判断一下工具名,然后调用对应函数。函数返回结果,再把结果塞回模型。
但这个结构很快会暴露几个问题。
第一个问题是命名。模型看到的工具名,入口层调用的工具名,代码里的函数名,如果没有统一注册,就很容易分叉。今天叫 read_memory,明天叫 memory.read,后天又在 TUI 里写成 /recall。名字一多,系统就开始靠约定而不是协议运行。
第二个问题是描述。模型要调用工具,必须知道工具能做什么、需要什么参数、返回什么结果。如果工具只是代码里的函数,描述就会散落在 prompt、注释和入口层帮助文本里。描述一旦不同步,模型就会开始猜。
第三个问题是错误。工具会失败。参数可能缺失,外部服务可能超时,权限可能不允许,内部实现也可能抛异常。如果每个工具自己决定怎么失败,Agent Loop 就很难统一处理。
第四个问题是观察结果。工具调用不是普通函数调用。普通代码拿到返回值后可以继续执行,但 Agent 需要把这个结果变成模型能理解的 observation。这个 observation 应该进入下一轮上下文,让模型基于新信息继续判断。
第五个问题是权限。读记忆、查时间和执行 shell 命令显然不是同一种风险。如果工具没有风险元数据,安全策略就只能散落在各个 handler 或入口里。等到后面要接 CLI、TUI、Gateway、定时任务时,权限边界会变得越来越难收拾。
所以工具层不能只是一个函数列表。它至少要成为一个稳定的注册和执行协议。
Tool Registry 负责什么
Tool Registry 的核心作用,是把“Agent 能做什么”从一堆临时函数,变成一组可描述、可枚举、可调度、可审计的能力。
它至少应该负责几件事。
第一,工具描述。
每个工具都应该有稳定的名字和说明。名字给模型和系统使用,说明帮助模型判断什么时候该调用它。工具描述不是装饰,它是模型决策的一部分。
第二,注册和去重。
工具应该通过统一入口注册。重复注册同名工具应该被拒绝,否则系统里会出现两个工具都声称自己叫同一个名字的情况。Agent 最怕这种模糊边界。
第三,统一执行入口。
Agent Core 不应该知道每个工具背后是函数、HTTP 请求、数据库操作,还是未来的 sandbox backend。它只应该把工具名和参数交给 registry,由 registry 决定怎么执行。
第四,错误包装。
工具内部可以失败,但失败不应该让整个 Agent Loop 崩掉。Registry 应该把错误包装成统一结果,让模型收到一个明确 observation,而不是让入口层直接面对异常。
第五,结果格式。
工具执行后要返回一个稳定结构,至少能表达成功还是失败,以及给模型看的内容。这个结构不一定复杂,但必须统一。否则 Agent Core 就要为每个工具写特殊分支。
第六,风险元数据。
工具不只是能力,也代表风险。只读工具、普通状态修改工具、高风险执行工具,应该有明确区分。这个区分不等于完整安全系统,但它是后续 approval 和 sandbox 的入口。
Tool Registry 的价值不在于代码量,而在于它让工具层变成系统边界。
一次工具调用应该如何流转
一个最小工具调用流程可以长这样:
Model tool call
-> Registry lookup
-> Policy gate
-> Handler
-> ToolResult
-> Observation
-> Model
模型先生成一个 tool call。这个 tool call 至少包含工具名和参数。
Agent Core 不直接调用某个具体函数,而是把工具名和参数交给 Tool Registry。
Registry 先查找工具是否存在。不存在,就返回一个统一的失败结果。
如果工具存在,Registry 再经过 policy gate。第一版 gate 可以很简单,比如默认允许低风险和普通工具,拒绝明确标记为高风险的工具。
通过 gate 后,Registry 才调用真正的 handler。
handler 执行完以后,不直接把内部返回值甩给模型,而是产出统一的 ToolResult。Agent Core 再把这个结果转成 observation,放回模型上下文。
关键点是最后一步:工具结果必须回到模型。
如果工具调用后不回到模型,Agent 就只是“模型触发了一段代码”。只有当 observation 重新进入上下文,模型才能基于行动结果继续决策。
这也是 Agent Loop 和普通函数编排的差别。
ToolResult 不是普通返回值
在普通程序里,函数返回值是给调用者继续计算的。
但在 Agent 系统里,工具结果还有另一个读者:模型。
模型不理解 Python 对象,也不关心某个内部类的字段设计。它需要看到一个清晰、稳定、可解释的 observation。
比如记忆写入成功,模型需要知道“这条记忆已经存下来了”。比如工具被策略拒绝,模型需要知道“这个工具没有执行,因为策略不允许”。比如工具参数错误,模型需要知道“调用失败,缺少必要参数”。
这些结果都应该以统一方式回到 Agent Loop。
更重要的是,observation 应该被记录。
如果一次工具调用改变了系统状态,却没有进入会话历史,那么下一轮模型就可能不知道刚才发生了什么。Agent 会变得像一个健忘的自动化脚本:行动过,但无法基于行动继续推理。
所以 ToolResult 的意义不只是“函数返回了什么”。它是行动结果进入模型世界的桥。
为什么第一版就要有风险等级
很多人会觉得,安全边界可以等到后面再做。
这个想法很自然。第一版通常只有几个低风险工具,比如读写记忆、查看时间。看起来没有必要提前设计 approval 或 sandbox。
但问题是,如果一开始完全没有风险概念,后面加高风险工具时,安全策略就只能补在各处。
CLI 里补一段判断,TUI 里补一段判断,Gateway 里再补一段判断。shell 工具有自己的规则,文件工具有自己的规则,浏览器工具又有自己的规则。最后系统不是没有安全策略,而是有很多份互相不一致的安全策略。
更稳的做法,是第一版就给工具标一个简单风险等级。
safe -> 默认允许
normal -> 默认允许
dangerous -> 默认拒绝
这个分类不需要一开始就很复杂。
safe 可以表示只读、低风险的工具。normal 可以表示会改变系统内部状态,但影响有限的工具。dangerous 可以表示可能读写文件、执行命令、访问外部系统,或产生不可逆影响的工具。
第一版只要做到一点:明确标成 dangerous 的工具默认不执行。
这不是完整 sandbox。
它不能隔离文件系统,不能限制进程权限,不能防止所有外部副作用,也不能替代人工审批 UI。
但它有一个很重要的作用:让系统从第一天起承认工具是有风险等级的。后续要接人工确认、权限策略、sandbox backend,就有了稳定的挂点。
Policy gate 不是安全终点
Policy gate 很容易被误解成“安全功能已经做完了”。
不是。
一个 registry 内的 policy gate,只能决定“这个工具在当前策略下要不要进入 handler”。它不能证明 handler 本身安全,也不能控制 handler 里面到底访问了什么资源。
真正的 sandbox 要解决的是另一个层面的问题:工具即使被允许执行,也只能在受限环境里执行。比如只能读某个目录,只能运行白名单命令,只能访问指定网络域名,或者必须经过用户确认。
所以第一版 policy gate 更像一道门,而不是一座堡垒。
它的价值在于把门的位置定下来。未来要加锁、加门禁、加审计、加隔离,都围绕这个位置扩展。
如果没有这道门,后面的安全系统就很容易长在错误的位置。
第一版应该克制
工具层很容易被过度设计。
可以一开始就设计复杂 schema,可以一开始就做插件生命周期,可以一开始就做多级权限系统,也可以一开始就接一个完整 sandbox。
这些东西都可能有价值,但不一定应该出现在第一版。
第一版更应该关心几个基础问题:
- 工具是否有统一名字?
- 工具是否有统一描述?
- 工具是否通过统一入口注册?
- 工具执行是否通过统一入口?
- 工具失败是否能被统一包装?
- 工具结果是否能变成 observation?
- 工具是否有最小风险元数据?
这些问题解决了,工具层才算站稳。
站稳以后,后面再加文件工具、shell 工具、浏览器工具、HTTP 工具、审批流程、sandbox backend,系统都有明确位置可以接。
反过来,如果第一版只是在 Agent Loop 里塞一堆条件判断,功能也许能跑,但每新增一个工具都会让内核更难维护。
这一篇的结论
从头搭建 Agent,工具调用不能只是“模型说要调哪个函数,我就调哪个函数”。
工具是 Agent 的行动能力。行动能力必须有协议。
Tool Registry 就是这个协议的最小形态。它让工具可以被描述、注册、查找、执行、记录和限制。它也让 Agent Core 保持稳定,不需要知道每个工具背后的细节。
一个健康的工具层,至少应该让这条路径成立:
工具调用 -> 统一执行 -> 统一结果 -> 观察结果 -> 再次决策
这条路径跑通后,Agent 才能安全地增加更多行动能力。
Discussion