定义自己的工具
本教程共 32 篇 · 第 21 篇 · 更新于 2026-08-15 · 约 6 分钟阅读
本节目标:用 defineTool 声明一个模型可调用的工具,理解参数校验、作用域注册与 schema 自动流入提示词的机制。
工具是什么
工具是 Agent 用来干活的函数。模型看到工具的名称、说明和参数 schema,决定什么时候调用、传什么参数。在 dsh 里,工具通过 ctx.tools.register() 注册进工具注册表。
工具定义是模型看到的「接口」。description 和 parameters 写得清楚,模型才知道你的工具是干嘛的、什么时候该用。
defineTool 的结构
defineTool 是定义工具的 DSL,接收一个描述工具全部信息的对象:
| 字段 | 作用 |
|---|---|
| name | 工具名,模型用它发起调用 |
| description | 工具说明,告诉模型何时使用 |
| parameters | 入参 schema,defineTool 据此推导并校验 args |
| output.schema | 返回值 schema,声明 execute 返回的规范值类型 |
| output.render | 把规范值转成面向模型的内容 |
| execute | 工具实现,真正执行逻辑 |
第一个工具
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools'] // 等工具注册表就绪再执行 apply
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}!`
},
}))
}
inject: ['tools'] 声明插件依赖 tools 服务。Cordis 保证服务就绪后才调用 apply,所以 ctx.tools 在 apply 里一定可用。
参数校验:谁在把关
模型发来的是工具名加一段 arguments 文本,不是可信的函数参数。defineTool 会在 execute 运行前按 parameters 校验:类型、必填键、字面量约束、联合分支、嵌套值,全部检查过才放行。因此 execute 里的 args 与 schema 推导出的类型一致。
Noteschema 只保证结构。非空字符串、正数这类约束 schema DSL 表达不了,要在 execute 里自己检查。直接注册原始 JSON Schema 的工具则自行负责输入校验。
注册时还有一道关:assertSupportedJsonSchema 检查 schema 是否落在可执行子集内(type、对象属性、数组 items、标量 enum/const、exact-one oneOf)。出现不支持或放错位置的关键字,注册直接失败并列出违规路径。schema 若只给模型看、运行时却不执行,工具作者会得到虚假的输入保证——这正是这套检查存在的理由。
作用域注册:工具挂在哪
ctx.tools.register() 把工具注册到当前作用域。返回值是精确的 disposer——插件卸载时自动调用,工具从注册表移除,模型下一次请求就看不到它。同名工具不是「后注册覆盖前注册」:同一层重复注册会失败,Agent 作用域里的定义可以遮蔽全局定义。
harness 有按 Agent 划分的作用域机制(scope),两个概念要分清:
- shadowing(遮蔽):在某个 Agent 作用域注册同名工具,只在该作用域内替换全局版本,其他 Agent 仍看到全局工具。
- restriction(限制):
ctx.tools.restrict()为单个作用域过滤全局工具集合。被过滤的工具既不进提示词,也拒绝执行——与不存在的工具无法区分。
Tip「仓库装了这个工具包」不等于「当前 Agent 能调用它」。还要看贡献挂在哪个作用域、有没有被 restriction 过滤、旧作用域是否已 dispose。
规范值与 render 分离
execute 返回规范值(canonical value)——output.schema 声明的结构化结果,可以是对象、数组、标量或 null。output.render 再把规范值渲染成模型可见的内容块。
两者解耦带来两个好处:
- 同一个规范值可以换不同的 render 格式。
- UI 卡片、持久化回放走独立通道,不污染模型看到的内容。
工具主体不要返回内容块,也不要迫使调用方从自然语言里解析 id 和字段。直接返回句柄与字段;面向人类的解释放进 output.render。
schema 自动流入提示词
工具注册后,name、description、parameters 会自动组装进系统提示词。模型「知道」有这个工具,才会在合适的时候调用。你不需要手写函数签名给模型——schema 就是模型看到的接口。
模型调用工具后,结果走完整执行管线(turn → step → 工具调用)。你的 execute 只负责最后一段。想观察每次工具执行,监听 tools/result 事件:
export const name = 'tool-logger'
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
const text = result.content
.map(block => (block.type === 'text' ? block.text : ''))
.join('')
console.log(`[tool-logger] ${exec.name} -> ${text}`)
})
}
exec 携带执行身份:callId、name、arguments、agent、token 和必填的 signal。整个分发过程中这些字段保持不可变,请把 args 当作只读输入。长时间运行的工作要遵守 exec.signal,信号触发时取消进行中的操作。
Warning
execute抛异常或返回不符合 output.schema 的值,都会被当作isError处理。基础设施故障抛异常;成功的领域结果即使表示不理想的状态,也应写入规范值,由 render 解释。
小结
defineTool六字段:name、description、parameters、output.schema、output.render、execute。- 参数在 execute 前按 schema 校验,
args与推导出的类型一致。 - 注册是 effect:插件卸载工具自动消失;同名遮蔽、restriction 过滤。
execute返回规范值,output.render负责转成模型可见内容。- schema 自动流入系统提示词,模型据此决定何时调用。