Agent技能调用命中率治理:从命名规范到分层路由实战
发布时间:2026/8/27 9:00:28 作者:尧图编辑部 阅读量:1,286

当 Agent 接管的技能从几个增加到几十个、上百个之后最让开发团队头疼的往往不是单条技能写不出来而是模型总是在一堆 Skill 面前“选错”。用户想画一张趋势图Agent 却去调用了天气查询用户说“帮我分析一下这份日志”Agent 却先执行了代码提交。这类问题频繁出现时表面上是模型理解能力不够实际是整个 Skill 体系的命名、描述、路由和评估机制没有跟上规模增长。这篇文章会从 Agent 调用 Skill 的基本机制讲起拆解 Skill 数量变多以后命中率下降的根本原因再用一个完整的实战案例演示如何通过技能注册表、语义路由、分层调度和命中日志把调用命中率从“靠运气”变成“可治理”。无论你是刚开始接触 Agent 开发还是已经维护了一套数量庞大的技能库这篇文章都值得收藏备用。1. 从“能用”到“好用”Agent 与 Skill 的命中率问题1.1 Skill 在 Agent 中扮演什么角色在 Agent 开发领域Skill 通常指一个可以被 Agent 调用的能力单元。它可能是一段 Python 脚本、一个 API 包装函数、一个命令行工具封装也可能是一个带提示词的工作流模板。不同的框架叫法不一样有的叫 Tool有的叫 Function有的叫 Action近两年受 Claude Code、Codex 等产品影响越来越多项目开始使用 Skill 这个叫法。Skill 和 Agent 的关系可以理解为Agent 是大脑Skill 是四肢。大脑负责理解用户意图、制定执行计划四肢负责具体干活。干活的质量取决于两个环节大脑是否想对了方向。四肢是否准确接到了指令。传统软件开发中函数调用是程序员在编译期就确定好的而在 Agent 应用中调用哪个 Skill 往往由大模型在运行时根据用户输入自行决定。这就引入了一个核心指标调用命中率。1.2 命中率为什么是工程问题调用命中率可以从三个层次理解选择命中在多个候选 Skill 中模型是否选到了最合适的那一个。执行成功选中 Skill 后参数填充、执行过程是否顺利完成。结果有效执行完成后返回结果是否真正解决了用户的问题。很多团队只关注第一层认为“只要模型选对了 Skill 就算命中”。实际上生产环境中三层都可能出问题。比如模型选对了“发送邮件”Skill但把收件人和邮件内容填反了执行虽然成功结果却完全无效。当 Skill 数量只有十几个时模型把所有技能描述都塞进上下文也不会超限命中率通常不会太差。但当技能数量过百情况会快速恶化上下文窗口被大量技能描述占满模型注意力被稀释。雷同描述互相干扰模型难以区分。部分冷门技能长期没有被调用描述质量无人维护。新技能与旧技能边界不清语义空间重叠。所以Skill 数量过百后的命中率问题本质上是一个技能库治理问题。它需要一套机制让 Agent 在海量技能中快速缩小候选范围、准确匹配用户意图。1.3 谁需要关注这个问题如果你是下面几类开发者这篇文章的内容直接与你相关正在基于 Claude Code、Codex、CrewAI 或自研框架搭建 Agent 应用。已经维护了 50 个以上 Skill开始发觉模型选择不稳定。想建立一套技能接入规范避免后期技能爆炸导致系统不可维护。在面试或团队分享中需要展示对 Agent 工程化落地的深入理解。对于初学者可以先照着本文的规范改造自己的技能定义对于已经踩过坑的团队可以直接跳到第 4 节的调度链路设计和第 6 节的排查清单。2. Skill 数量变多以后调用到底哪里会出问题2.1 LLM 的调用决策机制在没有特殊路由层的情况下Agent 调用 Skill 的过程大致如下收到用户消息。系统从技能库中取出技能列表连同用户消息一起交给大模型。大模型分析用户意图在技能列表中挑选匹配项。大模型生成结构化的调用参数。Agent 框架执行选中的 Skill把结果返回给大模型。关键在第 2 步和第 3 步。技能列表如何组织、如何截断、以什么格式呈现直接影响模型的决策质量。在 OpenAI 的函数调用Function Calling体系中每个函数都需要提供 name、description、parameters 三要素。Claude 的 Tool Use 机制类似。Skill 的概念通常在这之上做了更细的封装但底层依赖的属性并没有变。2.2 命中率下降的 5 个典型原因结合大量实际项目的问题反馈Skill 数量超过百个以后命中率下降主要集中在以下五个原因。原因一技能描述同质化{ name: get_weather, description: 获取天气 }, { name: get_air_quality, description: 获取空气质量 }这种描述方式中“获取”两个字完全无法帮助模型区分两个技能。当候选列表很长时模型只能靠猜。原因二技能命名不直观有的团队把内部服务名直接用作 Skill 名称例如query_yy_report_v3。这种命名对机器友好对模型不友好。模型无法从名称中推断出技能的实际用途。原因三技能粒度不均匀一部分 Skill 非常细比如“获取用户头像”另一部分又非常粗比如“执行日常运营任务”。粒度差异过大时模型在决策中容易偏向粒度更粗的技能因为它看起来“覆盖面更广”结果执行结果与用户预期严重不符。原因四候选列表无差别全量注入当上下文窗口容纳不下 100 个完整技能 Schema 时系统可能只截取前面一部分。如果被截掉的技能恰好是用户需要的那模型无论如何都不可能选对。更糟的是如果技能按字母排序常见技能可能永远排在后面。原因五缺少失败反馈闭环调用失败后Agent 往往只是把错误信息返回给模型重试。没有记录失败原因没有沉淀“哪个描述容易误召回”技能库就一直原地踏步。2.3 一个容易忽略的指标有效命中率除了整体命中率之外建议额外统计一个指标有效命中率Effective Hit Rate。有效命中率 正确选中且执行成功且结果满足用户需求的次数 ÷ 总请求次数比如一次请求中模型先选错了一个技能执行报错后重新选对了这时“最终命中”是成功的但多消耗了一次调用、多消耗了 token用户体验也变差了。真正需要优化的是第一次选择就命中的比率而不是重试后的最终成功率。3. 提高命中率的基础Skill 命名与描述规范3.1 命名规范让模型“一眼看懂”Skill 的命名虽然最终不直接暴露给用户但当技能列表被序列化交给模型时name 是模型最先看到的信息。命名建议遵循以下原则采用“动词 业务对象”结构例如send_email、create_issue、generate_chart。避免模糊动词如handle、process、do、run。避免纯缩写除非是行业内广为人知的术语如send_sms。同名不同功能需要加限定语例如query_order_oms与query_order_trade。下面是一个对比示例不推荐do_task_v2 推荐 send_meeting_invitation不推荐util_get 推荐 get_user_profile_by_id命名的目标是当模型看到名字时不需要再进入描述文本就已经能形成一个初步的语义判断。3.2 描述规范一句话职责加触发条件描述是模型做选择时的核心依据。一条高质量的 Skill 描述应该包含三个部分一句话职责说明这个技能做什么。典型场景什么情况下应该调用它。边界排除什么情况下不应该调用它。以一个“生成图表”的 Skill 为例{ name: generate_chart, description: 根据数据表格或数组生成可视化图表支持折线图、柱状图、饼图、散点图。当用户要求画趋势、对比、占比或分布图时使用。如果用户只是想查看原始数据不要调用本技能。 }这样的描述里包含了能力说明生成图表。参数范围支持哪些图表类型。触发场景趋势、对比、占比、分布。排除场景查看原始数据。模型在决策时看到这样的描述误召回概率会大幅降低。3.3 参数定义Schema 设计决定调用成败Skill 的 parameters 定义决定模型能否准确生成调用参数。需要重点关注几个问题必填参数和选填参数要明确。不该必填的一定不要设成必填否则模型会为了“填满”参数而编造内容。参数名使用语义化命名避免a、b、val这类无意义变量。在参数 description 中写清楚取值格式。例如日期参数要说明是YYYY-MM-DD还是时间戳。枚举类型要用 enum 显式限制取值范围而不是靠模型自由发挥。{ name: query_sales_report, description: 查询指定时间范围内的销售报表。适用于用户要求查看销售额、订单量、GMV 等经营数据的场景。, parameters: { type: object, properties: { start_date: { type: string, description: 开始日期格式为 YYYY-MM-DD }, end_date: { type: string, description: 结束日期格式为 YYYY-MM-DD }, metrics: { type: array, items: { type: string, enum: [sales, orders, gmv, users] }, description: 需要查询的指标列表 } }, required: [start_date, end_date] } }这段定义告诉模型三件事日期怎么传、指标有哪些可选值、哪些参数必填。模型不需要猜测填充错误自然就少了。3.4 常见描述误区误区问题正确做法描述只有一句话信息量不足模型难以判断边界包含职责、场景、边界描述过长占用上下文稀释其他技能权重控制在 50~150 字在描述中写“不要调用”但原因不清模型无法理解排除逻辑明确“什么条件下不要调用”参数名与描述里名词不一致模型生成参数时报错统一术语参数名与描述保持一致4. 实战案例100 个 Skill 场景下如何设计调用链路下面用一个具体的工程案例完整演示如何从零构建一套可扩展的 Skill 调度体系让 100 个甚至更多技能保持稳定命中。4.1 场景设定与技能清单假设我们正在开发一个团队协作 Agent技能库包含 120 个 Skill大致分为以下几类办公协同类创建会议、发送邮件、创建日程、邀请成员。数据查询类查询销售报表、查询用户增长、查询服务器监控。开发运维类创建分支、提交代码、触发部署、查看日志。内容生成类生成周报、生成图表、整理会议纪要。外部服务类查询天气、查询汇率、点外卖、订机票。如果把这 120 个 Skill 一次性塞给模型结果一定不稳定。因此需要采用“检索 路由 排序”三层结构。4.2 技能注册表设计所有 Skill 先以统一的 JSON Schema 形式登记在一个技能注册表中这是整个调度系统的数据基础。[ { name: send_email, category: office, description: 发送电子邮件给指定收件人。支持普通文本和 HTML 内容。当用户要求发邮件、发送通知、发送报告时使用。不适用于发送站内信或短信。, keywords: [邮件, 发送, 通知, 收件人, email], parameters: { type: object, properties: { to: { type: string, description: 收件人邮箱地址 }, subject: { type: string, description: 邮件主题 }, content: { type: string, description: 邮件正文 } }, required: [to, subject, content] } }, { name: generate_chart, category: content, description: 根据数据生成可视化图表支持折线图、柱状图、饼图、散点图。当用户要求画趋势图、柱状对比、占比分布时使用。如果用户只需要查看原始数据不要调用本技能。, keywords: [图表, 折线图, 柱状图, 饼图, 可视化, chart], parameters: { type: object, properties: { type: { type: string, enum: [line, bar, pie, scatter], description: 图表类型 }, data: { type: array, description: 图表数据每个元素包含 name 和 value 字段 }, title: { type: string, description: 图表标题 } }, required: [type, data] } } ]实际项目中这个注册表可以存放在 JSON 文件、数据库或配置中心中。关键是每个 Skill 都包含结构化字段name、category、description、keywords、parameters。keywords字段很容易被忽略但在后文的关键词召回阶段非常有用。它可以由开发者在创建 Skill 时手动补充也可以通过历史日志自动挖掘。4.3 第一层关键词与分类召回当用户输入到达时先不急着调用大模型而是用轻量级的规则做第一轮粗筛把候选技能从 120 个缩小到 10~20 个。# skill_router.py import json import re class SimpleRouter: def __init__(self, skill_registry_path): with open(skill_registry_path, r, encodingutf-8) as f: self.skills json.load(f) def keyword_recall(self, query, top_k20): query query.lower() scored [] for skill in self.skills: score 0 for kw in skill.get(keywords, []): if kw.lower() in query: score 1 # 技能描述中出现用户关键词给予较低权重 for term in re.findall(r[\u4e00-\u9fa5a-zA-Z0-9], query): if term.lower() in skill[description].lower(): score 0.5 if score 0: scored.append((score, skill)) scored.sort(keylambda x: x[0], reverseTrue) return [item[1] for item in scored[:top_k]]这段代码做了两件事遍历技能注册表中的keywords统计命中的关键词数量。检查用户查询中的词是否出现在技能描述中给予额外权重。纯规则方式的好处是零调用成本、响应快但它只能做粗筛不能理解复杂语义因此需要第二层向量召回。4.4 第二层向量召回与语义匹配关键词召回无法处理“用户没有提到任何关键词”的情况。例如用户说“帮我整理一下本周的情况”没有出现“周报”二字关键词召回可能找不到generate_weekly_report。这时需要引入向量召回。核心思路是把用户查询和 Skill 描述都转换成向量通过余弦相似度找到语义上最接近的技能。# vector_recall.py # 示例思路使用 OpenAI Embedding 接口或本地 embedding 模型 from openai import OpenAI client OpenAI() def embed_text(text): resp client.embeddings.create( modeltext-embedding-3-small, inputtext ) return resp.data[0].embedding def semantic_recall(query, skill_docs, top_k10): query_vec embed_text(query) scored [] for doc in skill_docs: skill_vec embed_text(doc[description]) sim cosine_similarity(query_vec, skill_vec) scored.append((sim, doc)) scored.sort(keylambda x: x[0], reverseTrue) return [item[1] for item in scored[:top_k]]生产环境中不可能每次请求都实时对所有技能做 Embedding通常做法是启动时对技能库做一次批量向量化存入向量数据库如 Chroma、Qdrant、Milvus。请求到达时只对用户查询做一次 Embedding然后从向量库中查找最近邻。可以根据历史命中数据动态调整每个技能的检索权重。如果不想引入外部向量数据库也可以把技能量控制在千级以内用 numpy 做内存向量检索性能完全够用。下面是一个简化的实现思路# memory_vector_store.py import numpy as np class VectorStore: def __init__(self): self.vectors [] self.metas [] def add(self, vector, meta): self.vectors.append(vector) self.metas.append(meta) def search(self, query_vector, top_k10): matrix np.array(self.vectors) query np.array(query_vector) # 计算余弦相似度 scores matrix query / (np.linalg.norm(matrix, axis1) * np.linalg.norm(query) 1e-9) idx np.argsort(scores)[::-1][:top_k] return [(self.metas[i], float(scores[i])) for i in idx]这里把 Embedding 细节抽离了生产环境中可以接入任意模型。设计上要注意技能描述更新后向量库中的旧向量需要同步更新否则会出现模型已经会新技能了检索层却还是旧数据。4.5 第三层LLM 精排与选择经过关键词召回和向量召回候选集合已经缩小到 10~20 个。接下来把这批候选技能的 name、description、parameters 交给 LLM让模型做最终选择。这一步的关键是提示词设计。可以把候选集按原始技能库中的分类组织而不是一长串平铺更有利于模型理解。你是Agent调度器需要从候选技能中选择一个最合适的技能响应用户请求。 如果候选技能都不适合请返回 no_match。 用户请求{query} 候选技能 {formatted_skills} 要求 1. 优先选择描述与用户请求最匹配的技能。 2. 如果多个技能都能完成选择参数约束更具体的那个。 3. 只返回技能名不要输出解释。候选技能格式化时建议按如下方式组织【办公协同】 - send_email: 发送电子邮件给指定收件人。支持普通文本和 HTML 内容。 - create_meeting: 创建线上会议并发邀请链接。 【数据查询】 - query_sales_report: 查询指定时间范围内的销售报表。 - query_user_growth: 查询用户增长趋势数据。这样模型不需要从 100 个扁平列表里挑而是先看分类再在同一分类下比较选择准确率会明显提升。4.6 上下文压缩与动态裁剪即使有检索层LLM 最终接收的候选技能仍然会占用上下文。为了进一步控制 token 消耗可以在精排前对 candidates 做字段裁剪只保留name、description、parameters丢弃内部使用字段。对参数定义做简化必填参数完整展示选填参数只保留名称和枚举值。同一业务域有多个相似技能时可以先展示一个“代表技能”其他作为别名提示。例如{ name: query_sales_report, description: 查询销售报表适用于日、周、月维度。, parameters: { start_date: YYYY-MM-DD必填, end_date: YYYY-MM-DD必填, dimension: day|week|month选填 } }把一个完整的 JSON Schema 压成紧凑格式上下文占用可以降低一半以上。4.7 完整调度流程汇总综合上面的分层设计一个完整的调用链路如下用户请求 | v [第一层] 关键词召回 分类过滤 | v [第二层] 向量语义召回 | v [第三层] 合并候选集去除重复 | v [第四层] LLM 精排选择最佳 Skill | v [第五层] 参数填充与执行 | v [第六层] 执行结果返回后写入命中日志分层调度的核心思路是能用规则解决的问题不麻烦模型必须模型决策的问题才交给模型。这样既控制了成本也提升了稳定性。5. 命中率评估与持续优化5.1 建立命中日志没有日志就无法优化。每次请求结束后记录以下字段用户 query候选技能列表最终选中的技能用户/测试脚本标注的正确技能是否命中执行是否成功失败阶段选择失败、参数失败、执行失败示例 JSON 日志格式{ query: 画一下本月每日订单量趋势, candidates: [generate_chart, query_sales_report, create_report], selected: generate_chart, expected: generate_chart, hit: true, execution_status: success, latency_ms: 320 }这些日志积累到一定量后可以作为离线评估集也可以反哺关键词表。5.2 离线评估集抽取 200~500 条历史日志人工标注每条 query 应该命中哪个 Skill形成评估集。然后每次修改技能描述、调整路由策略后在评估集上跑一遍观察命中率变化。评估指标包括Top-1 命中率模型第一次选择就命中的比例。Top-5 召回率正确技能出现在候选列表前 5 个中的比例。平均重试次数每个请求平均需要几次调用才能成功。Token 消耗每次请求用于技能筛选的平均 token 数。Top-5 召回率能反映检索层质量Top-1 命中率能反映精排效果两者需要同时关注。5.3 回归测试与版本管理Skill 描述是不断演进的。每次改动都可能影响其他技能的命中率所以要建立回归测试机制。建议做法技能注册表使用 Git 管理每次修改都走 MR/PR 流程。修改后的技能必须附带测试用例例如“这个技能在哪些 query 下应该被触发”。合并前在离线评估集上跑一次保证整体命中率不下降。git add skills_registry.json git commit -m 优化 generate_chart 描述增加饼图场景说明 git push origin main6. 常见问题排查与解决方案问题现象可能原因解决思路热门技能频繁选错描述与其他技能重叠边界不清重写描述明确“不适用”场景新技能始终没被调用注册表已更新向量库未重建检查向量库索引统一更新流程冷门技能长期无人使用用户 query 召回不到它从日志中挖掘关键词补充到 keywords调用时参数频繁缺失必填参数定义过多审视参数必要性减少必填项上下文 token 占用过高候选技能数量过大降低候选集规模压缩参数描述同一 query 结果不稳定精排阶段 LLM 温度过高将精排温度调低或改为确定性选择Agent 报 agent execution terminated 类错误技能执行异常后未兜底增加异常捕获与重试机制Agent 执行类报错也就是日志中常见的agent terminated due to error或agent execution terminated due to error很多时候并不是 Agent 框架本身崩溃而是某个 Skill 抛出了未捕获异常。排查时先定位执行日志中的技能名称再单独验证该技能入参和依赖环境。7. 最佳实践与工程建议7.1 Skill 粒度设计宁可多而细不要少而粗一个 Skill 最好只做一件事。粒度越细描述边界越清晰模型越容易判断是否匹配。如果一个 Skill 里塞了“查询订单、退款、改地址”三个功能模型很难通过描述准确触发正确的分支。反例名称handle_order 描述处理订单相关的一切事务包括查询、退款、修改地址。这种设计在技能数量少时还能用一旦技能多了模型会在“处理订单”和“查询订单”之间反复犹豫。最好拆成query_order、refund_order、update_delivery_address三个独立技能。7.2 技能描述要写成“面向模型”而不是“面向人”写 Skill 描述时记住阅读者是 LLM。描述要具体、结构化、包含触发条件和边界。不要用营销或宣传口吻例如“强大的数据分析能力”模型无法从这句话判断何时调用它。7.3 引入人工审核环节在技能注册流程中增加一道人工审核重点检查技能名称是否语义清晰。描述是否包含触发场景和排除场景。参数定义是否符合生产接口。keywords 是否覆盖了常见用户说法。每个新接入的 Skill 都至少准备 5 条测试 query验证模型能正确选到它。7.4 生产环境风险控制涉及真实业务操作时Agent 调用 Skill 需要加一层安全边界查询类技能可以自动执行。变更类技能发送、删除、修改、部署默认需要人工确认或经过审批流。高危操作留有审计日志记录触发来源、参数和执行结果。权限遵循最小原则Agent 只能调用当前用户有权限的技能而不是全部技能。7.5 监控看板建议为 Skill 命中率搭建一个简单的监控看板至少包含三个视图按时间维度展示整体命中率趋势。按技能维度展示被选中的次数、命中率、失败率。按 query 维度展示高频但未命中的请求样本。高频未命中的 query 是最有价值的优化入口。它说明用户经常提出这类需求而当前技能库没有覆盖好。8. 总结与下一步Skill 数量过百后Agent 调用命中率的问题本质上是技能库工程化治理的问题。本文从调用机制讲起拆解了命中率下降的五个原因给出了命名规范、描述规范、参数定义规范并通过一个完整的实战案例展示了“关键词召回 向量召回 LLM 精排”的分层调度架构。最后补充了命中日志、离线评估、回归测试和风险控制建议。如果你正在从零搭建自己的 Agent 技能体系可以先从以下三件事做起把现有 Skill 的描述全部按“职责 触发场景 排除场景”的格式重写一遍。为每个 Skill 补充 keywords 字段建立统一注册表。记录一次完整的调用链路日志找到当前命中率最低的技能分类。接下来可以继续深入了解如何用向量数据库替代内存检索、如何设计多轮对话下的 Skill 记忆、如何针对不同业务场景微调精排提示词。这些都是 Skill 规模扩大后绕不开的进阶话题。先把基础的命名、描述和路由机制做扎实再逐步叠加高级能力Agent 的可用性和稳定性会有非常明显的提升。