ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

0基础深入理解DeepSeek Harness 架构【6】工具执行流水线:把权力关进流程里

0基础深入理解DeepSeek Harness 架构【6】工具执行流水线:把权力关进流程里 第 6 章 工具执行流水线把权力关进流程里本章围绕「工具执行流水线」展开系统拆解一次工具调用从被模型点名到结果回填所经过的每一道关卡先以最小工具示例建立基准再给出完整字段表与三道 waterfall 的选择判断表随后深入单调守卫、决策类型、参数与结果的「物化 冻结」、并发调度、沙箱与审批、PTC mode、后台任务与 UI 卡片等关键机制最后以三句话收束全章要点。标签工具执行流水线单调守卫waterfallPTC mode并发调度沙箱与审批后台任务模型能要求做的事里最危险的就是「调用工具」。删文件、跑命令、发请求——这些都发生在这一步。这一章讲清楚一次工具调用从被模型点名到结果回填中间到底过了几道关每一道关你能挂在哪里。6.1 先看一个最小的工具官方给的最小形态是这个样子。别看它短它是后面所有讨论的基准import{readFile}fromnode:fs/promisesimporttype{Context}fromdeepseek-ai/cordisimport{defineTool}fromdeepseek-ai/dsh-toolsexportconstnamemy-toolexportconstinject[tools]exportfunctionapply(ctx:Context){ctx.tools.register(defineTool({name:read_file,description:Read a file from disk.,// 模型看到的唯一说明parameters:{path:{type:string,required:true,description:Absolute path},limit:{type:number},// 默认可选},output:{schema:{type:string},render:(_args,value)[{type:text,text:value}],},asyncexecute(args,exec){// args 已经被校验并推导出类型{ path: string; limit?: number }returnreadFile(args.path,{encoding:utf8,signal:exec.signal})},}))}这里面有几个概念是新手第一眼看不出来的但都很重要代码里的东西它在做什么defineTool({...})一个类型化辅助函数。它把「参数推导 校验」和execute绑在一起所以args是强类型的。你不需要手写输入校验output.schemarender工具必须同时声明两件事函数体返回的「规范值」长什么样schema以及这个值怎么变成模型能读的内容render。这个分离是刻意设计的exec.signal取消信号。协议要求你必须遵守它——信号触发时取消进行中的工作ctx.tools.register()注册是副作用。dispose 这个插件的 fiber 即注销该工具为什么要区分「规范值」和「渲染内容」因为同一个结果有两种读者程序和模型。程序尤其是 PTC mode 里模型写的代码需要结构化、带字段的返回值模型需要一段可读的文字。把两者混在一起就只能靠解析自然语言来取 id 和字段——官方明确说了这是要避免的「工具主体不要返回内容块也不要迫使调用方从自然语言中解析 id 和字段。」6.2 完整字段表一个工具可以说多少事defineTool背后是ToolDefinition。它的完整字段如下。读这张表时请注意最右一列——它回答的是「这个字段模型能看到吗」。字段必须作用模型可见name/description/parameters是协议层字段。description 是模型理解这个工具的唯一依据可见deferLoading否请求把工具定义延迟加载进模型上下文可见output是规范输出声明schema强制校验函数体返回值、render纯投影成模型内容、presentationMeta可回放的展示数据否render 的产物才进内容execute是真正干活的函数。返回规范 JSON 值不是内容块否projectContent否在执行后策略之前装入「执行准备好的内容」否但影响内容finalizeContent否面向模型内容的同步最后一公里变换。注册表对每个归一化结果恰好调用它一次包括绕过 post-execute 的流水线失败。它必须不抛异常否但决定内容timeoutMs否工具自己的超时预算。由 timeout 策略插件作为tools/execute包装层强制执行。声明它就等于承诺这个工具会把exec.signal转给一个能收敛的合作实现否isConcurrencySafe(args)否纯同步分类器判断本次调用能否与兄弟调用并行。只有返回严格true才算并行否presentCall(args)否怎么在界面里显示「正在调用」的卡片否presentResult(args, result)否怎么显示「已完成」的卡片否关于这张表官方有两句话值得背下来注册表的schemas()通过显式允许列表构建面向模型的ToolSchema[]。唯独isConcurrencySafe的约定是只有true才算并行省略、抛异常、返回非true、或defineTool参数非法一律归为「独占」。反直觉并发安全默认是关的而且是「fail-closed」失败即关闭。你可能觉得默认值应该宽松一点。但对工具来说宽松意味着两个并发调用可能互相踩内存、抢同一个文件、产生竞态两件事抢同一份东西最后结果取决于谁先到。这类问题往往难以复现。所以设计者选了一个方向**想并行你得主动声明而且要保证自己干净。**官方还补了一句要求声明并行的执行不得修改父级拥有的状态共享状态必须容忍并发分派只有能交换顺序或失败即关闭的竞态两件事抢同一份东西最后结果取决于谁先到。这类问题往往难以复现才是允许的。6.3 流水线全貌一次调用要过几道关现在讲本章的核心。官方为工具执行画了一张完整的流程图把它压缩成一句话是tools/pre-executewaterfall 首先运行随后是单调守卫然后运行tools/execute和tools/post-executewaterfall。图 20三道 waterfall 都可以改写一次调用但它们的位置决定了「改写的时机」和「能看到什么」。6.4 三道 waterfall 怎么选一张判断表这是本章最实用的一张表。官方在工具编写参考里给出了明确的选择规则你想做这件事挂在哪个扩展点为什么是它可扩展的允许拒绝询问策略tools/pre-execute它是可重排的策略层。可以 deny、可以 ask走审批、也可以 cancel设置最终、不可撤销的拒绝ctx.tools.guard()单调守卫没有 allow 结果所以后续监听器无法把拒绝翻回放行给分派加截止时间、重试或指标tools/execute它是环绕分派包装层可以替换exec.signal但不能移除它替换展示内容或返回值、阻止结果、附加模型可见的上下文tools/post-execute它能拿到结果并返回PostToolDecision观察不可变的最终结果审计、指标、捕获tools/result它是 emit只观察。监听器失败会被隔离不会影响结果官方在实操手册里还给了一条很重要的原则尽量不要把部署策略内建到工具中。理由很直接如果每个工具都自己实现一套权限检查那么「统一收紧权限」这件事就会变成一个不可能完成的任务。正确做法是工具只管干活策略统一挂在这几个扩展点上。6.5 守卫为什么是「单调」的「单调」这个词在这里有一个非常具体的含义值得单独讲。普通的 waterfall 监听器可以返回 allow 或者 deny。这意味着两个监听器可以互相「翻案」A 说不行B 说行最后听谁的取决于顺序。这在权限场景下是不可接受的。所以守卫被设计成只能缩小权限/** * 单调执行守卫在每个 tools/pre-execute 监听器之后、工具本体之前求值。 * 返回一个 reason 就拒绝该调用返回 undefined 表示不改变结果。 * 因为守卫没有 allow 结果所以监听器顺序无法把一次拒绝翻回允许。 */typeToolGuard(execution:ReadonlyToolExecution)string|undefined关键这条设计让「安全」可以被局部推理你只要证明所有相关守卫都不会放行就能证明这个调用会被拒绝而不需要去分析监听器的注册顺序。这是「让不变量不依赖顺序」这一思想在权限上的具体应用——和第 3 章agent/turn-stopping用数据而非返回值表决是同一个思路。6.6 决策类型你能返回什么两个关键 waterfall 的返回值都是类型化的「决策」。下面把选项列全写插件时直接查这张表。前置决策 PreToolDecisiontools/pre-execute 返回返回后果{ kind: allow }执行这个调用{ kind: deny, reason, info? }物化它的面向模型的原因可选带上结构化的错误身份{ kind: cancel }选择「规范的取消结果」但不呈现为一个策略拒绝这个区别对用户很重要取消 ≠ 违规{ kind: ask, reason?, displayReason? }只有审批服务返回allowed-once才继续执行否则拒绝。reason是审计用的原因displayReason是本地化的提示文案官方特意说明了一点参数不可被改写因为「历史记录、审计、UI 和执行必须保持一致」。而且ask在没有审批通道、或者没有 agent 的情况下会直接变成拒绝。后置决策 PostToolDecisiontools/post-execute 返回返回后果{ kind: accept, content? }接受但替换展示内容。保留规范值和现有元数据{ kind: accept, value? }替换规范值。会重新校验并重新计算内容与元数据{ kind: block, feedback }阻止移除值转成isError结果并带上纠正性反馈给模型这里有一条很容易被误用的规则官方专门警告了内容替换是展示策略而非保密策略需要隐藏程序化值的监听器必须阻止或替换该值。也就是说你把内容换掉了但value里的原始数据还在程序仍然能拿到。真想保密得用block或者替换value。6.7 参数与结果为什么都要「物化 冻结」官方在执行的描述里反复出现「物化」「快照」「冻结」这几个词。它们对应的是一套很扎实的防御设计机制它防的是什么参数在策略开始前一次性物化为无损 JSON 并深冻结防「策略看到的值」和「工具收到的值」不一致。也防有状态的 getter 在「校验时」和「存储时」给出两个不同的值分配一个不透明的exec.token一个 Symbol只用于身份比较让调用身份无法被伪造。callId、name、arguments、agent、token、调用方的signal全程不可变包装层可以替换exec.signal但注册表在调用本体前会重新融合调用方的 signal防「加超时之后用户按停止就无效了」。替换不能切断调用方的取消能力最终产出是一个深度冻结的快照实时观察者和持久化看到的是同一个防「观察者偷偷改了结果存到磁盘上的就不一样了」失败一律被归一化为isError而不是让异常穿透让流水线的任何一环出问题都留下一条可读的结果而不是让整个轮次炸掉反直觉「抛出异常或返回无效值意味着isError」但官方的建议是成功的领域结果即使表示不理想的状态也应该写入规范值。比如一个命令以非零状态退出——这不是异常这是一个正常的观察结果应该作为值返回由渲染器去解释。只有基础设施故障才该抛异常。这个分界线新手很容易划错结果是「命令失败 工具报错」让模型误以为工具坏了。6.8 并发调度独占屏障与滚动池一轮里模型可能一次要求调用好几个工具。这些调用怎么调度官方给的机制是agent loop 向注册表查询每个待处理调用的执行模式并据此形成独占屏障一道挡住去路的关卡屏障两侧的执行不能交错必须先等这一侧做完和滚动池并行执行。执行模式只有两种typeToolExecutionMode|{kind:parallel}// 可以与兄弟调用重叠|{kind:exclusive}// 单独运行形成一个排序屏障图 21并行是为了快但结果的呈现顺序依然是确定的。这两件事被分开了。6.9 沙箱与审批谁能拦住一次调用工具执行这条流水线上有几个专门的「安全部件」它们各自负责一个维度部件ctx 键它管什么沙箱ctx.sandbox消费方交出即将执行 spawn 的确切 argv后端按每次调用的策略包装该 argv并报告强制执行情况沙箱策略ctx.sandboxPolicy统一保存部署默认模式和工作区根目录。只有沙箱执行器和提供方读它这样 bash 和 fs 不会限制到不同的根目录审批ctx.approval一次性权限决策通过approval/requestwaterfall 分派。没有回答方时以unavailable关闭失败也就是拒绝权限预设ctx.permissionPresets面向用户的预设表workspace-writedanger-full-access把沙箱模式和审批策略选项组合在一起。一次切换写一个permission/preset事件并贯通到两个选项事件文件系统观测策略fs-observation-policy通过fs/*事件门禁贡献基于观测状态的检查。官方的说法是「文件系统的先读后编辑检查位于tool-fs之下」关于审批有一个设计细节很值得学ask的语义是「allowed-once」——一次性放行。这意味着每次询问只对当前这一次调用有效不会变成一条永久规则。而如果没有回答方比如在没有界面的 headless 环境里它不会卡住而是以unavailable关闭失败。关键「没有回答方 拒绝」这个默认值方向很重要。如果默认是放行那么只要你把界面拿掉所有审批就形同虚设。安全类的默认值必须往严的方向偏。6.10 PTC mode让模型写代码来调工具PTC 是这一章里概念最新的一块。它的思路是与其让模型一次次单独调用工具不如让它写一段代码在代码里批量调用。官方对它的描述是在 PTC mode 中每个可见的已注册工具都可以通过await tools.name(args)调用无需额外集成。生成的ToolArgsMap和ToolOutputMap会根据同一组 schema 分别派生精确的参数类型与规范返回类型调用则重新进入正常的执行流水线。几个关键事实成功调用会解析为策略处理后的最终规范 JSON 值而不是渲染后的自然语言内容。这就是前面强调「规范值与渲染内容分离」的原因。失败调用会以真正的ToolCallErrorreject程序只能检查它的name、toolName和可读的message拿不到内部错误代码。传输层是run_code。桥接层在策略之前只记录配对 id、名称与规范化参数——不序列化描述、参数 schema 或 schema 字段。子调用携带父级 token记录tool/ptc-dispatch事件把拒绝呈现为有约束力的驳回并省略additionalContexts以保持调用与结果相邻。官方还给工具作者提了一条很实用的建议关于怎么设计output.schema请把output.schema设计为实用的程序化 API直接返回句柄与字段当标量、数组或 null 确实就是结果时允许采用相应的根类型将面向人类的解释放入output.render。以及一个容易忽略的资源事实**中间值只存在于执行期间不会被持久化也不会按提示词上限截断而且不设字节上限。**所以「如实声明的采集边界和进程内存」仍然是你自己的责任——只有外层run_code的日志和结果会受到输出上限和面向模型的 spill 流水线约束。图 22PTC mode 不是「绕过安全」而是「换一种调用形态」——流水线照样走。6.11 后台任务工具跑了很久怎么办有些工具不是「几秒钟出结果」而是「跑几分钟」。官方为这种情况提供了一套后台任务机制挂在一个独立的服务上。注册任务的方式是ctx.jobs.start({kind,label,owner:exec.agent,run})官方描述的关键语义有几个必须记住规则说明预先中止的调用算失败注册表会在进入 producer 主体前把「signal 已经中止」的调用判为失败——因为此时根本没有任务它的 id 无法满足成功输出的 schema发布 id 之后用任务自己的取消信号ctx.jobs.start()发布 id 后应该使用任务自有的取消信号而不是exec.signal。因为之后取消外层调用只应该停止「等待本次调用」不应该终止已经发布的工作前台工作仍然与exec.signal耦合不走后台的调用仍然跟着调用方的取消信号走生命周期归谁管已发布工作的生命周期归job_kill、owner dispose 和服务 teardown 所有而且后台任务不是「扔出去就不管了」模型侧有控制工具job_*可以读取、列出、终止任务。成功的后台分支返回类型化的规范句柄比如{ kind: background, jobId }。官方的警告很到位**PTC mode 绝不能通过解析「started background job bash-1」这种文本去取得 id。**要拿 id拿结构化字段。6.12 UI 卡片一层独立的渲染意图这一节讲一个常被忽视但很有价值的分离。工具在界面里长什么样和它给模型什么内容是两件独立的事。官方的做法是让工具返回一个「card 标签的渲染意图」——一个可辨识联合类型UI 桥接层据此分发。可用的卡片类型有这么几种卡片阶段长什么样 / 谁在用generic待执行 已完成默认卡片。可以带kind图标和locations涉及的文件让编辑器能跟随跳转terminal待执行 已完成shell 命令。已完成时携带原始输出、退出码、信号。dsh-tool-bash用它diff待执行 已完成文件创建修改的行内 diff。dsh-tool-fs的writeedit用它read仅已完成带行号、可选语法高亮的代码窗口search仅已完成发现型搜索的结果按文件分组的匹配或扁平路径列表web仅已完成web 检索kind: search或fetch注意readsearchweb没有「待执行」版本。官方的解释很直白内容只在execute之后才存在所以它们的 pending 状态保持为 generic 卡片。关于这层的写法官方给了几条硬性规则都是「违反会出问题」级别的**必须是纯函数。**这些方法在「实时流式输出」和「会话日志回放」两种场景下都会跑。所以不能做 I/O、不能读会话状态、不能用时钟或随机数。**UI 格式不进入模型结果。**围栏代码块、diff、相对化路径都不应该仅为服务界面而进入规范值或模型内容。**defineTool对展示路径做软校验。**格式错误或来自旧版日志的参数会让包装器返回undefined走通用回退而不是抛异常——展示绝不能导致回放崩溃。还有一个实用提醒**内置 Web Client 不消费presentCall或presentResult。**它读的是page与follow运输的原始tool/call和tool/result事件由客户端插件在 keyed slot 里注册自己的组件。所以「只定义 Host 展示方法不会增加专用 Web 卡片」。6.13 这一章要带走的三句话这一章要带走的三句话**三道 waterfall 一道单调守卫就构成完整的策略层。**工具只管干活策略统一挂在这几个点上。**守卫只能拒绝不能强制放行。**这让安全不依赖监听器顺序。**规范值和渲染内容是两件事。**前者给程序后者给模型展示卡片给界面——三个读者三套产物。
返回列表