资讯详情

10. 工具注册表与执行管线:一次工具调用要过五道关

📅 2026/9/15 2:22:08 | 华诺云谱 👁 阅读
10. 工具注册表与执行管线:一次工具调用要过五道关
你在哪运行示例的第 8、9、10 步。模型说我要调read_file之后到结果落进日志之前中间这一段。读完你会知道ToolDefinition的字段哪些给模型看哪些绝不能泄漏、五段管线各自能改什么、allow/deny/ask 三态决策与 fail-closed、并发安全声明的默认值为什么是不安全以及 Code Mode 下模型直呼原生工具为什么被直接拒绝。缩写对照表缩写英文全称中文JSONJSON Object Notation一种数据交换格式DSLDomain-Specific Language领域特定语言UIUser Interface用户界面SDKSoftware Development Kit软件开发工具包PTCProgrammatic Tool Calling程序化工具调用Code Mode一、角色回顾拥有工具定义的注册与作用域过滤一次调用从模型说要调到结果定稿的五段管线并发调度分类。刻意不做不自己做审批转交ctx.approval、不把宿主字段泄漏给模型。在示例中出场第 8 步读文件、第 9 步编辑、第 10 步跑命令触发审批。二、ToolDefinition三层字段一个注册的工具由三部分组成③ 呈现UI 用可重放presentCall()presentResult()② 宿主专用绝不进请求execute()output规范输出声明finalizeContent()timeoutMsisConcurrencySafe()① 模型可见ToolSchemanamedescriptionparameters这张图回答的问题为什么工具描述和工具实现在这个项目里是同一个对象的不同层。schemas()用显式白名单构造模型可见部分——第 8 篇提过加新字段时默认安全。强制的规范输出声明interfaceToolOutputDefinition{readonlyschema:JsonSchemaNode// 对每个成功值强制校验render(args,value):ContentBlock[]// 纯投影 → 模型可见内容presentationMeta?(args,value):JsonValue// 纯投影 → 呈现元数据}这是强制字段。工具的execute()只返回规范的无损 JSON 值怎么把它变成模型看的内容、怎么把它变成 UI 卡片是两个纯投影。这个拆分买到了什么同一次执行的结果可以在重放时得到一模一样的渲染。presentCall/presentResult的文档明确要求“Pure and side-effect-free: a UI may call it during live streamingAND a session-log replay, so it must depend only onargs.”两个宿主专用字段的默认值timeoutMs?:number// 省略 没有截止时间isConcurrencySafe?(args):booleanisConcurrencySafe的语义写得非常严“Onlytrueopts in; omission, exceptions, non-truereturns, and invaliddefineToolarguments are exclusive.”省略、抛异常、返回非true、参数非法——全部按独占处理。这是正确的默认值方向并发是需要证明的不是默认假设的。timeoutMs也有一条同样风格的声明性约束“Declaring itasserts this tool forwardsexec.signalto a cooperative implementation that can reach quiescence when the signal aborts.”即声明超时 你保证你的实现是协作式可取消的。因为注册表杀不死同进程代码——它只能取消不能强杀。三、五段管线ToolExecutionInput调用方给物化参数一次性无损 JSON 深冻结① tools/pre-executeallow / deny / ask② 单调守卫③ tools/execute环绕包装工具的 execute()④ tools/post-execute检查/替换结果⑤ finalizeContent工具自己的最后一公里tools/result不可变的权威结果这张图回答的问题一次工具调用有几个可插入点各自能改什么。逐段说①tools/pre-execute—— 三态决策 waterfall“Allow, deny, or ask before dispatch.next()delegates to allow;missing approval support turnsaskinto denial.”是 waterfall可重排next()一路委托下去的默认结果是allow返回ask但系统里没有审批能力 →变成拒绝fail-closed异步的门必须观察exec.signal注册表在它们 settle 之后重新检查取消但不抛弃它们的 promise作用域过滤派发agent 作用域的监听器只收到那个 agent 的调用。② 单调守卫monotonic guards在 waterfall 之后、执行之前的一层。单调意味着它们只会收紧不会放松——扩展点可以变得更严格但不能把已经拒绝的变成允许。③tools/execute—— 环绕式包装唯一可以替换那个必需的 signal的视图。超时策略插件dsh-tool-call-timeout-policy就是一个tools/execute包装器。④tools/post-execute—— 检查或替换结果⑤finalizeContent—— 工具自己的最后一公里这个回调有几个刻意的设计“The registrysnapshots this callback when execution startsand invokes itexactly once for every normalized outcome, including pipeline failures that bypasstools/post-execute… The callbackmust be total and must not throw.”执行开始时快照中途改注册不影响这次调用每个归一化结局都调一次包括绕过了 post-execute 的管线失败所以它拿到的是不可变的执行对象而不是类型化参数——因为非法输入和外层管线失败也会到它这儿返回undefined表示保留原内容其余字段仍归注册表所有。最后tools/result不可变的权威结果。注册表在tools/result的观察者跑起来之前把整个执行对象冻结。四、两个跨调用机制工具体拿到的是ToolRunContext比输入多两个方法deferContext(context:UserMessage):void// 把上下文推迟到本次结果抵达循环之后concludeTurn():void// 把成功结果标记为本回合到此为止deferContext的用途一个复合工具比如子代理工具在内部派发了嵌套调用嵌套调用产生的上下文需要带回给外层——但不能在外层调用还开着的时候注入。所以推迟到tool/result之后由循环追加。concludeTurn的传播规则很讲究标记骑在这次执行自己的结果上复合工具从嵌套结果转发它“所以只有权威的嵌套成功才能结束外层的运行”。五、并发调度驱动向注册表询问每个待处理调用的执行模式typeToolExecutionMode{kind:parallel}|{kind:exclusive}然后组成 barrier 和有界滚动池第 7 篇讲过调度侧。工具侧的义务“Opted-in executionsmust not mutate parent-owned state. Shared state must tolerate concurrent dispatch; recorder races are permittedonly when they commute or fail closed.”“只有当竞态可交换或者失败关闭时才允许”——这是一句很硬的并发规范。六、Code Mode 下的特殊规则codePTCpreset 里模型不直接调工具而是写一段 TypeScript 程序由run_code执行程序内部通过 SDK 调工具。这带来一个安全问题模型能不能绕过run_code直接调原生工具名答案是不能而且拒绝发生得非常早“Undermode: code,only calls WITH a parent may execute a native tool name— a model-direct call (no parent) isdenied asUNKNOWN_TOOLbefore the policy pipeline.”机制是ToolExecutionInput.parent外层传输执行的不透明 token有parent→ 这是run_code的子派发 → 放行没有parent→ 模型直呼 →在策略管线之前就报UNKNOWN_TOOL。报UNKNOWN_TOOL而不是权限拒绝也是刻意的——在这个模式下原生工具名对模型来说本来就不存在回想第 8 篇那条被过滤掉的工具与不存在的工具无法区分。Code Mode 还有一个额外的 waterfalltools/code-dispatch-log可以改持久事件里那份结果内容的副本而程序拿到的结构化value和模型可见结果不受影响。这样日志里记什么和程序看到什么可以分别优化。七、失败行为出什么事怎么办ask决定但没有审批能力拒绝fail-closed审批返回rejected/cancelled/unavailable一律拒绝——只有allowed-once才放行工具往meta里塞了不可序列化的东西Session.append在源头拒绝第 6 篇工具卡死不响应 signal注册表无法硬杀同进程代码它保住调用方的取消语义但不抛弃 promisefinalizeContent抛异常违反契约“must be total and must not throw”Code Mode 下模型直呼原生工具UNKNOWN_TOOL策略管线之前某个宿主字段不小心想进模型请求白名单挡住⚓ 回到示例第 8 步read_file(package.json)走完整条管线模型输出的arguments字符串{path:package.json}被解析、一次性无损物化、深冻结tools/pre-execute文件工具的策略监听器看了一眼——读工作区内的文件next()委托 → 默认allow守卫层通过tools/execute超时策略包了一层execute()通过ctx.fs读文件第 11 篇返回规范 JSON 值output.render(args, value)把它变成模型可见的ContentBlock[]tool/result落日志。第 9 步编辑 README多一件事文件工具把结果时刻的上下文 diff挂在tool/result的meta上——这就是第 6 篇seq 78那个meta也是 UI 上那张 diff 卡片的数据来源。presentResult(args, result)是纯函数所以重放时卡片一模一样。第 10 步bash pnpm lint是这一篇的重头戏tools/pre-execute跑起来。权限相关的监听器判断这是一次会产生副作用的命令执行当前会话策略是ask→返回ask短路不调next()第 4 篇讲的策略型监听器注册表把ask转给ctx.approval第 11 篇——注意注册表自己不弹窗、不认识 UIWeb UI 的答复者渲染弹窗。审批请求里故意不带工具参数——它通过callId挂在已经流式呈现出来的那次工具调用上“而不是渲染第二份可能漂移的副本”你点允许 →allowed-once只有allowed-once才继续其余三种结局全部拒绝管线继续守卫 →tools/execute超时包装→ bash 工具的execute()→ctx.shell→ 沙箱 → 真正 spawn第 11 篇输出经output.render变成模型可见内容 →tools/post-execute→finalizeContentbash 工具在这里施加自己的内容长度上限→tool/result。第 6 步那个只有allowed-once才放行值得再强调一次如果审批服务本身挂了返回unavailable这次调用会被拒绝而不是被放行。安全默认值的方向是对的。上一篇← 09 · LLM 接缝下一篇→ 11 · 能力接缝文件、命令、沙箱、审批、子代理回到→ 系列索引 返回专栏目录
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。