第11讲 · 第一个自定义工具

《DeepSeek Harness 从上手到精通》系列 · 第四阶 工具开发篇

🎯 目标:从零写一个模型可调用的工具并挂载 难度 ★★☆ ⏱ 约 35 分钟 前置:第 4 讲 Cordis + 第 9 讲 patch

系列基于 deepseek-ai/deepseek-harness0.1.0-rc.5,MIT)。前三阶你都在"用配置换脑",从这一讲起,你给模型长出新手脚——写一个它原本不会调用的工具。需要一点 TypeScript 基础(async/await 即可)。preview 阶段 API 可能微调,以官方 packages/core 为准。


一、工具在 dsh 里到底是什么

对模型而言,一个工具就是一段"带 schema 的能力":它有名字、有参数说明、有返回值。模型在 turn 中根据系统提示词里的工具 schema 决定"点名"哪一个,然后把参数填好交给框架执行。所以写工具 = 写三样东西:

  • schema:参数定义(名字、类型、是否必填)与描述;
  • 实现:真正干活的 execute 函数;
  • 注册:把工具挂到 ctx.tools,模型才"看得见"。
自定义工具接入系统
图 1 自定义工具像一块拼图插进系统板:声明 schema、实现逻辑、注册到 ctx.tools,模型即可调用。

二、最小工具组件:schema + 实现 + 注册

下面是一个概念示意(Cordis 插件骨架 + 注册到 ctx.tools)。真实 API 以你本机 packages/core 为准,但套路一致:

// plugins/local-weather.ts import { definePlugin } from '@cordisjs/core' export default definePlugin((ctx) => { // 声明依赖:需要 tools 服务 ctx.inject(['tools'], (ctx) => { const myTool = { name: 'local_weather', description: '查询指定城市的当前天气(温度、天气状况)。' + '当用户询问某城市或本机所在地的天气时使用。', parameters: { type: 'object', properties: { city: { type: 'string', description: '城市名,如 "北京"' } }, required: ['city'] }, async execute({ city }: { city: string }) { // 这里接你的数据源;下面用示意数据 return { city, temp: 23, condition: '晴' } } } ctx.tools.register(myTool) // 可逆副作用:插件卸载时自动摘除 ctx.on('dispose', () => ctx.tools.unregister('local_weather')) }) })

注意 ctx.on('dispose', ...)——这正是 Cordis 的可逆副作用:插件挂上时注册工具,卸下时自动反注册,系统回到原样。

三、通过 --patch 挂载 / 卸载

写好插件后,用一份补丁把它作为入口挂进去(回顾第 9 讲 --patch):

# my-tools.patch.yml plugins: - entry: ./plugins/local-weather.ts # 挂载运行 dsh web --patch my-tools.patch.yml # 卸载?直接去掉 --patch 重启即可(可逆副作用生效)

插件构建(tsdown / 本地热重载 vendor/hmr)细节留到第 20 讲。本讲先用"能跑起来"为目标,构建报错就先确认入口路径与依赖是否装好。

四、工具描述写法:影响模型调用准确率

模型靠 description 决定是否调用你的工具。描述写得好不好,直接决定调用准确率。几条经验:

  • 写清什么时候用("当用户问天气时"),而不只是"这是什么";
  • 参数 description 也要写清含义与格式,避免模型乱填;
  • 避免和已有工具"语义重叠",否则模型会在几个工具间犹豫;
  • 返回结构稳定、字段命名清晰,模型后续才好组织答案。

五、动手练习:写一个"查本机天气/汇率"小工具

本讲产出:一个可挂载、模型可调用的实用小工具(天气或汇率任选)。

# 步骤 1) 按第二节模板写 plugins/local-weather.ts(execute 里接真实 API 或占位数据) 2) 写 my-tools.patch.yml 指向该入口 3) dsh web --patch my-tools.patch.yml 4) 在对话里问:"北京现在天气怎么样?" 5) 观察模型是否点名 local_weather、参数是否填对、返回是否回填

成功标志:模型主动说出"我来查一下天气",并调用了你的 local_weather 工具。改错描述再试,体会"描述即接口"的含义。

六、常见坑

  • 模型不调用工具:多半是 description 没说清触发场景,或和已有工具重叠;
  • 参数类型对不上:schema 的 required / type 要和 execute 实参一致;
  • 挂载没生效:检查 entry 路径、插件是否构建/转译、依赖是否安装;
  • 忘记 dispose 反注册:热重载时残留旧工具,加上 unregister 更干净;
  • execute 抛错未捕获:异常会中断 turn,记得 try/catch 并返回友好结构。

七、官方文档对应章节

  • docs/ 中 tools、tool-catalog、plugin 开发相关章节;
  • packages/core 的 tools 服务与工具类型定义(schema 字段以源码为准);
  • 衔接第 4 讲 Cordis(inject / dispose)、第 9 讲 --patch 挂载;下一讲(第 12 讲)讲工具注册表与执行流水线。

本讲内容基于 DeepSeek Harness 官方文档与项目源码整理,仅供学习参考,具体命令与配置请以官方最新版本为准。系列文章配合源码仓库食用效果更佳:https://github.com/deepseek-ai/deepseek-harness

咨询 DeepSeek Harness 相关问题,请加微信:A0qingfengyuan_01

个人观点,仅供参考,如有不对之处,请多包涵,欢迎指出。本文由 AI 协助完成!