首页 / DeepSeek Harness 入门教程 / 工具执行管线

DeepSeek Harness 入门教程

工具执行管线

本教程共 32 篇 · 第 15 篇 · 更新于 2026-08-15 · 约 6 分钟阅读

dsh工具管线waterfall并行工具spill错误处理

本节目标:看懂模型发出工具调用后,dsh 内部按什么顺序处理它——哪些环节能拦、哪些环节能改结果、并行调用怎么保持顺序、超长结果怎么处理。

从 turn 到工具调用

先复习第 10 章的执行流骨架:

  • turn(轮次):一次完整任务处理,turn/start 打开、turn/end 关闭;
  • step(步骤):一次模型请求 + 它请求执行的工具,step/startstep/end
  • 工具调用:模型在 step 内发出 tool/call,执行完成后写回 tool/result

一条典型时序是:

turn/start → step/start → llm/stream → tool/call → 工具执行 → tool/result → step/end → turn/end

模型发出的工具调用会在执行之前先写入会话日志(tool/call 事件),结果回来再写 tool/result。日志是唯一真源,后面所有环节都围绕它转。

管线六段

一次工具调用不是「调一下函数」,而是走一条固定顺序的管线。官方流程图可以简化成六段:

pre-execute → 单调守卫 → execute → 工具主体 → post-execute → finalizeContent → result

第一段 tools/pre-execute:可重排的策略层。监听器返回三种类型化决策之一:

决策含义
{ kind: 'allow' }放行,继续走守卫与后续环节
{ kind: 'deny'; reason }拒绝,工具主体被跳过,错误物化给模型
{ kind: 'ask'; reason? }询问用户,只有审批服务返回 allowed-once 才继续

它叫「可重排」,是因为监听器可以调用 next() 把决定权传给下一个,多个策略插件的顺序可以在配置里调。沙箱、权限门禁、plan-mode 都挂在这一层。

第二段 单调守卫ctx.tools.guard() 注册的最终防线。它的返回类型故意没有 allow 分支——返回字符串就是拒绝,返回 undefined 维持现状。因为没人能「放行」,后注册的监听器永远无法把一次拒绝变回允许。要保证「某个工具永远不可用」时用它。

第三段 tools/execute:环绕分发。超时、重试、指标收集都包在这里。它拿到的视图允许替换 exec.signal(但不允许移除),注册表会在调用工具主体前重新融合调用方信号。

第四段 工具主体:注册的 execute() 真正执行。文件类工具的变更还会经过 fs/write-intentfs/edit-intent 守卫(第 17 章细讲)。

第五段 tools/post-execute:检查或改写结果。三种决策:

  • accept + 替换展示内容(保留规范值与元数据);
  • accept + 替换规范值(会重新校验并重算内容);
  • block + 反馈:把纠正反馈变成错误结果。

第六段 finalizeContent + tools/resultfinalizeContent 是工具定义自己拥有的同步回调,做「最后的仅内容修正」,恰好调用一次。之后注册表物化并冻结结果,触发 tools/result 同步通知。观察者只能「看一眼」冻结的权威结果,改不了,也影响不了主流程。

Tip

记忆法:pre-execute 决定「能不能做」,execute 决定「怎么做」,post-execute 决定「结果怎么呈现」,result 只负责「看一眼最终结果」。

并行工具调用

模型一次可以请求多个工具。dsh 怎么保证「并行执行」和「历史可重放」不冲突?

  • 并行是选择加入的:工具通过 isConcurrencySafe() 声明自己能否与兄弟调用重叠执行。只有返回 true 的调用才可能并行,其余一律排他(executionMode 返回 parallelexclusive)。并行调用的函数体不得修改父级拥有的状态。
  • 重叠的是执行,不是提交顺序:调度器按模型给出的数组顺序规划,parallel 组内可以并发跑,但结果按模型顺序提交——后面的工具先完成,也不能越过前面未完成的调用写入会话。模型看到的 call/result 配对永远是确定性的。
  • 两个 id 各管一件事callId 维持模型协议层的配对;事件 seq 关联日志里的来源(tool/result 通过 sourceEventSeqs 指回 tool/call)。
  • 取消不留半对:已启动的调用先结算;未启动的调用会按原顺序收到合成错误结果。

结果回填与 spill

工具结果通过 tool/result 事件回填:既写入会话日志,也作为 user 角色消息回到模型。但结果可能很大——grep 整个仓库、web_fetch 抓一篇文章,几十万字不稀奇。全塞进模型上下文既浪费又容易截掉有用的结尾。

dsh 的做法叫 spill(外溢)tools/post-execute 上的 spill 策略把超过 maxInlineBytes 的纯文本结果替换成「首尾预览 + 定位符」,完整文本交给 ctx.spillStore 保存到会话作用域的私有文件。模型拿到定位符和检索提示(retrievalHint),需要时再用 readgrep 按需读取。

几个关键性质:

  • spill 只作用于模型副本,完整文本始终保存,不丢数据;
  • 保存失败(磁盘满、没装后端)只记警告并保留原内联结果,不会把成功的工具调用改判失败
  • 定位符是后端返回的不透明句柄,本地后端渲染成文件路径,远程后端可能是 URI 或键,别假设它一定是路径。

失败处理

工具失败不会终止轮次,而是归一化成结构化错误结果:

  • 结果带 isError: trueerror: { name, code },code 是稳定、可机器路由的字符串(如 UNKNOWN_TOOLFS_STALE_VERSION),重试、权限、UI 层按 code 分支,不解析文本;
  • 抛异常的监听器被隔离,不会污染主流程;
  • 超时由 tools/execute 包装层执行,工具必须转发 exec.signal 才能配合取消;
  • 调用方取消后,未开始的调用报 ABORTED_BEFORE_DISPATCH,已开始的调用结算为 ABORTED
  • 模型侧的失败(如上下文溢出)走 agent/request-error,重试策略在这里决策——重试要求产生持久化的缩减(比如先压缩再重试),不能原地死循环。
Warning

工具执行管线里的参数在进入策略前已被冻结。任何监听器都不能改写参数——历史记录、审计、UI 与执行必须保持一致。想限制行为,用 deny 或 block,不要试图改参数。

小结

  • 一次工具调用依次经过 pre-execute → 单调守卫 → execute → 主体 → post-execute → finalizeContent → result。
  • 可重排的策略用 waterfall,不可撤销的拒绝用 guard。
  • 并行调用只重叠执行,提交顺序保持模型原序,callId 与 event seq 各司其职。
  • 超大结果 spill 到文件,模型拿到预览 + 定位符按需读取。
  • 失败归一化为 isError + 稳定 code,按 code 路由,不解析文本。