DeepSeek Harness: 开发第一个工具:defineTool

最后更新:2026-08-31

工具是 Agent 的手——定义一个工具,就是给 Agent 一项新能力。defineTool 是 DSH 提供的工具定义 DSL,用声明式的方式描述工具的名称、参数和执行逻辑,让 Agent 理解何时、如何调用你的工具。

💡 提示:defineTool 的核心是"让 LLM 理解你的工具"——name 和 description 是给 LLM 看的,parameters 描述输入格式,execute 是实际执行。写好 description,Agent 才能在正确场景选择你的工具。

📋 前置知识:已完成 14-inject.md,理解依赖注入

1. 你将学到


2. defineTool DSL 语法

工具调用流水线

(1) ▶ 示例 1

TYPESCRIPT
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 返回一个工具定义对象,可以直接作为默认导出:

TYPESCRIPT
// 方式 1:默认导出
export default defineTool({ ... })

// 方式 2:命名导出
export const myTool = defineTool({ ... })

3. name 与 description

(1) 命名规范

工具名使用 snake_case 格式:

TYPESCRIPT
name: 'file_count'        // ✅
name: 'fileCount'         // ❌ 不推荐
name: 'FileCount'         // ❌ 不推荐
name: 'file-count'        // ❌ 不推荐

(2) description 写法

description 是 LLM 选择工具的依据,写法要点:

TYPESCRIPT
// ❌ 差的描述
description: 学会用 defineTool DSL 定义工具:声明参数 schema、描述功能、设置审批策略,打造可靠工具

// ✅ 好的描述
description: 学会用 defineTool DSL 定义工具:声明参数 schema、描述功能、设置审批策略,打造可靠工具

(3) 多语言描述

description 当前只支持英文。LLM 根据 description 理解工具用途,即使用户输入是中文。


4. JSON Schema 参数定义

(1) 基本结构

parameters 遵循 JSON Schema 规范:

TYPESCRIPT
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) 字符串参数

TYPESCRIPT
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) 枚举参数

TYPESCRIPT
properties: {
  sort_by: {
    type: 'string',
    enum: ['name', 'size', 'date'],
    description: 'Sort criteria'
  }
}

(5) 数组参数

TYPESCRIPT
properties: {
  extensions: {
    type: 'array',
    items: { type: 'string' },
    description: 'File extensions to include (e.g. [".ts", ".js"])'
  }
}

(6) 嵌套对象

TYPESCRIPT
properties: {
  options: {
    type: 'object',
    properties: {
      recursive: { type: 'boolean', default: false },
      includeHidden: { type: 'boolean', default: false }
    }
  }
}

(7) required 字段

TYPESCRIPT
required: ['path']          // path 是必填
// pattern 有 default,不是 required

5. execute 函数实现

(1) 函数签名

TYPESCRIPT
async execute(params: Params, ctx: Context): Promise<Result>

(2) ▶ 示例 2

TYPESCRIPT
async execute({ path }, ctx) {
  const count = await countFiles(path)
  return { count, path }
}

(3) 访问服务

execute 内可以通过 ctx 访问已注册的服务:

TYPESCRIPT
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) 错误处理

TYPESCRIPT
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) 返回值格式

推荐返回结构化对象:

TYPESCRIPT
// ✅ 结构化返回
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:

TYPESCRIPT
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) 注册多个工具

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.tools.register(fileCountTool)
  ctx.tools.register(fileSizeTool)
  ctx.tools.register(fileSearchTool)
}

(3) 动态注册

TYPESCRIPT
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 的工具列表中:

TEXT 📖 仅展示
┌─────────────────────────────────────┐
│ 🔧 Tools                            │
├─────────────────────────────────────┤
│ file_count  │ Count files in a dir  │
│ file_size   │ Get file size info    │
│ file_search │ Search for files      │
└─────────────────────────────────────┘

(2) name 和 description 的展示

(3) Agent 调用展示

当 Agent 调用你的工具时,Web UI 会展示:

TEXT 📖 仅展示
🤖 Agent:
🔧 Using tool: file_count
  → path: /home/alice/project
  
  Result: { count: 42, path: "/home/alice/project" }

▶ 示例 8 文件计数工具

将所有知识组合成一个完整的工具插件:

TYPESCRIPT
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')
}

启动验证:

BASH
# 注册到 cordis.yml
pnpm dsh web --patch

# 在 Web UI 中测试
# 👤 Alice: /home/alice/project 目录有多少文件?
# 🤖 Agent: 🔧 file_count → { count: 42, breakdown: { ".ts": 28, ".js": 10, ".json": 4 } }

❓ 常见问题

Q defineTool 和 ctx.tools.register 有什么区别?
A defineTool 是 DSL,创建工具定义对象。ctx.tools.register 是注册方法,将定义对象注册到工具服务。两者配合使用:先 defineTool 定义,再 register 注册。
Q 工具名可以重复吗?
A 不可以。同名工具后注册的会覆盖先注册的。如果需要同名工具共存,使用 scope 隔离(见 20-scope.md)。
Q parameters 不写 description 行吗?
A 技术上可以,但强烈不推荐。参数的 description 帮助 LLM 理解参数含义,缺失会导致 Agent 错误传参。
Q execute 可以返回流式数据吗?
A 当前 defineTool 的 execute 只支持返回完整结果。流式输出通过 LLM 适配器的 StreamChunk 协议实现(见 25-stream-error.md)。
Q 工具能调用其他工具吗?
A 可以,通过 ctx.tools.execute('other_tool', params) 调用。但注意避免循环调用。
Q 如何调试工具的参数解析?
A 在 execute 开头打印参数: typescript async execute(params, ctx) { ctx.logger.info('params:', JSON.stringify(params)) // ... } ---

📖 小节


📝 作业

1. ⭐ 基础题:使用 defineTool 创建一个 current_time 工具,接受一个可选的 timezone 参数(默认 UTC),返回当前时间字符串。注册到 DSH 并让 Agent 成功调用。

2. ⭐⭐ 进阶题:扩展 file_count 工具,添加 min_sizemax_size 参数(字节数),过滤文件大小范围。测试:统计 /tmp 目录下大于 1KB 且小于 1MB 的文件数量。

3. ⭐⭐⭐ 挑战题:创建一个 code_stats 工具,统计指定目录的代码行数。参数:path(目录路径)、languages(语言过滤数组)。返回总行数、各语言行数、空行数、注释行数。需要用正则区分代码行/空行/注释行。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏