跳过主要内容
全部文章
开发

如何用 defineTool 给 DeepSeek-Harness 添加自定义工具

一份逐字段拆解 defineTool 的指南——parameters、output.schema、output.render、execute,以及它需要的 inject 依赖,还有 Code Mode 为何不改变你写工具的方式。

一个 DeepSeek-Harness(dsh)工具,是在插件的 apply(ctx) 里调用 ctx.tools.register(defineTool({...})) 注册出来的,defineTool 来自 @deepseek-ai/dsh-tools。本文会逐一拆解 defineTool 接收的每一个字段——parametersoutput.schemaoutput.renderexecute——以及你要调用它就必须具备的那一个依赖(inject: ['tools'])。

最简单的工具

下面是 dsh 官方工具教程自己给出的例子,原样照抄:

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

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

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

这里发生了五件事:插件声明它需要 tools 服务(inject);服务就绪后拿到 ctx;调用 ctx.tools.register(),传入 defineTool 构造出的对象;而 defineTool 本身把名字、描述、输入形态、输出形态和具体实现组合到了一起。本文接下来会逐一展开这些部分。

inject: ['tools']——为什么它不是可选项

ctx.toolstools 服务加载之前是不存在的。声明 inject: ['tools'] 会同时告诉 Cordis 框架两件事:在这个服务就绪之前不要调用 apply;如果这个服务后来消失了,就自动卸载这个插件。跳过这条声明,你的插件可能会在 ctx.tools 存在之前就被加载,具体取决于加载顺序——inject 把这种依赖关系明确写出来,而不是靠假设,从而消除了这种竞态。如果你想了解被注入的依赖是如何驱动插件生命周期状态机的,参见《DeepSeek-Harness 中的服务、依赖注入与插件生命周期》。

namedescription

都是普通字符串。name 是模型看到的可调用函数名;description 决定模型什么时候会想到用它。两者除了"必须存在"之外没有额外校验——但它们都会直接进入 prompt 组装流程,所以要把 description 当成决定你的工具会不会被调用的那一句话来对待。

parameters——输入形态

parameters 里的每一个键都是一个参数,用 type、是否 required、以及一句人类可读的 description 来描述:

parameters: {
  name: { type: 'string', required: true, description: 'The name to greet' },
},

defineTool 用这个形状在你的 execute 函数真正运行之前先校验传入的参数——缺少必填参数或者类型不对的调用根本到不了你的代码里。再加一个可选参数,遵循完全一样的形状:

parameters: {
  text: { type: 'string', required: true, description: 'The text to count words in' },
  caseSensitive: { type: 'boolean', required: false, description: 'Whether casing affects the count' },
},

工具教程自己的示例只展示了一个必填字符串参数,所以对于超出这个基本 type / required / description 形状之外的东西——嵌套对象、数组、枚举——应该把它当成一个需要去查更深入参考资料的问题,而不是凭空猜测:工具教程自己指向的进阶 cookbook 是 docs/cookbook/adding-a-tool.md,涵盖更高级的参数与 schema 模式。

output.schemaoutput.render——两件不同的事

这是很多人第一次读容易理解错的地方,因为它看起来像一件事,实际上是两件:

  • output.schema 描述你的 execute 函数返回的规范值——也就是原始数据形态,和它如何被展示无关。
  • output.render 是一个函数 (args, value) => content[],把那个规范值转换成模型实际在上下文里看到的内容块。
output: {
  schema: { type: 'string' },
  render: (_args, value) => [{ type: 'text', text: value }],
},

在最简单的例子里两者都很trivial——规范值就是那个字符串,渲染它也只是把它包进一个 { type: 'text', text: value } 块里。当一个工具的真实输出是需要特定文本呈现方式的结构化数据(对象、列表)时,或者——按上面提到的同一份 cookbook 参考——需要 UI 卡片这类更丰富的内容时,这个拆分才真正体现出价值;工具教程把 schema/render 拆分确立为机制本身,但我们审阅的范围没有覆盖它支持的每一种内容块类型。

execute——真正干活的地方

async execute(args) {
  return `Hello, ${args.name}!`
},

args 到达时已经是根据 parameters 校验过的——等你的代码运行时,必填字段已经存在且类型正确。executeasync 的,所以从简单的字符串变换到一次 HTTP 调用或读文件,都只是一个 await 的距离。返回值必须匹配 output.schema,因为这正是 output.render 接下来会收到的值。

一个完整的第二例子

把这四个字段组合起来,做一个稍微不那么trivial的工具——统计一段文本里的单词数:

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

ctx.tools.register(defineTool({
  name: 'word-count',
  description: 'Count the words in a piece of text.',
  parameters: {
    text: { type: 'string', required: true, description: 'The text to count' },
  },
  output: {
    schema: { type: 'number' },
    render: (_args, value) => [{ type: 'text', text: `${value} words` }],
  },
  async execute(args) {
    return args.text.trim().split(/\s+/).filter(Boolean).length
  },
}))

execute 返回一个匹配 output.schema 的纯数字;output.render 把这个数字变成一句模型能读回来的、作为工具输出的话。

命名——哪些有文档,哪些没有

工具教程并没有对 defineToolname 给出一套通用命名规范,除了"取一个字符串"之外。dsh 明确写在文档里的那条命名规则,适用于一个相关但不同的场景:从 MCP server 桥接进来的工具会被命名为 mcp__<serverName>__<rawName>,规范化到 64 字符以内且只含 [A-Za-z0-9_-],两个名字冲突时会加一个哈希后缀——参见《如何在 DeepSeek-Harness 中使用 MCP Server》。这条相同的"字符集+长度"约束是否也适用于一个普通的 defineTool 名字,我们审阅的文档里没有明确写出来,所以如果你要取一个不寻常的名字,最好自己去核对 docs/user/develop/basic/tool.md,不要凭假设行事。

Code Mode 与你的工具

dsh 支持一个 DSH_TOOLS_MODE 环境变量,有三个取值:native(普通的 function calling)、code("Code Mode",模型写代码去调用你的工具,而不是发起原生的 function call)、以及 both。这个设置改变的是模型调用工具的方式,而不是你定义工具的方式——一个用 defineTool 注册的工具,不管部署跑在哪种模式下,工作方式都是一样的,因为"写代码调用这个工具"和"原生 function call 调用这个工具"之间的翻译发生在 defineTool 这一层之下。

FAQ

我需要自己为 parameters 手写 JSON Schema 吗?

不需要——defineToolparameters 对象用的是它自己的简写形式(每个字段 typerequireddescription),而不是原始 JSON Schema。defineTool 会根据这个简写自动为你推导出参数校验逻辑。

缺少必填参数时会发生什么?

execute 根本不会运行。针对 parameters 的校验发生在你的函数被调用之前,所以你不需要自己手动检查 args 里的必填字段是否存在。

execute 可以抛出错误吗?

工具教程没有为 execute 记录具体的错误处理约定,本文也不会凭空发明一个——如果你需要重试、超时或者带审批门槛的行为,官方文档给出的那个指向是 cookbook(docs/cookbook/adding-a-tool.md)。

output.render 是必需的吗,能不能直接返回一个字符串跳过它?

有文档记录的形态里,schema 总是和 render 成对出现——官方示例里没有展示过省略其中一个的捷径。

下一步

如果你还没搭好周边的插件,先从《从零构建一个 DeepSeek-Harness 插件》开始。工具跑通之后,用《用 Schemastery 让 DeepSeek-Harness 插件可配置》让它的行为可配置,而不是把值硬编码进代码里。想看一个真实的、高 star 的工具插件长什么样,可以浏览 modlens,或者 FindHarness 上更完整的 Tools & Capabilities(工具与能力)分类。