工具系统:模型怎样把“想做”变成“真的做”?
工具如何注册、展示、审批、执行,并把结果送回 Agent Loop?
模型会分析和生成文字,却不能直接读取你的磁盘、运行测试或访问网页。真正让 Agent 从“会说”变成“会做”的,是 DeepSeek Harness 的工具系统。
这一课不罗列每个工具,而是看所有工具共同遵守的路线:怎样加入列表、怎样让模型看懂、怎样接受权限检查,又怎样把结果送回下一轮思考。
工具不只是一个函数
假设你开发了一个“查询订单”工具。只写一个 queryOrder() 函数还不够,因为模型和系统还需要知道:
- 它叫什么;
- 什么时候应该使用;
- 参数有哪些、哪些必填;
- 成功后返回什么;
- 具体怎样执行;
- 在界面上怎样展示。
因此,一项 DSH 工具更像一份完整的岗位说明书:名字和参数给模型看,execute 是真正干活的人,输出声明负责验收结果,展示信息告诉 UI 怎样画卡片。
工具插件启动时,会把这份定义注册到 ctx.tools。多个插件可以各自注册工具,最终组成当前 Agent 可以看到的工具集合。
同一个 Host,为什么不同 Agent 能看到不同工具
工具注册表不是一张所有 Agent 完全共享的平面清单。它会结合 Cordis 作用域计算可见性:
- Host 层注册公共工具;
- Preset 层加入某类 Agent 专属的工具;
- Agent 层还可以进一步限制或增加工具。
例如,代码 Agent 可以看到文件、终端与 LSP;客服 Agent 只看到订单查询和退款申请;审计 Agent 可能只保留只读工具。
这也是为什么“给 Agent 增加工具”通常应该放在 Preset 中装配,而不是修改 Agent Loop。Loop 只向 ctx.tools 索要“当前作用域最终可见的工具”。
DeepSeek Harness 内置工具全景表
下面这张表依据源码生成的 docs/tool-catalog.zh.md 整理,覆盖当前仓库中已发布产品工具包提供的全部模型可见名称,包括按需启用和实验性工具。它不包含示例项目里的演示工具、外部 MCP Server 带来的工具,也不包含用户通过插件动态注册的新工具。
| 类别 | 工具名 | 主要用途 | 默认可见情况 |
|---|---|---|---|
| 用户交互 | ask_user_question | 暂停当前调用,向用户展示问题或选项并等待回答 | 标准、PTC、创造模式 |
| 规划 | exit_plan_mode | 提交完整计划供用户评审,获批后退出 Plan Mode | 完整 Preset;仅在 Plan Mode 下执行有效 |
| 任务清单 | todo_write | 写入或整体更新当前 Session 的结构化待办列表 | 标准、PTC、创造模式 |
| Code Mode | run_code | 运行模型编写的 TypeScript,通过 SDK 组合多个工具调用 | PTC 模式 |
| Shell | bash | 执行 Bash 命令;不同 Preset 可采用一次性进程或持久终端实现 | 非 Windows 环境;具体实现由 Preset 决定 |
| Shell | pwsh | 执行 PowerShell 命令;是 Windows 环境下与 bash 对应的工具 | Windows 环境;具体实现由 Preset 决定 |
| 文件编辑 | str_replace_editor | 查看、创建文件,执行唯一文本替换或按行插入 | 极简模式 |
| 文件系统 | read | 按行读取 UTF-8 文本文件 | 完整 Preset |
| 文件系统 | read_image | 读取 PNG、JPEG、WebP 或 GIF,作为图像交给支持视觉的模型 | 完整 Preset;需要图像能力与附件服务 |
| 文件系统 | write | 创建文件或用完整内容覆盖文本文件 | 完整 Preset |
| 文件系统 | edit | 通过精确的字面量替换编辑已有文本文件 | 完整 Preset |
| 文件搜索 | glob | 按 Glob 模式搜索文件路径 | 完整 Preset |
| 文件搜索 | grep | 使用 ripgrep 正则表达式搜索文件内容 | 完整 Preset |
| 语言服务 | lsp | 查询定义、引用、实现和 Hover 等语言服务器信息 | 按需启用,需要 LSP Provider |
| 持久终端 | terminal_open | 创建一个可以跨多次调用保留状态的终端会话 | 按需启用 |
| 持久终端 | terminal_list | 列出当前 Agent 拥有的持久终端 | 按需启用 |
| 持久终端 | terminal_read | 读取持久终端自上次读取后的输出 | 按需启用 |
| 持久终端 | terminal_send | 向持久终端发送命令或输入,也可转为后台任务 | 按需启用 |
| 持久终端 | terminal_signal | 向终端中的进程发送中断等信号 | 按需启用 |
| 持久终端 | terminal_close | 关闭持久终端并等待其进程树退出 | 按需启用 |
| 网页 | web_search | 搜索 Web 上的最新信息并返回摘要与来源 | 完整 Preset;需要 Web Provider |
| 网页 | web_fetch | 获取 HTTP(S) 页面并解码为文本 | 由 tool-web 配置决定,系统 Preset 默认可关闭 |
| 长期目标 | create_goal | 为需要跨多个自主 Round 推进的工作创建持久目标 | 完整 Preset |
| 长期目标 | get_goal | 读取当前目标、阶段、修订号和阻塞状态 | 完整 Preset |
| 长期目标 | update_goal | 编辑、暂停、恢复、完成目标,或将其标记为阻塞 | 完整 Preset |
| 定时任务 | schedule_create | 创建延时、定时或固定间隔的 Session 提醒 | 按需启用 Schedule 插件 |
| 定时任务 | schedule_list | 查看当前 Session 的活动提醒及其状态 | 按需启用 Schedule 插件 |
| 定时任务 | schedule_delete | 删除一条活动提醒 | 按需启用 Schedule 插件 |
| Skills | skill | 从当前目录中加载某项 Skill 的完整操作说明 | 标准、PTC、创造模式 |
| 工作流 | workflow | 运行 JavaScript 编排脚本,批量组织多个子 Agent 完成复杂任务 | 标准、PTC、创造模式 |
| 工作流 | ralph | 围绕固定目标启动多轮全新子 Agent,直到完成、阻塞或达到轮数上限 | 标准、PTC、创造模式 |
| 后台任务 | job_list | 列出后台 Shell、终端或子 Agent 任务及其状态 | 完整 Preset |
| 后台任务 | job_output | 获取后台任务的新增输出或最终结果 | 完整 Preset |
| 后台任务 | job_kill | 请求停止指定后台任务 | 完整 Preset |
| 子 Agent | subagent | 把一项独立任务委派给新的子 Agent | 标准、PTC、创造模式 |
| 子 Agent | subagent_fork | 携带当前已完成对话上下文,派生一次性子 Agent | 标准、PTC、创造模式 |
| 子 Agent | list_agents | 查看已创建的后台子 Agent 及其当前状态 | 完整 Preset;Agent Teams 也复用此名称 |
| 子 Agent | send_message | 向可继续工作的后台子 Agent 发送下一轮消息 | 完整 Preset;Agent Teams 也复用此名称 |
| 子 Agent | interrupt_agent | 中断后台 Agent 当前正在执行的轮次 | 完整 Preset;Agent Teams 也复用此名称 |
| 子 Agent | report | 子 Agent 主动向直接父 Agent 汇报进度或最终结果 | 仅在对应子 Agent 内可见 |
| Session 查询 | session_search | 在当前工作区的历史 Session 中搜索相关事件 | 按需启用,只读 |
| Session 查询 | session_trace | 查看一个 Session 的祖先、后代等谱系关系 | 按需启用,只读 |
| Session 查询 | session_event_search | 在指定 Session 的历史事件中搜索内容 | 按需启用,只读 |
| Session 查询 | session_event_read | 读取一个完整事件以及可选的相邻事件概述 | 按需启用,只读 |
| Session 查询 | session_event_trace | 查看事件的替换关系及其引用的来源事件 | 按需启用,只读 |
| Cordis 创作 | cordis_inspect_list | 列出当前 Host 与 Client 可用的只读 Inspect Provider 和方法 | 创造模式 |
| Cordis 创作 | cordis_inspect_query | 查询服务、事件、工具 Schema、UI Slot 或主题等精确信息 | 创造模式 |
| Cordis 创作 | cordis_inspect_self | 查看当前 Session 创建的动态 Plugin、版本源码和运行诊断 | 创造模式 |
| Cordis 创作 | cordis_define | 定义一个新的动态 Cordis Package,或为已有 Plugin 增加新版本 | 创造模式 |
| Cordis 创作 | cordis_run | 激活、更新或回退动态 Plugin 的某个 Package | 创造模式 |
| Cordis 创作 | cordis_stop | 停止动态 Plugin,同时保留定义和版本以便再次运行 | 创造模式 |
| Cordis 创作 | cordis_undefine | 永久移除当前 Session 创建的动态 Plugin 及其 Package | 创造模式 |
| Agent Teams | spawn_teammate | 创建一个具名、持久的团队成员 Agent | 实验性,默认禁用 |
| Agent Teams | followup_task | 向团队成员发送后续任务,并在需要时启动新一轮工作 | 实验性,默认禁用 |
| Agent Teams | team_task_create | 在共享团队任务板上创建任务 | 实验性,默认禁用 |
| Agent Teams | team_task_get | 读取一项共享任务的最新完整内容 | 实验性,默认禁用 |
| Agent Teams | team_task_list | 按状态、负责人或就绪情况列出共享任务 | 实验性,默认禁用 |
| Agent Teams | team_task_update | 领取、编辑、完成、重开、转交或删除共享任务 | 实验性,默认禁用 |
| Agent Teams | wait_agent | 等待团队成员状态、消息或共享任务发生变化 | 实验性,默认禁用 |
需要特别注意,表里的“内置”只表示 DSH 仓库已经提供了相应工具插件,并不表示每个 Agent 会一次性看到它们。标准模式、极简模式和创造模式会装配不同的工具;某些工具还要求 Host 先提供 LSP、Web、Schedule 或 Terminal 等底层服务。这样既能保持核心 Loop 稳定,也避免把与当前任务无关的大量工具说明全部交给模型。
bash 与 pwsh 还有一次性执行和持久执行两套插件实现,但它们对模型暴露的是同一个工具名。实验性的 Agent Teams 插件则会复用 list_agents、send_message 和 interrupt_agent 三个名称,把目标从普通后台子 Agent 换成持久 teammate,因此表中不再重复列出。
第一步:把工具说明交给模型
每个 Step 的 preStep() 会向 ctx.tools 获取当前 Agent 的工具说明,然后放进模型请求。
普通 Native 模式下,模型看到的是工具名、说明和 JSON Schema。它并没有拿到 execute 函数,只得到一份“可以怎样申请调用”的菜单。
模型返回 Tool Call 时,也不是直接运行本地代码,而是返回类似下面的结构化意图:
tool = "query_order"
arguments = { "order_id": "A1024" }
Agent Loop 收到这个请求后,才把它交给工具注册表。
第二步:工具调用进入统一流水线
工具注册表不会找到函数以后立刻执行。每次调用都会经过一条统一路线:
可以把它想成公司采购:提交申请以后,先检查表单,再走权限审批,随后由采购员执行,最后验收并归档。所有部门都走同一套流程,安全规则就不需要复制到每个工具里。
第三步:权限插件可以在执行前介入
上一课介绍的四种工作模式,最直接的区别之一,就是当前 Agent 能看到哪些工具,以及这些工具以普通 Tool Call 还是 Code Mode SDK 的形式呈现。至于某一次工具调用能否真正执行,还要经过这一课介绍的审批与安全检查。
工具进入 pre-execute 后,审批插件可以返回三种决定:允许、拒绝或询问用户。随后,文件沙箱和其他 Guard 还会检查这项调用是否越过不可突破的限制。
这两层作用不同:
- 审批解决“这一次要不要征求用户同意”;
- Guard 与 Sandbox 解决“即使有人同意,也绝不能越过什么边界”。
业务插件同样可以参与。例如公司内部 Agent 可以在退款工具执行前检查金额,在发布工具执行前要求工单编号,在查询客户数据前验证当前租户。
这些策略都挂在工具流水线上,不必修改工具函数,也不必修改 Agent Loop。
第四步:执行结果必须经过统一验收
工具 execute 返回以后,系统会检查结果是否符合工具声明的输出结构。随后 post-execute 可以补充、替换或拒绝结果,工具自己的最终内容转换再生成模型真正看到的文字、图片或其他内容块。
为什么不把任意 JavaScript 对象直接扔给模型?因为工具结果还要被记录、重放、通过 API 传输,并在不同 UI 中展示。统一的 JSON 结果与内容格式,让后续环节不必猜测一个工具返回了什么。
最终的 tool/result 会写入 SessionEvent。下一次模型请求通过历史看到它,于是 Agent 才能根据真实执行结果继续思考。
Native、Code 与 Both 是三种“呈现方式”
DSH 还允许工具以三种形式呈现给模型。这不是上一课的权限模式,而是模型“怎样看见工具”的选择:
| 呈现方式 | 模型看到什么 | 适合场景 |
|---|---|---|
| Native | 每个工具都是独立的函数调用 | 调用简单、步骤清晰 |
| Code | 只直接调用 run_code,在代码中使用生成的工具 SDK | 需要组合大量调用和处理中间数据 |
| Both | 同时拥有 Native 与 Code 两种入口 | 希望模型按任务自由选择 |
Code Mode 并不是绕过工具流水线。程序里的每次工具调用仍会重新进入参数校验、审批、Guard、执行和结果处理。变化的只是模型组织调用的方式,安全和日志主线没有消失。
多个工具能不能并行执行
可以,但必须由工具明确声明自己适合并行。
只读搜索、互不影响的查询通常可以组成并行调用;文件修改、终端命令和共享状态操作默认按独占方式执行。Agent Loop 会把连续的并行安全调用放进受限并发池,遇到独占工具就先排空前面的任务,再单独执行。
这不是单纯追求速度。并行工具必须保证相互交换顺序不会改变结果,否则日志顺序与真实副作用就可能对不上。
UI 为什么能为不同工具画不同卡片
工具定义还可以提供纯展示函数,根据调用参数和已经持久化的结果,告诉 UI 应该画通用卡片、终端输出、文件 Diff、搜索结果或网页结果。
展示函数不能依赖只存在于当时内存里的状态,因为页面刷新或 Session 重放时,UI 仍要画出同一张卡片。
因此 DSH 把三个责任分开:execute 负责做事,模型内容负责告诉模型结果,presentation 负责告诉人类怎样看。
开发业务工具时,应该把什么放在哪里
以“查询订单”为例:
- Tool Definition:名称、参数、输出和执行逻辑;
- Provider:真正连接订单数据库或远程 API;
- pre-execute 插件:租户、权限和审批策略;
- Preset:决定哪些 Agent 能看见这个工具;
- UI presentation:把结果画成订单卡片;
- SessionEvent:保存调用与结果事实。
这样划分以后,你可以换数据库 Provider、调整审批规则或把工具交给另一种 Agent,而不用重写整个工具调用流程。
下一课,我们把工具系统放回 Agent Loop:一条用户消息怎样触发模型请求、工具调用和下一次 Step,直到这一轮真正结束?
Agent Loop 是怎样循环工作的?
模型为什么会调用工具,又为什么会带着工具结果继续思考?