CH 20 · defineTool:给 Agent 造一个工具
本章目标
CH 18、19 我们写的插件只会打印日志——那只是验证"插件被加载了"。这一章写插件里真正值钱的东西:工具。
工具是 Agent 的"手":模型说一句话,你写好的代码就被调用、真实干活、把结果送回给模型。前面几章你一直在用 dsh 内置的工具(读文件、跑命令、搜索),这一章我们自己造一个,让模型真的伸手进来调用它。
工具是插件的灵魂
回顾一下你每天都在用的东西:dsh 里那个 Agent 之所以能读文件、写文件、跑命令,靠的是工具(ctx.tools 里注册的一个个能力)。模型本身只会"说话",是工具让它能"动手"。
工具插件的最简骨架:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
// 工具的四个部分,下面逐个拆
}))
}inject: ['tools'] 声明"我要用工具注册表",CH 19 学过;ctx.tools.register(...) 就是往注册表里挂一个工具。
defineTool 的四件套
defineTool 接收一个对象,告诉 dsh"这个工具叫什么、什么时候用、要什么参数、怎么干活"。一个工具 = 一份"给 Agent 的招聘 JD":
| 字段 | 含义 | 打个比方 |
|---|---|---|
name | 工具的名字,模型用它来调用 | 岗位名 |
description | 告诉模型"这工具是干嘛的、什么时候该用" | 岗位职责 |
parameters | 声明要哪些参数、哪些必填 | 需要递交的材料 |
execute | 真正干活的函数 | 入职后干的活 |
再看一个完整的最小工具(官方教程同款,我们稍作翻译):
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
name: 'greet',
description: '按名字打招呼',
parameters: {
name: { type: 'string', required: true, description: '要打招呼的人名' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}四个重点:
parameters写的是 JSON Schema:type: 'string'声明参数类型,required: true声明必填。框架会自动校验模型传来的参数,不合规会报错——你不用在execute里手动做类型检查。execute(args)是真正干活的函数:args已经被校验过,直接放心用。它返回一个"规范值"(这里是一段字符串)。output.schema声明返回值的形状,output.render把返回值转成模型能看到的内容(这里是文本块)。返回值先以规范形态存在,渲染层负责"翻译"给模型。description极其重要:模型靠它判断"这个工具现在该不该用"。写清楚、写具体,模型才懂得在合适的时机调用它。
动手:写一个 greet 工具,让模型真的调用
在一个你想放插件的工作区里建目录:
New-Item -ItemType Directory -Path "tool-demo\src" -Force第 1 步:装依赖
defineTool 来自 @deepseek-ai/dsh-tools,先在插件目录装上:
cd "E:\software-workspace\DeepSeek harness demo\tool-demo" # 换成你自己的目录
npm init -y
npm install @deepseek-ai/dsh-tools记得在 package.json 里加一行 "type": "module"(CH 19 说过,不加会打一堆 warning)。
第 2 步:写工具
新建 tool-demo\src\greet.js,内容就是上面那个 greet 工具。execute 里我加了一行日志,方便在终端确认它真的被调用了:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
name: 'greet',
description: '按名字打招呼',
parameters: {
name: { type: 'string', required: true, description: '要打招呼的人名' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
console.log('[greet] called with', args.name)
return `Hello, ${args.name}!`
},
}))
}第 3 步:声明并验证
新建 tool-demo\cordis.yml(路径换成你自己的,空格记得 %20):
- insert:
- id: greet
name: 'file:///E:/你的工作区/tool-demo/src/greet.js'用 headless 跑一次,让模型真的调用它:
cd 你的工作区目录
dsh --profile headless --patch "./tool-demo/cordis.yml" "你必须调用 greet 工具,跟 Ada 打个招呼,然后原样告诉我工具的返回结果"终端输出里你会看到:
[greet] called with Ada这就是 execute 被模型真实调用的证据——你的代码真的被模型伸手进来执行了。而模型的回复里会包含工具返回的 Hello, Ada!。

终端里黄色的 [greet] called with Ada 就是 execute 被模型真实调用的日志,下面的 Hello, Ada! 是工具返回给模型的结果。
让 dsh 自己搭:一条提示词搞定
写工具的流程和写插件完全一样,dsh 自己就能干,而且它比你更清楚 defineTool 有哪些字段、schema 该怎么写。
直接在 Web UI 的输入框发这条提示词:
在我当前的工作区帮我写一个工具插件:用 defineTool 定义一个最简单的工具(带一个必填参数,execute 里返回一段文字)。你先去 dsh 官方文档读清楚 defineTool 的字段和参数校验规则,按官方规范实现,再跑一次 headless 让模型真实调用它,最后告诉我结果和文件放在哪。它会自己去查文档、装依赖、写工具、验证模型真的调用了它。你只负责验收。

这是我实际跑这条提示词的结果:它自己读文档、建好了插件的三件套(声明 bundle 的 package.json、defineTool 的入口 index.js、一行 insert 的 cordis.patch.yml),还发现 Windows 下路径带空格会让安装命令解析出错,主动把插件挪到无空格路径再装——自己踩到坑,自己绕过去了。

跑完之后,它还把踩的坑整理成了一张清单:插件导出的形态要求、inject 必须显式声明、workspace 包要构建产物才能跑、路径空格的坑。
常见坑
| 问题 | 怎么回事 | 怎么处理 |
|---|---|---|
报 Cannot find package '@deepseek-ai/dsh-tools' | 插件目录没装依赖 | npm install @deepseek-ai/dsh-tools |
| 模型一直不调用你的工具 | description 写得不清楚,模型不知道何时用 | 把 description 写具体:"当用户要求 X 时使用" |
| 参数没传对 | schema 和模型理解不一致 | parameters 里给每个参数写清 description |
| 工具报了参数错误 | 模型传了非法参数 | required: true 标好必填项,类型写对 |
| 模型调用了但结果不对 | execute 里逻辑有误 | 在 execute 里加 console.log 调试,看日志 |
这一章你学到了什么
能自己完成下面几条,就算过关:
- [ ] 能说出 defineTool 四件套:
name/description/parameters/execute - [ ] 知道
parameters是 JSON Schema,框架会自动校验参数 - [ ] 会写一个工具插件并注册到
ctx.tools - [ ] 会用 headless 验证模型真的调用了你的工具
- [ ] 知道
description决定模型什么时候用你的工具
