Skill调用命中率骤降?从信息架构到测试治理的工程化解决思路 先说一个结论Skill 数量过百之后调用命中率拼的不是模型能力而是 Skill 库的信息架构。很多团队一开始只有十几个 Skill 时Agent 表现得还不错用户说“清理一下 Docker”它基本能找对技能。等到 Skill 数量超过 100问题开始集中爆发该调用 A 技能时调了 B 技能或者 Agent 干脆不调用任何 Skill直接自己“自由发挥”。这时候再靠改提示词去补效果会越来越差。原因很简单Skill 变多之后模型面对的候选集变大、描述重叠变多、触发条件越来越模糊命中率下降是必然的。这篇文章会讲清楚 Skill 调用命中率到底怎么定义为什么 Skill 过百后会断崖式下降以及如何在工程层面通过元数据、目录结构、测试集和治理机制把命中率重新拉回来。不管你现在用 Claude Code Skill、Codex Skill还是自研 Agent下面这套思路都能落地。1. Skill 调用命中率是什么为什么 Skill 数量过百后会下降先明确几个概念。Skill 本质上是一份“任务执行说明书”。它告诉 Agent在什么场景下使用这个能力、按什么步骤执行、哪些情况不能碰。它不是一个普通函数也不是一段被硬编码调用的逻辑而是需要 Agent 通过自然语言理解来“选中”的知识单元。Agent 则是能自主规划、拆解任务、调用技能和外接工具的大模型程序。用户提一个需求后Agent 会根据对话上下文、仓库内容、Skill 索引等信息判断当前该用哪个 Skill。调用命中率不是平台自带的标准指标更像是一个工程口径。通常可以拆成三层来看召回率在“应该调用某个 Skill”的场景里Agent 有没有把这个 Skill 选进来。精确率Agent 选中的 Skill 里有多少是真正应该选的。误用率在“不应该调用某个 Skill”的场景里Agent 是否错误调用了。如果只盯着一个综合命中率很容易掩盖问题。比如 Agent 特别激进无论什么场景都往同一个 Skill 上靠那召回率可能很高但精确率和误用率都会很难看。为什么会这样因为 Skill 数量过百后模型面临的不是“从 5 个技能里选一个”而是“从 100 多个结构相似的描述里选一个”。这时候会出现四类典型问题候选空间膨胀排序变难。当 Skill 只有 10 个时最相关的那个很容易排进前 3当 Skill 有 120 个时最相关的那个可能排进前 20 就不错了。如果 Agent 只允许读取 top-k 个 Skill 描述正确目标可能根本没机会进入上下文。自然语言重叠。两个 Skill 都用“清理”“优化”“检查”这类词模型很难区分它们的使用边界。上下文窗口限制。把所有 Skill 的完整描述都塞进 system prompt 不现实尤其当每个 Skill 都写了一大段说明时。增量为王旧 Skill 被稀释。每次新增 Skill 都会重新改变候选集合的语义分布老 Skill 的排名可能会被新 Skill 挤下去。所以Skill 数量过百后调用命中率不是模型一个环节的问题而是“索引、描述、边界、测试、治理”五个环节共同作用的结果。2. Skill 不是工具函数而是一份“何时使用”的决策说明书很多人在写 Skill 时有个误区把重点放在“怎么做”上却忽略了“什么时候做”和“什么时候不做”。结果就是Agent 面对一个用户请求时虽然能看到这个 Skill 的标题和描述但不知道它到底该不该用。尤其是当场景稍微换一种说法时模型就无法命中。在 Agent 体系里Skill、Tool、Agent、Harness、Memory 这几个概念经常被混在一起但和调用命中率直接相关的其实是 Skill 的“决策层”设计。概念是什么和调用命中率的关系Skill可复用的任务执行策略与操作步骤本次要命中的对象Tool / Function被 Agent 调用的具体能力如读文件、执行命令Skill 内部会使用一个或多个 ToolAgent能规划、决策、调用工具的模型程序最终决定调用哪个 SkillHarnessAgent 的运行循环负责上下文管理、工具调度决定 Skill 如何被加载和注入Memory长期状态、用户偏好、历史事实不要把 Memory 塞进 Skill两者职责不同从这里能看出Skill 的“第一步”不是执行而是说明自己应该被选中。真正决定命中率的是 Skill 的描述和边界。下面是一个比较典型的设计结构不同 Agent 平台字段名可能不同但核心思想一致# 文件路径skills/docker/docker-container-cleanup/SKILL.md --- name: docker-container-cleanup description: 当用户希望清理 Docker 残留资源、释放磁盘空间时使用。 keywords: - docker - container - exited - 清理 - 磁盘空间 - prune when_to_use: - 用户表达“磁盘满了帮我清理 Docker” - docker system df 显示可回收空间较大 - 需要清理 exited 容器、悬空镜像或 build cache when_not_to_use: - 用户想删除运行中的容器或持久化数据卷 - 用户只是查看容器列表并不需要清理 - 生产环境涉及数据销毁需要先走审批流程 steps: - 先执行 docker ps -a --filter statusexited 列出可清理对象 - 与用户确认清理范围 - 使用 docker container prune / docker image prune / docker builder prune - 输出释放空间前后的对比 version: 1.0.0 status: active owner: platform-oncall ---这个结构里最容易被忽视但又最关键的是when_not_to_use。它帮 Agent 排除了大量“看起来相似但语义不同”的请求。很多误调用问题不是模型不知道技能是什么而是模型不知道技能“不能做什么”。如果你的 Skill 已经超过 100 个建议先从补when_not_to_use开始。这一个字段往往比继续优化 prompt 更快提升命中率。3. Skill 库的目录结构与命名规范Skill 数量过百之后目录结构就是你的第一层索引。如果目录混乱后续的检索、测试、维护都会失控。先看一个推荐的目录结构skills/ common/ git-branch-cleanup/ SKILL.md docker/ docker-container-cleanup/ SKILL.md dockerfile-lint/ SKILL.md frontend/ react-performance-audit/ SKILL.md css-token-consolidation/ SKILL.md archive/ legacy-maven-troubleshoot/ SKILL.md注意两个关键点一个 Skill 一个目录。不要把多个 Skill 写在同一个文件里也不要把可执行脚本和描述文档混在一起。目录就是 Skill 的唯一标识。归档目录独立存在。过时 Skill 不要直接删除移动到archive/目录里。这样既不会污染线上索引又能保留历史资产。命名规范方面推荐采用“领域-动作-对象”的结构避免出现helper、utils、analysis这种泛泛的名字。不推荐推荐原因skill1docker-container-cleanup名字要能直接体现用途cleangit-branch-cleanup没有领域和对象容易误命中高级分析助手sql-slow-query-analyze避免营销化、抽象化testapi-contract-regression-test明确对象和动作文件名和目录名也会进入 Agent 的检索范围。越是模糊的名字越容易在语义检索中产生噪声。不要用中文拼音、营销词汇、或者无意义的编号当目录名。4. 可检索的元数据Index、关键词与语义描述Skill 数量变多后不能靠 Agent“凭感觉”从 100 多个完整文档里选。更合理的做法是建立一层“Skill 索引”让 Agent 先读索引再根据索引决定是否打开某个完整 SKILL.md。索引可以是一个 YAML 文件也可以是从所有 SKILL.md 自动生成的 JSON。下面是索引的核心字段# config/skill_index.yaml version: 1 default_top_k: 20 max_skill_context_chars: 800 skills: - name: docker-container-cleanup path: skills/docker/docker-container-cleanup status: active domain: docker keywords: [docker, 清理, prune, exited, 磁盘空间] description: 清理 Docker 残留资源并释放磁盘空间 risk_level: medium - name: git-branch-cleanup path: skills/git/git-branch-cleanup status: active domain: git keywords: [git, branch, 清理, 合并, 本地分支] description: 清理已合并的本地 Git 分支保持仓库整洁 risk_level: low这里有一个核心设计原则索引里的 description 要短但语义要准。不要在索引里写完整步骤否则检索层会被无关信息干扰。步骤属于完整 SKILL.md索引只需要负责“让 Agent 找到正确文件”。除了语义描述还可以给索引加上domain和risk_level字段。domain用来做领域过滤比如当用户请求明显和前端相关时可以优先从前端领域里检索。risk_level用来做安全控制高风险的 Skill 应该要求 Agent 在执行前显式确认。如果平台不支持自动生成索引也可以先用一个简单命令做“人工体检”# 快速查看所有 Skill 的 name 和 description找出重复项 grep -rE ^name:|^description:|^keywords: skills/*/SKILL.md这一步能帮你快速发现两个问题是否存在大量重复关键词。是否存在描述过于宽泛的 Skill。真正容易踩坑的地方是你把关键词塞得太多太杂。比如清理类 Skill 你加了“优化”“加速”“整理”这些词短期内可能命中率上升但长期反复使用后模型会把所有类似请求都带偏。关键词要有“辨识度”而不是追求“覆盖面”。5. 完整示例从 Skill 定义到命中率评估下面用一个完整的最小链路演示定义一个 Docker 清理 Skill生成索引然后用测试集评估命中率。5.1 定义 Skill继续使用上面那个docker-container-cleanupSkill。这里再补充一个测试文件用真实查询来验证它能否被正确命中。# tests/skill_hit_cases.yaml - id: docker-cleanup-001 query: 磁盘快满了帮我清理一下 Docker 残留 expected: docker-container-cleanup should_trigger: true - id: docker-cleanup-002 query: 查一下当前有哪些容器在运行 expected: docker-container-cleanup should_trigger: false第二条用例非常重要。它表示虽然请求里出现了“容器”但这个请求只是“查看”不是“清理”。如果 Agent 把这个场景误命中为清理 Skill说明描述里的边界还不够清晰。5.2 自动生成 Skill 索引可以写一个简单的 Python 脚本扫描所有SKILL.md把 frontmatter 解析成skill_index.json。# scripts/build_skill_index.py # 用法python scripts/build_skill_index.py skills/ import json import sys from pathlib import Path import yaml def parse_front_matter(text: str) - dict: if not text.startswith(---): return {} parts text.split(---, 2) if len(parts) 3: return {} return yaml.safe_load(parts[1]) or {} def main(skills_dir: str): root Path(skills_dir) index [] for md in root.rglob(SKILL.md): meta parse_front_matter(md.read_text(encodingutf-8)) index.append( { name: meta.get(name, md.parent.name), path: str(md.parent), description: meta.get(description, ), keywords: meta.get(keywords, []), when_to_use: meta.get(when_to_use, []), when_not_to_use: meta.get(when_not_to_use, []), status: meta.get(status, active), owner: meta.get(owner, ), version: meta.get(version, ), } ) out Path(skill_index.json) out.write_text( json.dumps({skills: index}, ensure_asciiFalse, indent2), encodingutf-8, ) print(fgenerated {len(index)} skills - {out}) if __name__ __main__: main(sys.argv[1] if len(sys.argv) 1 else skills)这个脚本的价值在于每次 Skill 变更后索引都能自动同步避免人工维护一份已经过期的清单。5.3 用测试集评估 Skill 命中率下面用 sentence-transformers 做一个最简单的“召回测试”。它不完整模拟 Agent 决策但能帮你提前发现明显的描述冲突和排序问题。# scripts/evaluate_hit_rate.py # 依赖pip install pyyaml sentence-transformers scikit-learn import json import sys from pathlib import Path import yaml from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) def load_cases(path: str): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def load_index(path: str): with open(path, r, encodingutf-8) as f: return json.load(f)[skills] def predict(cases, skills, top_k5): skill_texts [ f{s[name]} {s[description]} { .join(s.get(keywords, []))} for s in skills ] skill_vectors model.encode(skill_texts) results [] for case in cases: query_vec model.encode([case[query]]) sims cosine_similarity(query_vec, skill_vectors)[0] ranked sorted( zip(skills, sims), keylambda x: x[1], reverseTrue )[:top_k] predicted ranked[0][0][name] results.append( { id: case[id], query: case[query], expected: case[expected], predicted: predicted, should_trigger: case.get(should_trigger, True), top: [ {name: s[name], score: round(float(score), 4)} for s, score in ranked ], } ) return results def main(index_path: str, cases_path: str): skills load_index(index_path) cases load_cases(cases_path) results predict(cases, skills) tp 0 fp 0 fn 0 for r in results: if r[should_trigger]: if r[predicted] r[expected]: tp 1 status PASS else: fn 1 status FAIL else: if r[predicted] r[expected]: fp 1 status FAIL else: status PASS print(status, r[id], r[query], -, r[predicted], (expected, r[expected], )) if status FAIL: print( top:, r[top]) precision tp / (tp fp) if tp fp else 0 recall tp / (tp fn) if tp fn else 0 print(fprecision{precision:.2%} recall{recall:.2%} fp{fp} fn{fn}) if __name__ __main__: main( sys.argv[1] if len(sys.argv) 1 else skill_index.json, sys.argv[2] if len(sys.argv) 2 else tests/skill_hit_cases.yaml, )这个脚本不是完整的 Agent 评测而是帮你回答四个问题当前 Skill 索引能不能召回目标 Skill目标 Skill 是否排在 top-1不该命中的查询是否误命中了加入一个新 Skill 后老用例是否回归失败如果你的平台自带 Skill 调试工具可以以平台结果为准。脚本更适合做回归测试和批量体检。6. 如何验证与追踪调用命中率有了测试集还需要建立一套可重复的验证机制。Skill 的调用命中率不能只在开发期看一次每次新增 Skill、修改描述、升级模型后都要重新跑。推荐指标如下指标含义计算方式Precision命中的 Skill 中正确比例正确命中数 / 总命中数Recall应该命中的场景中正确比例正确命中数 / 总应该命中数False Positive不该命中却命中错误命中数 / 总不该命中数Top-1 Match Rate目标 Skill 排在第一位的比例Top-1 正确数 / 总测试数实际项目中我更建议把测试集分成两类正向用例用户请求应该命中某个 Skill。负向用例用户请求虽然涉及相关关键词但不应该命中该 Skill。只加正向用例的问题很常见Agent 确实召回了正确 Skill但它也把大量无关请求都召回了。负向用例能把这种“激进派”Agent 打回原形。回归测试执行成功后可以加入 CI/CD。当SKILL.md的 frontmatter、索引配置或测试集变更时自动触发评估。如果命中率下降超过阈值就阻断合并。在生产环境还要记录真实的 Agent 调用日志。至少包含这些字段session_id user_query skill_name skill_version trigger_mode execution_status error_message latency_ms有了真实日志你才能知道测试集里没覆盖到的低频场景到底在哪里漏调。这一步是“从 90% 到 99%”的关键。7. Skill 数量过百后的治理机制分级、归档、淘汰Skill 数量过百后真正需要解决的不是“继续增加 Skill”的问题而是“如何让新增 Skill 不拖累存量 Skill”。这里可以借鉴微服务治理的思路。7.1 给 Skill 分级不要把所有 Skill 都当成同等重要。建议分为四类core核心 Skill始终在索引首位比如“安全审批”“回滚操作”。normal常规 Skill进入默认检索池。beta待验证的新 Skill不直接全量开放。deprecated即将下线或已失效的 Skill不再进入默认检索。在索引中增加status字段Agent 只检索active和beta。archive/目录里的历史 Skill不参与线上索引。7.2 新增 Skill 必须过“相似度检查”凡是新增 Skill先和现有索引跑一遍相似度。如果新 Skill 的描述和某个存量 Skill 的 cosine similarity 过高就应该合并或重新划界。这一步不需要特别复杂用上面那个向量化脚本就能实现。重点不是追求绝对数值而是避免让两个 Skill 回答“同一个问题”却给出“两套做法”。7.3 淘汰机制Skill 也要有生命周期。建议每个 Skill 都写上owner负责人。version版本号。status当前状态。last_reviewed最近一次评审时间。超过一定时间没有使用记录的 Skill先标记deprecated再移动到archive/。不要直接删除因为历史用户会话可能还在引用它。8. 常见问题与排查思路下面整理了几个我在实际工程里经常遇到的典型问题。问题现象可能原因排查方式解决方案Agent 不调用 Skill直接自己执行description 太泛缺少触发时机看索引是否包含该 Skill检查日志中是否出现 skill 调用记录重写 description补充 when_to_use 触发场景调用了 A Skill实际应该用 B SkillA、B 描述和关键词重叠对比两者的 description、when_to_use、when_not_to_use明确边界加入互斥场景和负向测试Skill 偶尔命中偶尔不命中用户表达变化大完全依赖 LLM 自由决策检查同一 query 在多次测试中的 top-k 排序增加同义触发词或引入确定性检索层新增一个 Skill 后老命中率下降新 Skill 抢占语义空间跑全量回归测试观察 top-k 排序变化合并相似 Skill或冻结旧的 beta SkillAgent 打开 Skill 后不按步骤执行步骤描述太长、依赖不清晰检查完整 SKILL.md 里的步骤是否可独立执行把步骤拆成原子操作增加前置校验和输出约定Skill 误删/误写生产数据缺少风险等级和审批动作检查风险操作是否在 Skill 步骤中高风险 Skill 必须要求用户二次确认并支持 dry-run其中第二类问题最容易忽略。很多团队看到“这两个 Skill 都叫清理”就以为模型一定能区分。但实际上模型判断的是语义边界不是你的目录直觉。只有把互斥场景明确写出来才能减少错调用。9. 最佳实践与工程建议9.1 描述要短、边界要明、示例要具体索引里的 description 控制在两到三句话。不要写“这是一个强大的分析工具”这类空话。要写“当用户需要清理 Docker 残留资源时使用”。示例触发语句比抽象描述更有效。9.2 用“负向用例”保护重要 Skill每个核心 Skill 至少配两条负向用例。比如“查看容器列表”不应该命中“容器清理”。当负向用例开始失败时通常意味着 Skill 描述过宽或关键词污染。9.3 把 Skill 变更纳入代码评审Skill 文件也是代码。变更描述、修改关键词、调整步骤时要走 commit 和 review 流程。评审人要重点看这个改动会不会影响其他 Skill 的召回结果。9.4 不要引入高危命令绕过权限Skill 可以写操作步骤但不能绕过程序的权限控制。涉及删除容器、清理数据、修改线上配置时Skill 应该明确要求用户确认并优先提供 dry-run 模式。不要把凭据和密钥写进 SKILL.md。9.5 控制每次注入上下文的 Skill 数量不要把所有 Skill 都塞进 system prompt。索引层负责召回前 20 个或更少的候选Agent 再从中选择。上下文里塞得越多反而越容易让模型“花眼”。9.6 定期做全量回归建议每两周跑一次全量测试读取全部 Skill 索引执行正向用例和负向用例输出 precision、recall、fp、fn。测试结果变化能让你在用户反馈之前提前发现新 Skill 对老场景的冲击。10. 回到调用命中率一个可执行的改进顺序如果现在你的 Skill 数量已经过百不要一上来就重构所有描述。更稳妥的顺序是先给所有 Skill 补when_not_to_use。这一步成本低收益明显。建立 50 到 100 条真实查询测试集包含正向和负向用例。跑一次当前命中率找到失败集中在哪些 Skill。针对失败案例重写 description调整 keywords。把测试集接入 CI后续每次变更都自动回归。增加真实调用日志记录 skill_name、query、execution_status。最后再考虑是否能引入更复杂的语义检索或分层路由。与其纠结某个大模型会不会自己学会调用一百个 Skill不如在工程上把 Skill 库当作一个可检索、可测试、可治理的产品来维护。调用命中率不是 prompt 玄学而是索引、描述、测试和观测共同作用的结果。建议下一步先做两件事给现有 Skill 都补上一句when_not_to_use再往测试集里加 20 条历史真实 query跑一遍。你会立刻知道自己的 Skill 库最该优化的是哪一部分。