☰
技能库扩容后,Agent如何稳定找到并组合正确技能?
2026/10/1 4:18:59 网站建设 项目流程

两周前帮一个团队把他们的 Agent 技能库从 20 个 skill 扩容到 80 个,结果出了个很典型的问题:原本能稳定调对技能的 AI Agent,开始频繁选错工具,甚至完全无视技能库去瞎发挥。排查下来问题不在模型能力,也不在技能本身,而是技能库的设计没有跟上规模变化。这篇是 Agent Skills 系列的第 5 篇,我把这轮折腾的重点放在最核心的问题上:在几十上百个 Skill 的技能库中,怎样稳定找到正确的技能,又怎样把多个技能组合成一条完整链路。适合正在做 agent 开发、准备扩张技能库的团队,也适合独立开发者想绕开我踩过的这些坑来参考。

1. 换个思路:技能库本质上是一个路由系统

1.1 技能库不是“函数列表”,而是“可检索的索引”

很多同学把技能库理解成“把一堆工具函数集中登记,然后让 Agent 调用”,这个想法在小规模下没问题。当你只有 5 个技能的时候,直接把所有函数的 description 塞给模型就好了,上下文不会太长,模型也能轻松选择。可一旦技能数量超过三五十个,整套逻辑就会悄然转变:

  • 全量注入会让 prompt 变得极长,推理变慢、成本上升;
  • 长上下文中混入大量不相关工具,会对模型形成干扰,降低工具选择准确率;
  • 技能之间存在相似描述时,模型容易混淆,而这个问题靠肉眼维护几乎无法避免。

所以我把技能库拆成三个层次来理解:技能存储层、路由层、执行层。存储层只关心技能元数据怎么组织;路由层负责根据用户意图,从技能库里筛出最可能的几个技能;执行层才真正去跑被选中的技能。这里面最容易做扎实、也最容易被忽视的是路由层。

生活化地打个比方:技能库就像一家大型图书馆,书架上的书是各个 Skill,路由层则是目录检索系统。你不需要把整座图书馆的书都搬到书桌上,只需要根据查书请求,快速锁定几个目标书架的几本书,拿到手之后再翻内容。全量注入相当于把图书馆的书全部堆在读者面前,肯定能翻到,但代价太大,而且很容易看花眼。

1.2 从“全量注入”演进到“按需召回”

主流 Agent 框架早期都是走“全量工具列表注入”的路线。比如你给模型提供的 tools 数组里有 50 个函数,它会在思考阶段一次性看到全部函数签名和描述。这在工具数量少时非常稳定,但到了 100 个以上,模型计算量会明显激增,并且选择出错的比例上升。

我们后来改成了“按需召回 + 二次路由”的两段式结构:

  1. 第一步:把用户当前请求做向量化,在技能库索引里做相似度检索,召回 Top K(比如 5~10 个)最相关的 Skill;
  2. 第二步:把召回的 Skill 的详细描述(含参数 Schema)注入给 Agent,让模型从候选集中选出真正要用的 1~2 个。

这种设计的核心收益是,模型在决策时只面对一小撮精准的选项,而不是一个巨大的平铺清单。召回层负责“广撒网但不打扰”,路由层负责“精准定位”。从实测来看,按需召回能将工具选择准确率提升不少,特别是在技能库超过 100 个场景下,效果差距非常明显。

2. 决定技能能否被正确调用的四个核心细节

2.1 技能描述,既是说明书,也是检索锚点

先说结论:技能描述是路由系统的第一公民。它既影响向量召回效果,又影响模型二次决策时的理解力。很多新手写技能描述时喜欢写“这是一个用来抓取新闻的技能”“这个技能帮你分析数据”,这种写法等于没写。

我建议技能描述里至少包含四块信息:

  • 触发场景:什么类型的用户请求应该走向这个 Skill,什么时候不应该走向这个 Skill。
  • 输入要求:需要哪些关键字段,各字段的含义和格式;
  • 输出内容:技能会返回什么,是否执行副作用;
  • 典型示例:一句话示例,帮助模型建立直觉。

写一个对比。同样的“每日新闻汇总”技能:

写法一(不推荐): 抓取互联网新闻并汇总。 写法二(推荐): 根据用户指定的主题或关键词,抓取近24小时内国内外主流新闻源的文章,返回结构化列表:发布时间、来源、标题、摘要、原文链接。输入字段:topic(主题词,必填)、time_range(默认24h)、sources(可选,默认全部)。当用户说“今天有什么科技大新闻”“汇总关于新能源的最新消息”时,应该优先选择本技能。

第二种写法马上让模型知道“什么时候选它”。同时,这段文本也可以直接用作向量召回时的技能锚点。如果技能索引需要做成 embedding,我会用“触发场景 + 典型示例”区块来生成向量,而不是把整个函数体塞进去。

值得提一句,现在不少新框架开始把技能描述做成独立的 Markdown 文件,比如 Claude Skills 用 SKILL.md,里面会专门留出 YAML frontmatter 位用来写 name、description。这套思路和上面说的是同一件事:把“给模型看的说明”和“给代码看的实现”分开,描述越规范,路由越准。

2.2 参数模式:收敛自由度,避免“抓瞎”

技能参数描述是路由选中技能后的下一道关口。很多项目在技能库扩容后出现的奇怪 bug,不是技能选错,而是参数给了错误的值。问题根源通常在于JSON Schema 写得过松。

一个真实的案例:有个技能用来查询内部系统用户信息,参数叫user,类型只写了 string。结果 Agent 有时传入邮箱,有时传入工号,有时传一个 display name,后端逻辑反复判断还经常出错。后来我们把参数拆成三个字段:

{ "user_id": {"type": "string", "description": "员工ID,仅允许数字"}, "email": {"type": "string", "description": "员工邮箱,仅允许带@的格式"}, "name": {"type": "string", "description": "员工展示名,模糊匹配使用"} }

模型看到清晰的字段约束,就会先尝试从用户原句里提取匹配的字段;提取不到的字段留空,而不是强行塞数据。实际改完以后,参数错误率明显下降。

除了字段拆分,我还会在 Schema 里加两层约束:一是enum枚举,能枚举就枚举;二是default默认值,尽量给一个保守兜底值。比如“新闻源选择”技能,sources参数允许 “tech / business / world” 三个枚举值,超出范围的就走默认源,而不是报错。这样即便模型理解偏差,结果也在可控范围内。

2.3 命名与标签:组合技能时的隐形黏合剂

技能库规模一大,命名风格不一致会带来连锁问题。比如fetch_stock_price、query_stock_price、get_market_data这三个名字描述的是同一类能力,但在向量索引和筛选逻辑里会产生大量重复干扰。组合技能的时候更是灾难:技能 A 的输出字段叫price,技能 B 的输入字段叫current_price,脚本里不得不写一堆字段映射。

所以技能库建立初期就要定好三套规范:

  • 名称规范:统一动词开头,一个技能只做一件事,动词 + 对象是最稳妥的组合,比如fetch_stock_price、send_email_notification;
  • 标签体系:每个技能打上领域标签,比如domain:finance、domain:news,方便按领域批量过滤;
  • 输入输出字段规范:同类实体在不同技能中使用统一命名,比如时间统一start_time/end_time,用户统一user_id。

做组合时,技能之间靠的不是模型魔法,而是可预期的字段接口。标准化的接口让你能把多个技能像积木一样拼起来,否则每加一个技能组合就要重写适配逻辑。

2.4 路由阈值与兜底策略

按需召回不是“召回了就一定要用”。很多框架在实现时会设置一个相关度阈值,只有相似度分数超过阈值的技能才会进入候选列表。阈值设太低,会召回一堆无关技能干扰模型;设太高,在遇到长尾请求时又容易召回为空。

我的做法是分两层处理:

  • 召回层阈值相对宽松,保证 Top 10 里有一定冗余,宁可多召回也不要漏掉;
  • 路由层让 Agent 自动判断,并且在系统提示里加一句:“如果以下候选技能没有一个真正契合用户请求,请直接回复无法处理或提出澄清问题,不要强行调用。”

这条兜底规则很重要。它直接避免了一个典型 bug:模型面对若干候选技能时,倾向于“选一个看起来最像的”硬调用,结果产生一个错误的副作用。加了兜底规则以后,Agent 会大大减少幻觉式调用。

3. 实操过程:从零搭一条“查找 + 组合”的最小链路

3.1 最小技能库的数据结构

这里不依赖特定框架,我采用一套不绑定平台的通用设计。技能库根目录下用标准结构与统一 Schema:

skills/ fetch_git_commits/ skill.yaml executor.py classify_commits/ skill.yaml executor.py generate_report/ skill.yaml executor.py

每个skill.yaml结构如下:

name: fetch_git_commits description: | 根据指定的 Git 仓库地址和时间范围,拉取对应的提交记录。 输入:repo_url(仓库地址,必填)、start_time(起始时间)、end_time(结束时间,默认当前时间)。 输出:提交记录列表,每条记录包含 commit_id、author、message、timestamp。 当用户提到代码提交、git 记录、仓库历史时优先使用。 domain: git tags: [repository, commit, log] input_schema: repo_url: {type: string, required: true} start_time: {type: string, default: "1970-01-01"} end_time: {type: string, default: "now"} output_schema: commits: {type: array, items: {commit_id: string, author: string, message: string, timestamp: string}}

这一段 YAML 足够稳定。索引阶段,我会把description、domain、tags拼起来做 embedding,存入向量库;路由阶段,把用户请求向量和这些技能向量做相似度计算,取 Top K。

3.2 路由实现:向量召回 + LLM 精排

用一段伪代码表示核心链路:

def route_skill(user_request, top_k=10): # 1. 召回 query_vec = embed(user_request) candidates = skill_index.search(query_vec, top_k) # 2. 过滤:低于阈值的不进入候选 candidates = [s for s in candidates if s.score > 0.3] # 3. 精排:把候选技能的完整描述注入 LLM 决策 prompt = build_routing_prompt(user_request, candidates) selected = llm_select(prompt) return selected def execute_skill_chain(selected_skills, user_context): result = {} for skill in chain_order(selected_skills): result = skill.run(user_context, previous_result=result) return result

精排时的 prompt 这样组织:

用户请求:{user_request} 以下是候选技能列表: {编号. 技能名,描述,输入参数说明} 请从候选技能中选择需要的技能。可以选多个,也可以不选。 如果必须顺序执行,请按依赖关系排列。

关键点是:不要跳过向量召回直接把所有技能描述丢给 LLM 做选择。如果你这样做了,等于退回全量注入模式,技能库小时没事,规模大了迟早出事。

3.3 一个完整案例:自动生成研发周报

这里我用一个综合案例把“查找并组合正确 Skill”完整串起来。假设需求是:根据最近的 Git 提交记录,自动生成一份研发周报。

拆解下来,这个需求由三个 Skill 组合完成:

fetch_git_commits —— 拉取仓库提交记录 classify_commits —— 按类型把提交分组(feature/bugfix/docs/refactor) generate_report —— 根据分组结果生成周报文本

路由层会先判定用户请求存在三个独立子任务,然后按依赖顺序组合:

  1. fetch_git_commits先执行,拿到原始提交列表;
  2. classify_commits依赖第一步输出做分类;
  3. generate_report拿到分类结果,渲染成周报。

这个链路并不是把三个技能的输出简单拼接,而是有明确的数据依赖关系。我在实践里强烈建议把技能组合的状态流转画成 DAG(不需要多复杂的图,重点确认节点依赖),每条边就是上游输出到下游输入的字段映射。这样组合逻辑清晰,也能在中间环节插入人工确认或者容错重试。

组合后的系统提示,我通常会在里面加一段:

你在执行一个多技能链路:fetch_git_commits -> classify_commits -> generate_report。 请先完成第一步,确认拿到结果后,再继续执行第二步。 如果任何一步失败,不要继续执行后续步骤,直接报告失败原因。

这一段看似简单,但能挡住大量“上游失败了,下游还硬执行”的连锁故障。

4. 常见问题与排查技巧实录

4.1 技能库明明有技能,Agent 却总是选不到

每次排查我都不直接怀疑索引,而是先检查“描述是否被正确写入索引”。遇到过最典型的情况:skilled.yaml 里的description字段写得很完整,但 embedding 脚本只索引了name或tags,导致向量里根本没有描述信息,召回效果自然很差。

另外注意冷启动问题:新技能刚上线时没有历史调用记录,向量召回概率不一定高。我会在技能库维护脚本里设置“新手保护期”,新技能在索引时临时加一个分数加权,确保上线一两周内能被看见、被试探性地使用。

4.2 相似技能太多,模型强行选错

一旦技能库里同时存在“查询天气”“查询空气质量”“查询未来天气预报”这类描述高度相似的技能,模型在没有足够区分信息时很容易蒙。我处理这类问题的方法有两个:

  • 在互相容易混淆的技能描述里,互相写入“排除条件”,比如天气技能写“不处理空气质量指数,空气质量请用 query_air_quality”;
  • 用字段约束制造区分度,不同技能面对同一实体时用不同字段,模型更容易通过字段推断差异。

这个坑在技能超过 60 个之后特别容易出现,所以从 20 个技能阶段就要开始注意描述层面的“互斥性”。后期再做一轮描述去重,能显著改善选择准确率。

4.3 多个技能并行执行时的超时与限流

技能库组合之后,经常出现多个技能同时发起外部请求。一次联调中,我发现某个技能链会让 Agent 同时调用 8 个 HTTP 请求,结果被对方接口限流。后来我在组合层做了三件事:

  • 将必须有先后依赖的步骤串行,独立无依赖步骤才并行;
  • 给每个技能的执行设定超时时间,超时后返回局部结果并标记部分失败;
  • 给每个技能引用加上全局并发配额,比如同一个 API 域名最多并发 2 个请求。

这样设置以后,技能链路的稳定性提升非常明显,特别是在真实业务里接入内部服务和外部 API 混合场景时。

4.4 技能权限与沙盒隔离

技能库一旦被多个 Agent 共享,权限问题就要提前设计。之前遇到过某个技能意外调用了内部管理系统接口,被安全扫描拦下来的案例。我的建议是:

  • 每个技能声明自己需要的权限级别:只读、内部读、写操作、外呼 API;
  • 执行环境按权限级别做沙盒隔离,高危操作前强制人工确认;
  • 路由层在调度技能之前,先做权限校验,没权限的技能直接不进入候选列表。

这样做既减少安全隐患,也避免模型被引导去调用不该碰的能力。

4.5 可观测性:技能命中率不靠猜

技能库上线以后一定要留观测日志。我在项目里会记录每个请求的四个关键指标:

指标名称说明
技能召回列表每个请求召回了哪些技能,分数多少
最终选中技能LLM 精排后选中了什么
执行结果状态成功、部分成功、失败、超时
全链路耗时从请求到最终响应的总耗时

有了这些数据,就可以定期看技能的“命中率”和“错选率”。哪个技能长时间没有被召回,哪个技能经常被选错,都可以从日志里量化出来。实测下来,这些日志比任何调参都更能指导技能库优化方向。

最后

做技能库优化并不是一次性工程,而是持续迭代的过程。我个人经验是先用 10~20 个技能跑通整套索引、召回、精排、执行、观测的链路,把技术栈稳定下来,再逐步扩充技能数量。技能库真正成熟的标志,不是技能数量多,而是每个技能都能被稳定、准确地调度和组合。

最后分享一个小习惯:我每隔两天会拉一次日志,看看哪些技能从头到尾没被调用过。如果一个技能连续七天都无人使用,大概率是它描述有问题,或者本身就没有存在价值。删除或合并它,比继续堆新技能更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询