DeepSeek Harness 写第一个插件:从 hello world 到真工具
生命不息,折腾不止。基础装好 dsh 只是开始,亲手写个插件,才算真正「拥有」了这个 Agent。
上篇《DeepSeek Harness 接入中转站教程》带你把 dsh 跑了起来,能对话、能干活了。但 dsh 和别的 Agent 框架最大的不同,是那句写在官网首页的口号:一切皆插件(Everything is a Plugin)。模型适配器是插件、工具是插件、会话存储是插件,连 agent loop 和 Web UI 本身都是插件——这意味着你对哪一块不满意,都可以自己换、自己写,不用 fork 整个仓库。
今天这篇就手把手带你写两个插件:一个三分钟的 hello world,一个真正能被模型调用的工具,最后再把它升级成能查天气的实用工具。全程 TypeScript,但你没写过 TS 也照抄得动。
一、先搞懂:dsh 的插件到底是个啥
先看官方给的定义:插件是一个导出 apply 函数的 TypeScript 模块。框架加载插件时调用 apply,传进来一个 ctx(上下文对象),你通过 ctx 往系统里注册能力。
dsh 底层跑的是 Cordis 插件框架,它是个非常「小」的内核——只负责插件的挂载、卸载和依赖管理,其余所有业务逻辑都住在插件里。所以插件之间怎么协作?靠两个东西:
- 服务(Service):每个能力占一个稳定的挂载点,比如
ctx.tools(工具注册表)、ctx.llm(模型适配)、ctx.sessions(会话存储)。别的插件要用某个能力,直接按 key 找,不用管实现是谁。 - 依赖声明(inject):插件声明
inject: ['tools'],Cordis 会等tools服务就绪后才调用你的apply。加载顺序由依赖决定,而不是文件顺序。
还有个很爽的设计:注册是可逆的副作用。你通过 ctx 注册的一切——事件监听、工具、定时器——插件卸载时自动清理,不用手动 removeListener 或 clearInterval。有需要手动释放的资源(比如网络连接),包进 ctx.effect() 返回一个清理函数就行。
二、第一个插件:hello-plugin,三分钟跑起来
要写 harness 插件,最省事的路子是从源码运行(官方推荐,因为插件要用 --patch 挂进 Web UI)。前置环境:Node 22.19+ / 24+、pnpm 11.7.0 左右,然后:
1 | git clone https://github.com/deepseek-ai/deepseek-harness.git |
如果你只想跑现成插件不想写代码,一条
npx @deepseek-ai/dsh web就启动了,但今天我们要动手,建议还是 clone 源码。
在仓库根目录建一个临时项目:
1 | mkdir -p scratch-plugin/src |
创建 scratch-plugin/src/my-plugin.ts,这就是一个完整的最小插件:
1 | import type { Context } from '@deepseek-ai/cordis' |
然后创建 scratch-plugin/cordis.yml,把插件注册进去。注意两点:路径必须是绝对路径,patch 文件只贡献配置,不改变模块解析的目录:
1 | - insert: |
用覆盖层启动 Web UI:
1 | pnpm dsh web --patch ./scratch-plugin/cordis.yml |
打开 http://127.0.0.1:3080,同时看终端——启动过程中会打印 [hello-plugin] plugin loaded!。第一个插件,跑通了。
三、第一个真工具:greet,让模型学会「叫人」
光打日志不过瘾,插件真正的价值是给模型提供工具。工具在 dsh 里分两半:声明(name、description、parameters)会被组装进系统提示词,模型据此决定什么时候调、用什么参数调;执行(execute)在宿主进程里跑,结果再喂回模型。
官方工具定义 DSL 是 defineTool,五个要素:name、description、parameters、output、execute。看一个完整例子——greet 工具,让模型学会按名字打招呼:
1 | import type { Context } from '@deepseek-ai/cordis' |
保存为 scratch-plugin/src/greet-tool.ts,再写一个 patch 文件 scratch-plugin/greet.yml:
1 | - insert: |
启动并测试:
1 | pnpm dsh web --patch ./scratch-plugin/greet.yml |
在 Web UI 里输入:Use the greet tool to greet Ada. 模型会调用 greet,然后收到 Hello, Ada! 这个工具结果。
模型眼里你的工具长这样——注册表把声明投影成 ToolSchema,自动流进系统提示词:
1 | name: greet |
注意:output、execute、UI 展示方法这些实现细节绝不会出现在模型请求里。模型永远只看到「这工具叫啥、能干啥、参数长啥样」。
四、升级实战:写一个能查天气的工具
greet 只是教学玩具,我们来写个真有用的——查天气工具,接免费的 wttr.in API,不需要注册、不需要 API key。
wttr.in 的用法很简洁:https://wttr.in/城市?format=j1 返回 JSON,current_condition[0] 里有当前温度、天气描述。插件代码:
1 | import type { Context } from '@deepseek-ai/cordis' |
挂载、启动、测试:
1 | # cordis.yml 里 insert 的 name 指向 weather-tool.ts 即可 |
然后在 Web UI 里输入:帮我看看北京今天天气怎么样? 模型就会调用 weather 工具,返回类似 北京 当前 28°C,天气:Sunny,湿度 60% 的结果。
这个例子展示了插件的核心玩法:dsh 的工具本质上就是「给模型接一个函数」,外面世界的任何 API——天气、汇率、快递、你公司的内部接口——都能变成模型随手可调的工具。写插件要反复测模型调用,想多模型轮着试的话,中转站一个 key 就能把 DeepSeek、Claude、GPT 全接上,多模型对接、价格还便宜,挺适合折腾阶段(详情见上篇教程)。
五、避坑指南:新手最容易踩的四个坑
路径必须是绝对路径。
cordis.yml里插件name写相对路径,loader 会找不到模块。拿不准就pwd一下再拼。插件没反应?先查 fiber 状态。每个已加载的插件实例都有个状态机:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED。PENDING 表示插件已声明但依赖的服务还没就绪——「为什么我的插件没有输出」最常见的答案。检查是不是inject漏了、或者依赖的插件没启用。模型传的参数可能不合法,别慌。
defineTool在execute之前会按 schema 做运行时校验,校验失败抛ToolArgsError,注册表会把它作为模型可修正的错误返回——模型会看到哪里错了重新生成参数,而不是让插件崩溃。所以execute里的args和声明的类型是一致的。execute只返回规范 JSON 值,别返回内容块。像[{ type: 'text', text: '...' }]这种是render的职责,你返回普通值(字符串、对象、数组都行),注册表校验冻结后交给render转成模型可见内容。基础设施故障才抛异常;「不理想但成功」的领域结果(比如进程非零退出)放进返回值,让render解释。
写完这五个小节,你的 dsh 已经从「开箱即用的 Agent」变成了「能自己造零件的 Agent」。下一步值得玩的,是把多个 Agent 组织起来干活。
生命不息,折腾不止。下一篇:《DeepSeek Harness 多 Agent 协作实战》——把一个大任务拆给多个 Agent 并行干,让 dsh 从「单兵」变「战队」,敬请期待。