课程总纲 / 阶段 3 · 插件开发
L13

工具插件:defineTool

给模型造一个它能"看见并调用"的工具——最常见也最有成就感的插件类型。

预计阅读 35 分钟难度 ★★★★☆核心动手课

一、完整实战:greet 工具Hands-On

把 L12 的 scratch-plugin/src/my-plugin.ts 整个替换为:

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}!`
    },
  }))
}

重启(或热重载)后,在 Web UI 里对模型说:"用 greet 工具向 Ada 问好"——模型会调用 greet,并把收到的 Hello, Ada! 作为工具结果继续对话。

二、defineTool 解剖Anatomy

字段作用通俗理解
name工具调用名工具的"工号",模型按它发起调用
description何时该用这个工具写给模型看的说明书——模型全靠它判断"现在该不该用你"。写不好等于工具不存在
parameters参数 schema点菜单:收什么参数、什么类型、哪个必填
output.schema返回值 schema上菜的标准盘子形状
output.render值 → 模型可见内容把菜摆盘端给模型
execute(args, exec)执行逻辑后厨。args 已按 schema 校验过类型
schema 自动流转:注册后,parameters 自动变成模型可见的 JSON Schema,汇入每次请求的工具清单(提示词组装的一部分)——不需要任何额外接线。

三、execute 的六条契约The Contract

  1. 参数已校验。模型生成的 arguments 进入 execute 前已按 schema 验证(类型、必填、枚举);schema 表达不了的约束(非空字符串、正数、跨字段规则)要自己查。
  2. 返回规范值(canonical value)。只返回 schema 声明的那个值,框架负责快照、校验、冻结再交给 render。不要返回内容块,不要让调用方从散文里解析字段。
  3. 抛异常或返回非法值 = isError。基础设施故障就抛;业务上的"不理想结果"(如命令退出码非零)是正常返回值,交给渲染层解释。
  4. 尊重 exec.signal。取消信号,触发时应尽快中止在途工作(传给底层 API / 读文件都带上)。
  5. 把 args 当只读。执行身份(callId、name、arguments、token)在派发中不可变。
  6. 异步通知用 exec.agent.inject()——给下一次请求追加模型可见的上下文。它不是唤醒:空闲的智能体仍保持空闲(L07 的收件箱规则)。
// 一个更真实的例子:参数带取消信号的文件读取
async execute(args, exec) {
  // args 类型自动推断为 { path: string; limit?: number }
  return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
}

四、UI 渲染意图:工具的"门面"要提前想Render Intent

工具结果的 UI 展示是设计的一部分(仓库明文规定"写工具时就要决定"),通过纯展示函数声明卡片类型:

卡片适合例子
generic普通结果,可加图标/文件位置read
terminal你的调用就是一条命令bash
diff创建/修改了文件write / edit
search发现型结果(分组匹配/路径清单)grep / glob
web完成的网络检索web_search / web_fetch

两条硬规则:① 纯函数——展示函数在直播和回放时都会跑,禁止 I/O、读会话状态、时钟/随机;② UI 专用格式不进模型结果——代码块装饰、相对路径美化是 UI 的事,模型内容保持干净。

没有 UI 声明会怎样?退回通用卡片(标题=工具名,原始参数做输入)。所以最小实现(像 greet 只写 output.render)完全合法——UI 卡片是增强,不是负担。

五、两个进阶能力Advanced

后台任务:run_in_background 模式

长任务不堵会话:配置里开启后台门控 → execute 里走 ctx.jobs.start({ kind, label, owner: exec.agent, run }),成功返回类型化的后台句柄(如 { kind: 'background', jobId }),模型稍后用 job_* 控制工具查询/停止。Code Mode 绝不能从人类可读文字里解析 jobId——句柄必须是结构化的。

Code Mode 免费接入

Code Mode(模型写代码调工具的模式)里,每个已注册工具自动变成 await tools.greet({ name: 'Ada' })——参数和返回类型从同一套 schema 推导,调用走正常执行管线,你什么都不用做。因此 output.schema 要当真正的程序 API 设计:直接返回句柄和字段,人类解释留在 render。

defineTool
类型化的工具定义助手:schema 即类型来源。
canonical value
execute 唯一返回的那个规范 JSON 值。
render intent
工具 UI 卡片类型:generic/terminal/diff…
exec.signal
取消信号,在途工作应随它中止。
✏️ 动手练习
  1. 完成 greet 工具并在 Web UI 验证模型能调用它。
  2. 改造:加一个可选参数 style: 'formal' | 'casual'(union 类型),两种风格返回不同问候语;验证模型会正确传参。
  3. 把 output.schema 改为对象 { greeting: string, length: number },render 里拼文本——体会"规范值给程序、render 给模型"的分工。
  4. 故意在 execute 里 throw new Error('boom'),观察 UI 如何展示 isError 结果。
📝 自测(点击展开答案)
1. 模型凭什么决定"现在该调我的工具"?
主要靠 description——写给模型看的说明书,写不清工具等于不存在。parameters 里各字段的 description 同理。
2. execute 返回的东西去哪了?经历什么处理?
规范值 → 框架快照为无损 JSON → 按 output.schema 校验 → 冻结 → 交 output.render 生成模型可见内容;Code Mode 下直接拿到规范值本身。
3. "业务失败"和"基础设施故障"在工具里分别怎么表达?
业务失败(如退出码非零)是正常规范值,交给渲染层解释;基础设施故障直接 throw,框架标记 isError。混用会让模型看到不必要的报错。
4. 为什么展示函数(presentCall/presentResult)必须是纯函数?
它们在直播流和会话日志回放时都要运行;混入 I/O、会话状态或时钟,回放就无法复现当时的卡片。
5. 后台任务成功返回时,为什么句柄必须是结构化对象而不是一句"已启动任务 bash-1"?
Code Mode 的程序要靠返回值继续操作(查询/停止任务),从散文里解析 id 是脆弱的;规范值就是程序 API。
权威出处:basic/tool.zh.md(入门) · cookbook/adding-a-tool.md(本课主要依据) · dsh-tools README