dsh-skill-indexer
让 agent 用一句话找到对的技能。 把本机所有
SKILL.md统一建成两级(大类 → 技能)召回索引,让任意 agent 用一句话意图就能召回到正确技能。纯本地、只读源目录、宿主无关(WorkBuddy / DSH 双宿主技能树都能扫)。
A DSH host bundle plugin that builds a two-level (category → skill) recall index over every local SKILL.md and routes a natural-language intent to the best-matching skills — read-only against the source trees, zero third-party runtime deps.
本文档是 skill-indexer PRD(v1.1,2026-08-19) 的 TypeScript 重写版:PRD 原为 Python 3 零依赖 CLI,按用户要求全量重写为 DSH Cordis 插件。源 PRD 为本地规格文档,见文末「参考」。
为什么需要它
技能越装越多,「我记得有这样一个 skill」就越来越难。翻目录、猜名字、逐个打开 SKILL.md —— 都是纯浪费。这个插件把技能库变成可检索的索引:agent 输入一句意图, 命中大类 → 类内排序 → 低置信回退,一步到位;全程离线、只读、确定性。
是什么
- 离线:启动时(或
/skill-index scan)扫描~/.workbuddy/skills与$DSH_HOME/skills,解析 frontmatter,抽取触发词/关键词,归纳大类,产出skills-index.json(哈希增量,未变技能保留 usage 统计)。 - 在线:
skill_index工具 //skill-index query对一句话意图做两级路由——L1 大类打分(关键词覆盖 × IDF)命中大类,L2 类内技能排序;低置信回退全库 flat top-k;未命中明确告知。 - 三态:
hit(命中)/low(低置信回退)/miss(未命中)。
安装(启用)
本仓库只发布、不随发布启用。要在某台机器启用:
dsh plugin --profile web add github:NattoCB/dsh-skill-indexer
或手工把 cordis.patch.yml 的 insert 行合并进目标 profile 的 patch,并 pnpm install。
快速上手
安装后,host 启动即自动建索引。手动控制:
/skill-index scan # 重建索引(无变更时提示 "no changes")
/skill-index query 把这周的ETF拉出来画个图 # 意图召回
/skill-index categories # 打印大类表
agent 侧可直接调用模型工具:
skill_index(query="把这个PDF转成markdown")
两级路由原理
- 匹配原语:子串匹配(
w in q,英文小写化去标点、中文直接子串)——不是 token 集合交集。短查询「把这周的ETF拉出来画个图」靠etf一词即命中。 - L1 大类打分:
score(c,q) = Σ idf(w)·pos_weight(w),只用大类固定关键词表(不并入类下技能关键词,避免大类词表膨胀通吃)。idf=ln((N+1)/(df+1))+1,pos_weight=1/(1+start/10)。 - L2 技能打分:技能词表 = keywords ∪ triggers ∪ name;trigger 命中 ×1.5。
- 归一化:
score / max(1, K),K = 查询去重英文词数 + 中文连续串片段数。 - 三态判定:
max_c ≥ θ1→ hit;θ2 ≤ max_c < θ1或 Top-3 内 ≥2 分相差 <0.1 → low;max_c < θ2→ miss。默认θ1=0.35、θ2=0.12(PRD 标注为初始猜测值,应在目标语料上网格标定)。 - 确定性:全流程无随机数、不直接遍历 Set 输出、同输入字节级一致(除
generated_at)。
配置项(cordis.patch.yml)
- insert:
- id: skill-indexer
name: 'dsh-skill-indexer'
inject: [tools, commands]
config:
roots: [] # 缺省 = ~/.workbuddy/skills + $DSH_HOME/skills;可传路径数组或 [{path,type}]
theta1: 0.35 # 命中阈值
theta2: 0.12 # 未命中阈值
dataDir: '' # 索引/usage.log 输出目录,缺省 $DSH_HOME/skill-indexer
categoriesYaml: '' # 可选 categories.yaml(人工大类覆盖,优先级最高)
categories.yaml 格式:
overrides:
git-finish-work: ["git-workflow", "delivery"]
pdf-to-markdown: docs
目录结构
src/
index.js # 插件入口:apply + skill_index 工具 + /skill-index 命令
scanner.js # 目录扫描 + 手写 YAML 子集 frontmatter 解析
extract.js # 触发词/关键词抽取
classify.js # 大类归纳(内置词表 + categories.yaml 覆盖)
indexer.js # 索引构建、哈希增量、JSON 读写
score.js # 打分(覆盖度 × IDF)
router.js # 两级路由三态判定
output.js # 注入文本模板渲染
overrides.js # categories.yaml 解析
test/ # node:test 单测 + fixtures
scripts/ # demo_query.mjs 一键演示
数据模型(skills-index.json)
索引是 UTF-8、无 ASCII 转义、2 空格缩进的 JSON(generated_at 为 ISO 时间戳):
{
"version": 1,
"generated_at": "2026-08-19T00:00:00.000Z",
"roots": [{ "path": "/…/skills", "type": "workbuddy", "count": 77 }],
"categories": [
{ "id": "finance", "name": "金融数据", "desc": "…", "keywords": ["etf", "…"], "skills": ["etf-filter"] }
],
"skills": [
{
"id": "etf-filter",
"root": "/…/skills",
"path": "/…/skills/etf-filter/SKILL.md",
"name": "etf-filter",
"description": "…",
"one_liner": "…",
"triggers": ["…"],
"keywords": ["…"],
"categories": ["finance"],
"deps": [],
"hash": "sha256…",
"usage_count": 0,
"last_used": null
}
],
"warnings": []
}
id规则:唯一名 →id = name;跨 root 重名 →id = name@type(如pdf-to-markdown@dsh)。categories[].skills与skills[].id引用闭合。- 增量:
scan按hash对比,未变技能保留usage_count/last_used;无变更时命令提示 "(no changes)"。
验收结果
| 项 | 结果 |
|---|---|
单元测试 npm test |
28/28 通过 |
| A1 扫描 | 默认 roots 扫描成功,索引 skill 数与目录实际一致;warnings 为 0 或逐条可解释 |
| A2 warnings | 0(block scalar 全量解析,见「与 PRD 的差异」) |
| A3 增量 | 连续 scan 无变更提示 "(no changes)";索引除 generated_at 外字节一致 |
| A4 哈希增量 | 单测覆盖:未变技能保留 usage_count/last_used |
| A5 通用 ≤15% | 取决于语料:内置词表是 PRD 的 MVP 覆盖,未覆盖领域可用 categories.yaml / CATEGORIES 常量增补 |
| B1/B2 召回 | 三态判定(hit/low/miss)与注入文本验证通过;PRD 10 条目标技能需在 PRD 原始语料上标定(目标技能随语料而定) |
| B3 未命中 | 「帮我订个外卖」→ miss ✓ |
| B4 JSON 结构 | state/intent/categories/hits/text ✓ |
| B5 注入文本 | 模板一致(大类表 + 命中类 + 技能明细)✓ |
| C1 零依赖 | 仅 node 内置 + @deepseek-ai/dsh-tools(defineTool) |
| C4 只读 | 不写技能源目录,产物只落 dataDir |
三条硬约束
- 只读源目录:不修改/移动/删除
~/.workbuddy/skills、~/.dsh/skills下任何文件;索引与usage.log只写dataDir。无--write-frontmatter类回写能力。 - 零第三方运行时依赖:核心(scan/parse/extract/classify/index/route)只用
node:fs/path/crypto;唯一外部包是宿主自带的@deepseek-ai/dsh-tools。 - 不部署、不常驻:无常驻进程、无 HTTP 服务、无 UI、无 embedding/GraphRAG(对应 PRD 非目标)。
与 PRD 的差异
| PRD | 本实现 |
|---|---|
| Python 3 零依赖 CLI | TypeScript 重写为 DSH Cordis 插件(apply(ctx) + 工具/命令),逻辑与数据模型不变 |
| block scalar「取首个非空缩进行 + 告警」 | 全量解析连续缩进行(| 按换行、> 折叠)。大量技能用多行 description: |,截断会丢描述与触发词,故改为全量且不告警 |
| 大类内置词表(10 类) | 原样保留,categories.yaml 覆盖 + CATEGORIES 常量可扩展 |
| M3 usage 统计排序 / M4 serve / M5 embedding | 未实现(PRD 标注「交付即 M1+M2」);usage_count/last_used 字段与回填已实现,排序启用留待 M3 |
已知限制
- 中文关键词抽取按 PRD「连续中文串(按标点/英文/数字断开)」实现,长串会把短词粘住(如「当面对高杠杆」粘住「高杠杆」),短查询召回偏弱。改进方向:在 CJK 串内做 2–8 字重叠滑窗(未实现,以保持与 PRD 一致)。
- 内置大类词表是 MVP,覆盖随语料变化;跨机部署前建议在目标语料上做
θ1/θ2网格标定并增补词表。
开发 / 测试
npm test # node --test "test/**/*.test.js"
npm run demo # 扫描默认 roots 并路由几条示例意图(可传自定义查询)
fixtures 位于 test/fixtures/(workbuddy + dsh 两个 root,含跨 root 同名 skill 与 block scalar / 嵌套 metadata 边界)。
参考 / Reference
- skill-indexer PRD v1.1(2026-08-19)——本插件的源规格文档,含两级索引、三态回退、哈希增量与验收标准(A1–A5 / B1–B5 / C1–C4)。为本地文档,未公开发布。
- 文献调研(PRD §1.1 提及,仅作设计决策依据,非依赖):Graph-of-Skills、SRA / Progressive Disclosure、SkillReason、SkillFlow、Hi-RAG。本实现只取其中的轻量思想(两级分层 + 三态回退),用零依赖规则落地,未照搬上述论文的方法(embedding / 训练 / 图构建均明确不做)。
试一下: 装好后对 agent 说 skill_index(query="把这个PDF转成markdown"),或直接跑 /skill-index query 把这周的ETF拉出来画个图。索引或召回不准?提 issue 或按「已知限制」里的方向改进。
No comments yet. Be the first to write one.