工具路由器(tool-router)
定位:Agent 的神经系统,不是工具箱。工具箱管"我有什么",神经系统管"这一秒该动哪块肌肉"。
一句话:任务进来 -> 相关性打分 -> 只注入 0-3 个技能的一行描述 -> 确定后才读完整 SKILL.md。
何时用 / 何时不用
用:
- 任何非平凡任务开始前(判据:涉及专业领域、需要 ≥2 步、或你自己都不确定该从哪下手)
- 任务跨了多个领域,需要判断"先唤起谁"
- 上一轮明显选错工具了(该查资料在瞎猜、该调试在乱改)
- 想确认某个技能该不该用
不用:
- 纯寒暄、闲聊天
- 一眼就知道用哪个技能、且对话里已经唤起了
- 任务是纯代码阅读/文件读取这类不需要专业方法论的动作
- 需要串联多个技能(那是 skill-orchestrator,本 skill 只出"候选人名单")
分工边界(别混淆):
| 回答的问题 | 产出 | |
|---|---|---|
| tool-router(本 skill) | 这一步该唤起谁? | 0-3 个技能名 + 一行描述 |
| skill-orchestrator | 这几个技能该怎么串? | DAG 编排计划 + 执行日志 + 瓶颈复盘 |
正确用法是先 route 再 orchestrate。
核心工作流(两阶段渐进式披露)
用户任务
|
+-[阶段1 粗筛] python scripts/route.py "<任务>" <- 只输出 name + 一行描述,约 20-200 tokens
| v 拿到 0-3 个候选
|
+-[阶段2 精读] Read 命中技能的完整 SKILL.md <- 只对确定要用的读全文
| v
\-[执行] 按技能指引干活,必要时喂给 skill-orchestrator 做编排
铁律:阶段 1 绝不读任何 SKILL.md 全文;阶段 2 只读阶段 1 选中的。永远不要一次性把多个 SKILL.md 读进上下文——那正是本 skill 要消灭的行为。
三个脚本
| 脚本 | 作用 | 命令 |
|---|---|---|
scripts/build_index.py |
扫描全部 SKILL.md 建索引(缺 description 的自动从正文兜底) | python scripts/build_index.py [--dupes] [--stats] |
scripts/route.py |
路由主入口(含 --selftest 回归测试) |
python scripts/route.py "<任务>" [--top 3] [--explain] [--json] |
scripts/report.py |
路由健康度:谁被路由最多 / 谁从没被路由过 / 空结果率 | python scripts/report.py [--since 2026-09-01] |
依赖:仅 Python 标准库,无第三方包。实测性能:784 个技能,单次路由 3.3 ms。
一次性初始化(换机器或装了新技能后重跑):
cd ~/.workbuddy/skills/tool-router
python scripts/build_index.py # 建索引(约 5 秒)
python scripts/route.py --selftest # 12 条回归用例,必须全绿
实测基准(作者环境:785 个技能)
| 指标 | 实测值 |
|---|---|
扫描到的 SKILL.md 文件 |
878 个 |
| 去重后技能数(user > connector > plugin 优先级) | 785 个 |
| 合并掉的重复版本 | 93 个 |
| 索引体积 | 4.4 MB(core + body 双字段) |
| 单次路由耗时 | 3.3 ms |
| 每轮注入 token(Top-3) | 约 20-190 tokens |
有 description 的技能 |
784 / 785(1 个从正文兜底) |
| 抽不出触发词的技能 | 81 个(建议补 ## Keywords 或显式触发词) |
| 抽取到的触发词条目 | 7,772 条(已过质量闸门) |
| 回归用例 | 12 条,全绿 |
打分机制(BM25F-lite)
得分 = BM25(core字段) × 1.00
+ BM25(body字段) × 0.25 <- 正文深处提一句,远不如 description 里写着
+ 触发词整串命中 × 4.0
+ 技能名命中 × 3.0
最后过双阈值(绝对 + 相对):
保留条件:score ≥ max(threshold, top1 × rel_ratio)
默认: threshold=2.0, rel_ratio=0.30
一个都不满足就返回空——不硬套。空结果时正确行为是:直接用自己的能力处理,或提议新建 skill。
六个必须踩过的坑(实测记录)
坑 1:中文口语填充词拿到高 IDF,把真信号压死
现象:查询"算一下这个八字,排个盘",contract-review 靠 一下/下这 两个词拿到 10.20 排第一,
真正的 paipan-lib(命中 八字)只有 9.09 排第三。
根因:BM25 的 IDF 高 ≠ 词重要。一下/我看/看看/帮我 这类口语填充词只出现在少数文档里,
df 低 -> IDF 高 -> 分数虚高。
对策:分词之前先用正则把多字填充短语从文本里抠掉(不是只筛 bigram——那会留下 算一 这种碎片)。
索引与查询两侧共用同一个 tokenize(),天然一致。填充词表在 build_index.py: FILLER_PHRASES。
结果:paipan-lib 回到第一,contract-review 掉出榜单。
坑 2:不分字段,正文里提一句就能拿满分
现象:查询"今天天气怎么样",citywalk-designer、phone-wallpaper-customizer 都能排进 Top-5——
因为它们正文里提过"天气"。
根因:单字段索引下,"正文深处提一句"和"description 里就是干这个的"权重一样。
对策:把索引拆成 core(技能名+description+触发词)和 body(标题+正文),body 乘 0.25 衰减。
坑 3:绝对阈值挡不住水涨船高
现象:tushare-skill、qcc-company 这类通用技能只要有 2-3 个词蹭上,就能挤进 Top-3。
对策:加相对阈值 top1 × 0.30。绝对阈值保证"有信号",相对阈值保证"不比第一名差太多"。
效果:查询"C盘满了"从输出 6 个(含 5 个噪声)收敛到输出 1 个。
坑 4:SKILL.md 的 frontmatter 解析会被 BOM / CRLF 坑掉
现象:用 re.match(r'^---\s*\n(.*?)', txt, re.S) 解析,138 个文件全部返回空 description。
对策:必须 open(p,'rb').read().decode('utf-8-sig') + splitlines(),再按行解析。BOM 会让 ^--- 直接失配。
坑 5:抽出来的触发词可能是个通用疑问词,白送 4.0 分
现象:把 cdisk-health-scan 禁用后重跑"帮我看看C盘为什么满了",第一名变成 causal-systems,
分数整齐的 4.00,--explain 显示命中 触发词:为什么。同分的还有 qianying-persona-soul、simlab、
因果分析类技能(示例) —— 全部毫无关系。
根因:触发词从 description 引号里抽取时,某个技能的描述里出现过"为什么…",
于是 为什么 被登记成它的触发词。之后任何含"为什么"的查询都白送它 4.0 分,
而 4.0 已经高于阈值 2.0 —— 等于无证据霸榜。
对策(两层):
1. _meaningful_trigger() 质量闸门:抠掉填充短语后必须仍剩 ≥2 个实义字符。
为什么 -> 抠完为空 -> 拒绝。帮我看看C盘 -> 抠完剩 c盘 -> 通过。
2. 证据闸门:候选必须有 core 字段(名字/description/触发词)的实义命中,或触发词/技能名命中。
只在正文深处蹭到几个词的一律丢弃(--explain 会显示丢弃了几个)。
结果:deny 掉 C 盘技能后,输出从 5 条噪声收敛到 1 条。
坑 6:/tmp 在 Windows Git Bash 下对 Python 进程不可见
printf ... > /tmp/t.txt 之后 python route.py --batch /tmp/t.txt 报 FileNotFoundError。
批量路由的输入文件请写在项目目录内(示例见 examples/demo_tasks.txt)。
配置(index/router.config.json)
| 键 | 默认 | 说明 |
|---|---|---|
top_k |
3 | 每轮注入几个。3 是甜点,再多收益递减 |
threshold |
2.0 | 绝对阈值 |
rel_ratio |
0.30 | 相对阈值系数(相对 top1) |
body_weight |
0.25 | 正文字段衰减 |
idf_cap |
4.5 | IDF 软上限,防生僻词带偏 |
always_include |
[] |
常驻技能。默认留空——每个都吃 token,非必要不加 |
boost.trigger / boost.name |
4.0 / 3.0 | 触发词 / 技能名命中加成 |
deny |
[] |
禁用名单,用来屏蔽已知重复技能 |
交付前检查
- [ ] 路由结果里有没有"看起来相关但实际无关"的?-> 去
references/routing-matrix.md加消歧行 - [ ] 是否只读了选中技能的
SKILL.md?(读了没选中的就是违规) - [ ] 返回空结果时,有没有硬套一个技能?——没有硬套才算合格
- [ ] 用完之后,回填日志:
python scripts/route.py "<任务>" --chosen <实际用的技能> - [ ] 任务完成顺手看一眼
python scripts/report.py,有没有从未被路由过的技能值得清理
相关文件
references/routing-matrix.md—— 三维路由矩阵 + 实测混淆集消歧表(误路由高危区);含「待用户决策的 deny 清单」references/tuning-knowledge.md—— 调参护城河:两本书(张俊林 BM25/BM25F/精确率召回率 + Relevant Search 调试/分词/boosting/test-driven relevance)-> 实现决策逐条映射 + 参数共识表references/rollout-plan.md—— 落地执行路线图(分阶段 + 验收标准 + 每阶段该改什么/不该改什么)references/books-and-sources.md—— 书单与免费权威源(BM25 / 相关性工程 / 上下文工程)USAGE.md—— 面向使用者的输入输出说明与注意事项scripts/build_index.py—— 索引构建(含--dupes相似技能对检测)scripts/route.py—— 路由主入口(含--selftest)scripts/report.py—— 路由健康度报告(反馈闭环)examples/demo_tasks.txt—— 批量路由示例输入index/skills.index.json—— 生成的索引(4.4 MB)index/router.config.json—— 运行配置logs/routes.jsonl—— 路由日志(调参依据)
目录结构
tool-router/
+-- SKILL.md <- 本文件(入口)
+-- USAGE.md <- 使用说明(输入/输出/注意事项)
+-- references/
| +-- routing-matrix.md <- 三维路由矩阵 + 实测消歧表(21 组重叠对)+ 待决 deny 清单
| +-- tuning-knowledge.md <- 调参护城河:两本书 -> 实现决策映射 + 参数共识表
| +-- rollout-plan.md <- 落地路线图 Phase 0-4
| \-- books-and-sources.md <- 书单与免费权威源
+-- scripts/
| +-- build_index.py <- 索引构建 + 相似技能对检测
| +-- route.py <- 路由主入口 + 12 条回归用例
| \-- report.py <- 健康度报告 / 死技能发现
+-- index/
| +-- skills.index.json <- 生成物(4.4 MB,可随时重建)
| \-- router.config.json <- 运行配置(含 _pending_denials)
+-- examples/
| +-- demo_tasks.txt <- 批量路由示例
| \-- seed_tasks.txt <- 周报基线种子查询(12 条)
\-- logs/
\-- routes.jsonl <- 路由日志(反馈闭环)
当前进展(Phase 1-3 已完成)
- Phase 0(建技能):[OK] 索引 785 技能 / 路由 3.3ms / 12 条回归全绿 / 纯标准库零依赖。
- Phase 1(路由前置纪律):[OK] 已写入
SOUL.md「路由前置纪律」段——每次判断用哪个 Skill 前先跑route.py粗筛,而非靠 description 被动触发。 - Phase 2(消歧表):[OK]
routing-matrix.md已含「待用户决策的 deny 清单」(3 对 >0.85 高相似重复技能),router.config.json的_pending_denials已收口;deny主名单保持空,等你拍板后翻进去即时生效。 - Phase 3(每周 report):[OK] 已建递归自动化
automation-1789219868850(每周一 9:00 跑report.py);基线 12 次路由 / 空结果率 0% / 756 个技能从未被路由(即每周要盯的"死技能候选")。 - Phase 4(embedding):[暂停] 暂不做。自建技能才 163 个,BM25 够用;没有误路由案例的优化等于猜。
- 两本书已蒸馏:[OK]
references/tuning-knowledge.md把张俊林 + Relevant Search 的方法论逐条映射到实现决策与参数共识。
局限(必须知道)
- 本 host 内无法"减少系统提示里的技能清单"。WorkBuddy 会把技能列表注入系统提示,本 skill 改不了这一点。
所以:"省 94% token" 这个数字不适用于本机——它来自 OpenClaw 那种"全量注入 vs 按需注入"的架构对比。
本 skill 在本机的真实价值是:提高选对率 + 发现多技能任务 + 用 3 ms 替代 LLM 逐条权衡。
诚实地说,token 上的收益是每轮省下你自己"读 200 行清单做判断"的推理开销,不是省掉清单本身。 - BM25 不理解语义。"清理磁盘"与"C盘满了"能匹配,是因为共享了字面词;纯粹的近义改写会漏。
要补这一步需上 embedding,属于 Roadmap,不是当前能力。 - 索引需要重建。装了新技能后必须重跑
build_index.py,否则新技能永远路由不到。 - 消歧表是人工资产。相似度是机器算的(可信),但"该选哪个"的判据是归纳建议(待验证),
遇到反例直接改表,不要反过来怀疑数据。
版权与署名
本 skill 为原创工具,基于公开的相关性检索方法实现(BM25: Robertson & Zaragoza 1994;相关性调试方法论参见 Doug Turnbull & John Berryman《Relevant Search》与张俊林《这就是搜索引擎》),未包含任何第三方受版权保护的原文。
- 使用许可:MIT(见 LICENSE 文件)。可免费使用、修改、二次分享。
- 转载请注明出处:七仔的AI工具箱。
- SkillHub:qizai-tool-router
作者与联系方式
本 skill 由「七仔的AI工具箱」维护。欢迎就技能使用、问题反馈与二次分享交流:
- 微信公众号:七仔的AI工具箱
- 抖音号:七仔的AI工具箱
- CSDN 博客:七仔的AI工具箱
本 skill 可免费使用与二次分享;转载请注明出处「七仔的AI工具箱」。