Skip to content

07|工具系统才是工程地基:注册表、拦截链、闸门如何构成统一边界 ​

先别把工具注册表当成"名字到函数的字典" ​

第 06 章刚说:模型面对的工具只是敲门的方式,门后面是能力定义本身。下一句误读几乎立刻跟上——

"那工具系统本身就是一张字典。循环查出函数,调用,把返回值塞回日志。"

这会把第 03 章刚拆开的循环又喂肥。超时写在循环里,审批写在循环里,每个工具自己写一套取消逻辑,界面根据工具名字去猜该画终端还是差异对比。

工具系统给出的不是字典,是一条管道:工具执行前的检查点 → 不可翻案的闸门 → 真正执行 → 执行后的检查点 → 工具自己定义的最终内容处理 → 只许观察的结果通知。这条管道存在的理由很直接:政策、钩子、沙箱、结果改写、最终观察、界面渲染,全部不改循环。

循环在第 04 章只做一件与工具有关的事:看见模型返回里的工具调用请求,先记一条耐久的"工具调用"事件,再问工具系统执行,最后把权威结果记成一条耐久的"工具结果"事件。它不认识具体某个工具,也不认识超时。


1. 登记的是定义,执行的是管道 ​

先给判断:注册收下的不是一个函数,是一份带契约的定义。函数只是定义里最后才被碰到的那一部分。

登记必须带规范化的输出契约——包括输出的数据结构和渲染意图,缺了当场报错。登记发生在哪个层级也有讲究——普通插件挂到全局层,某个 Agent 自己的作用域登记只对那个 Agent 生效,同名的全局定义在那个作用域里会被遮蔽。同一层里重复的名字会被直接拒绝。登记本身走的是第 02 章那套可逆效果,返回一个释放动作。工具的参数描述会自动流进提示词组装环节,循环组装提示词时不必再问每个工具插件要一份清单。

所以"模型看得见哪些工具"和"运行时能执行哪些工具"是同一张表的两种读法:一种读法去掉执行函数,只留给模型看的参数描述;另一种读法按同一套遮蔽和限制规则解析出真正能执行的实现。第 08 章会拆作用域这件事。这里只需看见:登记表不是给循环用的查找表,是给提示词组装、执行、界面卡片共用的那一份能力目录。

有一种特殊的工作模式把"目录"和"模型看见的形状"又拆开了一毫米:默认情况下,模型看到的工具就是普通的函数调用列表;另一种模式下,只暴露一个保留名的"跑代码"工具,模型如果在生成的代码里直接点名了别的工具,执行器会在真正的政策检查跑起来之前,就把这次调用判定为未知工具直接拒绝。这样做的原因是:这类调用注定失败,执行前检查、审批、闸门都不该看见它,更不该批准它。某些预设场景能拥有的是"呈现方式"的定制,不是把整个注册表都搬进会话。目录没变,只是组装方式塌缩了。

如果登记的只是函数会怎样?没有统一的输出契约,每个工具自己决定返回值长什么样,模型历史无法重建(第 05 章的纪律会立刻破产)。没有自动灌进提示词的参数描述,有人会在循环里手写工具列表,两份清单对不上。没有"先塌缩再走政策",一次注定失败的调用会先弹出审批框,再告诉你这个名字根本不该存在。


2. 执行前检查点是可协商的链,闸门是不能反悔的关卡 ​

判断:执行前检查点和闸门同时存在,不是重复。一个允许协商,一个不允许翻案。

执行前检查点被定义成一条可否决、可委托的拦截链:允许、拒绝、或者转成一次询问;不委托就是拍板,委托就交给下一层,默认是放行。没有审批通道时,"询问"会自动降级成"拒绝"。这个检查点故意不提供改写输入参数的能力——政策可以拦,不能偷偷改模型已经说出口的参数。第 02 章提到的钩子桥示例就挂在这里:拒绝时直接返回、不委托,放行时才委托,让后面的政策还有机会否决。

问完这条可协商的链,才轮到闸门。决策仍然是"允许"时,才会去问闸门;闸门里任何一个返回了否决理由,调用就按否决收场,不会进入真正的执行逻辑。否决结果仍然会进入后面的执行后检查点,政策还能改写给模型看的那句错误信息,但翻不成允许。

闸门的类型设计本身就消灭了特殊情况:返回一个理由字符串就是否决,返回"没有意见"就是不动。没有"我允许"这个选项。这是故意的——闸门没有放行结果,监听器的先后顺序不能把否决改回允许。挂闸门的方式也分层级:普通上下文挂的闸门全局生效,某个 Agent 自己的作用域挂的闸门查询时会沿着这个 Agent 的整条作用域链走一遍——这条链不只包含它自己,还包含它加入的共享预设父层(第 08 章会拆这条父链),所以挂在预设常驻作用域上的闸门,会连带管住加入这份预设的所有 Agent,不止挂闸门时正盯着的那一个。

为什么不把所有政策都做成闸门?因为钩子和审批需要协商:先问人,人说好再走。可协商链的顺序可以重排,后面的人还能否决前面已经放行的决定。为什么不把所有政策都做成可协商链?因为"所有者政策"——比如"这个 Agent 就是不许跑命令"——一旦被某个排在最前面的监听器委托掉之后改口放行,就不再是所有者政策了。两种机制并排,特殊情况消失:想协商,走可协商链;想钉死,走闸门。

审批画在执行前检查点和闸门之间:一次"询问"决策会去问一个审批服务,得到一次性的许可,通过之后再进闸门;拒绝、取消,或者审批通道根本不存在,都直接否决。审批是人机平面(第 10 章的主题),工具管道只规定它站在哪一步。注册表对审批服务不做强制依赖——没挂审批的部署,"询问"降成"拒绝",注册表照样能跑。这是第 06 章"消费者注入一个键,键可以不在"的又一次应用。

如果只有一张字典会怎样?每个工具的执行逻辑开头都要复制一份权限判断。有人忘了,有人写成异步竞态,有人在函数中间才检查,取消和审批的语义每个工具一套。管道把"能不能跑"从"怎么跑"里拔出来,循环和工具本身都不必认识政策。


3. 超时、指标、重试套在执行外面,不是循环里的条件判断 ​

判断:真正执行这一步是一层"环绕式中间件"。它碰得到正在跑的那次调用,所以超时住在这一层,不住在执行前检查点,也不住在循环内部。

这一层的合同很硬:委托下去会拿到已经规范化的结果;包裹层只能改这次调用的取消信号,调用身份本身不可变;注册表在真正进入执行逻辑之前,会把调用方原始的取消信号重新熔合进去——替换取消信号不能顺带取消掉本来的取消能力。真正调度执行的那一步兑现这条合同:把两个取消信号熔接起来,跑真正的执行逻辑,结束后把信号改回去。取消发生在执行启动之前,会标记成"启动前中止";启动之后的取消只能把成功结果替换成"已中止",已经启动的那个执行过程必须被排空、不能强行打断。

超时策略是这条合同的一个典型消费者:它依赖工具系统,监听"真正执行"这一步——工具没声明超时就原样放行;声明了超时就换上一个到期后触发的取消信号,等委托执行排空,确认真的是自己的定时器响了,才把结果换成一个带明确超时标记的结构化失败。结束时把上游的取消信号还回去,执行后检查点看不到这条可能已经被中止的超时信号。

这类插件的身份很明确:它们是核心服务和扩展点的自包含消费者,不是又一条可替换能力。超时不是第三条能力链,是挂在工具管道上的部署级政策。沙箱否决挂在执行前检查点或第 06 章那个沙箱实现者上;重复调用提醒挂在执行后检查点的附加上下文里。各站各的位,没有一个需要打开循环内部代码。

执行后检查点看的是已经规范化的结果:接受、换内容、换值、判定为失败、附带上下文。不能同时换值和换内容。判定失败会丢掉工具自己想追加的上下文,只露出否决方显式给出的上下文。然后才是工具定义自己拥有的最终内容处理:对每一次规范化结果恰好跑一次,包括绕过执行后政策的失败,只能换内容。再然后才是只许观察的结果通知——观察时,结果已经冻住。

有两个容易撞车的名字必须分清。"只许观察的结果通知"是一个活的事件,观察最终结果。真正记入日志的"工具结果"是循环随后追加的耐久会话事件。界面画完成态卡片读的是耐久事件,模型下一轮读的也是它。第 05 章的投影只认识后者。

如果超时写进循环会怎样?每个表面、每个司机都要抄一份。如果写进每个工具的执行逻辑,超时预算和错误码会有很多种拼写方式。如果做成执行前检查点的否决,你否决的是"还没开始的调用",套不住已经在跑的执行过程,也无法在排空之后把"工具自己的中止结果"替换成统一的超时码。环绕式中间件让特殊情况消失:大家都委托下去,谁拥有这次失败,谁替换结果。


4. 卡片长什么样,是工具设计的一部分 ​

判断:界面不准根据工具名字去猜。呈现意图跟参数描述一起出生,并且是参数的纯函数。

工具的界面渲染意图是设计的一部分,事先定成通用、终端、差异对比这几种标签(加上位置信息);渲染呼叫和渲染结果这两个方法都是参数的纯函数。这些呈现结果是带标签的数据,不是真正的界面组件。管道图里,渲染呼叫挂在工具调用记录之后、执行之前;渲染结果挂在耐久的工具结果记录之后。循环不画卡片,界面按标签选择渲染器。

这和第 06 章同一品味:模型契约、执行世界、呈现意图,变化速率不同。换一套界面不该逼你改某个具体工具的执行逻辑;换一套执行器不该逼你改终端卡片的字段。工具定义把三件事收在同一份定义里,但执行管道和呈现函数互不调用。

还有一些更细的机制——"跑代码"模式、并发分类、能力限制的组合规则——都还挂在这张注册表上,不是这一章的主角。你现在需要带走的只有切割线:循环面对管道入口,政策面对可协商链和闸门,超时面对真正执行这一步,工具作者面对执行逻辑、输出契约、呈现函数。谁越过切割线,地基就开始长成泥。


5. 结语:带走一句话 ​

工具系统的核心不是登记了多少函数,而是模型和运行时之间有没有一条统一的管道——政策、超时、沙箱、呈现都挂在这条管道上,循环永远不必认识某一个具体工具的执行逻辑。