当一个 Agent 同时挂载数十个 Skill,主流做法是把 Skill 描述成可调用的工具目录,再以分级路由 + 检索召回 + 工具选择三段式策略让模型先缩范围、再做最终挑选;只要 Skill 描述按契约写清「做什么、不做什么、参数边界」,即便超过 50 个工具,准确率也能稳住在生产可用区间。
一、Skill 数量超过 20 个就开始掉准度
把 Skill 直接全量塞进模型上下文,命中率会随数量级递减。多个一线团队在生产中观察到,单步工具目录超过 20–30 后,模型选择错误率明显抬头;越过 50 个量级,工具选错的比例会迅速放大,原因是大模型对长 prompt 中的工具描述记忆与对比能力有限,远短于它对短而聚焦的列表的判别能力。
实际落地时,会把 Skill 池切成两层:先用一个轻量召回层把候选压到 5–10 个,再让模型在压缩后的目录里做最终决定。召回层的输入一般是「当前目标 + 当前状态 + Skill 元数据」三元组,输出是 Top-K 个候选 Skill 列表,模型只看压缩后的目录,等同于把”目录噪音”前置过滤掉。
二、Skill 描述必须写成”契约”而不是注释
工具描述是模型选择 Skill 的唯一依据。描述含糊(”做邮件相关的事”)会让模型把不同意图都映射到同一 Skill,描述过度(”在 A 且 B 且 C 时用”)又会让模型对边缘 case 失忆。业内共识是把描述写成”何时用 + 何时不用 + 参数边界”三段式。
| 描述要素 | 作用 | 写法要点 |
|---|---|---|
| 何时用 | 给出正面触发条件 | 用一句话点出典型场景和输入形态 |
| 何时不用 | 划清与邻近 Skill 的边界 | 明确”不适用于 XX”以减少误选 |
| 参数说明 | 让模型填对入参 | 每个字段标注类型、必填、单位、示例 |
| 错误码与返回 | 便于模型回读 | 列出主要返回结构与失败模式 |
对比一个”含糊”与”清晰”描述的效果:把 search 写成「搜索互联网内容」会让模型把任何信息查询都路由过去;改成「search(query: string, recencydays?: number): 在公网抓取网页与摘要,recencydays 未指定则取近 30 天结果,不适用于已知 ID 查单条记录」后,与 get_record_by_id 的误判明显下降。
三、分层路由:三段式召回 + 二次选择
把整套路由拆成三段,可在不增加模型成本的前提下把准确率拉回生产区间。
- 意图分类(轻量模型或规则):先用一个小分类器或规则判定当前请求属于”读 / 写 / 搜索 / 通讯 / 编码”哪个大区,缩到对应分组;
- 向量召回(Top-K):在分组内对 Skill 描述做 embedding 相似度检索,取 Top-K(一般 5–10),把候选目录推给主模型;
- 最终选择(主模型 Function Calling):主模型在压缩后的目录里按描述做最终决定,并按 schema 填参。
# 三段式路由:意图分组 + 向量召回 + 模型选择
from typing import List, Dict
def route_skill(user_goal: str, skills: List[Dict], embedder, k: int = 8) -> List[Dict]:
# 1) 意图分组(用关键词或小模型把 skill 池切成 5–8 组)
groups = group_by_intent(skills) # {"search": [...], "db": [...], ...}
intent = classify_intent(user_goal, groups.keys())
# 2) 组内向量召回,取 Top-K
goal_vec = embedder.encode(user_goal)
cands = sorted(groups[intent], key=lambda s: cosine(goal_vec, s["vec"]), reverse=True)[:k]
# 3) 仅把候选目录交给主模型做最终选择
return cands
召回层用便宜的 embedding 模型即可,候选压到 8 个后,主模型一次推理就能稳定选对。Anthropic 工程团队在公开经验里也强调,目录规模是 Agent 可靠性研究中权重排在前面的单一变量,超过阈值就要前置过滤,不能”硬塞”。
四、用工具描述的四个必填字段
工程上要把每个 Skill 注册成可被 Function Calling 消费的 schema,必备四件套:
| 字段 | 含义 | 选型要点 |
|---|---|---|
| name | 工具唯一名 | 命名贴近用途,避免 tool1、tool2 这类无意义名 |
| description | 工具的契约说明 | 三段式:何时用、何时不用、参数边界 |
| parameters | 入参 schema | JSON Schema 描述,每个字段带类型、必填、说明 |
| returns | 返回结构 | 列出主要字段、可能为空、错误码语义 |
name 是模型推断用途的第一信号,起名时尽量带意图词(如 get_weather 而非 tool_1);description 是消耗最多工程精力的字段,对选择准确率影响最大;parameters 用 JSON Schema 描述能让模型按结构填参,避免手误;returns 提前声明返回结构,方便模型下一步解析与串联。
五、并行调用与错误回退
当一个任务需要多个独立信息(比如同时查天气与股价),模型可一次推理发出多路调用,并发执行后按调用 id 归并结果,能把端到端延迟压到一轮。但并发不是越多越好,常见的反模式是”为并行而并行”,把本来有依赖的两次调用并发出去,导致第二次拿不到第一次的结果。
错误处理采用三层策略:临时错误(5xx、超时、限流)走指数退避重试;schema 不匹配或工具选错走”重规划”——把失败原因作为观察塞回上下文,让模型另选;多次重试仍无进展则升级人工或停止循环。三层策略之间的阈值要根据业务敏感度调整:写操作的重试上限要严于读操作。
六、易踩的两个坑
第一是描述”伪精确”。写「send_email 用于发送邮件」几乎等价于没写,模型无法靠它区分”发送通知邮件”与”发送营销邮件”两个相邻 Skill。补救方式是把场景词写进描述(「send_email 用于向单收件人发送事务性通知,支持主题与纯文本正文,不用于群发或带附件」)。
第二是目录合并后丢失分组信息。召回层把候选压到 8 个,但若 8 个全部来自同一组,模型反而缺了”应该选另一组”的能力。召回阶段要保留分组多样性约束(每组至少 1 个候选),让最终选择阶段还能在组间对比,而不是只在同组内二选一。
到这里,”在 Skill 数量大时让模型选得准”的链路就完整了:把 Skill 当成可调用工具、把描述写成契约、用召回 + 二次选择替代全量塞入、最后用并行与重规划托底稳定性。目录规模与描述质量决定了天花板,分层路由决定了能否稳定达到这个天花板。
常见问题(FAQ)
Q1:Skill 数量到 30 个就必须分层吗?
不一定。若每个 Skill 描述粒度细、相互之间无重叠,30 个以内仍可全量塞入;超过 50 个建议强制分层。
Q2:向量召回的 K 怎么定?
通常 5–10。K 越大选择越准但 token 开销上升,K 越小省 token 但误选率升,需结合业务测一组 A/B。
Q3:Function Calling 与 Tools 协议是否通用?
主流平台(OpenAI、Anthropic、Google、主流开源推理框架)都遵循同一类 schema 协议,但字段命名与并行语义略有差异,跨平台切换时要重写适配层。