DeepSeek Harness: 开发第一个工具:defineTool
最后更新:2026-08-31
工具是 Agent 的手——定义一个工具,就是给 Agent 一项新能力。defineTool 是 DSH 提供的工具定义 DSL,用声明式的方式描述工具的名称、参数和执行逻辑,让 Agent 理解何时、如何调用你的工具。
📋 前置知识:已完成 14-inject.md,理解依赖注入
1. 你将学到
- defineTool DSL 语法
- name/description/parameters 声明
- JSON Schema 参数定义
- execute 函数实现
- 工具注册到 ctx.tools
- 工具在 Web UI 中的展示
- 完整示例:文件计数工具
2. defineTool DSL 语法
(1) ▶ 示例 1
import { defineTool } from '@deepseek-ai/dsh'
export default defineTool({
name: 'tool_name',
description: 'What this tool does',
parameters: {
type: 'object',
properties: { /* ... */ },
required: [ /* ... */ ]
},
async execute(params, ctx) {
// 工具逻辑
return result
}
})
(2) 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | ✅ | 工具唯一标识,小写+下划线 |
description |
string | ✅ | 工具功能描述,给 LLM 阅读 |
parameters |
JSON Schema | ✅ | 参数定义 |
execute |
function | ✅ | 执行逻辑 |
(3) 导出方式
defineTool 返回一个工具定义对象,可以直接作为默认导出:
// 方式 1:默认导出
export default defineTool({ ... })
// 方式 2:命名导出
export const myTool = defineTool({ ... })
3. name 与 description
(1) 命名规范
工具名使用 snake_case 格式:
name: 'file_count' // ✅
name: 'fileCount' // ❌ 不推荐
name: 'FileCount' // ❌ 不推荐
name: 'file-count' // ❌ 不推荐
(2) description 写法
description 是 LLM 选择工具的依据,写法要点:
- 说明工具做什么
- 说明什么时候应该使用
- 避免"测试工具"、"示例工具"等无意义描述
// ❌ 差的描述
description: 学会用 defineTool DSL 定义工具:声明参数 schema、描述功能、设置审批策略,打造可靠工具
// ✅ 好的描述
description: 学会用 defineTool DSL 定义工具:声明参数 schema、描述功能、设置审批策略,打造可靠工具
(3) 多语言描述
description 当前只支持英文。LLM 根据 description 理解工具用途,即使用户输入是中文。
4. JSON Schema 参数定义
(1) 基本结构
parameters 遵循 JSON Schema 规范:
parameters: {
type: 'object',
properties: {
param_name: {
type: 'string',
description: 'Parameter description'
}
},
required: ['param_name']
}
(2) 支持的类型
| JSON Schema 类型 | TypeScript 类型 | 说明 |
|---|---|---|
string |
string | 字符串 |
number |
number | 数字 |
integer |
number | 整数 |
boolean |
boolean | 布尔值 |
object |
object | 嵌套对象 |
array |
array | 数组 |
(3) 字符串参数
properties: {
path: {
type: 'string',
description: 'Directory path to count files in'
},
pattern: {
type: 'string',
description: 'Glob pattern to filter files (e.g. *.ts)',
default: '*'
}
}
(4) 枚举参数
properties: {
sort_by: {
type: 'string',
enum: ['name', 'size', 'date'],
description: 'Sort criteria'
}
}
(5) 数组参数
properties: {
extensions: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to include (e.g. [".ts", ".js"])'
}
}
(6) 嵌套对象
properties: {
options: {
type: 'object',
properties: {
recursive: { type: 'boolean', default: false },
includeHidden: { type: 'boolean', default: false }
}
}
}
(7) required 字段
required: ['path'] // path 是必填
// pattern 有 default,不是 required
5. execute 函数实现
(1) 函数签名
async execute(params: Params, ctx: Context): Promise<Result>
params:用户(Agent)传入的参数,类型由 parameters 定义推断ctx:Cordis 上下文,可访问已注入的服务- 返回值:任意 JSON 可序列化的数据
(2) ▶ 示例 2
async execute({ path }, ctx) {
const count = await countFiles(path)
return { count, path }
}
(3) 访问服务
execute 内可以通过 ctx 访问已注册的服务:
export const inject = ['fs']
export default defineTool({
name: 'file_count',
// ...
async execute({ path }, ctx) {
const files = await ctx.fs.readdir(path)
return { count: files.length }
}
})
(4) 错误处理
async execute({ path }, ctx) {
try {
const files = await ctx.fs.readdir(path)
return { count: files.length, path }
} catch (error) {
return {
error: true,
message: `Failed to read directory: ${error.message}`
}
}
}
工具不应抛出未捕获的异常——返回错误对象让 Agent 理解失败原因,比崩溃更友好。
(5) 返回值格式
推荐返回结构化对象:
// ✅ 结构化返回
return {
count: 42,
path: '/home/alice/project',
breakdown: {
typescript: 28,
javascript: 10,
other: 4
}
}
// ❌ 纯文本返回
return 'Found 42 files in /home/alice/project'
结构化返回让 Agent 可以程序化地使用结果,而不仅仅是展示文本。
6. 工具注册到 ctx.tools
(1) 在插件中注册
defineTool 定义的工具需要通过插件注册到 ctx.tools:
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'
const fileCountTool = defineTool({
name: 'file_count',
description: 'Count files in a directory',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Directory path' }
},
required: ['path']
},
async execute({ path }, ctx) {
const files = await ctx.fs.readdir(path)
return { count: files.length }
}
})
export const name = 'tool-file-count'
export const inject = ['tools', 'fs']
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
}
(2) 注册多个工具
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
ctx.tools.register(fileSizeTool)
ctx.tools.register(fileSearchTool)
}
(3) 动态注册
export function apply(ctx: Context) {
const tools = [fileCountTool, fileSizeTool]
for (const tool of tools) {
ctx.tools.register(tool)
ctx.logger.info(`registered tool: ${tool.name}`)
}
}
7. 工具在 Web UI 中的展示
(1) 自动展示
注册到 ctx.tools 的工具会自动出现在 Web UI 的工具列表中:
┌─────────────────────────────────────┐
│ 🔧 Tools │
├─────────────────────────────────────┤
│ file_count │ Count files in a dir │
│ file_size │ Get file size info │
│ file_search │ Search for files │
└─────────────────────────────────────┘
(2) name 和 description 的展示
name:作为工具标识显示description:作为工具说明显示
(3) Agent 调用展示
当 Agent 调用你的工具时,Web UI 会展示:
🤖 Agent:
🔧 Using tool: file_count
→ path: /home/alice/project
Result: { count: 42, path: "/home/alice/project" }
▶ 示例 8 文件计数工具
将所有知识组合成一个完整的工具插件:
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'
export const name = 'tool-file-count'
export const inject = ['tools', 'fs']
const fileCountTool = defineTool({
name: 'file_count',
description: 'Count the number of files in a directory. Returns total count and breakdown by file extension. Use when user asks about file statistics or directory contents.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Absolute or relative path to the directory'
},
recursive: {
type: 'boolean',
description: 'Whether to count files in subdirectories',
default: false
},
extensions: {
type: 'array',
items: { type: 'string' },
description: 'Filter by file extensions (e.g. [".ts", ".js"]). Count all if omitted.'
}
},
required: ['path']
},
async execute({ path, recursive, extensions }, ctx) {
try {
const entries = recursive
? await ctx.fs.readdirRecursive(path)
: await ctx.fs.readdir(path)
let files = entries.filter(e => !e.isDirectory)
if (extensions && extensions.length > 0) {
files = files.filter(f =>
extensions.some(ext => f.name.endsWith(ext))
)
}
const breakdown: Record<string, number> = {}
for (const f of files) {
const ext = f.name.includes('.')
? '.' + f.name.split('.').pop()
: '(no extension)'
breakdown[ext] = (breakdown[ext] || 0) + 1
}
return {
count: files.length,
path,
recursive,
breakdown
}
} catch (error: any) {
return {
error: true,
message: `Failed to count files: ${error.message}`
}
}
}
})
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
ctx.logger.info('file_count tool registered')
}
启动验证:
# 注册到 cordis.yml
pnpm dsh web --patch
# 在 Web UI 中测试
# 👤 Alice: /home/alice/project 目录有多少文件?
# 🤖 Agent: 🔧 file_count → { count: 42, breakdown: { ".ts": 28, ".js": 10, ".json": 4 } }
❓ 常见问题
defineTool 是 DSL,创建工具定义对象。ctx.tools.register 是注册方法,将定义对象注册到工具服务。两者配合使用:先 defineTool 定义,再 register 注册。ctx.tools.execute('other_tool', params) 调用。但注意避免循环调用。typescript async execute(params, ctx) { ctx.logger.info('params:', JSON.stringify(params)) // ... } ---📖 小节
- defineTool 是 DSH 的工具定义 DSL:name + description + parameters + execute
- name 用 snake_case,description 要清晰说明工具用途和使用场景
- parameters 遵循 JSON Schema,支持 string/number/boolean/object/array
- execute 接收 params 和 ctx,返回结构化结果
- 工具通过
ctx.tools.register()注册,自动出现在 Web UI - 返回结构化对象优于纯文本,错误时返回 error 对象而非抛异常
📝 作业
1. ⭐ 基础题:使用 defineTool 创建一个 current_time 工具,接受一个可选的 timezone 参数(默认 UTC),返回当前时间字符串。注册到 DSH 并让 Agent 成功调用。
2. ⭐⭐ 进阶题:扩展 file_count 工具,添加 min_size 和 max_size 参数(字节数),过滤文件大小范围。测试:统计 /tmp 目录下大于 1KB 且小于 1MB 的文件数量。
3. ⭐⭐⭐ 挑战题:创建一个 code_stats 工具,统计指定目录的代码行数。参数:path(目录路径)、languages(语言过滤数组)。返回总行数、各语言行数、空行数、注释行数。需要用正则区分代码行/空行/注释行。