跳到主要内容
路径文档

写一个工具

一句话版:工具定义的核心是 parameters(JSON-Schema)+ output.schema/render + execute(args, exec)。注册后自动出现在模型工具清单。标准入口是 @deepseek-ai/dsh-toolsdefineTool()

动手篇第二篇。读完你能写一个规范、安全的工具给模型调用。

一、最小工具(defineTool)

import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'tool-greet'
export const inject = ['tools']

export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: '向某人打招呼',
parameters: {
name: { type: 'string' },
excited: { type: 'boolean' },
},
output: {
schema: { type: 'string' },
render(_args, value) {
return [{ type: 'text', text: typeof value === 'string' ? value : 'ok' }]
},
},
timeoutMs: 10_000,
async execute({ name, excited }, exec) {
return `${excited ? 'HELLO' : 'Hello'}, ${name}!` // 与 output.schema:{type:'string'} 一致,返回字符串
},
}))
}

defineTool 只构造 ToolDefinition(帮你做类型推断与字段校验),注册一律走 ctx.tools.register():MCP 服务发现工具、运行时动态工具也是调它注册。

二、defineTool 帮你做了什么

  1. 类型化参数:parameters 编译成 TS 类型,executeargs 被精确推断
  2. 自动校验:参数在执行前校验,缺必填 / 错类型 → ToolArgsError(INVALID_ARGS)走普通错误路径
  3. 推断输出:从 output.schema 推断返回类型与纯渲染

参数 DSL 支持 string/number/integer/boolean/null/array/object/json/oneOf;显式 additionalProperties: true|false(裸 JSON Schema 保持 open 默认)。

三、output 契约(必须规范)

工具只能返回 output.schema 声明的那个规范 JSON 值;注册表校验、冻结再渲染。

字段要求
schema强制,规范 JSON-Schema,声明返回值的"规范形式"
render(args, value)强制,把规范值渲染成 ContentBlock
presentationMeta(args, value)可选,派生 JSON 元数据(随 tool/result 持久化)
  • 缺失/不支持 output → 注册失败;render 缺失会 TypeError
  • 执行器返回的规范值,不直接给模型:模型看到的是 render 输出 + 呈现

结果形状

// 成功
{ isError: false, value, content, meta?, additionalContexts? }
// 失败(无 value)
{ isError: true, error: { message }, content, meta?, additionalContexts? }

四、execute(args, exec)

exec(ToolRunContext)是工具访问作用域/会话/ctx 的唯一路径:

  • 没有 exec.scope/exec.session/exec.ctx:一律经 exec.agent(.id / .ctx / .session),agent 由 agent-loop 注入
  • exec.signal:AbortSignal,取消契约(必须转发/观察,注册表无硬杀)
  • exec.deferContext(UserMessage) / 结果 additionalContexts(UserMessage[]):执行期把上下文还给循环
  • exec.concludeTurn():把成功结果标记为终结当前轮次

五、注册规则(源码约定)

规则说明
必须 output缺/不支持 → TypeError
重名同层抛错
timeoutMs正数/有限;由 dsh-tool-call-timeout-policy(插件 id timeout-policy)以 tools/execute 包装强制,不发给模型
run_code无条件保留,不可注册/遮蔽
返回register() 返回 () => void(卸载函数)
isConcurrencySafe(args)返回恰好 true 才放行并行;否则 exclusive

六、作用域与否

注册位置可见性
普通插件上下文全局注册
agent.ctx仅该 agent,遮蔽同名全局工具

ctx.tools.restrict(filter) 能给继承工具加 allow/deny 掩码(不影响自己注册的)。

七、呈现给模型(mode)

tools:
mode: native # native(默认)| code | both

工具插件无需关心呈现:注册表按 mode 呈现。run_code 是 code 模式的传输名,始终保留。

八、你的工具会被包在哪(流水线)

tools/pre-execute(门禁)→ 守卫 → tools/execute(环绕)→ 你的 execute
→ tools/post-execute(改写)→ finalizeContent → tools/result(观察)

详见 工具执行

九、MCP 工具桥

dsh-mcp-client 把外部 MCP 工具注册进 ctx.tools,命名 mcp__<server>__<raw>,走同一条流水线。函数名遵循 DeepSeek 约束(≤64 字符、[A-Za-z0-9_-])。见 MCP

十、最佳实践

场景用工具 / 别用
给模型一个可调的能力用工具
多个工具共享逻辑逻辑放 服务,工具做薄壳
只需要进程内自己人用服务/方法即可,不必工具
读一个文件tool-fs 可能已够,别重复发明

十一、验证

dsh web --dump-config | grep -A3 greet
# 会话里调用一次,看 tool 事件(默认 zstd、两级目录、单数 tool/ 前缀)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd \
| jq -r 'select(.type | startswith("tool/"))' | tail -3

下一步