Skip to content

开发你的第一个 Tool

本章目标

  • 理解 Tool 在 DeepSeek Harness 中的角色:模型如何看到它、如何调用它
  • 掌握 defineTool 的基本形态(name / description / parameters / output / execute
  • 了解参数运行时校验(validateArgs)与 execute 的返回值约定(规范 JSON 值)
  • 把工具注册到 ctx.tools,并在 cordis.yml 中启用插件
  • 亲手运行一个完整可用的 greet 工具

工具:模型与宿主之间的接口

一个 Tool 本质上是「模型可见的声明 + 宿主执行的函数」两部分:

  • 声明部分namedescriptionparameters)会被组装进系统提示词,模型据此决定何时、以什么参数调用这个工具;
  • 执行部分execute)在宿主进程内运行,返回的结果再喂回模型,成为下一次请求的上下文。

注册表只把声明中的一部分投影给模型(ToolSchema):namedescription 和 JSON Schema 形态的 parametersoutputexecute、UI 展示方法等实现细节绝不会出现在模型请求里——模型永远只看到「这个工具叫什么、能做什么、参数长什么样」。

defineTool 的基本形态

defineTool 定义一个工具,并注册到工具注册表:

ts
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 文本,不能假设它一定合法。defineToolexecute 运行之前,会按统一的 ParameterSchemaSpec 对模型生成的 arguments 做运行时校验——类型、必填键、字面量约束、恰好匹配一个分支的联合(oneOf)、嵌套值——校验失败会抛 ToolArgsErrorINVALID_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 文件:

yaml
# cordis.yml —— 用 --patch 把它插入 Web UI
- insert:
    - id: greet
      name: '/绝对路径/dsh-plugin-tutorial/examples/greet-tool/src/index.ts'
      # config: 留到下一章——插件配置

在 deepseek-harness 仓库根目录启动:

sh
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 并组装进系统提示词,模型在每次请求中都能看到,大致形如:

yaml
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 五要素:namedescriptionparametersoutputexecute
  • 参数由注册表在 execute 前统一校验,违规作为模型可修正的错误返回;
  • execute 只返回规范 JSON 值,render 负责转成模型可见内容;
  • 注册是副作用,dispose 即注销;声明自动流入系统提示词。

下一步

  • 插件配置:把问候语从硬编码改成可配置;
  • 官方《工具编写参考》(cookbook 的 adding-a-tool):嵌套 schema、后台任务(ctx.jobs)、策略钩子(pre-execute / guard / post-execute)、Code Mode 与 UI 卡片。