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

AI Agent技能体系设计与实战:从工具调用到稳定复用

AI Agent技能体系设计与实战:从工具调用到稳定复用 ★ FEATURED ARTICLE
从AI Agent项目落地那天起我就发现一个奇怪现象很多团队的Agent原型跑得飞快一上生产就崩不是模型不够聪明而是Agent压根不知道自己“会什么”。你说你有100个API可大模型面对的是一个模糊的函数列表它怎么知道什么场景该调什么、参数怎么填、失败怎么办这个问题其实就是agent-skills要解决的。今天这篇我围绕“技能体系”Skills这个核心把Agent从“能跑Demo”推向“可稳定复用”的关键设计、实现方式和踩坑经验整理一遍。适合正在做Agent应用开发、想搞懂工具调用背后机制、或者被技能调用稳定性折磨过的人。1. 先想清楚Agent的“技能”到底是什么1.1 技能不是工具也不是工作流我跟不少人聊过发现“工具Tool”和“技能Skill”这两个词经常被混用。我自己的区分方式很简单工具是单点能力比如“查天气”这一个函数技能是一个带有完整上下文语义的行为单元它包含了触发条件、输入约束、执行逻辑、错误处理甚至示范样例。而工作流则是一组技能的有序组合是编排层面的东西。举个例子。你写了一个函数get_stock_price(symbol)这是工具。但你在Agent里注册这样一个技能——“我可以根据公司名或股票代码查询实时股价当用户询问某家公司当前股价时使用需要传入参数symbol。若用户只给了公司名先通过company_to_symbol映射转换。”这就不只是工具了它是一个完整的技能定义包含了“何时用”“怎么用”“输入什么格式”这三件最关键的事。技能体系解决的核心问题是让大模型在推理时能像人一样“知道自己拥有哪些能力”并且按照一种约定好的协议去调用这些能力。没有这层抽象Agent就是一堆裸函数的堆砌模型全靠猜生产环境就是灾难现场。1.2 一份合格的技能定义要包含什么基于我自己的实现经验一份可以被Agent稳定消费的技能定义至少包含下面五个部分技能名称name全局唯一、简短、语义清晰比如search_web、calc_expression。技能描述description核心中的核心。必须写清楚“解决什么问题”和“什么时候用它”甚至写明“什么时候不要用它”这对大模型做路由决策非常重要。参数规范parameters用JSON Schema描述每个参数必须写清楚类型、是否必填、取值范围、示例值。模型会根据这里的约束来生成参数规范越清晰调用成功率越高。执行逻辑returns技能调用后返回什么结构成功与失败分别返回什么。这一点可以放在实现里但要在定义中有所体现方便上层对结果做统一解析。示例examples一两个典型的调用示例能让模型“照葫芦画瓢”。对开源小模型尤其重要参数格式的示范价值非常大。这一套下来技能才不是“一个函数”而是“一段可被模型理解的能力声明”。我后面会展示一个完整示例。2. 技能库的架构设计注册、发现与路由2.1 为什么不能把一堆函数直接丢给模型最早我做Agent原型的时候图省事把所有函数的签名塞进一个巨大的Prompt里。结果模型在只有三五个函数时还凑合一旦超过十个就开始出现幻觉不是编造不存在的参数就是明明该调函数A却去调函数B。仔细分析后发现不是模型笨而是“信息过载”——每个函数的签名只有参数类型和一句话描述模型很难在上下文中快速区分能力边界。技能库的引入核心就是在模型和具体函数之间加一个“能力中间层”。这个中间层做三件事注册把分散的函数封装成统一格式的技能元数据登记在技能注册表中。发现根据用户问题或当前上下文从技能注册表里筛出最相关的Top-K个技能。路由把筛出的技能描述拼装进模型上下文让模型在候选集内做决策而不是面对全量函数。这样做的收益非常直接模型每次只需要在5-8个候选技能里做选择而不是盲猜100个函数。准确率会显著提升Token消耗也会大幅下降。2.2 技能发现机制怎么设计技能发现不是简单的关键词匹配。我建议先做一个“技能索引层”把技能描述做向量化然后用向量召回结合关键词过滤形成一个两级路由。具体流程大致是这样系统启动时加载所有已注册技能为每个技能生成向量索引。用户提问进入推理流程后先对问题进行向量化用余弦相似度召回Top-20。再用规则过滤掉与当前场景明显无关的技能比如用户没提股票就不建议召回行情类技能。最后保留5-8个技能送入模型上下文让模型做明确路由。是不是感觉很像推荐系统对本质就是“技能推荐”推荐的精准度直接决定Agent调用技能的准确性。所以技能描述的质量就是这套推荐系统的“特征工程”是决定上限的地方。3. 实操从零搭一个最小可用的技能库3.1 目录结构和技能注册表我这里的示例基于一个常见的Agent开发场景语言用Python框架不绑定特定库你完全可以用自己的主技术栈替换。先看目录组织方式skills/ ├── registry.json ├── web_search/ │ ├── SKILL.md │ └── impl.py ├── calculator/ │ ├── SKILL.md │ └── impl.py └── weather_query/ ├── SKILL.md └── impl.py每个技能一个独立目录SKILL.md写元数据和描述impl.py是执行逻辑。registry.json是全局注册表记录每个技能的启用状态、版本、依赖关系。这套结构的优点是每个技能自包含方便单测、调试和复用新增技能不需要改动主程序代码只需在注册表里登记。registry.json的初始内容长这样{ skills: [ { name: web_search, version: 1.0.0, path: web_search, description: 使用搜索引擎检索网页结果当用户需要最新信息、特定主题的网页资料时使用, enabled: true }, { name: calculator, version: 1.0.0, path: calculator, description: 计算数学表达式支持四则运算、幂运算、三角函数, enabled: true }, { name: weather_query, version: 1.0.0, path: weather_query, description: 查询城市实时天气与未来三天预报当用户询问天气、温度、降雨概率时使用, enabled: true } ] }3.2 一个完整的技能定义长什么样我把SKILL.md里最核心的部分拆开讲因为大部分问题都出在这份文件上。--- name: calculator description: | 计算数学表达式支持四则运算、幂运算、三角函数sin/cos/tan。 当用户要求计算数值、验证数学结果、单位换算时使用。 注意不处理代数方程求根、不处理微积分若用户需要这些请告知能力边界。 --- parameters: expression: type: string description: 待计算的数学表达式字符串 required: true example: (132 448) / 2 * (8^2) returns: result_type: number description: 计算结果 examples: - user: 帮我算一下(132448)/2*8^2等于多少 assistant_call: {expression: (132 448) / 2 * (8^2)} - user: cos(0)是多少 assistant_call: {expression: cos(0)}这份描述里我把三个关键信息塞得非常明确正面用途什么场景用用户提到哪些关键词可以触发。负面边界什么场景不用避免模型拿这个技能去做它根本做不到的事。示例真实输入输出对照模型稍微带点few-shot就能学会格式。很多团队写技能描述还是“一句话带过”这等于把能力决策权完全交给模型的临场发挥。我自己是把描述当成“使用说明书”来写越详细越具体模型调用越稳。impl.py内部实现我就不贴完整代码了核心逻辑是通过eval安全封装计算字符串但生产环境建议用解析器而不是裸eval。重点是每个技能模块实现统一接口比如execute(params: dict) - SkillResult这样技能库加载器就可以用统一的姿势去调用和捕获异常。3.3 技能加载器和路由逻辑主进程里技能加载器做三件事读取注册表、动态导入所有启用的技能、构建技能索引。我习惯用一个简短的伪代码来理解这个过程class SkillLoader: def __init__(self, registry_path: str): self.registry self._load_registry(registry_path) self.skills {} self.vector_index None def load_all(self): for item in self.registry[skills]: if not item[enabled]: continue module self._import_item(item[path]) self.skills[item[name]] { meta: item, impl: module.SkillImpl, } # 构建向量索引 self.vector_index build_index( [s[meta][name] s[meta][description] for s in self.skills.values()] ) def retrieve(self, question: str, top_k: int 5): # 向量召回 关键词过滤 candidates self.vector_index.search(question, top_ktop_k * 2) # 二次过滤: 排除描述负面边界中命中的项 return candidates[hits][:top_k]路由时把retrieve出来的技能描述转成模型可读的函数定义塞进系统Prompt或作为工具列表。拿到模型的调用意图后在注册表里查实现执行再把结果回填给模型。整个链路跑通之后新增一个技能只需要写一个目录 - 填SKILL.md - 写impl.py - 注册表里加一行。主流程完全不需要改。4. 技能调用最常见的五个坑4.1 描述写得含糊模型该触发时不触发这是我最常见到的问题。很多团队把技能描述写成“这是一个天气查询服务”这种废话级别模型看到之后一脸茫然这个“服务”到底是干嘛的什么时候用要不要我主动调我的解决办法是“场景化描述”不只写功能要写用户意图。比如“当用户提到日期概念说‘外滩明天会下雨吗’这里的核心意图是天气查询应调用该技能”。描述要让模型看到一段文本就能条件反射地匹配到技能。但这里要平衡描述长度。描述越长模型路由越准但也会吃掉越来越多的上下文Token。我的经验是每个技能描述控制在120-200字之间正面场景写2-3个典型用户说法负面边界写一句。超过200个字就先精简措辞而不是继续堆场景。4.2 参数依赖另一个技能的返回值复杂交互崩了技能不是孤岛很多业务场景需要先调用A拿到ID再调用B查详情。但模型在处理多步技能链时经常忘了把A的返回结果传给B或者把B的必填参数留空。我自己的经验是不要在技能描述里暗示“你必须先调用另一个技能”那只会增加模型的负担。更稳的做法是在参数规范里显式标注“该参数可以通过xx技能的结果获取”然后在路由层做上下文悬挂。也就是让Agent的主循环维护一个“最近技能结果”的记忆槽当模型调B但缺参数时路由器自动从记忆槽里找匹配的字段填充。这种方式比指望模型自己记忆靠谱得多。毕竟开源模型的上下文注意力一旦被拉长最先忘掉的就是中段位置的工具返回结果。4.3 技能个数一多召回就乱技能数量上了30-50个之后向量检索也会开始误召回。原因往往是技能描述之间的区分度太低——比如“查询A股股价”和“查询美股股价”描述里可能一大半词是重复的向量距离太近误召回概率就上来了。两个解决思路第一明确技能边界把“能力重复但数据源不同”的技能合并为一个技能用参数区分数据源而不是拆成两个独立技能。比如“查询股价”技能参数加一个market字段足以覆盖A股和美股。第二在描述里刻意差异化关键词一个强调“国内交易所”另一个写“境外股票”让向量空间里的位置拉开。4.4 技能执行失败模型还接着瞎编技能执行返回异常后如果Agent主循环不做处理模型经常就会开启“脑补模式”没查到的数据它编一个没计算出来的结果它算一个“大概值”。这在金融、医疗场景里非常危险。我必须在技能返回结果里留一个结构化错误字段让主循环感知到调用失败并触发一个降级策略要么提醒用户“该技能当前不可用建议稍后再试”要么自动尝试备选技能。不要把错误堆栈直接甩给模型那只会让模型更懵。我的习惯是把错误信息“人话化”之后再作为消息发给模型比如“天气服务API超时未能获取结果”模型就知道该怎么向用户解释了。4.5 版本迭代后老技能失效技能库上了生产之后一定会遇到升级问题。今天改了某个技能的返回结构明天发现模型还在按旧结构解析。尤其技能比较多的时候最怕“悄悄改坏”。我的做法是每个技能配置version字段并且注册表记录兼容性要求。任何返回结构的破坏性变更都必须升级主版本号并在技能元数据里标记“breaking change”。主循环调用技能前校验模型给出的调用协议与当前技能版本的兼容性不兼容直接拦截。这个校验逻辑写起来也就十几行但能挡掉无数线上事故。5. 技能描述优化让大模型“看到就懂”5.1 用用户视角写描述而不是系统视角很多技术背景的开发者写技能描述下意识会用“该模块提供XX接口支持XX协议”这种系统视角的写法。但大模型读描述的时候是在理解“用户的意图”和“我的能力”之间的映射不是在读API文档。换一个写法把描述改成用户会说的话“用户想知道XX信息时使用本技能查询。”我来做一个直接对比写法A提供股票行情数据接口支持实时价格、K线、成交量查询。写法B当用户询问某只股票当前价格、涨跌幅、历史K线或成交量时使用例如“茅台今天多少钱”“比亚迪最近一周的涨跌情况”。第一种更像函数注释第二种才像技能使用指南。实测下来仅描述改写这一项就能把技能路由准确率提升不少尤其是在中文场景下效果比调整模型温度参数明显得多。5.2 负面边界一定要写我见过很多人写技能描述只写“能做什么”从来不写“不能做什么”。结果模型把一堆不该归这个技能管的请求全路由过来了。比如一个“计算器”技能用户问“1美元等于多少人民币”模型居然调计算器因为这里包含“等于”这种数学表达。在描述末尾加一句“该技能不做单位换算不做汇率查询遇到此类需求请直接告知用户不支持”就能让模型快速收回这个意图。负面边界等于给模型划了一道“减速带”它撞过一次就不会再撞了。5.3 示例要覆盖“困难的调用”示例不是越多越好我一般一个技能放2-3个但一定有一个“困难示例”。什么叫困难示例就是用户问法绕、参数提取难的场景。比如用户说“帮我看看南京西路那边明天适合跑步吗”这个query里没有“天气”两个字但意图就是天气查询而且地点是“南京西路”这种非标准城市名。把这种例子写进SKILL.md模型在真实输入遇到弯弯绕绕的问法时就有可以参考的范式。6. 性能与资源技能库不能拖垮推理6.1 技能描述占用的Token要控制技能描述是直接进Prompt的属于“常量开销”。技能数量多、描述长意味着每一次模型推理都要背上几十个技能的描述Token。一旦召回到8个技能、每个技能200字光技能描述就可能占掉2000-3000个Token。对长对话场景来说这是巨大压力。优化思路有两个一是压缩描述用短句而不是长句精简修饰词二是做“动态技能摘要”只在路由阶段传完整描述一旦确定调用某个技能后续的模型上下文里只需保留该技能的参数细节。我在实际项目里是用一个精简摘要字段做召回把完整描述留到调用阶段再注入效果很好。6.2 技能执行要有超时与熔断把外部API封装成技能时上游接口的稳定性会直接传导给Agent。某个技能上游超时5秒整个Agent的响应就可能卡死。所以技能执行器必须要做超时控制——执行时间超过阈值直接返回错误不阻塞主循环。如果同一个技能连续多次失败触发熔断后续请求直接走降级路径不再实际调用。这个和微服务里的熔断逻辑一模一样只是作用在技能层。6.3 技能测试要自动化技能库的回归测试很容易被人忽略但技能描述改一个字可能就导致路由行为变化。我现在每个技能都配套两个层面的测试一个是单元测试测impl.py的执行逻辑另一个是路由测试用一批标注好的query验证技能召回结果是否符合预期。每次改动技能描述后跑一遍全量路由测试看有没有别的query被“带偏”到新描述上。这个测试集不需要很大50-100条query就能覆盖大部分常见场景。7. 一套顺手的技能库调试流程技能的坑往往不是一次就能踩完的。我自己沉淀了一套调试流程分享给你。先说步骤用固定的10条测试query跑一遍路由记录每个query命中的技能。逐条看模型实际选择找“该命中没命中”和“不该命中却命中”的case。对每个失败的case打开日志看模型当时拿到的技能描述和模型自己的路由理由。根据失败原因改描述目标是把“模型产生了歧义”的地方说得再直白一点。每次只改一个技能的描述改完重跑全量测试。这个过程听上去没技术含量但恰恰是最有效的方法。我见过太多团队一遇到技能调不准就直接换大模型或者加Prompt长度反而把问题搞复杂了。绝大多数路由问题都能通过“把描述写得更像人会做的判断”来解决。8. 技能体系的后续演进方向技能库这件事现在看起来还是一个偏工程化的“函数注册表”但我觉得它很快会变得比这更复杂。方向之一是技能的自学习当某个技能调用频繁、效果也好系统自动把它在特定场景下的成功调用样本沉淀成新示例反过来增强描述。方向之二是技能的动态组装未来的Agent面对一个复杂任务时不是一个一个技能地调而是把多个技能“编排”成一条临时流水线像流水线工人一样协作完成目标。这已经不是单纯技能库的范畴而是向工作流引擎演进了。但不论怎么演进技能定义的质量始终是地基。地基不牢楼层越高越危险。我现在做的每一个Agent项目无论底层模型是商业API还是开源模型都会在技能体系上花足心思因为这是唯一能把“大模型的能力”稳定转化成“业务价值”的工程化环节。如果你刚开始搭技能库我的建议很简单先把一个技能的定义写得足够好——好到你愿意把它打印出来拿给同事看然后再去扩展第二个、第三个。技能体系维护到后期真正考验的不是编码能力而是把业务语言翻译成模型能理解的“能力声明”的能力这个功力越早练越好。
阅读完成 · 觉得有帮助?
咨询建站