开发你的第一个 Tool
本章目标
- 理解 Tool 在 DeepSeek Harness 中的角色:模型如何看到它、如何调用它
- 掌握
defineTool的基本形态(name/description/parameters/output/execute) - 了解参数运行时校验(
validateArgs)与execute的返回值约定(规范 JSON 值) - 把工具注册到
ctx.tools,并在cordis.yml中启用插件 - 亲手运行一个完整可用的
greet工具
工具:模型与宿主之间的接口
一个 Tool 本质上是「模型可见的声明 + 宿主执行的函数」两部分:
- 声明部分(
name、description、parameters)会被组装进系统提示词,模型据此决定何时、以什么参数调用这个工具; - 执行部分(
execute)在宿主进程内运行,返回的结果再喂回模型,成为下一次请求的上下文。
注册表只把声明中的一部分投影给模型(ToolSchema):name、description 和 JSON Schema 形态的 parameters。output、execute、UI 展示方法等实现细节绝不会出现在模型请求里——模型永远只看到「这个工具叫什么、能做什么、参数长什么样」。
defineTool 的基本形态
用 defineTool 定义一个工具,并注册到工具注册表:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
// 插件身份:包级别的唯一名字
export const name = 'greet-tool'
// 依赖声明:tools 注册表就绪后,apply 才会被调用
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
// 模型看到的名字:调用时使用 greet(name)
name: 'greet',
// 模型看到的描述:写清楚工具做什么、何时使用
description: 'Greet someone by name.',
// 参数 schema:required: true 的键才是必填,其余默认可选
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
// 输出声明:规范 JSON 值 + 渲染成模型可见内容
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
// 工具主体:收到类型由 parameters 推导出的 args
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}五个字段各司其职:name 是模型调用时使用的标识符;description 是模型看到的说明;parameters 声明参数形状并推导 args 的类型;output.schema 声明 execute 返回值的形状(根可以是对象、数组、标量或 null);output.render 把规范值转成模型可见的内容块。inject: ['tools'] 让 Cordis 等到工具注册表就绪后再调用 apply。
参数运行时校验
模型生成的是 JSON 文本,不能假设它一定合法。defineTool 在 execute 运行之前,会按统一的 ParameterSchemaSpec 对模型生成的 arguments 做运行时校验——类型、必填键、字面量约束、恰好匹配一个分支的联合(oneOf)、嵌套值——校验失败会抛 ToolArgsError(INVALID_ARGS),注册表把它作为模型可修正的错误结果返回:模型会看到哪里错了并重新生成参数,而不是让插件崩溃。因此 execute 内的 args 与推导出的类型一致。
schema DSL 表达不了的约束(例如非空字符串、正数、跨字段规则)需要你在 execute 里手动检查;直接注册原始 JSON Schema 的工具(如来自 MCP 的工具)则自行负责输入校验。
execute 的返回:规范 JSON 值
execute 只返回 output.schema 声明的规范 JSON 值。注册表会把返回值快照为无损 JSON、完成校验与冻结,再交给 output.render(args, value) 转成模型可见内容。由此得到几条规则:
- 不要返回内容块(如
[{ type: 'text', ... }])——那是render的职责; - 抛异常或返回无效值会被视为
isError:基础设施故障请抛异常;「不理想但成功」的领域结果(如进程非零退出)应写入规范值,由render解释; args是只读输入;第二个参数exec携带执行上下文:exec.signal用于协作式取消(信号触发时停止进行中的工作),exec.agent可发送异步通知。
注册到 ctx.tools
注册基于副作用:ctx.tools.register(...) 在 apply 中执行一次,插件 fiber 被 dispose(卸载)时工具自动注销。注册后无需手动维护提示词——工具 schema 会自动流入系统提示词的组装过程。
完整示例:greet 工具
把上一节的代码保存为 examples/greet-tool/src/index.ts,并准备 patch 文件:
# cordis.yml —— 用 --patch 把它插入 Web UI
- insert:
- id: greet
name: '/绝对路径/dsh-plugin-tutorial/examples/greet-tool/src/index.ts'
# config: 留到下一章——插件配置在 deepseek-harness 仓库根目录启动:
pnpm dsh web --patch /绝对路径/dsh-plugin-tutorial/examples/greet-tool/cordis.yml打开 http://127.0.0.1:3080,输入:Use the greet tool to greet Ada. 模型会调用 greet,并收到 Hello, Ada! 这一工具结果。
模型如何看到你的工具
启动后,注册表把 greet 的声明投影成 ToolSchema 并组装进系统提示词,模型在每次请求中都能看到,大致形如:
name: greet
description: Greet someone by name.
parameters:
type: object
properties:
name:
type: string
description: The name to greet
required:
- name这份 schema 同时也是后续章节要讲的 Code Mode(await tools.greet(args))程序化调用的契约基础。
小结
defineTool五要素:name、description、parameters、output、execute;- 参数由注册表在
execute前统一校验,违规作为模型可修正的错误返回; execute只返回规范 JSON 值,render负责转成模型可见内容;- 注册是副作用,dispose 即注销;声明自动流入系统提示词。
下一步
- 插件配置:把问候语从硬编码改成可配置;
- 官方《工具编写参考》(cookbook 的
adding-a-tool):嵌套 schema、后台任务(ctx.jobs)、策略钩子(pre-execute/guard/post-execute)、Code Mode 与 UI 卡片。