ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent技能系统构建实战:从定义到路由与调优

AI Agent技能系统构建实战:从定义到路由与调优 如果你的AI Agent还停留在能聊天但不会干活的阶段那问题大概率不在模型而在没有给模型配置一套真正可用的技能系统。我最早接触agent-skills这个概念的时候以为它就是给Agent塞一堆API工具定义跑通之后就完事了。实际做下来才发现技能系统远比想象中复杂它决定了一个Agent是看起来聪明还是真的有用。这篇文章我打算把从零搭建一套Agent技能系统的过程拆开讲。从技能应该怎么定义、参数怎么设计到技能库怎么组织、怎么挂载到Agent上再到实测阶段会遇到哪些坑和优化方向全部基于我自己的实操经验不是教科书式的概念复述。适合正在做AI Agent应用的开发者阅读也适合想理解Agent能力边界到底由什么决定的产品和算法同学参考。1. 为什么Agent的能力首先体现在技能上1.1 我为什么开始拆解技能这件事有一次我让Agent帮我做一份竞品分析报告结果它输出了一个目录结构的markdown文件前三章写得工工整整后面全是待补充三个字。我一度以为是模型推理能力到了瓶颈后来把链路拆开才发现根因是我根本没有告诉它如何调用外部搜索、如何筛选可信信息源、如何把结构化数据转换成报告章节。换句话说模型有分析能力但它没有调用外部能力的技能。agent-skills这类项目的核心思路就是把模型本身的能力和模型可以调用的外部能力做分层。模型负责语义理解、规划、生成技能库负责提供可执行的操作单元。这个分层看起来简单但落地时涉及大量细节技能怎么命名才能被模型理解、参数怎么设计才能减少模型的自由发挥空间、技能执行结果怎么反馈给主流程每一层都可能出问题。我后来的做法是把Agent看作一个拥有工具箱的工人模型是工人工具箱里每一件工具就是一项技能。工人不可能记住所有工具的说明书所以工具箱里的每一项技能都需要自带清晰的使用说明。1.2 技能框架是什么样的一组边界我实践的agent-skills本质是一个技能框架层它不直接提供某个具体业务功能而是定义了技能应该长什么样、如何注册到系统、如何被模型发现并调用。整体从上到下可以拆成三层技能目录层负责技能的注册、存储和索引每个技能在这里有唯一的ID和元信息。技能执行层负责把模型发来的调用请求翻译成具体的函数调用、API请求或者脚本执行。技能描述层负责生成给模型看的技能说明文本也就是系统提示词里的工具调用说明。这个分层思路最重要的好处是把技能的展示和技能的执行解耦。比如同一个技能在浅层对话场景只需要简短描述在深度任务场景需要带详细参数约束的完整说明技能执行层不用改动只需要切换描述模板即可。我之前犯的一个错误是反着来的直接把工具函数塞给模型每个函数带上三四种调用时序结果模型根本搞不清楚应该先调哪个。后来借鉴agent-skills模式统一了技能定义规范每个技能只做一件事描述里明确写出前置条件和成功判定标准调用准确率才真正拉起来。1.3 技能和工具有什么本质区别很多人一上来会问技能不就是工具函数吗区别确实存在而且很关键。传统意义上的工具是一个函数、一个API描述清楚参数就行。但技能包含几个工具之外的维度使用条件什么场景下该用这个技能什么场景下不该用这个模型必须一眼能判断。执行步骤有些技能内部包含多个步骤比如获取资讯要先调用搜索、再做去重、再按时间排序。反馈格式技能执行完应该以什么数据结构返回成功和失败分别长什么样。后续动作执行完之后是直接返回结果给用户还是把结果交给另一个技能继续处理。就拿获取资讯来说如果按照工具函数定义可能就是search(keyword)一个接口。但是按照技能定义它要包含是否指定时间窗口、是否需要过滤某些来源、返回结果是否需要按相关性排序、要不要附带原文摘要。这些约束如果全交给模型临场发挥结果极不稳定。技能框架要做的事就是把这些不确定性在定义层就尽量收敛让模型只需要做选择题而不是做自由论述题。2. 技能系统的核心从意图到描述再到验证2.1 一组好的技能定义长什么样我现在的技能定义统一走一套schema核心字段大概是这样的结构skill_name: 技能名称全部小写用下划线分词 description: 一句话说清楚这个技能做什么以及什么时候使用 trigger_conditions: 触发该技能的前置条件 parameters: - name: 参数名 type: 参数类型 description: 参数含义 required: 是否必填 enum: 可枚举值时列出来 default: 默认值 success_criteria: 执行成功的判定标准 failure_handling: 执行失败时的处理建议这里我想特别说一下两个容易被忽略的字段。第一个是trigger_conditions它解决的是模型什么时候应该调用这个技能的问题。很多工具定义只写了这个函数做什么但没说应该在什么情况下使用导致模型经常在不合适的场景调用。比如一个获取天气的技能trigger条件里应该写清楚当用户询问当前或未来某地的天气状况时调用不适用于询问历史气候。第二个是failure_handling。实际跑Agent的时候技能执行失败是非常常见的事模型如果不知道失败后该怎么办要么卡死要么自作主张瞎编结果。在技能定义里明确写出失败处理建议比如搜索无结果时尝试降低关键词粒度再搜一次API报错时等待5秒后重试一次模型的行为会可控得多。2.2 参数设计的关键经验参数设计是我踩过最多坑的地方总结下来有三条比较重要的经验。第一条参数宁少勿多能省则省。每个参数对模型来说都是一个记忆负担参数太多模型在处理时会忘记填、填错类型、甚至凭空捏造。我曾经设计过一个生成报告的技能有12个参数结果测试的时候模型有三分之一的情况会出现参数缺失。后来我把参数压到4个把其余参数全部下沉到配置里准确率立刻就上来了。第二条能用enum枚举的就不用自由文本。给模型一个开放式的参数相当于把选择权交给它自由发挥的后果就是格式五花八门。我在查询订单技能里把状态字段定义为[待支付, 已支付, 已发货, 已完成, 已取消]模型基本不会选错而最早用自由文本时出现过已发、发货中、运输中等各种变体下游解析直接崩溃。第三条参数的description要用模型听得懂的话而不是程序员看得懂的话。同一个概念写createTime创建时间的Unix时间戳模型理解得一般但写成createTime用户下单的时间格式为10位秒级Unix时间戳例如1699999999就清晰很多。说白了这个文本是给大模型读的要按它的理解方式去写。2.3 技能描述里的系统提示词技能描述本质上就是微型的系统提示词它在模型决定要不要调用、怎么调用时扮演了核心角色。我在实践中发现描述文本的措辞变动会对调用行为产生显著影响。一个很典型的例子是温度查询技能的description最初写的是查询天气信息返回温度、湿度、风向。这个描述太宽泛模型在用户问明天适合穿什么时也会去调这个技能因为描述里没有约束场景。改成当用户询问当前或未来的天气、温度、体感时调用提供结构化温度信息不用于穿衣建议、出行规划等综合分析之后误用率明显下降。还有一点技能描述的句式会影响模型对这是不是一个必须调用的判断。加一句如果用户只是想闲聊不要调用此技能这样的否定语句比单纯写该技能用于XXX更有区分效果。我现在每个技能定义里都塞一条否定条件实测对降低误召回非常有效。描述之外技能的验证环节同样重要。我每写一个技能会先准备一组典型的用户问题作为测试集跑一遍看实际调用率、参数正确率、结果格式准确率。这三个指标不过关的技能宁可先不上线因为模型一旦习惯了错误调用方式后续要改成本很高。3. 技能库的目录组织与版本管理3.1 目录结构与命名规范技能数量少的时候怎么放都行一旦超过几十个目录结构就成了维护性的关键问题。我目前使用的目录结构是按照领域层次双维度划分的skills/ productivity/ # 生产力域 schedule/ # 日程相关 create_event.yaml query_events.yaml mail/ # 邮件处理 compose_mail.yaml parse_mail.yaml knowledge/ # 知识域 search/ # 检索相关 web_search.yaml knowledge_base_query.yaml extract/ # 抽取相关 article_extract.yaml data_analysis/ # 数据分析域 sql_execute.yaml chart_generate.yaml我建议每个技能的yaml文件里加一个owner字段写上这个技能的维护人。技能挂在业务线上跑的时候一旦出现问题可以快速定位到人这个字段在跨团队协作时特别有用。命名上强制小写加下划线禁用缩写。skill名称就是模型的记忆锚点一个叫get_proj_sts的技能和一个叫get_project_status的技能对模型的辨识度完全不同。模型对语义化名称的理解远好于缩写。3.2 技能的三种分类原子、组合、认知技能不全是同一类我按照可复用粒度把技能分成三种这个分类直接影响了技能是怎么被模型调用的原子技能不可再拆的最小操作单元。调用一次执行一个动作返回一个结构清晰的结果。比如通过订单ID查询订单详情。组合技能内部串联多个原子技能对外只暴露一个入口。比如生成竞品分析报告内部会调用信息检索、页面抽取、文本聚类、报告模板渲染等原子技能。认知技能不操作外部系统而是对模型已有的信息做加工。比如摘要生成观点提取翻译改写这类技能依赖模型自身能力但通过技能化的方式定义了标准输出格式。这个区分在实践中的价值在于路由策略。组合技能和认知技能通常由模型在高层级规划时调用原子技能则更多在下层执行阶段调用。如果所有技能在同一个平面里模型在第一步规划时就会陷入细节导致规划质量下降。3.3 技能版本演进方式Agent系统上线之后技能不可能一成不变。调参、增加约束、修复bug都会带来技能定义的变化如果技能没有版本管理线上出了问题根本不知道用的是哪份定义。我现在给每个技能定义文件都维护一个version字段同时在技能库里放一份CHANGELOG记录每次变更的原因、变更点、影响范围。上线新版本之后把旧版本文件保留三个月方便快速回退。更关键的是技能版本升级的时候需要重新跑回归测试。技能A的定义变化可能会影响技能B的触发判断因为它们处于同一个提示词空间里。我遇到过的情况是给日程创建加了一个参数之后日程查询的调用准确率突然掉了一截原因就是模型在技能选择的注意力分配上发生了偏移。回归的时候不要只验技能A要把经常和它搭配使用的周边技能一起验。4. 把技能装进Agent挂载、路由与上下文管理4.1 技能挂载的两种形态技能定义好之后要挂到Agent上才能被模型感知到。挂载方式我试过两种静态挂载和动态挂载各有适用场景。静态挂载就是在系统提示词里把所有技能描述一次性给到模型适合技能数量少、又希望模型随时可精确调用的场景。它的优点是实现简单、模型响应稳定缺点是技能太多的时候会挤占上下文窗口而且模型在长技能列表中的注意力会被稀释。动态挂载则是先给模型一个技能大纲里面每个技能只有一句话摘要模型决定要执行某个技能时再把完整定义加载进来。这个方案解决了技能数量大的问题但引入了新的难点大纲摘要写不好模型在第一层级就排除了正确技能后面再没有挽回机会。我实测下来的经验是以80个技能为分界线低于80个静态挂载效果更好高于80个建议走动态挂载。超过160个技能还继续全量塞给模型上下文会被技能描述吃掉三分之一以上整体响应质量和速度都不理想。4.2 路由选择策略有了技能列表接下来是模型怎么选技能的问题。业界讨论较多的有两种策略让模型自己选还是用规则/向量检索帮他选。我在实践中尝试过两种方案的组合。让模型自己选的优势是灵活模型能结合上下文语义做判断。但问题是技能数量大的时候选择准确率下降而且模型偶尔会选一个看起来相关但实际不匹配的技能。用向量检索做预筛选把候选技能缩小到5个以内再交给模型选准确率确实能提升。做法是把每个技能的name、description、trigger_conditions拼接成一段文本做embedding用户请求来的时候用同一embedding模型做相似度检索取top-k作为候选集再让模型在这几个候选里做最终决策。我目前的方案就是向量粗筛模型精排。粗筛保证候选集覆盖正确技能精排保证在相似技能之间做出准确区分。实测下来技能命中率从裸模型自选时的82%提升到96%左右代价是增加了一步向量检索的延迟大约多出几十毫秒完全可接受。4.3 上下文管理的边界约束技能在运行过程中会产生大量中间数据比如技能执行后的结果、循环调用的中间状态、错误信息等这些上下文如果不做管理上下文窗口会迅速膨胀。我现在用一套上下文预算机制来管理。给每类内容分配一个max_token预算比如系统提示词不能超过3000 token、技能执行结果缓存不能超过5000 token、历史对话保留最近20轮。超过预算的部分按优先级丢弃历史对话最优先丢弃、技能执行中间结果次之、系统提示词和当前任务描述永不丢弃。上下文管理不只是节省窗口更重要的是防止模型看到太多历史残留信息产生错误判断。有一次Agent在查询订单时突然返回了另外一个订单的结果排查下来发现是上个任务的查询结果没有被清理模型在历史上下文里看到了旧的订单号。从那之后我在技能执行链路的开头和结尾都加了上下文清理步骤这个问题就消失了。5. 实测一线从能跑通到能打的打磨过程5.1 技能命中率不高的问题第一版技能系统上线之后我跑了一批真实业务请求发现技能命中率只能在八成左右徘徊。也就是说十个请求里有两个模型选错了技能或者压根没选技能。这个水平肯定没法交给业务方用。我逐条分析了错误case发现命中失败可以分成几类。最大的一类是技能描述没有覆盖用户的真实表达方式。比如技能描述里写的是查询订单物流信息而用户说帮我看看我的快递到哪了模型就犹豫了没有匹配到技能。后来我把描述扩成一组同义表达当用户询问订单物流、快递位置、包裹到哪了、配送进度时调用写成枚举式的触发条件集合命中率立竿见影地上升。可见技能描述不是给程序看的接口文档是给语言模型看的使用手册必须站在语言模型理解的角度写。第二类是多个技能之间确实有重叠模型对选用哪一个产生困惑。比如发送邮件草稿和保存邮件草稿两个技能描述里都可能包含邮件草稿收件人这些词。我最后的处理方式是给技能加互斥提示在每个技能的description里加一行本技能仅负责X发送动作请使用send_mail技能保存动作请使用save_draft技能。模型在做技能选择时看到这行互斥提示就不纠结了。第三类是有些复杂任务确实需要多个技能串成一个工作流而模型卡在一个任务只能选一个技能的思维里。这个问题的解法是把组合技能直接定义成一个带step列表的技能内部展开成多个原子技能的调用序列对外只给模型一个统一入口。模型不需要理解内部流程只需要决定要不要做这个任务。5.2 技能间的相互干扰技能数量增多之后出现了一个意料之外的问题新技能上线旧技能的调用突然不灵了。起初我以为是模型不稳定后来统计了多次运行的结果发现规律是固定的——新技能的描述如果和旧技能有相似词汇旧技能的调用率就会下降。这个现象本质上是一个注意力分配问题。模型在读取技能列表的时候对每一个技能分配注意力资源新增技能分走了一部分原本属于旧技能的注意力。明白了原理之后我在每次新增技能时都会做一次全量技能列表的回归测试重点检查与新增技能语义相近的旧技能是否受到影响。如果影响确实存在我会在旧技能的description里补充一句当用户意图是X时使用不要与skill_new混淆用显式消歧来对抗注意力稀释。另一个干扰从另一个方向来也就是技能的冗余。新功能加着加着某个旧技能的功能事实上已经被新技能覆盖了旧技能留在列表里没有任何价值只会增加模型的选择负担。我养成了一个检查习惯每个月梳理一次技能调用日志找出长时间零调用的技能评估是删除还是合并再顺手把功能被覆盖但我还没删干净的技能标记成废弃状态。5.3 测出过一组让我后怕的技能在技能系统基本稳定之后我做过一轮更刁钻的测试专门构造一些语义绕着走的问题来试探技能调用的鲁棒性。有个case让我印象特别深我让Agent处理帮我挪一下明天的会这个请求结果它调用了创建日程技能而不是修改日程技能。表面上看都是日程操作但本质上一个新建一个变更动作完全不同。用户说挪一下隐含的是日程已存在需要改时间模型没有识别出这个前提条件直接走错了方向。这个case让我回头调整了日程类技能的定义在创建日程的trigger_conditions里加上了仅当用户在创建一场新的日程时调用如果用户提到修改、取消、调整已有日程属于日程变更请使用update_event技能。加了这种边界条件之后再跑这类口径刁钻的测试行为就稳定了。还有一个让我比较意外的case是用户问帮我整理一下收藏夹里的技术文章。按我的设计意图正确路径应该是先读收藏夹、再对文章做分类、最后生成一份整理报告。但模型直接调用了技术文章搜索技能它把整理收藏夹理解成了搜索更多技术文章。这类意图理解偏差靠修技能描述解决不了需要在任务规划层增加一步意图预判先判断请求是否涉及已有资源再判断是否需要外部信息。现在我的所有技能统一标注是否涉及存量数据操作模型在处理请求时先走这个判断再进入技能调用环节这个case就没再出现过。6. 后续可以往哪个方向扩展6.1 低频技能如何低成本维护技能系统跑了一段时间后会出现一个尴尬的局面有些技能非常高频每天被调用成百上千次被模型使用得很娴熟很少出错。但另一些低频技能一个月只被调用几十次却占了技能列表里将近一半的描述空间维护成本还很高。低频并不意味着不重要比如导出年度报表这个技能一个月就用七八次但关键客户提出来的时候一次做错就是事故。我现在的处理思路是用按需加载把低频技能和高频技能分到两个池子。高频池里的技能全量挂载保证模型随取随用。低频池里的技能只保留一句话摘要摘要写清楚这个技能管什么、典型的请求长什么样模型判断需要时再展开完整定义。这样既不丢功能又省下大批量占用的上下文空间。6.2 技能评测自动化人工回归测试在技能数量少的时候还行技能一多每次变更都靠人工验证不现实。我现在给技能库加了一套半自动化的评测流程。每个技能定义里挂一组evaluation_cases每个case包含一个用户输入和一个期望调用的技能ID。跑评测的时候把这批case灌给Agent统计技能选择命、参数填充正确率、执行成功率、结果格式合规率等指标。技能定义变更后先跑一遍全量评测指标没回落的才允许合并上线。评测case的构造也不难直接从历史真实请求里挖挑那些代表性的、容易引发歧义的输入存下来慢慢积累一套技能评测集。这个评测集就是技能系统的质量护城河比任何线上监控都更能防止改一个技能坏一片功能的回归问题。6.3 技能的可演进性技能系统还有一个被低估的维度它在用户使用过程中产生的数据本身其实是一个新技能的原料仓库。我目前积累了大量用户用什么说法触发什么技能的真实日志这些数据后续可以用来做技能描述自动优化甚至让模型自己提议新的技能拆分方案。我自己目前还没跑完整条链路但经验已经明确了一个方向技能系统不应该是一个静态配置它应该和业务一起生长。用户提出新需求先看现有技能能不能组合出方案如果组合不出才考虑新增技能而且新增技能的定义要尽可能复用已有的原子能力。保持技能库精简而强健比让它大而全可靠得多。
返回列表