从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: "帮我算一下(132+448)/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_k=top_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还是开源模型,都会在技能体系上花足心思,因为这是唯一能把“大模型的能力”稳定转化成“业务价值”的工程化环节。
如果你刚开始搭技能库,我的建议很简单:先把一个技能的定义写得足够好——好到你愿意把它打印出来拿给同事看,然后再去扩展第二个、第三个。技能体系维护到后期,真正考验的不是编码能力,而是把业务语言翻译成模型能理解的“能力声明”的能力,这个功力越早练越好。