dsh 写第一个插件:30 行给 Agent 加个会干活的工具
生命不息,折腾不止。上次把 DeepSeek Harness 接上中转站只是「会用」,这篇带你走进它的灵魂——自己动手写插件,30 行代码给 Agent 加一个会干活的工具。
一、插件到底是个啥:一切皆插件
DeepSeek Harness(命令行叫 dsh)是 DeepSeek 官方开源的 Agent 框架,它的核心设计就一句话:一切皆插件。这句话不是营销,是字面意思——模型适配器、工具注册表、会话日志、甚至 agent 主循环本身,全都是插件。整个产品就是启动时从若干层配置里组合出来的一棵插件树。
底层驱动是一个叫 Cordis 的插件框架(DeepSeek 魔改了一份 vendored 版本)。在 dsh 眼里,插件就是一个 TypeScript 模块,导出 apply(ctx) 函数,框架加载插件时会调用它,把 ctx 上下文对象传进来,你就在里面注册各种能力。
1 | import type { Context } from '@deepseek-ai/cordis' |
这就是一个插件的全部骨架。好处显而易见:想加什么能力不用改主程序,写个插件挂上去就行;插件卸载时,通过 ctx 注册的一切(事件监听、工具、定时器)会自动清理,不用担心残留。
插件有三种形态:函数插件(最常见,上面的就是)、对象插件(带 apply 方法的对象)、类插件(Service 子类,适合对外提供服务)。在你需要公开服务之前,一直用函数形态就够了。
二、环境准备:把源码仓库拉下来
写 dsh 插件,官方推荐在源码仓库里开发,这样能用仓库自带的命令行和脚本(typecheck / lint / test / build),还能用 vendored 启动器做不需要 API Key 的纯链路验证。
环境要求:Node.js ^22.19 或 >=24,包管理器用 pnpm(仓库用 Corepack 锁了版本)。
1 | git clone https://github.com/deepseek-ai/deepseek-harness.git |
装完先跑一把确认环境 OK:
1 | pnpm run typecheck # 类型检查(strict) |
注意:dsh 目前是 developer preview(v0.1 预览版),README 里大写加粗写着「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」——接口会变,插件开发时留意版本,后续文章会讲怎么 pin 版本。
三、30 行写第一个插件:hello 走一个
在仓库根目录建个临时目录,创建 tmp/hello-plugin/hello.ts:
1 | import type { Context } from '@deepseek-ai/cordis' |
再建一个 cordis.yml(Cordis 配置清单,loader 按它挂插件):
1 | - name: './hello.ts' |
然后从 tmp/hello-plugin 目录用仓库自带的 vendored 启动器跑起来(这一步不需要 API Key):
1 | node --import tsx ../../vendor/cordis/bin.js |
预期输出:
1 | [info] hello from my first plugin |
发生了什么?启动器创建根 Context 并挂上 Loader 插件 → Loader 读取 cordis.yml → 把 ./hello.ts 作为子插件挂载 → Cordis 调用你的 apply(ctx)。链路通了,插件的「最小闭环」就成立了。
一个正式的函数插件通常导出四样东西,认识一下:
| 导出 | 作用 |
|---|---|
name |
插件显示名,仅用于诊断日志 |
inject |
声明依赖的服务(如 ['tools']),loader 会等它们就绪才执行 apply |
Config |
可选,部署期配置的校验 schema |
apply(ctx, config) |
插件主体,注册一切能力 |
两个细节:函数插件必须用命名导出(export function apply),别配默认导出,否则 loader 会丢掉 inject 元数据;有 Config 时 apply 签名是 (ctx, config),没有时是 (ctx)。
四、把插件挂到 dsh 上:三条路
写好的插件怎么让 dsh 真正加载?三条路,按场景选:
路 1:临时 overlay(调试最快)。写一个 patch 文件,用 --patch 参数启动:
1 | # scratch-plugin/cordis.yml |
1 | pnpm dsh web --patch ./scratch-plugin/cordis.yml |
打开 http://127.0.0.1:3080(dsh Web UI 默认端口),终端里能看到 [hello-plugin] plugin loaded!。注意插件路径必须写绝对路径。
路 2:外置插件(正式安装,推荐)。把插件做成独立 npm 包(package.json 里声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }),然后用 dsh plugin 子命令装进 profile——它本质是在 profile 目录里调 pnpm,所以 pnpm 的子命令全能透传:
1 | dsh plugin --profile web add ./hello-plugin # 本地包 |
profile 是 dsh 的「可运行环境」,目录在
$DSH_HOME/profiles/<名字>/,web、headless 是内置模板。装完插件记得重启 dsh web,宿主代码在启动时加载,只刷新页面不够。
路 3:看插件树 debug。插件装了却没生效,别对着界面猜,直接打印最终组合出来的配置树:
1 | dsh --profile web --dump-config |
任何打印出来的行,都可以用你自己的 patch 按行 id 替换。记住 dsh 配置的四层加载顺序:profile bundles → profile 的 cordis.patch.yml → 家目录 $DSH_HOME/cordis.patch.yml → 每个 --patch overlay。后层按行胜出,patch 替换的是整行 config(不是深度合并),改一个字段也得把整行键重述一遍——这是新手最常踩的坑。
五、实战:给模型加一个会干活的工具
插件最常见的用途就是给模型加工具。工具注册在 ctx.tools 上,schema 会自动进入 prompt 组装,模型就能「看到」它并主动调用。
下面是一个完整可运行的最小工具插件(30 行上下,名副其实):让 Agent 能读文件。核心 API 是 defineTool(来自 @deepseek-ai/dsh-tools)+ ctx.tools.register():
1 | import { readFile } from 'node:fs/promises' |
拆开看 execute() 契约的几条硬规则:
- args 自动校验:
defineTool会在execute前校验模型生成的参数(类型、必填、枚举、嵌套),你拿到的args类型和 schema 一致; - 只返回一个规范 JSON 值:
output.schema定义返回值,execute只返回它;抛异常 = 出错(isError),业务上的非理想状态(比如非零退出码)也要放进规范值返回; - 遵守
exec.signal:信号触发时要取消进行中的工作,别硬扛; - 只注册一次:注册借用的是只读定义,事后别改 schema;想换工具就释放它所属的 effect 再注册新的。
这样一个插件装好后,你在 Web UI 里跟 Agent 说「读一下 /opt/xxx/config.yaml 的前 50 行」,模型就会自己调 read_file 工具,把内容读回来再回答你——这就是给 Agent 装「手」的过程。
六、进阶玩法:inject 依赖与事件钩子
不想加新工具,只想在某个环节「插一脚」?用事件钩子。dsh 主循环是事件驱动的,钩子插件就是往这些事件上挂监听器。比如下面这个权限门插件,在每次工具调用前拦截,按规则允许或拒绝:
1 | import type { Context } from '@deepseek-ai/cordis' |
tools/pre-execute 是 waterfall 事件:监听器收到 (...args, next),必须调用 next() 把结果传给下一个监听器;不调 next() 直接 return 就是短路(截断整条链)。这是写监听器最容易踩的坑——忘了 next()。
常用扩展点速查:
| 你想做什么 | 用哪个 |
|---|---|
| 允许 / 拒绝 / 询问工具调用 | tools/pre-execute,返回 { kind: 'deny' } / { kind: 'ask' } |
| 工具调用必须被最终否决、不可撤销 | ctx.tools.guard() |
| 包裹工具执行生命周期(超时/重试/指标) | tools/execute |
| 改写工具结果或呈现内容 | tools/post-execute |
| 只观察最终结果(审计/记录) | tools/result |
七、避坑提醒
- 安全第一:插件跑在 dsh 宿主进程里,属于可信代码——装第三方插件前,先看它仓库是否公开、许可证和维护者是否清楚、要什么权限,别因为一行安装命令就跳过检查(官方文档自己都反复强调);
- 预览版接口会变:dsh 还是 v0.1 preview,插件契约可能随版本变动,开发时把 harness 版本记进 README,跟 GitHub Discussions 的变更公告;
- 函数插件别配默认导出:会丢
inject元数据,导致依赖的服务没就绪就执行apply; - patch 替换整行:不是深度合并,改一个键也要重述整行;
- 跑真实模型要 API Key:工具插件要真正被模型调用,得有模型可跑。官方 key 直接填
DEEPSEEK_API_KEY就行;想一个 Key 玩遍 Claude、GPT、Gemini、DeepSeek 全系模型,可以接 ai.aklibk.com 中转站——OpenAI 兼容接口、人民币按量付费,DeepSeek 系模型价格还便宜,settings 里改个 base URL 就换底层模型,插件的价值直接翻倍。
生命不息,折腾不止。下一篇《DeepSeek Harness 多 Agent 协作实战》,教你把一个大任务拆给多个 Agent 并行干活,让 dsh 从「单打独斗」升级成「团队作战」。