Skill 数量过百之后,真正的问题不是“有没有 Skill”,而是“Agent 到底能不能在正确的时候选中正确的 Skill”。很多团队在初期只有十几个 Skill 时,靠提示词描述、名字前缀、少量示例就能跑通;但当 Skill 数量超过 100 个,模型的选择空间变大,描述冲突变多,上下文被无关内容挤占,调用命中率会肉眼可见地下降。这篇文章围绕 Agent 调用 Skill 的命中率问题,从选型、命名、描述、召回、评估、监控六个维度展开,适合正在做 Agent 应用开发、需要管理大量 Skill 的工程师阅读。
在实际项目里,Skill 数量过百以后,调用命中率下降通常不是某个大模型能力不够,而是 Skill 管理方式还停留在小规模阶段。小规模时,开发者可以在 System Prompt 里把所有 Skill 列一遍,模型基本能凭名字匹配;规模上到 100 以后,System Prompt 长度受限,模型注意力被稀释,Skill 之间的语义差异变小,重名和模糊描述开始互相干扰。这篇文章会先解释 Skill 调用链路里最容易出问题的环节,再给出环境搭建、Skill 规范设计、召回优化、评估方法和监控手段,最后提供一个可以落地的排查清单。
1. 先理解 Skill 调用命中率为什么会在过百后下降
1.1 Skill、Tool 和 Agent 之间的关系要先对齐
Skill 在不同 Agent 框架里的叫法不完全一致。有的框架叫 Tool,有的叫 Action,有的叫 Function Calling,还有的框架把一组相关 Tool 的集合称为 Skill。为了避免后续讨论出现歧义,这里统一约定:
- Tool 是 Agent 可以调用的最小能力单元,通常对应一个函数、一个 API 或一段可执行脚本。
- Skill 是由多个 Tool、提示词模板、参数规则和调用说明组成的能力包,描述的是“完成某类任务”的整体能力。
- Agent 是负责理解用户请求、规划步骤、调用 Skill、处理返回结果的执行体。
在 Claude Code、Codex、自研 Agent 框架等场景里,Skill 通常以目录或配置文件形式存在。一个 Skill 目录下可能有SKILL.md、示例文件、脚本、参数定义和校验规则。模型在收到用户请求后,会先判断当前任务是否需要某个 Skill,再从 Skill 列表里选中它,最后执行 Skill 内部逻辑。
当 Skill 数量较少时,Agent 可以依赖“名字 + 单行描述”完成匹配。当数量超过 100 个,模型需要处理的候选变多,描述语义重叠变多,选择错误的概率随之上升。这个阶段开始,命中率问题本质上是信息检索和决策链路的工程问题,而不只是提示词问题。
1.2 命中率下降的四个直接原因
从实际表现看,Skill 过百后的命中率下降可以归纳为四个原因:
第一,候选集过大导致注意力稀释。Agent 的上下文窗口虽然越来越大,但把 100 个 Skill 的完整描述全部塞进 System Prompt 后,真正决定调用的关键描述会被淹没。模型在处理用户请求时,需要从大量文本里找到最相关的那一段,信息密度越低,选错概率越高。
第二,Skill 描述语义重叠。很多 Skill 在定义时使用了相近词汇,比如“生成周报”“生成日报”“生成项目报告”,三个 Skill 的名字和描述高度相似。模型无法从短文本里准确区分边界,出现“本想调用日报 Skill,结果选了周报 Skill”的情况。
第三,命名和描述与用户口语表达不一致。Skill 描述写的是开发视角的语言,用户请求则是自然语言。比如 Skill 名叫parse_resume,描述是“解析简历文件并提取字段”,但用户说“帮我看下这份 PDF 里有哪些候选人信息”,模型需要把口语意图映射到 Skill 语义,映射失败时命中率自然下降。
第四,没有召回阶段,直接让模型做全量选择。小规模时可以直接让模型在所有 Skill 里选择;过百后应当先通过关键词、向量或规则做一次召回,把候选集从 100 个缩小到 5 到 10 个,再让模型做最终决策。这一步缺失,命中率会明显受噪声影响。
这四个原因在真实项目中往往是叠加的。只优化其中一个,命中率提升有限。下文会从工程角度给出系统化处理方式。
1.3 命中率不能只看“是否选对了 Skill”
评估 Skill 调用命中率时,还需要区分几个层次:
- 意图识别是否准确:用户请求是否真的需要调用 Skill。如果不需要调用却调了,属于误调用。
- Skill 选择是否准确:需要调用时,是否选中了正确的 Skill。
- Skill 内部执行是否成功:选中正确 Skill 后,参数、权限、脚本是否顺利执行。
- 返回结果是否满足用户需求:执行成功不代表结果正确。
本文讨论的“调用命中率”主要指第二层,即从多个 Skill 中选出正确 Skill 的比例。但监控时不能只统计这一层,因为第一层误调用和第三层执行失败也会表现为“好像没命中”。建立完整评估链路后,才能定位问题到底出在召回、选择还是执行阶段。
2. 环境准备:先搭一个可统计命中率的 Skill 管理工程
2.1 建议的技术栈和目录结构
如果你的 Agent 项目已经存在,可以直接在现有项目上增加 Skill 管理和评估模块。如果从零开始验证,推荐使用一个相对独立的最小工程。下面是一个参考目录结构,适用于大多数支持 Skill 的 Agent 框架:
agent-skill-lab/ ├── skills/ │ ├── weekly-report/ │ │ ├── SKILL.md │ │ ├── scripts/ │ │ │ └── generate_report.py │ │ └── examples/ │ │ └── input_sample.json │ ├── daily-report/ │ │ ├── SKILL.md │ │ └── scripts/ │ └── resume-parser/ │ ├── SKILL.md │ └── scripts/ ├── config/ │ ├── skill_registry.yaml │ └── eval_config.yaml ├── evaluator/ │ ├── build_eval_set.py │ ├── run_eval.py │ └── analyze_result.py ├── retriever/ │ ├── keyword_retriever.py │ ├── vector_retriever.py │ └── hybrid_retriever.py ├── logs/ │ └── skill_call.log └── requirements.txt这个结构把 Skill 定义、召回模块、评估脚本和日志分开。skills目录放各 Skill 的定义文件和资源,retriever目录放召回逻辑,evaluator目录放评估数据集和评估脚本,logs目录用于后续监控分析。
在实际项目中,Skill 目录可能由多个团队共同维护,因此配置管理比目录结构更重要。skill_registry.yaml建议作为统一注册入口,避免直接扫描目录导致命名混乱。
2.2 定义统一的 Skill 元数据格式
无论你使用什么 Agent 框架,都应该为每个 Skill 定义一组结构化元数据。元数据越统一,后续召回和评估越容易。下面是一份参考 YAML 配置:
- name: weekly-report-generator display_name: 周报生成器 version: 1.2.0 description: 根据本周工作记录生成结构化周报,支持按项目、按时间范围过滤,输出 Markdown 或 Word 格式。 category: report tags: - 周报 - 工作汇报 - 项目管理 trigger_examples: - "把本周的工作记录整理成周报" - "帮我生成这周的周报" - "整理一下项目 A 的周度进展" inputs: - name: start_date type: string required: false description: 开始日期,格式 YYYY-MM-DD - name: project type: string required: false description: 项目名称 owner: team-report visibility: public enabled: true eval_set: - input: "帮我生成这周的周报" expected_skill: weekly-report-generator这份配置的含义是:每个 Skill 除了描述,还包含分类、标签、触发示例、输入参数、负责人、可见性和评估用例。其中trigger_examples对召唤召回和评估特别重要,它提供了用户口语表达与 Skill 语义之间的映射桥梁。
eval_set里的用例可以直接用于后续命中率评估。建议每个 Skill 至少配置 5 条真实用户表达,条件允许时配置 10 条以上。评估用例如果只依赖开发者自己编,容易和实际用户表达脱节;更好做法是从日志里抽取历史用户真实请求。
2.3 环境依赖与版本确认
在开始写召回和评估代码之前,先确认依赖环境。不同 Agent 框架对 Skill 的支持方式不同,但多数会用到 Python 生态中的以下组件:
| 组件 | 用途 | 示例 |
|---|---|---|
| Agent 框架 | Agent 调度和 Skill 执行 | Claude Code、Codex、自研框架 |
| 向量数据库 | Skill 描述向量召回 | Chroma、Milvus、FAISS |
| Embedding 模型 | 将 Skill 描述和用户请求编码为向量 | text-embedding-3-small、bge-m3 |
| 编排框架 | 定义 Agent、Tool、Skill 生命周期 | LangGraph、CrewAI、自研 |
| 日志和监控 | 记录每次调用和选择结果 | ClickHouse、Elasticsearch、文件 |
这里要特别注意:原始材料没有指定某个固定技术栈。上面表格列的是常见选型,实际项目落地前要先确认自己的 Agent 框架是否支持这些组件,以及版本是否兼容。如果只是验证 Skill 召回思路,可以先不引入向量数据库,用关键字召回加规则召回就能跑通第一版。
下面是一个最小requirements.txt示例:
openai>=1.30.0 chromadb>=0.5.0 pydantic>=2.0.0 pyyaml>=6.0 pandas>=2.0.0 scikit-learn>=1.3.0注意:版本号只是示例,实际安装时要以官方当前稳定版本为准,避免因为 API 变更导致代码失效。
3. 让 Skill 定义本身先“可选对”:命名、描述和触发示例规范
3.1 命名规范:从“开发视角”切到“用户视角”
Skill 数量过百后,命名直接影响候选召回和模型判断。命名不能只看开发者是否容易理解,还要看用户请求是否容易映射到这个名字上。
推荐命名规则:
- 使用动词开头,表达能力含义,例如
generate_weekly_report、parse_resume、search_employee_info。 - 避免只使用技术名词,例如
ReportUtil、ParserJob这类命名对模型没有任何语义提示。 - 长度控制在 3 到 6 个单词之间。过长会稀释注意力,过短则丢失语义。
- 同一分类下的 Skill 前缀保持一致,例如
report_weekly、report_daily、report_project,这样召回阶段可以按前缀快速过滤。
错误示例:
| Skill 名 | 问题 | 推荐命名 |
|---|---|---|
handle_data | 语义太泛,无法判断是什么能力 | clean_csv_data |
export_v2_final | 包含版本号和冗余后缀,表达不清晰 | export_order_excel |
API_Utils | 技术视角命名,用户请求无法映射 | query_order_status |
3.2 描述规范:写“做什么、什么时候用、不做什么”
Skill 描述是模型选择和召回的关键信号。描述写得太短,模型缺少判断依据;写得太长,上下文被无关内容占满。建议每个 Skill 的描述控制在 3 到 5 句话,覆盖三个维度:
- 做什么:明确 Skill 的执行结果和输出格式。
- 什么时候用:说明该 Skill 适用哪些用户意图。
- 不做什么:说明边界,避免和相邻 Skill 混淆。
示例:
description: 根据本周工作记录生成结构化周报,支持按项目、按时间范围过滤,输出 Markdown 或 Word 格式。 使用场景:当用户要求整理周报、周度汇报、工作周总结时使用。 不适用场景:如果用户只需要查看工作日志,不需要生成汇总文档,请使用 search_work_log。这样写的好处是让模型在模糊请求下能通过“不适用场景”排除错误候选。类似的 Skill 越多,越要写清边界。
3.3 触发示例:让用户口语表达与 Skill 语义对齐
描述解决的是“Skill 想表达什么”,触发示例解决的是“用户实际会怎么说”。建议为每个 Skill 配置 5 到 20 条触发示例,覆盖常见表达、同义表达和边界表达。
例如weekly-report-generator:
trigger_examples: - "把本周的工作记录整理成周报" - "帮我生成这周的周报" - "整理一下项目 A 的周度进展" - "周报还没写,帮我根据日志生成一份" - "我要向上级汇报本周工作,出个周报"这些示例不仅用于提示词或召回索引,还可以当作评估集的种子数据。后续做命中率评估时,可以把每条触发示例变成一条测试用例,统计模型或召回模块的命中情况。
这里有一个容易被忽略的点:触发示例不能只写“理想表达”,还要写用户常见的模糊表达和错误表达。例如“我要总结一下工作”可能既会被归类到周报 Skill,也会被归类到日志查询 Skill。把这种边界表达写进示例,模型才能学会判断。
3.4 分类和标签:给召回阶段提供第一层过滤条件
当 Skill 数量过百,让模型在全部 Skill 里做最终选择是不现实的。合理做法是先用分类和标签缩小候选集,再让模型在小集合里决策。
分类建议从业务维度划分,例如:
- report:报告生成类
- data_query:数据查询类
- document:文档处理类
- code_execution:代码执行类
- communication:消息通知类
- system_admin:系统管理类
标签可以更细,例如“周报”“日报”“简历解析”“SQL 查询”“文件转换”。标签的作用是让召回模块可以按标签过滤,同时让模型在候选集里更容易理解每个 Skill 的差异。
在skill_registry.yaml中,分类和标签应该与描述保持一致。如果分类写report,描述却写“查询员工信息”,那么召回和模型判断会产生矛盾。
4. 召回层设计:从“全量选择”变成“先召回再选择”
4.1 为什么必须加召回层
当 Skill 数量只有 20 个时,把全部 Skill 列表交给模型选择不会有太大问题。当数量过百,上下文里的 Skill 描述会占用大量 token,同时无关 Skill 会对选择产生噪声干扰。召回层的目的是把候选集从 100 个缩小到 5 到 10 个,减少模型决策负担,同时保留真正相关的选项。
召回层需要平衡召回率和精确率。召回率低,正确 Skill 被过滤掉,模型无论如何都选不中;精确率低,候选集里全是噪声,模型决策容易出错。实际项目中通常采用混合召回,兼顾关键词、向量和规则。
4.2 关键字召回:快速、可解释、适合精确词匹配
关键字召回的核心思路是对用户请求和 Skill 元数据做词法匹配。优点是没有额外模型依赖,速度快,结果可解释;缺点是无法处理同义表达和上下文歧义。
最小实现可以基于 TF-IDF 或 BM25。下面是一个基于scikit-learn的简单示例:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity skill_docs = [ "周报 生成 工作日志 汇报", "日报 生成 每日工作记录", "简历 解析 提取 候选人 字段", ] user_query = "帮我根据日志整理本周周报" vectorizer = TfidfVectorizer(token_pattern=r"\b\w+\b") vectors = vectorizer.fit_transform(skill_docs + [user_query]) query_vector = vectors[-1] skill_vectors = vectors[:-1] scores = cosine_similarity(query_vector, skill_vectors).flatten() top_indices = scores.argsort()[-3:][::-1] for idx in top_indices: print(f"Score: {scores[idx]:.4f}, Skill: {skill_docs[idx]}")这个示例说明关键字召回的逻辑:先把每个 Skill 的索引文本拼成一段,再用 TF-IDF 计算用户请求与各 Skill 的相似度,最后取 Top N。生产环境建议直接使用 Elasticsearch 的 BM25 或专门的关键词检索引擎。
4.3 向量召回:处理同义表达和跨语言表达
向量召回使用 Embedding 模型将 Skill 描述和用户请求编码成向量,再计算余弦相似度。它能处理“周报”和“工作汇报”“weekly report”等不同表达之间的语义关联,解决关键字召回无法覆盖的同义问题。
下面是一个基于 Chroma 的最小示例:
import chromadb from chromadb.utils import embedding_functions client = chromadb.PersistentClient(path="./chroma_db") embedding_fn = embedding_functions.DefaultEmbeddingFunction() collection = client.get_or_create_collection( name="skills", embedding_function=embedding_fn, ) # 添加 Skill 索引 collection.upsert( ids=["weekly-report", "daily-report", "resume-parser"], documents=[ "根据工作日志生成结构化周报,适合周度汇报场景", "根据每日工作记录生成日报,适合日度汇报场景", "解析简历文件并提取候选人关键字段,适合招聘筛选场景", ], metadatas=[ {"category": "report", "name": "weekly-report-generator"}, {"category": "report", "name": "daily-report-generator"}, {"category": "document", "name": "resume-parser"}, ], ) user_query = "帮我写一份这周的汇报材料" results = collection.query( query_texts=[user_query], n_results=2, ) for doc, meta in zip(results["documents"][0], results["metadatas"][0]): print(meta["name"], doc)这里使用collection.query返回最相似的 Skill。生产环境通常使用更专业的向量数据库,如 Milvus、Qdrant、Elasticsearch 的向量检索能力。向量召回的优势是语义泛化,但也需要控制索引内容质量,避免把相似描述都塞进索引导致结果区分度下降。
4.4 混合召回:兼顾精确匹配和语义相似
混合召回的基本思路是:先通过规则或关键字召回获取第一候选集,再通过向量召回获取第二候选集,合并后去重,保留 Top K 给模型选择。不同召回结果的分数需要归一化,避免某个召回源垄断候选集。
一个简单实现:
def hybrid_recall(user_query, top_k=8): keyword_candidates = keyword_retrieve(user_query, top_k=top_k) vector_candidates = vector_retrieve(user_query, top_k=top_k) merged = {} for skill_name, score in keyword_candidates: merged[skill_name] = max(merged.get(skill_name, 0), score) for skill_name, score in vector_candidates: merged[skill_name] = max(merged.get(skill_name, 0), score) sorted_skills = sorted(merged.items(), key=lambda x: x[1], reverse=True) return sorted_skills[:top_k]生产实现时还需要考虑:
- 规则召回:某些用户请求带有明确指令词,例如“导出”“解析”“生成”,可以直接映射到特定 Skill。
- 同义词扩展:为常用概念维护同义词表,例如“周报”与“weekly report”互相关联。
- 热门 Skill 兜底:如果召回结果为空,可以返回最近调用频率最高的几个 Skill 作为降级策略。
4.5 召回效果不好时先查这些地方
召回效果差大概率不是模型问题,而是索引数据或召回逻辑的问题。按以下顺序排查:
| 问题现象 | 可能原因 | 检查方式 | 解决建议 |
|---|---|---|---|
| 正确 Skill 没有被召回 | 索引文本与用户请求语义差异过大 | 打印召回 Top 10,确认正确 Skill 是否出现 | 增加触发示例,优化描述关键词 |
| 召回结果全是无关 Skill | 索引文本重复度高或向量模型不匹配 | 检查向量相似度分数分布 | 调整索引文本,换更合适的 Embedding 模型 |
| 同义表达召回失败 | 关键字召回无法处理同义词 | 构造同义表达测试 | 引入同义词表或向量召回 |
| 候选集不够稳定 | 混合召回分数归一化不一致 | 打印每个召回源的分数 | 统一分数范围,使用加权融合 |
召回层是整个命中率提升的基础。如果召回阶段已经把正确 Skill 过滤掉了,后面让模型选择也没有意义。因此,在优化模型提示词之前,先确保召回层在评测集上的召回率足够高。
5. 命中率评估:用数据说话,而不是靠感觉
5.1 构造评估集:来自日志、触发示例和人工标注
评估集是命中率优化的关键基础设施。没有评估集,你无法判断改动是变好还是变坏。
建议按以下来源构造评估集:
- 线上日志:从 Agent 调用日志里抽取真实用户请求,覆盖不同表达方式和错误场景。
- 触发示例:每个 Skill 的
trigger_examples直接转换为评估用例。 - 人工补充:邀请产品、运营和真实用户写出他们可能使用的表达。
一份评估集的最小格式如下:
[ { "id": 1, "input": "帮我根据日志生成这周的周报", "expected_skill": "weekly-report-generator", "source": "log", "difficulty": "easy" }, { "id": 2, "input": "把今天的进展整理成文档发给领导", "expected_skill": "daily-report-generator", "source": "manual", "difficulty": "hard" } ]expected_skill必须是 Skill 注册表里的规范名称,评估脚本才能自动统计命中率。difficulty可以按“简单、中等、困难”划分,用于分析不同难度下的表现。
5.2 评估指标:命中率、召回率、误调用率
统计命中率时,需要把结果分成几类:
| 结果 | 含义 | 对用户的影响 |
|---|---|---|
| 正确命中 | 选中的 Skill 与期望一致 | 正常 |
| 错误命中 | 选中的 Skill 与期望不一致 | 用户得到错误功能 |
| 漏调用 | 需要调用 Skill 但没有调用 | 用户需求未满足 |
| 误调用 | 不需要调用 Skill 但调用了 | 执行了多余动作 |
| 执行失败 | 选中正确 Skill 但内部执行报错 | 用户看到错误信息 |
最终统计指标建议包括:
- Skill 调用命中率:正确命中数 / 所有需要调用 Skill 的用例数。
- 召回成功率:正确 Skill 出现在召回结果 Top K 中的比例。
- 误调用率:误调用数 / 所有调用数。
- 平均检索位置:正确 Skill 在候选集中的平均排名。
这些指标分别对应不同问题:命中率低但召回成功率高,说明问题在模型选择阶段;召回成功率低,说明问题在召回层。
5.3 最小评估脚本
下面是一个最小评估脚本示例:
import json import yaml def load_skill_registry(path): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def load_eval_set(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def recall_candidates(skill_registry, user_query, top_k=5): # 这里替换成你自己的召回实现 # 返回值是 Skill 名称列表 return [] def run_eval(eval_set, skill_registry): total = len(eval_set) hit = 0 recall_success = 0 misinvoke = 0 for item in eval_set: expected = item["expected_skill"] query = item["input"] candidates = recall_candidates(skill_registry, query, top_k=5) # 检查召回阶段是否包含正确 Skill if expected in candidates: recall_success += 1 # 检查模型最终选择是否命中 selected_skill = model_select(query, candidates) # 替换成实际 Agent 调用 if selected_skill == expected: hit += 1 elif selected_skill is not None: misinvoke += 1 return { "total": total, "hit_rate": hit / total, "recall_success_rate": recall_success / total, "misinvoke_rate": misinvoke / total, } def model_select(query, candidates): # 这里应接入实际模型调用,并限定候选集 return candidates[0] def main(): skill_registry = load_skill_registry("config/skill_registry.yaml") eval_set = load_eval_set("config/eval_set.json") result = run_eval(eval_set, skill_registry) print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()这个脚本的结构提示了评估链路:召回和选择是两个独立阶段,可以分别统计。实际生产环境建议把每个评估用例的结果落盘,包括候选列表、最终选择、分数和日志,方便后续分析错误样本。
5.4 分析错误样本:从“命中率”到“为什么没命中”
评估结束后,要进入错误样本分析阶段。建议把错误样本按类型分组:
- 语义映射失败:用户表达与 Skill 描述不在同一语义空间。
- 描述覆盖不足:Skill 描述没有包含该场景的关键信息。
- 候选召回失败:正确 Skill 不在召回 Top K 中。
- 候选排序不佳:正确 Skill 在召回结果里,但排名靠后,模型选了其他项。
- 规则优先级错误:某些规则强制把请求映射到了错误 Skill。
处理方式:
- 对语义映射失败,增加对应 Skill 的触发示例,优化描述关键词。
- 对召回失败,调整索引文本或引入向量召回。
- 对排序不佳,调整混合召回权重或重新生成 Embedding 索引。
- 对规则优先级错误,检查规则表和业务含义,必要时增加优先级字段。
评估不是一次性工作。建议每次增加 Skill、修改描述或调整召回逻辑后都重新运行评估集,并记录历史指标,避免“改一个 Skill 导致另一个 Skill 命中率下降”。
6. 运行时监控:日志、告警和持续迭代
6.1 记录每次调用链路的完整信息
评估集只能覆盖一部分情况,生产环境中的真实请求才是 Skill 管理的主要数据来源。每次 Agent 调用 Skill 时,至少记录以下字段:
| 字段 | 说明 |
|---|---|
| request_id | 请求唯一标识 |
| timestamp | 调用时间 |
| user_query | 用户原始请求 |
| recall_candidates | 召回阶段返回的候选 Skill 列表 |
| candidate_scores | 每个候选的分数 |
| selected_skill | 模型最终选择的 Skill |
| execution_status | Skill 执行状态:成功、失败、超时 |
| error_message | 如果执行失败,记录错误信息 |
| latency_ms | 从请求到选择的耗时 |
| model_name | 使用的模型名称和版本 |
日志格式可以使用 JSON,便于后续入 ClickHouse、Elasticsearch 或文件检索。下面是一个日志示例:
{ "request_id": "req_20250101_001", "timestamp": "2025-01-01T10:00:00Z", "user_query": "帮我根据日志整理本周周报", "recall_candidates": ["weekly-report-generator", "daily-report-generator"], "candidate_scores": [0.92, 0.61], "selected_skill": "weekly-report-generator", "execution_status": "success", "error_message": "", "latency_ms": 320, "model_name": "gpt-4o" }6.2 建立命中率报表和告警规则
持续统计命中率时,按小时或按天聚合数据。比较简单的做法是把日志读入 DataFrame,然后按时间窗口统计:
import pandas as pd logs = pd.read_json("logs/skill_call.log", lines=True) logs["event_time"] = pd.to_datetime(logs["timestamp"]) # 每天命中率 logs["is_hit"] = logs["execution_status"] == "success" daily_rate = logs.groupby(logs["event_time"].dt.date)["is_hit"].mean() print(daily_rate) # 每个 Skill 的调用次数和失败率 skill_stats = logs.groupby("selected_skill").agg( call_count=("request_id", "count"), fail_count=("execution_status", lambda x: (x != "success").sum()), ) skill_stats["fail_rate"] = skill_stats["fail_count"] / skill_stats["call_count"] print(skill_stats.sort_values("fail_rate", ascending=False))告警规则可以设置:
- 命中率低于某个阈值,例如低于 85% 时触发告警。
- 某个 Skill 连续失败超过 5 次。
- 召回阶段覆盖率明显下降。
- 用户请求中包含大量未知表达,说明 Skill 登记存在覆盖缺口。
告警不是目的,关键是告警后能定位到具体 Skill 和具体请求样本,判断是描述问题、执行问题还是模型问题。
6.3 Skill 生命周期管理:上线、下线、版本更新
Skill 数量过百后,必然会遇到下线、废弃、合并和版本更新问题。建议建立明确的 Skill 生命周期流程:
| 状态 | 含义 | 处理方式 |
|---|---|---|
| 草稿 | 开发中,未进入正式索引 | 不参与召回 |
| 已发布 | 可在生产环境被调用 | 进入召回索引和模型候选 |
| 已废弃 | 不再推荐使用 | 从召回层移除,保留日志用于历史分析 |
| 已下线 | 当前不可用 | 不参与调用,但记录名称防止冲突 |
Skill 生命周期状态迁移:草稿 -> 已发布 -> 已废弃 -> 已下线实际项目很容易出现的问题是:某团队改了一个 Skill 描述,但线上索引没有更新;或者某个 Skill 已下线,但用户请求仍被规则强制路由到它。建立生命周期状态后,每次变更都要走更新任务,并同步更新注册表和召回索引。
6.4 持续迭代:以周为周期管理 Skill 质量
Skill 管理不是一次性建设,而是一个持续维护过程。建议按周执行以下循环:
- 从日志中抽取近一周的低命中率样本。
- 低命中率样本归类:描述问题、召回问题、执行问题、模型问题。
- 对描述问题更新
SKILL.md和注册表。 - 对召回问题更新索引文本、触发示例或召回权重。
- 对执行问题查看 Skill 内部脚本和依赖。
- 更新评估集,加入新出现的表达方式。
- 重新运行评估脚本,对比历史指标。
如果团队规模允许,可以指定一个 Skill 仓库负责人,负责合并描述变更、处理冲突、审查新增 Skill。因为当 100 个 Skill 由多人维护时,命名冲突、语义重叠、索引覆盖缺失几乎必然发生。
7. 最佳实践与常见坑
7.1 至少三个与 Skill 过百相关的常见坑
坑一:只优化 System Prompt,不优化召回层。
有人在命中率下降后不断加长 System Prompt,把所有 Skill 描述都写进提示词,结果上下文被撑满,模型决策反而更差。正确做法是先做召回,缩小候选集,再让模型在小集合里选择。
坑二:触发示例只写“理想表达”,不写边界表达。
如果触发示例都是“生成周报”“周报生成”这类理想表达,模型遇到“我下周要跟领导汇报,帮我把内容整理下”时仍然可能选错。边界表达必须来自真实日志和用户反馈。
坑三:评估集和日志分离,导致无法定位问题。
只统计“命中率是否低于 90%”,却不记录具体是哪个 Skill、哪个用户请求、哪个召回阶段出错,就无法推进优化。评估和监控必须保留完整调用链路信息。
7.2 可复用的 Skill 质量检查清单
每次新增或修改 Skill 时,建议按以下清单检查:
- [ ] Skill 名称是否使用动词开头,长度是否控制在 3 到 6 个单词。
- [ ] 描述是否包含“做什么、什么时候用、不做什么”。
- [ ] 是否配置了 5 条以上触发示例,且包含边界表达。
- [ ] 分类和标签是否与其他 Skill 冲突。
- [ ] 索引文本是否及时同步。
- [ ] 是否新增了对应评估用例。
- [ ] 本次修改是否会影响其他 Skill 的召回。
- [ ] Skill 依赖的脚本、API 是否在目标环境可运行。
- [ ] 是否有负责人和生命周期状态。
- [ ] 是否登记到
skill_registry.yaml,而不是只放在目录里。
7.3 命中率优化的优先级建议
如果资源有限,建议按以下顺序推进:
- 先建立评估集和日志采集,让命中率可度量。
- 再统一 Skill 元数据格式,重点补触发示例和边界描述。
- 然后引入召回层,先关键词召回,再向量召回。
- 最后用历史日志持续迭代评估集,形成周级别维护循环。
不要一开始就追求复杂的向量数据库和重排序模型。先把基础数据质量和日志链路做扎实,命中率提升会更稳定。
7.4 Agent 与 Skill 体系扩展方向
当 Skill 数量继续增长,比如超过 1000 个,还需要考虑更复杂的分层检索、Skill 组合编排、权限隔离和动态加载。Skill 之间如果存在依赖关系,例如某个 Skill 依赖另一个 Skill 的输出,还需要为这种依赖关系建模,而不是只做平面列表召回。
另一个方向是让 Skill 调用结果反过来参与召回。同一个用户请求如果多次调用某个 Skill,可以把该 Skill 的调用记录作为特征,影响后续召回排序。结合用户历史和业务偏好,召回准确率可以进一步提升。
对新手来说,最有价值的练习是在自己的 Agent 项目里构造 100 个 Skill,先跑出命中率,再复现本文提到的命名、描述、召回和评估优化流程。通过亲手复现命中率下降和回升的过程,会比只看任何一篇教程都更深刻。