资讯动态

Agent多技能分层加载:从全量塞入到按需召回,准确率提升至94%

发布时间:2026/10/13 14:46:33 来源:尧图企业网站定制
1. Skill 一多就失控我的工具清单把 Agent 给「喂废」了最开始的 Agent 原型很简单我给它挂了十来个 Skill也就是让 Agent 能调用外部工具的技能定义。把每个 Skill 的名字、描述、参数规则全部塞进系统提示词里效果相当不错Agent 能准确判断什么时候该调工具、该调哪个工具。做到后面业务场景越铺越开Skill 数量很快涨到了八十多个我还在继续往里加。结果某天测试时发现了一个很反直觉的现象Agent 的决策准确率没有随技能变多而提高反而直线下降。原来是上下文被几十段技能描述占得满满当当真正留给任务上下文、历史对话和推理指令的空间越来越少Agent 开始频繁选错工具。有一回用户让它「查一下上个月的报表数据」它偏偏去调了「发送周报邮件」的技能因为两者描述里都有「report」这个词相关度打分完全被冲昏了。这个现象让我意识到多 Skill 场景下真正的问题不是「怎么把一百个技能都接进来」而是「Agent 怎么知道在正确的时刻只想起正确的那个技能」。我后来花了差不多三周时间把整套分发流程从「全量暴露」改成了三层结构也就是标题里说的分层加载机制。这篇博文就是把这次重构的思路、代码和踩坑过程完整拆一遍。内容适合两类人一类是正在做 AI Agent 应用、技能数量在二三十个以上开始觉得提示词塞不下的开发者另一类是想设计多工具调度的产品同学。纯提醒一句如果你现在只有几个 Skill这套机制属于过度设计先不用上。2. 问题的本质为什么 Skill 超过一定数量全量加载必然崩盘2.1 三个维度同时达到瓶颈先说结论Skill 多了导致 Agent「变笨」不是某一个环节出问题而是三个维度同时碰壁第一上下文窗口的资源挤占。系统提示词里技能描述越长留给对话历史和用户任务的 token 就越少。当时我统计了一下一个 Skill 平均描述大约 200 到 300 个 token一百个就是两三万 token很多模型的上下文窗口直接被吃掉四分之一以上。这显然不可持续。第二选择准确率下降。站在模型的角度想它在一次推理中需要从一百个候选里挑出一个正确技能。候选越多名称相似、描述重叠、边界模糊的干扰项就越多模型出现幻觉和混淆的概率成倍上涨。我实测下来技能数从 20 涨到 100工具选择的准确率从 94% 掉到了 78%。第三维护成本的失控。新增一个技能经常要全局检查一遍所有已有技能的描述防止新描述和旧描述在语义上打架。更烦的是改动一个技能的入参格式要确保所有相关描述同步更新漏一处就可能在线上复现奇怪的调用错误。这三个瓶颈叠加就不是「忍一忍优化下提示词」能解决的问题了。2.2 一句话总结核心矛盾其实把问题抽象一下就很清晰Agent 的技能使用链路分两步——第一步是「选哪个技能」第二步是「怎么调用它」。全量加载的做法是让 Agent 一次性看到所有技能的全部信息相当于把一本厚手册整本塞给模型让它读完全场再答题。分层加载的思路反过来了第一步只给模型看一个很薄的「目录」——每个技能的名称、一句话描述和标签模型先做粗筛等选中某个方向后第二步再动态加载这个技能的完整参数定义。这个思路跟人做决策的模式很像你不会每次干活前把整本操作手册背一遍而是先想起「我有个工具能处理这个事」走到它跟前再打开说明书看详细步骤。于是整个机制拆成了三个层级索引层负责维护技能目录和元数据召回层负责根据任务内容选出候选技能注入层负责把选中的完整技能定义在合适的时机放回上下文。3. 索引层的搭建给每个 Skill 建一张「身份证信息卡」3.1 技能元数据 Schema 的设计要做分层加载第一步不是写代码而是重新定义技能的描述方式。以前我写技能描述是很随意的自然语言一大段想到什么写什么。现在每个 Skill 必须附带一份结构化的元数据信息卡我把它接到一个注册中心里统一管理这个中心维护了统一的 JSON Schema内容包含核心字段name技能唯一标识机器使用。display_name展示名给用户看或给模型做粗理解。description一句话说清「这个技能是干什么的」要求控制在 40 个汉字以内。keywords触发该技能的典型同义词和近义短语这是关键词召回层的核心依赖。tags业务域标签比如「数据分析」「日程管理」「内容生成」用于快速过滤。input_schema完整参数定义通常用 JSON Schema 格式表达。这一部分是加载量最重的也是延迟到最后一层才注入的内容。trigger_rules使用边界说明技能在什么情况下适用、什么情况下不适用。下面是当时用的 Schema 配置实例你可以直接参考改{ name: scheduler.get_calendar_events, display_name: 获取日历日程, description: 查询用户的日程安排支持按日期范围和日历账号筛选, keywords: [日程, 日程安排, 我有什么安排, 哪几天有时间, calendar, events, meetings], tags: [scheduler, productivity], input_schema: { type: object, properties: { start_date: { type: string, description: 查询起始日期格式 YYYY-MM-DD }, end_date: { type: string, description: 查询结束日期格式 YYYY-MM-DD }, calendar_ids: { type: array, items: { type: string } } }, required: [start_date] }, trigger_rules: 仅当用户需要查看既有日程或确认忙闲时间时使用不适合创建、修改或删除日程那属于另一个技能 }3.2 为什么 description 必须压到一句话我踩过的坑是早期我把 description 写得很详细企图靠「详细说明」提升模型理解结果恰恰适得其反。详细描述里包含的大量细节和边界说明在两个技能语义交叠时常常变成无害但干扰巨大的噪声。把 description 压到一句话之后我做了一轮所有技能的改写纠偏效果提升非常明显模型在粗筛阶段的误判率降低了约三成。原因很简单召回层要的是「快而糙」不是「细而慢」。一句话能让模型快速分辨这个技能是否和当前任务相关完整参数交给最后一步处理分工才清晰。另外每个技能必须配置 trigger_rules这一点容易被忽略。它相当于给技能划了一道「红线」——这条规则不是用来告诉模型什么时候该用而是告诉模型什么时候不该用。很多技能表面相似实际边界完全不同比如「发送邮件」和「发送日程邀请」都和「发送」相关但适用场景差别很大如果不写明边界模型很容易跨线误调。4. 召回层的实现从百级技能到 Top 5 候选的三种策略索引层搭好之后真正费功夫的是召回层。目标很明确给定用户的当前任务从上百个技能里返回 5 到 10 个候选供最终决策。这里我试过多种召回策略实测下来单独用哪一种都有硬伤最后做的是加权融合。4.1 策略一关键词冷启动最朴素的召回方式就是关键词匹配。利用元数据里的 keywords 字段对用户消息做分词处理取交集并打分得分高的技能优先入选。关键词匹配的好处是稳定、快、不出格但它有两个天然缺陷第一用户的话经常口语化问题里可能完全没有关键词的形态第二跨语言场景下英文关键词和中文表述全靠同义词典覆盖覆盖不全。所以它适合做冷启动和兜底不适合做主力。4.2 策略二语义向量召回这个思路是把所有技能的 description、keywords、tags 拼接后做向量化同时把当前用户请求做向量化算余弦相似度取 Top N。当时我用了开源的向量化模型对文本做 embedding索引存在支持向量检索的数据库里。比较下来语义召回能把「帮我看看我下周三下午有没有课」这种没有直接说「日程」的请求正确召回日程类的技能这是关键词匹配做不到的。但它也有两个问题一是向量相似度不等于任务意图匹配经常把概念相关的技能召回进来二是模型的 embedding 会有「近义词团簇」效应比如「发邮件」「发消息」「发通知」三个描述可能被映射到接近的位置导致候选列表里清一色全是某个类别其他类别的正确技能被挤掉。这个坑我在后面有详细展开。4.3 策略三规则与状态过滤第三种策略来自工程经验很多技能能不能用不是语义决定的而是状态决定的。比如某个技能只对管理员开放某个技能依赖前一个步骤产生的数据。这些规则必须写在召回逻辑里。我在元数据里加了一个visibility字段和requires_context字段召回时先做硬性过滤。凡是当前用户角色无权限的技能、当前会话上下文不满足前置条件的技能一律不进候选集。这个策略一句话讲就是先把不可能是答案的删掉剩下的再排序效果立竿见影还能直接少干掉一批误召回。4.4 融合打分权重不要拍脑袋定三种策略各自返回了候选集合最终需要一个融合排序逻辑把它们合成一个列表。我最初的方案是给三路结果各设一个权重然后加权求和取总分。跑了一轮测试后发现权重不能拍脑袋写死正确做法是做一个标注集用真实任务去校准权重。我当时的标注集规模不大大约几百条真实用户请求每条标注了正确技能。在这些标注集上分别统计三种策略各自的召回命中率用这个数据反过来确定初始权重。最终收敛下来的权重比例大体是语义召回 0.45关键词召回 0.3规则过滤结果加权 0.25。这个比例仅供参考不同场景差异很大但这个流程值得照做。融合后的候选集我一般保留 Top 7 到 Top 10。太多会让模型重新陷入「选择困难」太少又可能因为召回偏差把正确答案筛掉。这里有一个保守经验宁可多召回一个不相关的也不要漏掉一个正确的因为最终决定权还在模型手里漏了才真的是彻底错误。5. 注入层的精细控制什么时候把完整技能定义放进上下文召回层选出了候选注入层的工作是决定「到底把哪些技能的完整定义塞进上下文、什么时候塞」。完整定义包含 input_schema 和 trigger_rules这部分 token 最重是分层加载机制能不能省 token 的关键。5.1 动态构建系统提示词具体做法是Agent 接受到用户请求后先完成召回然后把召回结果的完整技能定义拼进系统提示词。注意这里拼的不是全部技能的定义而只是被选中的那七到十个。上下文占用从全量的一两万 token 直接降到两三千效果立竿见影。拼接格式上有一个细节值得注意被召回的技能不要全部都给出完整定义而是分成两组。第一组是「高置信命中」给完整定义第二组是「低置信补充」只给名称和一句话描述并告诉模型这些技能存在但信息不完整如果需要再追问获取详细参数。这样既保证了主要候选能被正确调用又不会让补充候选过度分走模型的注意力。5.2 兜底机制候选列表之外还有一个「全局技能思维」分层加载机制引入后有一个天然风险技能不在候选列表里Agent 就完全「看不到」它即使它实际上是正确答案模型也不会用。这是分层加载对比全量加载最重要、也最容易被忽略的一个缺点。我的对策分两层第一层是系统提示词里放一段固定文本「技能列表仅为可用技能的局部视图若用户需求无法由列表中的技能满足请明确告知用户能力边界不要强行调用列表内技能。」第二层是提供一个skill_search工具本身。模型在发现候选集不匹配需求时可以反过来调用这个工具去搜更多技能。这就形成了一种「热替换」能力——候选集不是一成不变的模型可以自主扩展搜索范围。这两层兜底做完我才敢放心地把全量技能从上下文中摘掉。5.3 常用技能的运行时缓存实际跑起来之后还会遇到一个效率问题某些技能几乎每条请求都会被召回比如「获取当前时间」「查询天气」每次都把完整定义重新拼一遍不仅浪费 token还会让重复信息占据上下文位置。解决办法是做一个轻量热度缓存——统计每个技能的召回命中频率高频技能直接常驻在上下文中作为「热技能」低频技能走按需加载「温技能」则用一个精简版长期驻留。这个思路类似操作系统的内存分层效果也很直观上线后高峰期单次请求的 token 消耗又少了一截。这里值得强调的一点是常驻热技能的 token 成本是固定的所以每次新增热技能前要先评估它是否为真的高频否则常驻列表膨胀又会回到老路上去。6. 实测环节从选择准确率 71% 到 94%我踩过的三个大坑6.1 实测数据与指标对比在某个模拟项目 X 上我做了几次完整的版本对比。用的评测集是大约两百条标注好的真实任务请求核心指标包括「工具选择准确率」「用户任务完成率」「平均单次调用 token 消耗」。版本工具选择准确率任务完成率单次平均 token 消耗全量加载80 技能全塞上下文78%82%约 1.8 万分层加载初版仅关键词召回84%85%约 0.6 万分层加载融合召回 热缓存最终版94%93%约 0.4 万从数据能明显看出来分层加载在准确率和成本两个维度上都全胜。不过代价也很现实系统复杂度上来了从一段提示词变成一个可观测的三层系统工程投入和维护成本都不低这也是为什么技能数量不上来完全没必要做这套。6.2 踩坑一description 写太长语义召回集体跑偏初版元数据里很多技能 description 写了三行甚至五行还带参数说明。实测效果差到什么程度呢「获取用户日程」和「添加日程事件」两个技能在语义空间距离极近经常互相抢占候选名额导致模型选到错误动作。后来我把所有技能 description 统一压缩成一句话再去校准召回权重这一项的准确率提升最明显。6.3 踩坑二多头向量检索把同类技能一网打尽这里必须单独讲一下「近义词团簇」问题。某个版本的召回结果里用户明明问的是「帮我订个餐厅」候选列表里却全是和「预订」沾边的技能——订酒店的、订机票的、订会议室的全被召回进来了真正需要的「餐厅预订」反而排到了第七名开外。排查后发现是 embedding 模型把所有「订 XX」的语义聚得很近导致描述只要带「预订」「booking」就会被集体召回。解决办法是在融合打分里引入一个「类别多样性惩罚项」同类标签的技能最多进两个超过两个自动降权。这样候选列表的覆盖度明显改善。6.4 踩坑三动态加载让 Agent「忘了自己会什么」分层加载还有一个隐蔽问题技能定义不在上下文里时模型不会主动想起来调它。有好几次用户问的能力明显在技能库里但因为候选集没召回Agent 一本正经地回答「抱歉我暂时没有这个能力」。这个坑靠技术修复只能解决一半另一半要靠产品逻辑。我除了上文说的skill_search兜底工具还在提示词里加了一条规则「如果用户请求看起来像是你有可能处理的真实任务不要急着说自己做不到先确认是否已检查全部可用技能。」这句话的效果比想象中好它把 Agent 的行为从「基于已有信息回答」改成了「先确认搜索边界再作答」。6.5 追加的技能边界标注画出每个技能的「不该做什么」最后再补一个很有效的细节。我在 trigger_rules 里明确写「什么情况下不能使用本技能」一开始纯粹是为了防误用后来发现它对召回层的帮助更大。规则有了这些负向约束融合打分时可以针对明显矛盾的情况做降权处理。例如用户说「帮我把日程删掉」技能「获取日历日程」的 trigger_rules 里写明这个动作属于另一个技能打分时就能避免这类错误候选占用名额。这套经验做完之后我又把它复用到另一个对话机器人的意图识别模块上逻辑完全搬得过去。多技能分层加载说白了就是一种「先粗筛、再精排、最后按需加载」的系统设计思想并不绑定具体模型或平台做 Agent 工程化的人都值得了解一下。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑