灯下哥谭 灯下哥谭
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

灯下哥谭

灯还亮着
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • OpenClaw

  • Hermes-Agent

  • Claude-Code

  • LLM推理

  • DeepSeek-Harness

    • DeepSeek Harness 实战 01|同一条纪律,写在两个位置,模型只听一个
    • DeepSeek Harness 实战 02|三个 Harness 的解剖:同一个文件名,三种加载语义
    • DeepSeek Harness 实战 03|学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型
    • DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力
    • DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆
    • DeepSeek Harness 实战 06|学习笔记:插件、工具、技能不在同一个维度上
      • 1. 先看三个数字
      • 2. 底座机制:插件是一份会被撕掉的租约
        • 租房比喻
        • 租约是套着的
        • 租客会被反复请出去再请回来
      • 3. 实例解剖:一个官方插件从挂载到卸载
        • 它在哪
        • 它是怎么被挂上的
        • 它住进来办了两件事
        • 调用链
        • 卸载之后会怎样
        • 官方自己就关过它
        • 伴生插件
      • 4. 「有多少插件」这个问题没有单一答案
      • 5. 技能是数据,不是代码
        • 关系不是并列,是嵌套
        • 一次完整的加载
        • 三条由此而来的实用结论
        • 一张收尾的图
      • 6. 坑与边界
        • 坑一:等待态不可区分
        • 坑二:按 id 覆盖是整行替换,不做深度合并
        • 坑三:同一个服务被注册两次,整个配置树拒绝加载
        • 坑四:初始化代码可能跑很多遍
        • 边界:能配的只有插件,没有工具
      • 7. 自检:怎么确认你真的理解了分层
      • 8. 想写自己的插件?官方指的不是提 PR
      • 9. 小结
      • 参考
  • AI-Agent
  • DeepSeek-Harness
灯下哥谭
2026-09-11
目录

DeepSeek Harness 实战 06|学习笔记:插件、工具、技能不在同一个维度上原创

# 学习笔记:插件、工具、技能不在同一个维度上

读 DeepSeek Harness(下称 dsh)的文档时,plugin、tool、skill 三个词高频出现,而且经常挨在一起。初读会自然地把它们当成三类并列的扩展方式——「我要加个能力,选哪一种」。这个理解是错的,而且错得代价不小:它会让你在排查「功能没出现」时反复查错层级。

真实的关系是嵌套的:插件是代码单元,工具是插件往外开的一个口子,技能是被某个工具读取的数据文件。三者的生命周期、修改成本、失败形态完全不同。本文把这条链拆开,并落到一个可以逐行对照的官方插件实例上。

版本说明

基于 0.1.5-rc.2(developer preview)的仓库源码实测,所有计数均来自对源码树的实际统计。插件级配置字段以你本机安装目录里各包自带的 README.zh.md 为准——公开文档站给的是全局架构,本文引用的多数细节在站上查不到。该项目仍处于 developer preview,读到与本文不符时以实机输出为准。


# 1. 先看三个数字

这三个数字是整篇文章的地基,因为它们直接否掉了「一个插件等于一把工具」这个默认联想。

统计项 数量
仓库内的包总数 278
其中源码里调用过 ctx.tools.register 的 31
共享底座 dsh-base 的组装清单行数 84

88% 的包一把工具都不注册。 它们做的是别的事:提供服务、监听事件、往系统提示词里贡献一段、在界面上挂一个面板、维护会话日志。

如果「工具」就是「插件」,剩下那 240 多个包无处安放。


# 2. 底座机制:插件是一份会被撕掉的租约

要理解后面所有现象,先要接受一个换算:dsh 里的「插件」不是一个已安装的模块,而是一次运行实例。框架把这个实例叫 fiber,源码注释写得很直白:

Runtime instance of one plugin application. A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by ctx.plugin().

注意 instance。插件本体是一个函数或类(模板),fiber 是它被挂载的那一次。同一个包在配置树里写两行,就是两个 fiber,各记各的账。

# 租房比喻

把插件想成租客,fiber 就是这个人这一次的租约:

  • 他住进来办的每件事——装宽带、办门禁卡、订牛奶——框架都往租约后面记一笔(这一笔在源码里叫 effect,产出一个 disposer)
  • 退租时,物业照着租约从后往前撕,一笔一笔销掉

这解释了读源码时最容易起疑的一件事:整套代码库几乎没有「卸载逻辑」。不是作者偷懒,是不需要——回收单位不是你注册的那一条,而是承载它的 fiber。

# 租约是套着的

源码里有关键的一行:子 fiber 的 disposer,本身就是注册在父 fiber 上的一个 effect。

所以销毁会沿树级联:二房东退租时,「销掉转租合同」只是他清单上的普通一笔,转租的人连带着走,框架不需要任何额外的递归清理代码。

# 租客会被反复请出去再请回来

这是最容易踩的一点。fiber 不是「启动一次就定型」,它持续盯着自己声明的依赖:

  • 依赖服务出现 → 重新执行插件体
  • 依赖服务消失 → 回到等待态,这一批 effect 全部被 drain 掉
  • 依赖再回来 → 再执行一遍

推论:插件的初始化代码里绝不能放「只该做一次」的副作用——追加写文件、发一次通知、给全局计数器加一。它会随依赖抖动被重复执行,而框架认为这完全正常,因为每次执行的账都被上一次的 drain 抹平了。


# 3. 实例解剖:一个官方插件从挂载到卸载

抽象讲完,落到实处。选 todo_write 这把工具背后的包,因为它足够小、职责单一、效果肉眼可见。

# 它在哪

packages/todo/tool-todo/
├── package.json      # @deepseek-ai/dsh-tool-todo
├── src/index.ts      # 插件本体
├── src/invariant.ts  # 一个独立的伴生插件
└── lib/              # 编译产物,实际加载的是这个
1
2
3
4
5

装好的环境里,它在 node_modules/@deepseek-ai/dsh-tool-todo/。

# 它是怎么被挂上的

不是「安装」,是在一张组装清单里写了一行。packages/bundle/base/cordis.patch.yml 里:

- id: tool-todo
  name: '@deepseek-ai/dsh-tool-todo'
  config:
    allowParallelInProgress: true
1
2
3
4

三个字段各司其职:

字段 作用
id 这一行的名字,后续所有覆盖靠它认人
name 去哪里解析这个包
config 配置,此处唯一字段是「允不允许多个任务同时处于进行中」

这一行就是签租约。系统读到它,为这个插件开一个 fiber。

# 它住进来办了两件事

apply() 函数通篇只做两件事——这两件就是记进租约的那两笔:

第一笔:注册一个会话投影单元 todos。 告诉界面「会话里有个叫 todos 的东西可以显示」,并规定它怎么演进:写了新清单就换成新的,下一轮开始时清空。

第二笔:注册工具 todo_write。 在模型可见的工具列表里加一把锤子,附带一段说明书。

就这两笔,没有第三件。

# 调用链

工具不由人调用,由模型调用。模型发来一份完整清单:

{"todos": [
  {"content": "读配置文件", "status": "completed"},
  {"content": "改端口号",   "status": "in_progress"},
  {"content": "重启服务",   "status": "pending"}
]}
1
2
3
4
5

插件收到后做三步:校验(内容非空、不重复;若配置禁止并行,进行中超过一个直接报错)→ 往会话日志追加一条 todo/write 事件 → 返回计数。

关键细节:清单本身不存在这个插件里。 它被写进了事件溯源的会话日志。插件只是「验货 + 转交」的窗口,当前清单等于日志里最后一条 todo/write,回放时后写覆盖先写。

这带来一个容易忽略的性质:清单归属发起它的那一个 agent 会话。子 agent 有自己的一份,互不相通,也没有跨 agent 共享的通道。

# 卸载之后会怎样

把那一行删掉或改成 disabled: true,fiber 退租,记账本从后往前撕:

撕掉的那一笔 现实后果
工具 todo_write 注销 模型的工具列表里没有这把工具——不是调用失败,是压根不知道它存在
投影单元 todos 注销 界面上的任务清单面板空了,数据没人往外送

不受影响的:已经写进会话日志的历史记录还在。日志是事件流,插件退租不回头删日志。其他功能也不受影响——这个包是纯叶子节点,只消费不提供,没有别的包依赖它。

反过来,它依赖谁:源码里声明了两个必需服务(工具注册表与会话投影注册表)。这两个服务缺任何一个,它就停在等待态——不报错、不启动、工具列表里没有它。

# 官方自己就关过它

packages/bundle/web-app/cordis.patch.yml 里有这么一行:

- id: tool-todo
  disabled: true
1
2

web 模式把 base 里那一行否掉了。原因是在那条路径上,todo_write 改由 agent preset 提供——标准 preset 的清单里有一行一模一样的挂载,于是它从「整个应用挂一次」变成「每个 agent 会话各挂一次」。

这一个动作同时演示了两件事:

  1. 按 id 覆盖——后面这层只写 id 和 disabled,就把前面的整行否掉了
  2. 同一个包挂在不同位置,语义完全不同——挂在应用根上是全局共用一份清单,挂在会话上是每个会话独立一份

# 伴生插件

src/invariant.ts 是另一个独立的插件,名字 tool-todo-invariant,依赖不变式服务。它的职责是在 todo/write 写进持久日志之前校验格式。

有意思的是它刻意不检查「同时几个进行中」。包自带 README 给的理由很实在:那是部署策略而非数据规则,今天允许并行时写下的日志,明天把策略收紧后仍然必须能回放,把不变式绑到当前配置会让合法的历史被拒绝。

两个插件是两份租约,可以分别挂、分别退。退掉伴生的那个,只是少一层校验,工具照常可用。


# 4. 「有多少插件」这个问题没有单一答案

统计过一轮之后会发现,这个问题至少有三个层级的答案,而且差得很远。

第一层:仓库里有 278 个包。 这是货架上有多少种货。其中导出插件形状(有 apply 或是 Service)的约 124 个,其余是被引用的普通库,不上租约。

第二层:跑起来实际挂了多少。 这才是有实际意义的数字。算法就是「按 id 覆盖、最后一次写入生效」:

base 底座        84 行
+ 模式层新增     68 行
− 模式层关掉     24 行
─────────────────────
≈ 128 个 fiber
1
2
3
4
5

各模式的组装清单行数差别极大:

模式 清单行数 说明
base(共享底座) 84 web / headless / sdk / acp 都从它起步
web-app 94 底座上加 68 关 24
headless 5 只改 5 行,其余全继承底座
acp-app 4 同上
sdk-app 4 同上
sdk-minimal 31 不继承底座,自建一棵完整的树

headless 与 acp 各自只改 4~5 行——它们不是「另一套系统」,是同一个底座上打的几个补丁。只有 sdk-minimal 是独立树。

第三层:每个 agent 会话还会再挂一批。 上面那约 128 个是整个应用挂一次的;每开一个 agent 会话,会按它用的 preset 再挂一棵子树:

preset 清单行数
standard 31
ptc 31
cordis 19
minimal 2

minimal 只有 2 行——这就是「极简 preset 把人设换成一句话、其他提示词段落全部消失」的机械原因:它整棵子树只挂了两个插件。

结论:插件不是「装在系统里」,是「挂在某棵树的某个位置上」。同一个包挂三次就是三个 fiber、三份独立的账。这也解释了为什么运行时清单接口只能给一张当下快照,且不告诉你条目来自哪个 bundle 或 override——它数的是活着的 fiber,不是装了几个包。


# 5. 技能是数据,不是代码

现在回到开头的三层区分,把最后一层补上。

# 关系不是并列,是嵌套

plugin(4 个插件共同撑起 skill 体系)
   └─ 其中一个注册了一把 tool,名字就叫 `skill`
         └─ 这把 tool 干的事:读磁盘上的 .md 文件,把正文交给模型
               └─ 那个 .md 文件,才是 skill
1
2
3
4

skill 不与 plugin 平级。skill 是某个 plugin 提供的 tool 所读取的数据。

dsh 的整个技能体系由四个包拼成:

包 职责
skill 注册表——汇总各来源的技能,裁决重名冲突,来源不可用时保留可用结果
skill-filesystem 从磁盘发现技能,监视目录变化
skill-badge 一个内置技能提供方(随附组合里默认禁用)
tool-skill 注册那把叫 skill 的工具,让模型能加载

四个插件,只为了让模型能读你写的 Markdown。

# 一次完整的加载

  1. 你在被扫描的根目录下放一个目录 bundle(含 SKILL.md)或平铺的 <name>.md
  2. skill-filesystem 监视着这些目录,发现变化 → 解析 YAML frontmatter → 报给注册表。无需重启,包自带 README 明写「新增、改名或删除的 skill 无需重启即可到达 agent」
  3. tool-skill 把所有技能的名字 + 有长度上限的描述汇成目录,塞进首次请求——只有名字和描述,没有正文
  4. 模型看到目录里有用得上的,调用 skill 工具,参数是名字
  5. 工具返回该文件的完整正文
  6. 人直接打 /name 也是同一条路,直接触发第 5 步

# 三条由此而来的实用结论

其一,技能是按需加载的,插件不是。 插件在启动时全部挂上,不用也占着;技能平时只占一行描述,模型觉得要用才把正文拉进上下文。所以技能可以写得长而啰嗦,而插件贡献的提示词段落必须精简——前者不用不花钱,后者每次请求都在。

其二,改技能免重启,改插件必重启。

改了什么 要不要重启
某个技能 .md 文件 不用,有插件在监视目录
组装清单里某一行插件 要
插件源码 要

分界线就是数据与代码:数据能热读,代码在 import 时已经进了内存。

其三,判断该用哪个的标准很干脆。 想让模型「学会某个规程」用技能;想让模型「能做某件事」用插件。技能改不了模型能做什么,只改它知道该怎么做。

# 一张收尾的图

        代码层                              数据层
┌──────────────────────┐          ┌────────────────────┐
│  plugin(约 128 个)  │          │  skill(.md 文件)  │
│  ├─ 注册 tool ────────┼───读──→  │  随时改,免重启      │
│  ├─ 提供服务          │          │  按需加载正文        │
│  ├─ 贡献提示词段落    │          └────────────────────┘
│  └─ 挂界面面板        │
│  启动时定,改了要重启  │
└──────────────────────┘
1
2
3
4
5
6
7
8
9

tool 在这张图里没有自己的格子,因为它不是独立实体,是插件往外开的一个口子。


# 6. 坑与边界

这套设计的代价集中在一处:失败几乎都是静默的。

# 坑一:等待态不可区分

fiber 的状态是算出来的,不是设上去的,算法只有四个分支:已销毁 / 失败 / 活跃 / 其余一律等待。

后果是——「依赖服务缺失」与「这个插件压根没被挂上」落在同一个等待态,界面上、日志里都长得一模一样。你把配置写对了、重启了、功能还是不出现,看到的现象与「这行配置根本没写」完全一致。

可用的规则:遇到「配置不生效」,第一诊断不是查字段拼写,而是确认承载它的插件在当前启动模式下有没有被加载——CLI 与 Web 两条路径的插件树不一样。

# 坑二:按 id 覆盖是整行替换,不做深度合并

用户层补丁按 id 定位目标行,然后替换它的整个 config。

你只想改一个字段,也必须把这行里想保留的每个字段全部重述一遍。 漏写的字段不是「继承默认值」,是消失。

# 坑三:同一个服务被注册两次,整个配置树拒绝加载

这一条与前两条相反,是响亮的失败,但形态出人意料:不是某个插件不工作,是整个 profile 起不来。

包自带 README 给的具体例子:在沙箱化的文件系统提供方旁边再挂一个普通提供方,两者注册同一个服务,加载直接失败,二选一。

# 坑四:初始化代码可能跑很多遍

前面已经说过机制,这里只重申后果:插件体里不能放只该执行一次的副作用。依赖抖动会让它重复执行,而框架认为这是正常工况。

# 边界:能配的只有插件,没有工具

配置文件里能写的每一行都是一个插件,没有任何一行是一把工具。

想开关某把工具,只能开关提供它的那个插件。而如果那个插件带了六把(例如终端相关的那个包一次注册 6 把工具,terminal_open / terminal_send / terminal_read / terminal_signal / terminal_close / terminal_list),你只能六把一起开或一起关,没有单独摘掉其中一把的配置入口。


# 7. 自检:怎么确认你真的理解了分层

下面四步都可以在本机验证,不需要改动生产配置。

第一步,确认「插件 ≠ 工具」。 在源码树里统计有多少包调用过工具注册接口,与包总数对比。预期输出:注册工具的包是少数(本文实测 31 / 278)。

第二步,确认「一个插件可以带多把工具」。 找到终端相关的那个工具包,数它注册了几把。预期输出:6 把,且配置文件里只有一行对应它。

第三步,确认「同一个包能挂在不同层」。 在底座与 web 模式两份组装清单里搜同一个 id,对照它在两处的写法。预期输出:底座里是完整定义,web 模式里是一行 disabled: true,而同名条目出现在 agent preset 的清单里。

第四步,确认「依赖缺失是静默的」。 在一个隔离的测试组合里,给某个插件写一个不存在的依赖名,重启。预期输出:没有任何错误日志,该功能直接不存在——这正是坑一描述的形态。


# 8. 想写自己的插件?官方指的不是提 PR

这一节是个必要的提醒,因为它与多数开源项目的默认预期相反。

dsh 仓库的 CONTRIBUTING.zh.md 写得很明确:

「DeepSeek Harness 仍处于早期阶段,并在积极开发中。很抱歉,我们目前无法接受外部 PR。」

但同一份文件紧接着给出了官方指定的替代路径,态度也很清楚:

「创建令你感兴趣的插件,并分享给其他人:为你的 GitHub 项目添加 dsh-plugin 话题,让其他人更容易找到你的插件。」

「我们并不认为官方仓库中的包天然就比社区开发的包更重要。你可以将本仓库看作一种理念、一份官方示例以及一处灵感来源,而不是我们要求社区遵循的方向。」

结合本文第 4 节的机制,这个安排是自洽的:加载一个插件就是在配置树里写一行包名,社区包与官方包在加载时地位完全平等,没有任何优先级差异。插件生态不需要中心仓库,因为组装本来就发生在使用者这一侧。


# 9. 小结

维度 plugin tool skill
是什么 会被启停回收的功能单元 模型能调的一个动作 写给模型看的指令文本
形态 代码 代码里的一个入口 Markdown 文件
谁提供 开发者 / 你 由某个 plugin 注册 任何人
何时确定 启动时读配置树 跟着 plugin 生灭 运行时随时改
改了要重启吗 要 跟随 plugin 不要
配置文件里能写吗 能 不能 不能(写的是扫描根)

最后给一个可以直接用的排查口诀,三种现象的根因在三个不同的层:

  • 模型「不知道有这个技能」 → 查技能发现插件扫的是哪几个根目录,你的文件在不在里面
  • 模型「没有这个工具」 → 查提供它的插件起来没有(多半卡在静默的等待态)
  • 模型「知道但做错了」 → 是技能正文写得不清楚,跟前两层没关系

查错层,就会在字段拼写上空转很久。


# 参考

dsh 的公开文档站有中文版:DeepSeek Harness 参考手册 (opens new window),仓库在 deepseek-ai/deepseek-harness (opens new window)。再次提醒:插件级配置字段与「已知限制」原文,以本地安装目录里各包自带的 README.zh.md 为准——本文引用的多数细节在站上查不到。

本系列前几篇:

  • 提示词分层与技能加载实测——persona 与工作区指令走的是两条独立通道
  • 三个 Harness 的解剖——本文填的正是那篇结尾预告的「插件一词的不同含义」
  • 五个核心心智模型——本文第 2 节的 fiber 机制在那里有更完整的骨架
  • 三处设计张力——「配置不生效先查插件树」那条规则的出处
Agent 可直接解析的元数据块
{
  "_meta": {
    "doc_version": "2026-09-11",
    "article_id": "dsh-06-plugin-tool-skill",
    "profile_context": "any",
    "estimated_setup_time": "15min"
  },
  "quick_start": {
    "step_1": "统计源码树里调用工具注册接口的包数与包总数,确认二者量级差异",
    "step_2": "选一个单职责工具包,逐行读它的 apply(),数清它注册了哪几笔",
    "step_3": "在底座与模式层两份组装清单里搜同一个 id,对照覆盖写法",
    "step_4": "在隔离组合里给插件写一个不存在的依赖,重启并确认无报错"
  },
  "layers": {
    "plugin": "代码单元,一次挂载等于一个 fiber,启动时由配置树决定,改动需重启",
    "tool": "由插件注册的模型可调入口,无独立配置层,跟随插件生灭",
    "skill": "Markdown 数据文件,由技能体系的四个插件发现并按需加载,改动免重启"
  },
  "safety_rules": [
    "插件初始化代码里不要放只该执行一次的副作用,依赖抖动会导致重复执行",
    "按 id 覆盖是整行替换而非深度合并,覆盖时必须重述所有想保留的字段",
    "不要在沙箱化服务提供方旁再挂同名服务的普通提供方,会导致整个配置树拒绝加载",
    "不要指望通过配置单独禁用某一把工具,最小粒度是提供它的插件"
  ],
  "verification": {
    "check_tool_registrar_ratio": "统计注册工具的包数占包总数的比例,确认工具不等于插件",
    "check_multi_tool_package": "确认终端工具包在一行配置下注册了 6 把工具",
    "check_id_override": "对照同一 id 在底座与模式层清单中的不同写法",
    "check_silent_pending": "给插件写不存在的依赖后重启,确认无错误日志且功能缺失"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

AI Agent 使用指南:

  • 当用户说「模型没有这个工具」→ 查提供它的插件是否启动,而非查工具名拼写
  • 当用户说「技能没生效」→ 先确认文件是否落在被扫描的根目录内,技能改动不需要重启
  • 当用户说「配置写了没效果」→ 按 check_silent_pending 排除静默等待态,再查字段
  • 当用户说「怎么给 dsh 提 PR」→ 告知官方不接受外部 PR,正路是自建仓库并打 dsh-plugin 话题
#AI Agent#Agent架构#插件系统#DeepSeek Harness
上次更新: 9/11/2026

← DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆

最近更新
01
DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆 原创
09-09
02
DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力 原创
09-08
03
DeepSeek Harness 实战 03|学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型 原创
09-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式