首页 / 资讯中心 / 文章详情

AI Agent技能层设计:函数路由、参数校验与工程化落地

AI Agent技能层设计:函数路由、参数校验与工程化落地 ★ FEATURED ARTICLE
做AI Agent最容易被忽视的往往不是模型本身的能力而是那些看起来不复杂、实际却决定成败的细节。我第二次做Agent项目时踩了个大跟头——把二十多个函数一股脑塞给大模型结果系统Prompt膨胀到四千多token模型在无关对话里乱调技能参数格式各种臆造一度让我怀疑是不是该把方案推倒重来。后来我停下来把所有能力重新梳理成一套独立技能层局面才开始转好。如果你也在做类似的事或者正准备给自己的智能体加技能体系这篇文章会把我的完整设计思路、踩坑过程和修复方案都写出来可以直接照着落地。1. 技能层是Agent从Demo到可用的分水岭1.1 我从一次一地鸡毛的集成里得到的教训当时我在负责一个内部知识库答疑助手的改造业务方希望它能查文档、拉工单、翻项目进度还要能读邮件。第一版图省事我把所有外部接口的函数名和参数说明拼进system prompt里让模型自己选。Demo演示时一切正常因为演示用的几条问题都是精心挑好的背后对应的函数也就那么三四个。等放到测试环境跑了一周问题全暴露了。首先是模型开始幻觉调用。比如用户问这个需求什么时候上线模型居然调用了查询天气的接口因为它的描述里写了用于查询与时间相关的信息这个描述太宽泛了。其次是函数互相抢活儿查项目和查工单两个接口功能有重叠模型每次选哪个全看心情数据来源不稳定。最头疼的是每次加一个新接口都要重新编排prompt测试同学被迫把整条链路回归一遍一周能改三次。后来我找了一个做后端的老同事聊他点了一句话你这不是模型问题是把路由层的活硬塞给提示词。这句点醒了我。大模型确实能做一定程度的函数选择但它需要的不是一份函数清单而是一套能被它理解和约束的技能说明同时还需要后端的校验和降级兜底。于是我决定单独抽一层技能层这也是项目名agent-skills的由来。1.2 技能层的定位与边界很多刚接触Agent的同学会混淆几个概念工具、函数、技能、插件。我现在的理解是这样的概念作用例子函数最底层的一段可执行代码查询订单状态的SQL封装工具对函数做一层外部接口暴露HTTP接口接收参数返回JSON技能给工具加上给模型看的使用说明书包含名称、描述、参数约束、触发条件插件一组技能的集合可按需加载客户服务插件包含查订单、退款、开发票三个技能技能层不负责实现业务逻辑它的核心职责只有三件事让模型知道系统里有什么能力知道什么场景下该用哪个能力知道怎么用才算用对。这三件事听起来简单但每件都要写得很讲究。技能层还承担着安全边界的作用——不该暴露给模型的内网接口绝不能出现在任何技能描述里这是铁律。我当时给自己定了一个原则如果某个功能不能写清楚什么时候用、什么时候不用、用错了有什么后果那它就不配做成技能宁可在产品上砍掉。正是这条原则帮我挡掉了不少后来会成为坑的需求。1.3 技能层带来的三个可见变化重构之后最直观的变化是system prompt的涵洞。原来四千多token的函数清单缩成了技能层的索引每个技能单独管理模型推理时只需要看到当前会话相关的技能集合。系统Prompt瘦身到一千token以内响应速度肉眼可见地变快。第二个变化是排错路径终于清晰了。以前模型乱调函数你根本分不清是描述问题还是模型抽风。有了技能层之后每次调用都会被记录下来调用了哪个技能、传了什么参数、成功还是失败。排查时先看是不是模型选错了技能再看是不是参数格式不合法最后才是后端逻辑的问题。三层分开定位效率高了很多。第三个变化是回归测试终于能自动化了。每个技能都对应一组回归用例改动技能描述后只需要跑对应的用例集不用再全链路手点。我在下一节会详述技能描述该怎么写因为这是整个技能层里最花心思、也最容易被做坏的部分。2. 先把技能描述写成模型能读懂的样子2.1 一套可用性较高的技能定义格式我给每个技能定义了统一的字段结构用JSON表示存到独立目录里。每个技能一个文件后面再做注册表聚合。一个完整的技能文件看起来像这样{ id: query_order, name: 查询订单详情, version: 1.2.0, description: 根据订单号查询订单的当前状态、金额、物流信息。当用户询问我的订单到哪了这个订单什么状态时使用。如果用户只提供订单号也可以直接查询。此技能只读不会修改任何订单数据。, parameters: { type: object, properties: { order_id: { type: string, description: 用户提供的订单号格式通常为13位数字如202501010001。 }, include_logistics: { type: boolean, description: 是否返回物流轨迹默认false。, default: false } }, required: [order_id] }, examples: [ { user: 帮我看看订单202501010001到了没, api_call: query_order(order_id202501010001, include_logisticstrue) } ], triggers: [订单状态, 物流, 快递, 到哪了], not_use_when: [ 用户想修改订单或退货应引导到售后技能, 用户询问的是虚拟商品兑换码但不查询具体物流 ] }这里最关键的不是字段有多少而是description和not_use_when。我见过很多团队把description写成给开发看的接口注释什么根据ID获取订单信息模型看到这种描述根本不知道什么时候该调用结果就是乱调或者不调。2.2 写好description的四个原则第一个原则是写给模型看不是写给同事看。模型和人不一样它不会主动推理这个函数大概是干嘛的它只能根据你的文字描述去匹配用户意图。描述必须讲清楚触发条件。比如当用户询问订单状态、物流进度时使用这比查询订单四个字有用十倍。第二个原则是必须写负面条件。也就是not_use_when这个字段我后来把它合并进了description的末尾因为有些模型不会去看独立字段。效果就是让模型在模糊场景下敢于不调用技能避免幻觉调用。比如此技能只读不会修改任何订单数据这句话看似多余实际上能拦掉很多模型自作主张的越权操作。第三个原则是参数描述要给格式示例。光写订单号是不够的要写通常为13位数字例如202501010001。模型对具体数字的敏感度远高于抽象描述给了示例之后参数填错率能下降一大截。这是我实测得到的结论。第四个原则是每个技能都要配一两个示例。示例的作用类似于少样本提示模型在看到完整示范之后能更好地理解参数之间的逻辑关系。尤其是包含布尔参数、可选参数的时候示例几乎是必须的。2.3 参数Schema的几个设计要点参数这块我踩过很多坑最后沉淀下来几条比较稳的做法。尽量用枚举值取代自由文本。比如订单状态与其让模型填已完成完成completed这种变体不如直接枚举pending, paid, shipped, completed, cancelled。模型在面对明确选项时的表现远远好过让它自由发挥。必填参数不要太多。每个额外必填参数都会增加模型调用失败的概率。能用默认值解决的就给默认值能通过自动推断解决的就别让模型猜。比如查询订单用户只发了一个我订单到哪了但没有订单号模型既不能编订单号也不该直接拒绝——这时候可以让技能支持查询最近一笔订单的降级逻辑用user_id来替代order_id查询。不要完全信任模型传来的参数类型。我在后端统一加了一层schema校验不合法就直接返回明确错误信息让模型根据错误自行修正。这个会在第四章展开细说。2.4 技能描述的长度控制与索引策略技能描述写得太长会导致推理时token消耗大、模型注意力分散。写得太短又会导致语义匹配不准。我尝试下来description控制在80到140个字之间效果比较平衡算上参数和not_use_when一个技能整体在250到350个字左右。超过这个范围就要考虑拆技能了。当技能数量超过15个全部塞进上下文里会让模型混淆度显著上升。这时候我做了两层设计第一层是技能索引用一句话概括每个技能的功能放在系统Prompt里第二层是完整技能描述只加载本轮对话可能涉及的那几个。如何判断可能涉及我用了一个轻量级的关键词分类器把用户输入和技能triggers做一次粗匹配命中才加载完整描述。这个做法让我在技能数量继续扩张时没有重新陷入混乱。3. 技能编排当多个能力撞在一起时的路由逻辑3.1 技能越来越多之后误选率开始抬头技能层单独跑通之后我一度以为问题解决了。但没过多久新的麻烦来了技能多了以后模型开始抢答。典型场景是用户说最近有什么需要我处理的任务吗结果项目进度查询、工单列表、待办事项三个技能都有可能被调用而且文档里描述都说得通。模型选哪个完全是运气这直接导致同一个问题三次回答内容来源不一致用户直接质疑数据对不上。这就是技能编排问题。技能层不能只是被动地让模型挑而要在设计上主动帮模型降低选择难度。我后来总结了三板斧技能分组、冲突消解、拒绝策略。3.2 用技能组实现意图隔离我先对所有技能按业务域做了分组比如订单域客服域知识库域日历域。然后对每个组增加了加载条件只有用户的问题命中了该组的触发词这组技能才会被完整加载进上下文。举个例子用户说帮我把这周二的会议安排一下系统先判断命中日历域于是加载日历域的技能全集包括创建会议、查询日程、取消会议等同时知识库域和订单域的技能只保留一行索引摘要。这相当于给模型做了一个前置过滤。分组之后模型在五个技能里做选择比在二十个技能里做选择靠谱太多。分组条件的设定也有讲究。不能太宽泛否则一个组里堆十几项等于没分也不能太窄否则意图稍微偏一点就落到空组。我当时定的标准是一个技能组最多容纳六到八个技能超过就继续拆子组。每个组的触发词尽量使用用户自然会说的口语词而不是内部术语。3.3 冲突消解当两个技能都能满足时怎么办即便分了组同组内仍然会有功能相近的技能。最典型的是查询项目进度和查询项目详情——用户说看看项目X现在咋样了你很难说该走哪个。我处理这种冲突的办法是三个层级。第一层是描述互斥。把两个技能的not_use_when写得互相排斥比如项目进度技能写明本项目详情技能已包含基础信息但若要获取甘特图时间线请使用进度技能这种显式说明对模型的效果很好。第二层是优先级标注。在技能定义里加一个priority字段当模型判断两者都可以时优先返回高优先级技能。第三层是融合技能。如果两个技能经常被同时命中干脆把它们合并成一个复合技能返回结果时一次拿全。我实测下来融合技能的体验最好因为数据口径不会再打架。还有一个细节有些技能之间是串联关系。比如先调用查订单得到order_id再调用修改订单备注就需要带上这个id。我会在技能的description里显式写出依赖查询订单技能的返回结果请先调用query_order获取订单号。这种前置依赖说明比在代码里写死更灵活也让模型对调用顺序有了概念。3.4 拒绝也是一种正确输出很多做Agent的人容易忽略一件事技能不是越多越好也不是每次都必须要调用一个。我在设计技能层时单独规定了一种无匹配的响应模式。当模型判断所有技能都无法满足当前用户问题时它应该诚实地返回拒绝信息而不是硬选一个最接近的技能。这个拒绝逻辑既要在描述里强调如果以上技能均不适用直接说明无法处理不要编造也要在后端兜底。后端在收到模型返回结果时如果发现调用名称不在技能注册表里或者参数校验不通过应该返回一个统一错误并提示模型重新描述用户需求。这套机制跑通后模型开始学会说我没有你想要的权限或者这个问题需要转给人工客服这比它硬着头皮调用一个错技能然后返回一堆胡话要体面得多。4. 实测中的典型翻车场景与修复过程4.1 翻车案例一模型把字符串参数写成了嵌套JSON技能层刚上线那阵错误日志里出现频率最高的一类问题是参数格式错乱。比如查询订单技能要求order_id是字符串模型实际传的却是{ order_id: { value: 202501010001, source: user_input } }我在日志里看到时整个人是懵的模型怎么会把一个简单字符串包装成对象后来复盘发现是因为我在参数描述里写了订单号来自用户输入模型就贴心地把来源信息也塞了进去。这类问题的修复不能靠改prompt因为模型会不停发明新的包装方式。我的做法是在所有技能后端统一套一层参数净化器用JSON Schema做严格校验不合法就直接按技能定义里的类型转换一次转换不了就返回错误要求重试。在校验器里我还特意关了coerce_types避免模型传来的123被悄悄转成123后又引发别的问题。修复之后我把这类错误按周统计发现从第一周的一百二十二次降到了第八周的三次。这个结果很有说服力——靠提示词不如靠硬校验。4.2 翻车案例二无关闲聊触发了业务操作有一次测试同学打开调试台对着机器人发了一句今天天气怎么样结果系统居然调用了创建工单技能。虽然最终因为参数校验不通过没有真的创建成功但这个惊险一幕让我意识到光是说要使用条件明确还不够必须加上强约束的语言。我给创建类技能统一加了一句硬性说明此技能会修改真实业务数据必须用户明确表达创建、提交、下单等操作意图时才可调用仅在用户询问能不能怎么办时不得调用。同时在后端做了操作确认机制凡是写操作技能的调用必须带上一个confirmation字段值为true才会真正执行。模型发现需要主动确认时通常会先反问用户是否确认创建这就把一次危险调用变成了一个安全的会话节点。4.3 翻车案例三技能列表太长后面的技能被模型完全遗忘有段时间我供着三十多个技能每个都写了详细描述系统Prompt里塞得满满当当。结果日志显示排在列表末尾的五六个技能一次都没有被成功调用过。这不是模型歧视是注意力被长列表稀释了更准确的说是排序在前面的技能拦截了大部分语义匹配。我当时的解决思路是放弃全量常驻改成上文提到的索引加按需加载。技能索引要足够短一句话就够。然后我把触发判定从模型判断改成了规则判定加上模型判断的混合模式先用规则粗筛出候选技能通常三到五个再让模型在这几个候选者里做选择。这么做之后长尾技能的调用次数从零变成了每周十几次因为只要有用户问到规则层就能把它们捞出来。4.4 修复效果的量化对比改造做完之后我做了一次连续六周的对比统计这里列几个关键数字供大家参考指标改造前均值改造后均值变化技能调用成功率67%92%提升无关会话误调用率14%2%下降平均单次推理耗时1.8秒1.2秒下降因参数错误引发的后端报错每周40次每周9次下降新增技能所需联调时间2天0.5天下降数字不会骗人。最让我意外的是最后一行新增技能联调时间的大幅缩短——因为技能定义规范化之后测试用例可以直接从样例里生成后端校验器也可以自动识别新技能团队协作的摩擦一下子就小了。4.5 日志与回归测试是技能层的生命线整个修复过程里我最庆幸的是从一开始就保留了完整的调用日志。日志记录的字段包括会话ID、用户输入摘要、召回技能列表、最终调用技能、原始参数、净化后参数、后端返回码、模型重试次数。有了这些数据我才能够定位上面那些翻车场景不然全靠肉眼去复现对话效率会低很多。后来我把这组日志做成了每周一次的自动分析任务重点盯三个指标误调用率、空结果率、参数修正率。前两个指标上升说明技能描述出了问题第三个指标上升说明模型在频繁生成不合法参数需要加强示例或参数校验。这相当于给技能层装了一个仪表盘所有改动都能被观测到而不是拍脑袋调完就说应该好了。5. 后续演进技能复用与共享的实践经验5.1 从单Agent到多Agent技能描述的一致性危机项目跑顺之后团队开始把agent-skills的能力复用到其他几个Agent上比如客服机器人、内部工单助手、项目周报自动生成器。这时候冒出了一个新的问题同一个业务能力在不同Agent里的技能描述被复制了好几份改了一个忘了另一个行为不一致。比如查询用户订单这个技能在客服机器人里的描述写的是当用户询问订单状态时使用在工单助手里写的却是当需要获取用户订单信息以填写上下文时使用。结果工单助手时不时把用户订单状态当作工单处理结果输出数据串味了。我意识到技能需要有一个单一事实来源而不是每个Agent各存一份。5.2 技能注册表与版本管理思路我搭了一个简单的技能注册中心只做三件事存技能定义、管技能版本、提供检索接口。所有Agent启动时从注册中心拉取自己需要的技能列表不再在各自代码库里维护JSON文件。版本管理上我采用语义化版本技能定义的变更规则是修复描述、调整参数默认值属于Minor更新不影响调用方修改参数结构、删除技能字段属于Major更新必须同步更新所有调用方Agent。每个技能变更后注册中心会自动跑一遍该技能自己的回归用例通过才允许发布新版本。这里有一个容易被忽略的细节技能的description变更看起来只是文字改动实际影响面特别大。因为模型的行为完全由描述牵引可能只是把触发词从查询改成查看就会让模型在某个会话里的选择行为发生变化。所以我把description的变更也归为Major级别对待必须走完整回归。曾有一位同学吐槽我小题大做直到他改了一个词导致客服机器人间歇性抽取错误订单号之后就再也没质疑过了。5.3 多Agent协作时的技能边界设计多Agent之间还会出现互相调用技能的场景。比如工单助手需要了解订单信息时我一开始直接让它调用工单域的查询订单技能后来发现会造成重复查询和权限过宽。更合理的做法是让上层Agent只暴露获取订单摘要这个精简技能内部再自行调用完整技能。这有点像接口的瘦身设计——对外暴露的永远是经过裁剪的、符合最小权限原则的能力。我在技能定义里加了visible_to这个字段标记某个技能可以被哪些Agent看到。不可见的技能Agent在推理时完全不知道存在自然也就不会去调用。这个隔离方案在权限管理上帮了大忙尤其是有两个Agent对同一条业务数据不同口径时直接让它们各看各的视图即可。5.4 日常运营技能的迭代节奏技能不是写完就完事的它会随着业务变化持续迭代。我现在维护技能的节奏大概是这样的每周看一次调用日志和误调用日志每两周和业务方碰一次新需求每月做一次技能清理。清理的标准是近一个月调用次数为零且没有合理的保留理由就直接下线。因为不用的技能放在注册表里只会增加检索噪音和误调用的概率。还有一个比较微妙的地方同一个用户问题在不同上下文中应该触发不同技能。比如这个单子能改吗如果是客服场景应该走售后技能如果是销售场景应该走改价审批技能。我现在是通过场景标签来实现的Agent启动时带上自己的场景标签技能描述里也会声明自己适用的场景。这种场景级的隔离比单纯靠技能描述硬扛要稳得多。这套agent-skills体系从最初被我嫌弃的prompt胖卷到如今成了团队里所有Agent的地基回头看最有价值的不是某个具体写法而是把模型能力和业务能力之间的连接做成了可维护的工程结构。如果你也在做Agent建议从给自己的技能写一份合格的使用说明书开始再一步一步把注册、编排、回归都补上。这条路不复杂但每一步都得走扎实。
阅读完成 · 觉得有帮助?
咨询建站