在任何Agent项目里,最让人头疼的往往不是模型选型,也不是流程编排,而是“技能”本身——怎么定义、怎么封装、怎么让模型稳定地调用,最终形成一个能真正干活的“技能库”。我自己打磨“agent-skills”这套方案时,就是从这个痛点切入的。它本质上是一套面向大模型Agent的技能封装与调度范式,核心是把每项可执行能力做成结构化、可描述、可校验的“技能包”,让模型在推理时像查字典一样选对工具、填对参数、拿到预期结果。
这篇文章我打算从设计思路、技能构造、完整实现路径到常见坑位,完整复盘一遍。无论你是刚接触Agent开发,还是已经被Function Calling折磨过一阵子,应该都能从这里找到一些能直接落地的经验。
1. 整体设计与思路拆解
1.1 为什么“技能”不能靠提示词硬堆
先讲个大多数人都会踩进去的坑:早期做Agent,我习惯把所有能力说明写进系统提示词。比如让模型“当你需要查询天气时,调用get_weather接口,参数传城市名”。表面看逻辑通顺,实际跑起来问题一堆——提示词稍微长一点,模型就开始选择性失忆;接口参数稍微多一点,它就瞎填;加一个新技能,又要重新调提示词,牵一发而动全身。
本质上,这是把“推理”和“记忆”混在一起了。大模型的上下文窗口是有限的,提示词越长,对每一条指令的注意力权重就被稀释得越厉害。尤其当Agent需要同时掌握十几个甚至几十个技能时,一次性塞进上下文的做法几乎必然失败。
agent-skills这套方案换了个思路:技能不靠“告诉”模型,而靠“让模型自己去发现”。每个技能是一个独立的、自描述的单元,包含功能说明、参数Schema、调用规范、返回处理逻辑。模型需要某个能力时,先通过技能描述检索到对应技能,再按Schema生成结构化调用请求。整个过程像人用工具箱——你不会把几十把扳手全摆在工作台上,而是用到哪把拿哪把。
1.2 技能库的核心边界:描述够细,执行够黑盒
一个合格的技能单元,至少要满足两个条件。第一,描述必须足够细,细到模型能准确判断“这个技能是不是解决当前问题的那个”。第二,执行过程对模型来说足够黑盒,模型只需要按规范传参,不需要关心内部实现有多复杂。
举个例子。假设你有一个“发送企业微信通知”的技能。面向模型的描述不应该只写“发消息”,而是要写清楚:发送目标类型支持哪些(成员、部门、群聊)、消息格式支持哪些(文本、Markdown、文件)、有什么频率限制、发送失败时返回什么错误码。这些细节决定了模型在真实场景中能不能做出正确选择。
而执行层面,技能内部可以随便折腾——调HTTP接口、读数据库、甚至拉起一个子Agent都行。模型核心只关心“Schema里要求的参数我填没填对”和“返回结果我怎么解读”。
1.3 与传统Function Calling的区别
理解这套方案,最直观的方式是和OpenAI的Function Calling对比着看。
Function Calling是很早期的工具调用范式,它把函数列表一股脑塞给模型,让模型从里面挑一个并生成参数。技能数量少、参数简单时好用,一旦技能库上到几十个,两个问题马上浮出来:一是Token开销呈线性暴涨,每个请求都要把全部函数定义送给模型;二是模型在长列表中的选择准确率下降,经常选错工具或者张冠李戴把A技能的参数填到B技能上。
agent-skills在处理这两个问题上走了一条中间路线:技能描述可以做得比Function定义更丰富,但不需要全部塞进上下文。当模型需要能力时,通过一个“技能路由器”完成检索匹配,再只把候选技能的完整Schema注入当前轮次的推理。本质上,这是给Agent加了一层“短暂记忆”或者叫“工作台”——工作台上永远只放当前任务需要的几把工具。
提示:技能路由器不一定要用模型实现。简单场景下,基于关键词和向量检索的混合匹配就够了,搜出Top-K个候选技能,再交给模型做最终选择,成本和性能都会好看很多。
2. 核心细节解析与实操要点
2.1 技能清单:让Agent知道“有什么可用”
一个真实的Agent项目,技能数量从几个到几十个不等。如果全靠模型瞎猜,效率太低。所以每个技能包首先要维护一份机器可读的清单,把技能的标识、名称、一句话概括、触发关键词全部列出来。这份清单有两个用途:一是供路由器做初次筛选,二是作为模型上下文里常驻的“索引页”。
我一般把技能清单做成这样:
- id: skill_001 name: "query_weather" summary: "查询城市实时天气与未来预报" keywords: ["天气", "气温", "降水", "weather", "temperature"] enabled: true endpoint: "http://internal-mock-server:9000/skills/001"这里有个容易忽略的细节:清单条目必须写得特别克制。一句话概括加上几个触发关键词就够,绝不展开细节。原因还是上下文开销——清单要是写得太长,等于又把整个技能库塞回提示词里了。一句话概括的目的只有一个:让路由器能够通过关键词/向量召回把范围缩到3个以内,然后把这3个技能的完整Schema交给模型精确选择。
2.2 技能描述:写“边界”比写“功能”更重要
很多人在写技能描述时只写功能不写边界,这在我做项目复盘时是最典型的问题。比如描述一个“生成报表”技能,你写“能够生成销售数据报表”,模型确实会在用户说“出个报表”时调用它。但用户如果说“帮我看下上个月哪个品类的退货率最高”,它可能也会选这个技能——实际这个需求要的是数据分析,而不只是把数据摆进表格里。
正确的做法是,在描述里规定清楚“什么情况用”“什么情况坚决不用”。还是那个报表技能,优化后的描述是这样:
description: 将结构化数据渲染为标准报表文件(PDF/Excel),并输送到指定存储位置。 when_to_use: 用户明确要求“生成报表”“导出表格”“做一份Excel/PDF文件”时使用。 when_not_to_use: 不负责数据统计分析,若用户只是询问数据趋势、异常归因,应优先选择数据分析类技能;用户未指定输出格式时,建议先询问需求再调用。这一段让模型对技能的适用边界有了硬约束。实测下来,加了边界描述之后,工具选错的概率基本能下降一半以上。你甚至可以理解为,这是在给模型划“责任田”——边界越清晰,模型越不容易跨专业乱接活。
2.3 参数Schema:模型理解的唯一依据
技能的参数定义,直接决定了模型能不能正确“填空”。这一步没有任何捷径,必须按JSON Schema规范来。有几个关键原则:
- 每个参数必须有description。别觉得参数名起得够直白就不用写,模型对英文命名和中文语义的对应关系,理解准确率没有你想象中那么高。比如参数名是
user_id,描述就写清楚“接收方用户唯一标识,来自通讯录列表,非手机号”。 - 必填项一定要声明required。选填项如果影响结果准确性,强烈建议都设计成必填。模型很懒,你越给它“可填可不填”的空间,它越倾向于少填,然后返回一堆“不确定”的结果。
- 枚举值要写成选项列表。比如“时间粒度”只有
hour、day、week、month四种,就明确枚举出来,不要写“时间粒度类型’这种开放定义。模型一旦面对开放取值,能不能蒙对完全看运气。
我自己踩过的真实教训是:给一个“发送邮件”技能定义attachment_paths参数时,没有写明白是否支持多个附件,也没有规定路径的传递格式。结果模型有时候传数组、有时候传字符串、有时候直接传一个含通配符的路径。后来统一成“字符串数组,元素为服务器本地绝对路径,最多5个”之后,一次就没再出过问题。
2.4 返回结构:可解析比可读更重要
Agent技能调用的返回结果,往往不是给人看的,而是给模型“读”的。所以返回结构一定要固定、扁平、机器友好。
我通常会统一成这套结构:
{ "code": 0, "message": "success", "data": { "outcome": "success", "result": { ... }, "meta": { "cost_ms": 120, "from_cache": false } } }这里想强调两点。第一,业务上是否成功不要用HTTP状态码表达。HTTP 200也可能业务失败(比如查无此人、接口限流),模型无法从200判断下一步该怎么办。第二,error信息要写成模型能理解的话。比如“ERROR_10007”这种代码,模型根本不知道什么意思,你还得专门写个表去解释。不如直接返回“未找到用户ID对应的记录,请核对用户ID或确认用户是否已被禁用”,模型直接就能据此调整策略或向用户提问。
2.5 技能路由器的两级策略
前面提到路由器做初筛,这里展开讲下两级路线的实现思路。第一级是“轻量召回”,基于关键词匹配、别名表、向量相似度混合检索技能清单,筛出Top-K候选。第二级才是“模型决策”,把候选技能的完整Schema和当前对话的最近几轮内容一起交给模型,让模型选一个并生成参数。
轻量召回阶段一定要做到毫秒级响应,因为你不能为了“选个技能”就让用户多等一次模型推理。我实测中用的方案是:把技能清单加载进内存,用简单的BM25做关键词召回,再叠加一层向量召回(embedding用小型本地模型),两边结果合并去重后取Top5。这个级别完全不需要上重型的检索服务。
模型决策阶段反而要克制,每轮只放候选技能。如果TopK=5,那就只把这5个技能的定义交给模型。为什么不是一次性全部?回到第一点说的上下文控制——你让模型在5个选项里做判断的准确率,一定高于让它从50个选项里选。另外,同一轮如果用户的需求涉及多个技能(比如“先查天气再订个闹钟”),也要允许模型输出多组调用请求,而不是只允许它挑一个。
注意:技能路由器的召回逻辑,本质上决定了Agent能力的“天花板”。一个技能写得再好,召回阶段就被过滤掉,那它也永远没有机会被模型使用。所以新的技能上线后,一定要反复检查和现有技能之间的关键词重叠度。
3. 实操过程与核心环节实现
3.1 从零搭建一个技能的最小可用闭环
现在完整走一遍从定义技能到Agent成功调用的全过程。我这里用一个“仓库库存查询”的模拟技能做示例,核心是让你看到每个环节的长相,而不是拘泥于某个具体框架。
第一步,创建技能描述文件。先写清id、name、summary和keywords:
id: skill_warehouse_stock name: "query_warehouse_stock" summary: "查询指定仓库内商品的实时库存数量与批次信息" keywords: ["库存", "仓库", "SKU", "存货", "stock", "warehouse", "批量"] enabled: true第二步,把功能描述和边界规则补上:
description: | 根据商品SKU和仓库编码,查询当前仓库的实时库存总量、可用量以及最近批次的到期日期。 when_to_use: | 当用户需要确认某商品是否缺货、查询当前库存数量、询问可用销售库存时使用。 when_not_to_use: | 不负责库存的环比变化趋势分析;不负责自动补货建议;若用户同时问多个仓库的汇总库存,应先提示当前技能仅支持单仓查询。第三步,定义参数Schema。这一步需要结合真实业务字段做收敛:
{ "type": "object", "properties": { "sku_code": { "type": "string", "description": "商品唯一编码,必填。通常以字母开头,如ABC123。" }, "warehouse_code": { "type": "string", "description": "仓库编码,枚举值:WH001(华东主仓)、WH002(华南主仓)、WH003(华北临时仓)。" }, "require_batch_info": { "type": "boolean", "description": "是否需要返回批次明细,默认false。仅当用户明确需要查看批次/效期时设为true。" } }, "required": ["sku_code", "warehouse_code"] }第四步,实现技能的执行函数。内部逻辑完全自主,可以调真实业务接口,也可以先打桩返回模拟数据。关键是返回结构必须遵循统一规范:
def execute(sku_code: str, warehouse_code: str, require_batch_info: bool = False) -> dict: # 这里是业务处理逻辑,通常请求内部服务或数据库 # 为了演示,直接构造模拟数据返回 data = { "outcome": "success", "result": { "sku_code": sku_code, "warehouse_code": warehouse_code, "total_qty": 1520, "available_qty": 1330, "safety_stock": 500, "batch_list": None } } if require_batch_info: data["result"]["batch_list"] = [ {"batch_no": "B20240601", "qty": 800, "expiry_date": "2025-06-01"}, {"batch_no": "B20241001", "qty": 720, "expiry_date": "2025-10-01"} ] data["result"]["meta"] = {"cost_ms": 15, "from_cache": True} return {"code": 0, "message": "success", "data": data}第五步,注册技能到路由器的技能中心,并把技能中心的检索逻辑调通。这一步之后,一个最小闭环就已经成立了。用户提出“华东仓的ABC123现在有多少库存”,系统经过召回、模型筛选、参数生成三步,就能拿到结构化调用请求并执行。
3.2 技能入参校验的强制方式
很多人在这一步偷懒,想着让模型按Schema生成JSON就完了,结果模型生成的参数常常不严格合法。我的经验是,不管模型输出得再规范,也要在系统侧强制做一遍JSON校验和逻辑校验。
校验分两层。第一层是格式校验,直接用JSON Schema校验库跑一遍,不符合就返回“参数不符合规范,请重新生成”。第二层是业务校验,比如SKU在系统里存在吗?仓库是禁用状态吗?有些逻辑模型是不知道的,必须由技能执行方硬判断。
这里有个实战技巧:校验失败时,不要只返回一句“PARAM_ERROR”,而是把具体哪个字段错了、为什么错、应该怎么改写清楚。模型拿到这种错误信息之后,自我纠错的成功率会大幅提升。返回“sku_code格式不对,应为字母开头后跟数字,如ABC123”这样的信息,模型大概率下一轮就自己修正了。
3.3 多技能协作时的调用编排
单个技能做好只是第一步,真实的Agent任务往往会触发多个技能。比如“帮我把华南仓所有库存低于安全库存的SKU汇总一下”,就可能要先后调用“查询仓库SKU列表”和“查询单个SKU库存”两个技能。
这种情况下,最容易犯的错误是让模型一次性把所有调用的参数都推理出来。准确率会很低——因为后一个技能的输入参数往往依赖前一个技能的返回结果。比如你必须先拿到“库存低于安全库存”的SKU清单,才能逐个去查明细。
正确的编排方式是:让模型分步决策,每轮只关注当前需要的技能调用。第一轮,模型调用“查询仓库SKU列表”技能,拿到所有SKU及各SKU库存数量;然后模型结合返回结果,筛选出低于安全库存的SKU;第二轮,再调用“查询单个SKU库存”技能取明细。模型在执行完一个技能、看到返回结果之后,才进行下一步决策,效果远比一口气规划完更稳定。
实际工程中,我会在技能中心框架里加入一个“任务栈”的概念——每一轮模型推理结束后,检查有没有未决的子任务;如果有,把已完成技能的结果摘要拼到下一轮上下文中,再让模型决定下一个技能调用。这是一个笨但极其有效的方法。
3.4 Token成本的关键控制策略
做Agent项目,成本难以避免。但agent-skills这套方案在Token控制上有天然的优势,前提是你做对了两件事。
第一件事,技能包本身要分层。每个技能分成“light描述”和“full描述”。清单里只有light描述,路由召回后只返回候选技能的full描述。用户如果连续多次触发同一技能,可以设计一个轻量的“技能会话缓存”——在短时间内命中同一技能,就直接沿用它的参数结构和上下文,不再重新注入技能描述。
第二件事,给技能描述做“压缩”。不要直接把自然语言描述原样塞进模型上下文。可以把常用的技能描述预编译成半结构化的摘要,把修饰语、冗余词、背景说明全部拿掉,只保留“何时用、何时不用、关键参数说明”。我实际对比过,同样的技能,压缩后的描述Token量能降到原来的40%~60%,而模型的选择准确率几乎没有下降。
提示:技能数量到一定规模后,建议对技能的embedding做定期重建。因为业务语义会随产品演进发生变化,比如你早期定义的“用户ID”可能只是登录账号,到后来可能是多端关联ID了,不及时更新技能描述和召回索引,Agent就会在旧语义里打转。
4. 常见问题与排查技巧实录
4.1 模型“乱选技能”怎么定位
这是Agent开发里最磨人的问题。表面上看模型选了技能A,但实际上用户的需求应该用技能B。处理这类问题,第一反应不该是改提示词,而是先查“它是真不知道有B,还是知道却选错了”的。
我自己的排查顺序是:先打开单轮调试记录,把模型当时的输入上下文完整拎出来看。如果上下文里根本没有B技能的描述,那是召回的问题,去调路由器的召回阈值和关键词覆盖。如果B技能的描述就在候选列表里,但模型选了A,那才是技能描述写法的问题——多半是因为B的when_to_use写得太笼统,或者A的when_not_to_use没有把不适用的情况拦住。
另外强烈建议给技能选择留“trace_id”。就是每次模型选中技能时,把候选列表、模型原始输出、最终选择的技能ID全部打印出来。这看起来多花了几个字的日志成本,但在问题排查时效率能提升十倍。
4.2 模型频繁“编造参数”怎么破
参数编造是另一种高频故障。表现是:技能选对了,但传入的参数在业务系统里根本不存在,比如查一个不存在的用户ID。破局的思路有两个。
第一个思路是“限制开放性”。凡是用户没提到的参数,一律不给默认值,让模型在生成前先判断“当前对话里有没有这个信息”。没有就调用“追问用户”技能,而不是猜一个值。比如用户只问“库存还有多少”,没说仓库,模型应该反问,而不是默认选华东仓。第二个思路是“加强约束”。在参数Schema里,给每个参数都加上“来源”说明——来自用户原话的哪一部分、来自哪个前置技能的返回结果。如果参数来源无法对应,校验层直接打回。
4.3 技能路由召回精度不足怎么办
如果你发现模型经常“看到”某个技能,但实际该技能和当前用户问题八竿子打不着,多半是召回阶段把无关的技能兜进来了。我踩过的教训是:在召回阶段,不要只依赖关键词匹配,要同时跑多路召回合并。
多路召回指的是:关键词匹配一路、向量相似度一路、别名/同义词典一路。三路结果取并集后,再做一轮相关性得分排序。这样做的目的不是提高单路的精确率,而是提升整体的召回率。毕竟“技能库里存在备选技能但没被召回”才是更危险的故障——它无声无息地让Agent变蠢。召回宁可多,不能漏,精确性的判断全部留给模型做。
4.4 返回结果太长导致模型“看不懂”
这个坑很隐蔽。有些技能的返回结果特别长——比如查出一千条库存明细,全部塞回上下文。模型接到超长结果后,往往只“看到”最前面几条或者最后几条,经常基于不完整的信息给出错误总结。
解决方案统称为“结果裁剪”。在技能执行函数内部,对返回内容做摘要化、截断化处理:量级数据先聚合,明细只保留Top-N条,并附上一句“共N条明细,如需完整列表请单独说明”。模型要的是“决策信息”而不是“原始数据”。让模型根据摘要做判断,再按需拉取明细,这比把海量原始数据直接堆进上下文要高效得多。
4.5 更新技能后旧调用记录失效
最后一个问题,发生在迭代上。你更新了某个技能的参数定义,然后旧记录的缓存还在按旧格式解析。这个问题经常会引发“灵异事件”——代码没变,Agent却突然表现异常。
我的建议是,给技能包的每次变更维护一个版本号,并把版本号连同对话Session一起存储。当技能版本和Session历史记录不匹配时,优先按新版本重新解析参数。另外,旧技能在灰度期建议保留一个“兼容模式”,至少在参数格式变化时做一个自动转换层,而不是一刀切切掉旧调用。
写在最后的个人体会
把agent-skills从最初的一张纸梳理成如今这套可复用的方案,我最大的感受是:Agent真正困难的地方,不在模型多聪明,而在工程系统多规整。技能描述写得好不好、路由召回准不准、返回结构稳不稳,这些细节才决定了Agent的最终体验是“像一个靠谱的实习生”还是“像一个随时随地放飞自我的机器人”。
如果你准备在自己的项目里尝试这套思路,我给的建议很简单:先别急着铺十个八个技能,把两三个核心技能打磨到极致——边界清晰、算法规整、调用稳定。跑通了,再逐步加码。技能库这东西,宁缺毋滥,每个不达标的技能都会变成Agent的拖累。
最后再分享一个小技巧:技能描述写完别急着上线,先拿过去半年里真实用户问得最多的几十个问题,做一轮离线测试,看每个问题能否命中正确技能。这一步不花多少时间,但能帮你提前规避大量线上翻车。