DeepSeek Harness 工具执行流水线:一次工具调用的守卫、执行与定格

AI资讯 13小时前 charles
690 0

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 TD    A[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

流水线从模型输出一个工具调用块开始:

  1. Assistant message 包含 tool-call 块——模型决定调用某个工具;
  2. Session event:tool/call 在执行之前就被记录下来(持久化事实,可回放);
  3. 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
定格后
仅内容不变式、同步通知
❌ 只读、不改写

九、几个值得记住的设计点

  1. 三处 waterfall 是"一次调用可被改写的全部":所有钩子、策略、审批都挂在这三个点上,过了 tools/post-execute 就再没人能动它;
  2. 守卫只能拦、不能放:单调守卫 deny or abstain,且身份受保护——这保证了"安全策略不可被绕过";
  3. 拿不到许可 = 拒绝ctx.approval 一次性询问,缺席或无法回答一律 deny,工具体被跳过;
  4. 文件系统先读后写不碰 schema:通过 fs/* 意图事件实现,是 tool-fs 之下的门禁,而不是工具定义的改动;
  5. 结果只有一份:中间再多的 replace / add context,最终都以单一 tool/result 事件呈现给模型——回放时只有一个"权威答案"。

一句话收尾:dsh 用"三道瀑布 + 一次定格"把一次工具调用变成了可审计、可拦截、可改写、且只留一份权威结果的过程。理解了这条流水线,就理解了为什么在 dsh 里加一个审批策略、换一个沙箱、或改造某个工具的行为,都是在"往流水线上挂插件",而不是改工具本身。


登录查看剩余 70% 内容

相关文章