本文系统解析DeepSeek Harness工具执行流水线,拆解工具调用中的三组瀑布式事件与结果定格机制,帮助你理解工具调用从发起到返回结果的完整生命周期。核心内容:1. 前置瀑布(pre-execute):守卫与准入机制(权限、沙箱、审批等)2. 执行瀑布(execute):执行控制机制(超时、重试、指标包裹等)3. 后置瀑布(post-execute):结果处理与结果定格(快照、替换、上下文注入等)

DeepSeek Harness工具执行流水线:一次工具调用的完整解析
本文围绕dsh官方参考中的「工具执行流水线」展开深入解读。在上一篇《Agent生命周期》中,工具调用在时序伪代码里只体现为一行tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*。而这篇文章将把这条链路完整展开,详细说明一次工具调用如何经过三组waterfall(瀑布式事件)逐步处理与改写,最终形成一条tool/result事件。
〇、先记住一句话
一次工具调用 = 三次 waterfall(pre-execute/execute/post-execute)+ 一次结果定格(快照 →finalizeContent→tools/result)。
- 前置瀑布(
tools/pre-execute)负责“能不能执行”:钩子、权限、沙箱、审批; - 执行瀑布(
tools/execute)负责“怎么执行”:超时、重试、指标包裹工具主体; - 后置瀑布(
tools/post-execute)负责“结果如何使用”:接受、阻止、替换、附加上下文; - 结果定格:注册表先对结果做无损快照,
finalizeContent执行最后的仅内容不变式,tools/result再同步通知,最终冻结为唯一一份对模型可见的权威结果。
只要理解这条主线,后续所有细节都只是对它的展开说明。
一、全景:一次工具调用的完整旅程
先看整体流程图,再逐段理解每个环节。
graph TDA[Assistant message 含 tool-call 块] --> B[Session event: tool/call执行前记录]B --> C[UI pending cardpresentCall args]C --> D[”tools/pre-execute waterfallhooks · permission · sandbox”]D --> E{单调守卫 deny 或 abstain+ ctx.approval 一次性审批}E -- ”denied 或 审批被拒” --> F[工具体被跳过denied · rejected · cancelled]E -- allow --> G[”tools/execute waterfalltimeout · retry · metrics 环绕 dispatch”]G --> H[注册的工具 execute body]H --> I[Tool-owned 事件todo/write · fs/observed · hook/* · tool/code-dispatch]I --> J[”tools/post-execute waterfallaccept · block · replace · add context”]J --> K[Registry 外层规范化无损快照 pipeline/result]K --> L[ToolDefinition.finalizeContent最后仅内容不变式]L --> M[tools/result 同步通知冻结的权威结果]M --> N[Active-batch additionalContexts FIFO结果之后注入 user/message]N --> O[Session event: tool/result单一 model-facing 结果]O --> P[Tool batch settled 批次结算]P --> Q[UI completed cardpresentResult args result]
这张图里最关键的一点是:tools/pre-execute → 单调守卫 → tools/execute → tools/post-execute 这三段 waterfall 可以对一次工具调用进行改写;而 finalizeContent 与 tools/result 则发生在其后,由工具定义自身收尾,不再参与调用改写。
二、起点:模型发出 tool-call
DeepSeek Harness 工具执行流程从模型输出一个工具调用块开始:
- Assistant message 包含 tool-call 块——意味着模型决定调用某个工具;
- Session event:
tool/call会在真正执行前先被记录下来(属于可持久化、可回放的事实); - UI pending card:界面会立即展示一张“进行中”的卡片,并通过
presentCall(args)向用户展示调用参数。
从这一刻开始,调用正式进入工具执行流水线。这里要特别注意顺序:先记录tool/call事件,再进入守卫逻辑——即使后续调用被拒绝,这次“尝试调用”的事实也已经被完整留档。
三、第一道闸门:tools/pre-execute waterfall
这是工具调用能否继续执行的第一道,也是最核心的一道闸门。它主要承载三类关注点:钩子(hooks)、权限(permission)和沙箱(sandbox)。各种插件通常都会挂接在这里,对调用作出判断与裁决。
3.1 可能的裁决
瀑布式事件允许每个监听者针对当前调用给出自己的决策:
| 裁决 | 含义 |
|---|---|
allow | 允许执行 |
deny | 拒绝执行(本轮调用不会继续) |
throw | 抛出异常(wrapper 抛错会继续向上冒泡) |
ask | 需要先询问用户 |
allowed-once | 仅本次放行 |
3.2 单调守卫(monotonic guards)
除了瀑布本身之外,注册表还维护着一组单调守卫:
- 每个守卫只能选择
deny或abstain(弃权)——也就是说,守卫不能主动放行,只能阻止或不介入; - 守卫身份受保护:它们的裁决不会被后续环节绕过、覆盖或重排;
- 所有“不得重新排序的所有者策略”同样以已注册守卫的方式存在——即使存在
ctx.approval之类的交互审批流程,这些守卫仍然会被执行。
3.3 ctx.approval:一次性审批
ctx.approval 提供的是一次性(one-shot)审批机制:当某个工具调用需要用户确认时,它会发起一次询问。
- 它会在单调守卫之前处理“询问”这一环节;
- 如果审批缺失、用户未响应或系统无法得到明确答复,结果统一按
deny处理——没有明确许可,就不会放行。
3.4 被拒后的结果
只要出现 denied 或 approval refused,工具主体(tool body)就会被完全跳过:既不会执行,也不会产生任何副作用,整个调用将直接以拒绝、取消或不可用等状态结束(如 rejected、cancelled、unavailable)。
小结:tools/pre-execute 决定的是“这次工具调用是否有资格执行”。
四、执行:tools/execute waterfall
通过守卫后,调用进入真正的执行阶段。这个 waterfall 主要负责把各种环绕分发(dispatch)的控制逻辑包在工具主体外层。
4.1 环绕关注点
| 关注点 | 作用 |
|---|---|
| timeout(超时) | 限制单次调用的最长执行时间,到期后强制终止 |
| retry(重试) | 允许对失败调用进行有限次数的重试 |
| metrics(指标) | 采集调用耗时、成功率等可观测数据 |
这些能力通常都由其他插件在 tools/execute 上完成包装实现,因此工具主体本身无需关注这些基础控制逻辑。
4.2 注册的工具 execute body
最内层是注册工具的execute()主体,也就是实际完成任务的代码。执行过程中通常会产生两类事件:
① 文件系统意图事件(仅针对 tool-fs 的变更)
fs/write-intent(写入意图)fs/edit-intent(编辑意图)
这些事件是“先读后写”策略中的关键门禁点:文件系统的先读后编辑检查位于 tool-fs 之下,通过 fs/* 事件实现。它一般由专门的策略插件(如 dsh-fs-observation-policy)挂接完成,而不是通过修改工具 schema 实现。
② Tool-owned 会话事件(由工具自身发出)
todo/write(任务列表更新)fs/observed(文件已被观察)hook/invoked、hook/result(钩子调用及返回结果)tool/code-dispatch(code 模式下的代码分派)
如果 wrapper 在执行过程中抛错(wrapper throws),异常会沿着 tools/execute 继续向上冒泡,并按 throw 路径处理。
五、收尾:tools/post-execute waterfall
执行结束后,返回结果会先进入后置瀑布。这里是“结果级处理”的最后一次改写机会:
| 行为 | 含义 |
|---|---|
| accept | 接受当前结果 |
| block | 阻止该结果(视为失败或直接丢弃) |
| replace | 使用新内容替换原结果 |
| add context | 为会话追加额外上下文 |
走到这里,一次工具调用在可改写层面的处理就全部结束了。接下来进入“结果定格”阶段——此后结果不会再被 waterfall 改写。
六、结果定格:从快照到权威结果
6.1 Registry 外层规范化:无损快照
注册表会先对候选结果执行外层规范化(outer normalization):
- 对
pipeline/result进行无损快照(snapshot); - 如果快照过程本身失败,会先把失败状态规范化(例如把
throws转成isError之类的结构),再继续后续不变式处理。
快照的核心价值在于:确保后续回调看到的是同一份固定结果,而不是可能被并发修改的活动对象。
6.2 ToolDefinition.finalizeContent:最后的仅内容不变式
finalizeContent由工具定义自身声明,是最后一道仅内容(content-only)不变式:
- 它以同步方式执行,并且只允许调整内容;
- 它处理的是已经通过快照固定下来的结果;
- 它不属于 waterfall 改写链路——这是工具定义自己完成收尾的最后一步。
6.3 tools/result:同步通知冻结结果
tools/result 是一次同步通知,用于将已经冻结、已经定型的权威结果分发给监听者。到这个时刻,监听者只能观察结果,不能再进行修改。
6.4 Active-batch additionalContexts FIFO
如果当前调用属于一个“活动批次”,该批次中的 additionalContexts 会按照 FIFO 顺序,在已记录的工具结果之后注入为 user/message。这样可以保证:新增上下文始终排在本批结果后面,不会打乱既有时序。
6.5 Session event:tool/result
最终,整个工具执行流水线会产出一条 tool/result 会话事件——这也是唯一一份面向模型(model-facing)的结果。无论中间经历了多少次改写,模型最终可见的只有这一个定格后的结果。
七、批次结算与 UI 呈现
- Tool batch settled:当一批调用(例如模型一次输出里包含的多个工具调用)的所有
tool/result事件都记录完成后,整个批次才会结算; - UI completed card:界面会把“进行中”卡片切换为“已完成”卡片,并通过
presentResult(args, result)同时展示原始参数和最终结果。
到这里,一次工具调用的完整流程就结束了:从 tool/call 到 tool/result,全链路都有记录、可审计、可回放。
八、三个 waterfall 能力速查表
| Waterfall | 时机 | 承载能力 | 能改写调用吗 |
|---|---|---|---|
tools/pre-execute | 执行前 | 钩子、权限、沙箱、审批(含单调守卫 + ctx.approval) | ✅ 可拒绝/放行 |
tools/execute | 执行中 | 超时、重试、指标;工具主体本体;fs/* 意图与 tool-owned 事件 | ✅ 可抛错 |
tools/post-execute | 执行后 | accept / block / replace / add context | ✅ 可改结果 |
finalizeContent + tools/result | 定格后 | 仅内容不变式、同步通知 | ❌ 只读,不再改写 |
九、几个值得记住的设计点
- 三处 waterfall 构成了“一次调用可被改写的全部范围”:所有钩子、策略与审批逻辑都挂在这三个节点上,过了
tools/post-execute之后,就没人能再改结果; - 守卫只能拦截,不能放行:单调守卫遵循
deny or abstain,且身份受保护——这保证了安全策略不会被绕过; - 拿不到许可就等于拒绝:
ctx.approval是一次性询问,缺席或无法回答都会被视为deny,工具主体将直接跳过; - 文件系统“先读后写”不修改 schema:它通过
fs/*意图事件实现,是位于tool-fs之下的门禁机制,而不是对工具定义本身做改造; - 最终结果只有一份:即使中间经历多次 replace 或 add context,最终呈现给模型的仍然只有单一的
tool/result事件——回放时也只认这一个“权威答案”。
总结:dsh通过“三道瀑布 + 一次结果定格”,把工具调用变成一个可审计、可拦截、可改写,并且始终只输出一份权威结果的标准化流程。理解这套 DeepSeek Harness 工具执行流水线 后,你就会明白,为什么在 dsh 中添加审批策略、更换沙箱,或扩展工具行为,本质上都更像是“在流水线上挂插件”,而不是直接修改工具本身。
登录查看剩余 70% 内容
