Agent X-Ray
RuntimeNotesAbout
Notes/源码拆解/DeepSeek Harness/第7章

第7章:工具系统 —— 五段流水线与两种呈现

6 分钟 · 更新于 2026-09-01

第7章:工具系统 —— 五段流水线与两种呈现

工具是 agent 的"手脚"。但手脚怎么被管住,才是架构题。这一章看 dsh 的工具系统:一次工具调用要经过哪五道关卡、工具向模型呈现的两种形态(原生 Function Calling 与 Code Mode)有什么区别、以及本机实测的 27 个工具是怎么来的。

核心结论先行:dsh 把「工具执行」拆成了注册表 + 事件流水线,工具本体只是执行器,所有横切策略(超时、溢出、权限、重复提醒)都是插在流水线上的监听器。


一、工具的两种形态:先解决"模型怎么看见工具"

在讲执行之前,先讲呈现 —— 因为这是 dsh 工具系统最特别的决策。

模型调用工具有两种范式:

范式机制优点缺点
原生 Function Calling把每个工具描述成 JSON Schema,模型返回结构化调用标准、可靠工具多时 schema 占大量上下文
Code Mode只给模型一个 run_code 工具,让它写代码来调其他工具上下文极小模型要会写代码

dsh-toolsmode 配置可以三选一 [官方文档]:

注册表还决定以何种方式向模型呈现工具:mode 配置可以选择原生 Function Calling、Code Mode,或同时选择两者;单个 agent 可用 presentAs 为自己遮蔽该默认值。 —— dsh-tools/README.zh.md

presentAs 是关键 —— 呈现方式不是全局的,可以按 agent 遮蔽。你可以在 preset 里给某个子 agent 配成 Code Mode(省上下文),给主 agent 配 Function Calling(可靠)。

Code Mode 长什么样

Code Mode 下模型看到的不是一个工具清单,而是一个 run_code 工具 —— 模型用代码(比如调用一个函数库)来间接使用其他工具。dsh-code-runtime-worker-thread 是它的执行后端。

理解:这类似 Claude Code 的 Bash 全能模式 —— 牺牲一点可靠性,换一个干净得多的上下文。第 1 章说过 dsh 的系统提示词实测 9927 字符,如果全用 Function Calling 塞 27 个工具的 schema,会大得多。Code Mode 是 dsh 控制上下文的杠杆之一。


二、五段流水线:一次工具调用的完整旅程

工具调用五段流水线|900

配图说明:pre-execute(允许/拒绝门禁,可以否决)→ 单调守卫(只能更严不能更松)→ execute(环绕分发包装层,超时/重试/指标挂这)→ post-execute(检查/替换结果,spill-policy 挂这)→ finalizeContent(工具自定义收尾)→ tools/result(只读通知)。底部是两种呈现:原生 Function Calling(每工具一个 schema,可靠但占上下文)vs Code Mode(一个 run_code 工具,上下文极小)。

dsh-tools 的 README 第一段就给出了完整流水线 [官方文档]:

工具插件注册各自的 schema 和执行器;agent loop 依次让每次调用经过 tools/pre-execute(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → tools/execute(供超时/重试/指标插件使用的环绕分发包装层)→ tools/post-execute(检查/替换结果、附加上下文)→ 由工具定义持有的 finalizeContent 边界 → 仅观测的 tools/result 通知。 —— dsh-tools/README.zh.md

画成图:

text
模型返回 tool call
        │
        ▼
① tools/pre-execute     ← 允许/拒绝门禁(可以否决)
        │
        ▼
② 单调守卫              ← 已注册的守卫,只能收紧不能放开
        │
        ▼
③ tools/execute         ← 环绕包装层(超时/重试/指标插件挂这)
        │
        ▼
④ tools/post-execute    ← 检查/替换结果、附加上下文
        │
        ▼
⑤ finalizeContent       ← 工具定义自己的收尾
        │
        ▼
⑥ tools/result          ← 只读通知(观测用)

逐段解释:

tools/pre-execute —— 允许/拒绝门禁 可扩展的决策点。监听器可以 block(拒绝)或 accept(放行)。拒绝时工具不执行。

② 单调守卫(monotonic guard) "单调"的意思是:守卫只能让结果更严格,不能让结果更宽松。这是安全关键 —— 如果某个守卫能放开另一个守卫的拒绝,防线就破了。守卫注册后对每次调用生效。

tools/execute —— 环绕分发包装层 这是"横切策略的挂点":超时策略、重试策略、指标收集都包装在这一层。它们包住真正的工具执行器,而不需要改工具本身。

tools/post-execute —— 检查/替换结果 可以改写结果。第 9 章会看到 spill-policy 就挂在这里 —— 超长结果被替换成预览 + 文件路径。

finalizeContent —— 工具定义的收尾 工具自己持有的边界,处理"结果怎么变成最终内容"。

tools/result —— 只读通知 仅供观测(日志、UI、统计)。不能改结果。


三、实测:本机 27 个工具的完整清单

从会话日志的 request/header 事件提取 [实测],27 个工具按功能分组:

分组工具
文件系统read · write · edit · glob · grep
执行pwsh(Windows)/ bash(Linux,本机未启用)
任务管理todo_write
目标create_goal · get_goal · update_goal
后台任务job_output · job_list · job_kill
子 agentsubagent · subagent_fork · send_message · interrupt_agent · list_agents
编排workflow · ralph
搜索web_search
提问ask_user_question
技能skill
图像generate_image_gpt · generate_image_gemini
计划exit_plan_mode
图像读取read_image

工具清单为什么是 27 个? 回看第 3 章:工具属于 agent 平面,由 agent preset 决定。本机的 dsh-toolbelt preset 装了什么,模型就有什么工具。官方基础工具(pwsh/fs/glob/grep/todo/web…)加上 toolbelt 的图像生成、跨 agent 记忆等,凑成了 27 个。

这也解释了为什么两个 dsh 实例的工具清单可能完全不同 —— 工具集是配置,不是代码。


四、三个横切策略插件

第 4 章说过:横切关注点不占 seam,做成事件监听器。工具系统里就有三个活例子:

4.1 dsh-tool-call-timeout-policy:给每个工具装截止时间

工具调用超时策略:一个 tools/execute 包装器,在 exec.signal 上武装每工具截止时间,超时则返回 TOOL_TIMEOUT。 —— dsh-tool-call-timeout-policy/README.zh.md

工具自己的 schema 可以声明 timeoutMs(比如 web 工具的 fetchTimeoutMs / searchTimeoutMs 配置),策略在 tools/execute 层强制。

理解:超时不在工具内部实现,而在流水线上统一实现。任何工具想有超时,声明一个字段即可。

4.2 dsh-spill-policy:结果太长就溢出到文件

工具结果 spill 策略:一个 tools/post-execute 转换器,防止过大的纯文本工具结果进入模型上下文。当最终结果超过 maxInlineBytes 时,通过 ctx.spillStore 保存完整文本,并将面向模型的结果替换为有界的首尾预览、后端定位信息与取回指引。 —— dsh-spill-policy/README.zh.md

这正是本教程写作过程中反复出现的机制 —— 超长工具输出会变成:

text
(首尾预览……)

(Omitted N bytes. Full formatted result stored at: /.../xxx.txt. Use read with offset/limit, or grep this path to search within it.)

两个刻意的跳过项:嵌套执行跳过(复用外层结果)、read 工具跳过(避免 read → spill → read again 循环)。

4.3 dsh-repeat-tool-reminder:重复调用提醒

重复工具调用守卫插件:当 agent 在完全相同的工具调用上循环时给出建议性提醒。 —— dsh-repeat-tool-reminder/README.zh.md

注意"建议性"(advisory)—— 它只提醒,不拦截。真正的拦截属于 pre-execute 门禁。


五、实例解剖:dsh-tool-fs 的三个工具

以文件系统工具为例,看"工具"和"策略"怎么分工。

dsh-tool-fs 是三个面向模型的工具的执行器:

工具行为
read读取文件,返回编号行 + 渲染 footer
write原子写入 + 创建目录
edit字面量替换

而"编辑前先读"、"版本防护"、"写前检查"这些策略不在工具里,在 dsh-fs-observation-policy —— 第 4 章解剖过,它是通过 fs/* 事件门禁工作的,没有服务

dsh-tool-fs 的 README 用一句话点破:

工具就是执行器;策略是事件门禁。 —— dsh-tool-fs/README.zh.md

翻译dsh-tool-fs 只负责"面向模型的 schema + 调用 ctx.fs + 渲染结果",所有"该不该做"的判断都在流水线的其他环节。工具层换成别的实现,策略一行不动。


六、工具参数 schema 与 UI 呈现

两个补充设计:

Schema 子集dsh-tools 强制执行一个"原始 JSON Schema 子集"(官方文档有专门小节),工具定义用受限的 schema 描述参数。这保证所有工具的 schema 都能被可靠地验证和呈现。

工具自带 UI:每个工具定义可以声明 presentCall / presentResult,把工具调用在 UI 里渲染成卡片而不是裸 JSON。第 12 章会看到 Web 层的 dsh-client-ui-tool 怎么消费它。


七、动手复核

powershell
# 1. 自己会话里模型的工具清单(从 request/header 事件提取)
# 用第 5 章的会话解压脚本,取最新的 request/header,读 header.tools

# 2. 流水线的五个事件
Select-String -Path "$env:DSH_HOME\profiles\node_modules\@deepseek-ai\dsh-tools\lib\index.js" `
  -Pattern 'pre-execute|post-execute|finalizeContent|tools/result' | Select-Object -First 10

# 3. spill-policy 的跳过规则(read 工具跳过)
Select-String -Path "$env:DSH_HOME\profiles\node_modules\@deepseek-ai\dsh-spill-policy\lib\index.js" `
  -Pattern 'exec.name === "read"' -Context 2,2

# 4. 27 个工具的实际名字
# 在会话日志里 grep '"name":"pwsh"' 这类工具调用记录,或看 request/header 的 tools 数组

八、总结

工具系统是 dsh 流水线思想的典型体现:

  1. 两种呈现:Function Calling(可靠)vs Code Mode(省上下文),mode 配置 + presentAs 按 agent 遮蔽
  2. 五段流水线:pre-execute 门禁 → 单调守卫 → execute 环绕 → post-execute 变换 → finalizeContent → result 通知
  3. 工具是执行器,策略是事件:超时、溢出、重复提醒全是挂流水线的监听器
  4. 27 个工具是配置产物:由 agent preset 决定,不是代码写死的
  5. UI 呈现由工具自声明:presentCall / presentResult 让工具调用可视化

下一章是这一切落盘的地方:会话 —— 事件溯源与 surface 投影


  • README-教程总览
  • 第6章-模型调用-provider中立的LLM-seam
  • 第8章-会话-事件溯源与surface投影
  • 第10章-沙箱与权限-fail-closed的四层防线 —— pre-execute 门禁与沙箱的关系

本章目录
一、工具的两种形态:先解决"模型怎么看见工具"二、五段流水线:一次工具调用的完整旅程三、实测:本机 27 个工具的完整清单四、三个横切策略插件五、实例解剖:dsh-tool-fs 的三个工具六、工具参数 schema 与 UI 呈现七、动手复核八、总结Related Documents
苏ICP备2025204887号-2