1. 先把“agent-skills”拆开看:它到底在解决什么问题
这两年只要做 Agent 相关项目的人,基本都会遇到一个很尴尬的现状:模型能力越来越强,可每次落地一个新场景,还得从头把工具、流程、边界条件一点点喂给模型。今天写一个 prompt 告诉它“调用天气接口时记得传城市编码”,明天换个项目再来一遍。agent-skills 这个概念,说白了就是把“让 Agent 干活的技能”从模型和业务代码里单独抽出来,做成一套可注册、可复用、可组合的独立模块。模型只负责理解意图、拆解任务和做生成,真正执行动作的技能放在一个统一的能力层里,谁需要谁取用,而不是每个项目都重复造轮子。
我最早接触这类思路,是在给一个客服机器人做功能扩展的时候。当时团队里同时维护了三套工具注册逻辑,一套写死在 prompt 里,一套挂在业务流程中,还有一套散落在各种函数代码里。改一个业务参数,得同时改好几个地方,那种感觉像是把一套乐高积木拆散后丢进三个抽屉,玩的时候还得来回翻。后来我把所有可执行能力整理成一份技能清单,每项技能有明确的描述、输入输出、调用地址和权限边界,Agent 根据用户问题自动选择技能,整个系统一下子清爽了很多。这也是我为什么想认真聊聊 agent-skills 这个方向。
这篇文章不是要给你讲某个现成框架的 API,而是想从“设计方法论 + 真实落地经验”的角度,把 agent-skills 这套东西拆开:它解决什么问题、建技能库时有哪些细节容易踩坑、如何让多个技能组合起来干一件复杂事,以及项目上线后最常遇到的故障怎么排查。如果你正在做 Agent 应用,或者想把团队里的智能体能力沉淀成可复用的资产,这篇文章应该能给你提供一套可以直接照搬的思考框架。
1.1 技能与模型的边界:为什么需要单独抽一层
很多初学者会问:我直接把工具函数给模型不就行了吗?模型本身能调用函数,为什么还要抽象一层“技能”?问题在于,模型对工具的理解是“一次性的”:你给它一个 JSON Schema,它知道怎么传参,但不知道这个工具在什么业务场景下该优先使用、什么情况下必须拒绝调用,更不知道多个工具之间的先后依赖关系。
举个真实例子。我做过一个企业内部知识库问答 Agent,最初直接把搜索接口、文档解析接口、权限校验接口全部塞给模型。结果模型经常乱来:用户问“帮我看看 A 部门的报销制度”,它先调了文档解析接口,再去调搜索,顺序完全反了,导致解析了一堆空文档,然后报错。后来我把这些接口封装成三个技能:查询权限技能、搜索文档技能、解析文档技能,并且在技能描述里写明“必须先调用权限校验,确认可访问后再搜索”。模型再傻,看到技能描述里的使用约束,也能按正确路径走。
单独抽一层的核心价值,就是把“模型不知道的规则”写到技能里。技能的描述、前置条件、后置动作、适用场景、禁忌,都是给模型看的“说明书”。模型不需要记住每个 API 的内部逻辑,只需要从技能清单里选出合适的技能并调用它。这相当于给 Agent 配了一本完整的《岗位操作手册》,而不是把一堆散落的工具清单扔给它。
另外,从工程角度讲,技能层还能统一处理日志、监控、熔断、审计。如果没有这层抽象,每个工具函数都得自己写日志,出了问题你在日志系统里看到的是一堆互相割裂的调用记录;有了技能层,一个技能调用的开始、结束、输入、输出、耗时都能统一埋点。后续做评估、做成本分析、做安全审计,都轻松很多。
1.2 一套可复用技能栈能省掉多少重复劳动
我见过不少团队,项目做完了,沉淀下来的只有代码仓库里一堆业务代码,下次开新项目又从头写一遍工具调用逻辑。这其实是很大的浪费。如果从一开始就把技能设计成可复用的模块,效果会完全不一样。
打个比方,技能层就像一个家庭的“工具箱”。过去你需要修水管的时候,临时去五金店买一把扳手,修完就扔;现在你有一面工具墙,每件工具都有自己的位置和标签,需要时直接取用,用完再归位。以后哪怕换了个房子(换了个项目),这面工具墙还能搬过去继续用。
具体能省哪些?首先是 prompt 编写成本。传统做法里,每接入一个新工具,都要在 prompt 里写一堆工具说明,还要小心控制 token 长度。技能层的做法是让 prompt 保持精简,只放技能清单的索引信息,详细的描述可以放到技能元数据里,按需加载。其次是工具注册逻辑。封装成技能后,外部服务接口变成统一的标准函数签名,底层是 HTTP、RPC 还是本地函数,Agent 不关心。再次是测试用例。技能库建起来后,每个技能都有独立的测试集,新项目直接复用测试集,不用重新构造场景。
还有一个容易被忽略的好处:团队协作。过去一个 Agent 项目里,提示词工程师写 prompt,后端工程师写接口,前端工程师调接口,大家各干各的,你也不知道对方的模块到底能不能配合。技能层相当于一个统一协议,后端只要按协议暴露技能,提示词工程师只要消费技能清单,两边可以不互相等待。这个协作效率的提升,对稍微大一点的团队影响非常明显。
2. 构建技能的核心细节:从输入输出规范到工具约定
光有“把工具封装成技能”的概念还不够,真正落地时最考验人的是细节。技能不是简单包一层函数就完事,它要解决模型的“选择问题”和“执行问题”。选择问题是指模型能不能在合适的场景下选中这个技能,执行问题是指模型能不能正确调用技能。围绕这两点,我总结了几个必须花心思去设计的核心环节。
2.1 技能描述:让 Agent 知道“什么时候该用”
技能描述可能是整个技能库设计中最容易被低估的部分。很多人写技能描述时特别随意,比如“获取天气”,结果模型在用户问“明天适合去爬山吗”的时候,反而不调用天气技能,因为描述里没有“爬山、出行、户外活动”这些触发词。另一个极端是描述写得太长,把所有可能的情况都罗列进去,模型反而被冗长的信息干扰,选错技能。
我常用的一个技巧,是把描述写成三段式:功能概述、触发场景、不能做什么。功能概述一句话讲清楚技能的作用;触发场景列举 3 到 5 个典型问题句式;不能做什么用否定句明确边界,比如“不要用于查询历史天气,仅支持实时天气”。这个做法在实测中效果非常好,模型的技能选择准确率明显提升。
另外,技能描述里一定要包含“使用前置条件”和“调用后果”。比如一个发邮件的技能,描述里要写清楚“调用后将真实发送邮件,无法撤回,需用户明确确认后才能执行”。模型看到这样的描述,在用户没有明确同意之前,大概率会先做确认动作,而不是擅自杀出去执行。这属于用描述做安全边界的一个典型手法。
2.2 输入输出与副作用定义:老司机和新手最容易在这里分歧
技能输入输出的定义,直接决定了模型能不能正确传参。这里有一个原则很重要:输入参数的命名和枚举值,要尽量接近自然语言习惯,而不是贴近后端变量名。
举个例子,我做过一个查天气技能,最早的参数名是city_code、weather_type,模型经常传错,因为用户不会说“city_code”,用户只会说“北京”。后来我把参数改成city_name(城市名,支持中文名),并在参数描述里加了“如果用户说北京,则传入‘北京’,不要自行转换编码”。模型准确率立刻上来了。这个改动成本几乎为零,但效果却非常显著。
另外还要特别关注“副作用”的定义。所谓副作用,就是技能调用后对外部世界产生的影响,比如发送消息、写入数据库、扣费、创建订单。这类技能必须和只读类技能在定义上做出明显区分,我习惯在每个有副作用的技能描述里加一个字段requires_confirmation: true,同时要求模型的系统 prompt 里约定:遇到带此标记的技能,必须先向用户确认再执行。
输出也不只是函数 return 值那么简单。技能的返回结果最好带一层状态包装,至少包含status、data、error_message。模型拿到返回结果后,能根据状态字段判断是否调用成功,而不是自己去解析一堆异常。否则模型在结果里看到 HTTP 500,根本不知道该怎么向用户解释。
2.3 技能注册与权限边界:别让 Agent 随意“踢门”
技能注册表是 Agent 的“通讯录”。注册表里除了技能 ID、名称、描述之外,还应该包含权限级别、调用频率限制、超时时间等元数据。比如“查询数据库”和“删除订单”绝不能有相同的权限级别,前者可以允许模型在无人工干预时调用,后者必须设置为高风险状态。
权限边界这一块,我的建议是宁可先收紧,再根据实际场景放开。因为模型在复杂对话里偶尔会“自作主张”。之前我们上线过一个运维 Agent,它可以根据用户指令执行服务器命令,早期我们把权限定得比较宽,结果有一次用户说“帮我查下磁盘空间”,Agent 不知道从哪里学来的习惯,顺手执行了rm -rf /tmp/cache。虽然没造成严重后果,但把团队吓出一身冷汗。后来我们把所有写操作和高危命令都设置成必须经过二次确认,并对命令做静态白名单校验,只允许在提前声明的几个目录下执行。后来验证下来,虽然多了一步人工确认,但安全性提升了一个量级。
技能注册表还决定了 Agent 的能力面。很多项目里,模型对技能的选择是基于“可见技能列表”,如果某些技能不在注册表里,模型根本不可能调用它。这就意味着你想让 Agent 做什么,就把它对应的技能暴露出来;不想让它做的,就别注册。不要指望“模型有判断力”,模型的能力边界是你定义的,不是它自己决定的。
3. 实操:从零落地一个 agent-skills 技能库
理论部分聊了不少,下面直接进入实操环节。我会用一个尽量完整的例子,带你走一遍技能库的最小落地流程:定义技能、注册技能、把技能交给 Agent、让多个技能配合完成一次复杂任务,最后再用测试集验证。我尽量还原实际操作中的步骤和代码,你可以直接照着改。
3.1 第一步:从最小技能开始,先跑通一个完整闭环
最小技能不要贪多,选一个简单的只读功能即可,比如获取指定城市的实时天气。目的不是功能本身,而是把“技能定义 → 技能调用 → 返回结果 → 模型组织回复”这条链路完整打通。
我在实际项目中使用的技能定义大概是这样的,用 JSON 格式记录元数据:
{ "skill_id": "weather_query", "name": "实时天气查询", "description": "查询指定城市的实时天气。适用于用户询问天气、出行建议、户外活动安排等场景。仅支持国内主要城市,不支持查询历史天气。", "input_schema": { "type": "object", "properties": { "city_name": { "type": "string", "description": "城市中文名,例如:北京、上海、广州" }, "date": { "type": "string", "description": "查询日期,格式为 YYYY-MM-DD。若用户未指定日期,则默认当天,传空字符串即可。" } }, "required": ["city_name"] }, "output_schema": { "type": "object", "properties": { "status": { "type": "string", "enum": ["success", "error"] }, "data": { "type": "object", "properties": { "city": { "type": "string" }, "temperature": { "type": "number" }, "humidity": { "type": "number" }, "wind_level": { "type": "string" } } }, "error_message": { "type": "string" } } }, "auth": { "level": "read_only" }, "timeout_ms": 3000 }看到这个定义,你已经能理解技能层的基本结构:description是给模型看的,input_schema和output_schema是给模型传参和解析结果用的,auth是做权限控制的,timeout_ms是给执行层用的。所有信息都结构化,模型和程序都能消费。
接下来是执行层的代码。技能执行函数不复杂,只需要接收一个参数对象,返回一个统一结构的结果即可。以天气查询为例,伪代码如下:
def weather_query_executor(params): city = params.get("city_name", "") date = params.get("date", "") # 这里替换为真实的天气服务调用 # 我一开始是先用 mock 数据跑通的 if not city: return {"status": "error", "error_message": "缺少城市名"} return { "status": "success", "data": { "city": city, "temperature": 26, "humidity": 60, "wind_level": "3级", }, }这里有一个容易踩的坑:执行函数里不要再尝试做“智能判断”。比如不要写“如果城市名是北京,就特殊处理”,业务逻辑里的特殊处理应该放在数据层,而不是技能执行层。技能执行层一旦复杂,模型可能猜不透它的行为,测试也不好做。
3.2 技能注册与动态加载:让 Agent 能“看到”它
技能定义好之后,需要注册到 Agent 的技能表里。注册方式取决于你的技术选型。早期我们用的是直接写死在系统 prompt 里的方案,但技能一多,prompt 长度立刻爆炸,而且每次新增技能都要重新发一次消息,成本非常高。
后来我改成了“动态技能发现”机制:用一个注册表服务维护所有技能,Agent 启动或每轮对话开始时,只拉取当前用户权限范围内的技能清单,并优先输出技能名称和一句话描述给模型。模型需要查看某个技能的输入 schema 时,再通过一个专门的查询接口按需获取。
注册表的数据结构可以是这样的:
| 字段 | 说明 | 示例 |
|---|---|---|
| skill_id | 技能唯一标识 | weather_query |
| name | 技能名 | 实时天气查询 |
| description | 给模型的说明 | 查询指定城市的实时天气... |
| input_schema | 输入参数定义 | JSON Schema |
| output_schema | 输出结果定义 | JSON Schema |
| auth_level | 权限级别 | read_only / write / high_risk |
| requires_confirmation | 是否需要用户确认 | false |
| version | 技能版本 | 1.2.0 |
| enabled | 是否启用 | true |
到这里,Agent 已经可以根据用户对话内容,从这两三个字段里判断该不该调用weather_query。我实测过,在技能数量不超过 30 个时,这种“先给名称和短描述,再按需加载详情”的方式,既省 token 又不会让模型迷失在大量工具定义里。
注册完技能后,还要在 Agent 的系统层加一个调度函数,逻辑大概是:接收模型输出,解析出意图和参数,查找命中技能,执行,返回结果。这部分不同框架差异很大,但核心思想一致:模型不是直接调函数,而是请求调度器执行某个技能。
3.3 编排多个技能:把顺序、条件和兜底都写清楚
单个技能跑通并不等于完成,真实业务中会遇到多技能协同的场景。比如用户问“我下周去杭州出差,帮我看看那时天气怎么样,顺便推荐两个适合带小孩去的室内景点”。这个问题至少涉及三个技能:查询行程日历拿具体日期、查询杭州天气、查询适合亲子游的室内景点。而且这三个技能是有顺序依赖的:必须先确定出行日期,才能查天气;必须知道天气是下雨还是暴晒,才能推荐“室内”还是“室外”。
技能编排方案我见过几种,最粗暴的是让模型自己在一次回复里连续调用多个工具。这种方式对话轮次多、失败率高。另一种是设计一个“编排技能”,它的执行逻辑里串联多个子技能。比如定义一个travel_plan_skill,它的 executor 内部依次调用日历查询、天气查询、景点查询,最后汇总结果。这样模型只需要调度一个技能,子技能之间的依赖关系由代码保证,可靠性比让模型一步步自己调高很多。
在编排技能时,有两个细节很重要。第一是子技能的中间结果要能暂存,避免重复查询;第二是任一子技能失败时,要有兜底方案。比如天气服务超时,可以让它返回“天气未知”,但景点推荐依然要继续,而不是整体失败。这样用户至少能得到部分有用信息。
我还习惯在编排技能的描述里写明“该技能会自动完成多步查询,适合复杂旅行规划咨询;如果用户只问单一景点,不要调用此技能”。这个负负描述,其实也是给模型做路由,让简单问题走轻量技能,复杂问题走编排技能,避免杀鸡用牛刀,也避免模型把简单问题错误地编排成一大串流程。
3.4 技能测试与评估:不能只试“一次成功”
很多人建立技能库后,只在开发环境里手动调几次,看着模型成功调用了就认为大功告成。但真实场景中,用户问题千变万化,同样的技能可能在十种语境下触发失败。所以技能测试一定要做成自动化回归集。
我目前用的测试框架不算复杂:维护一批 prompt 测试用例,每条用例标注预期应该调用的技能、预期参数、预期返回状态。代码跑起来后,自动调用 Agent,比较实际调用的技能/参数是否和预期一致。这个回归集既可以在开发阶段用,也可以在每次修改技能描述后跑一遍,防止“按下葫芦浮起瓢”。
举例来说,天气技能至少要有这些用例:
- 用户说“北京今天冷吗”,预期命中
weather_query,参数city_name=北京 - 用户说“上海明天下午会下雨吗”,预期命中
weather_query,参数city_name=上海 - 用户说“查一下历史天气”,预期不应命中
weather_query,模型应回复不支持并引导用户使用其他服务 - 用户说“帮我查一下纽约天气”,预期模型应询问是否指代其他城市,而不是直接传
city_name=纽约(因为技能只支持国内城市)
这类测试看起来简单,但非常能发现问题。我跑第一轮测试时,发现模型在“历史天气”这种否定场景中经常会误调用,后来在描述里加了“不支持查询历史天气”,错误率立刻下降。可见评估数据不是摆设,它是让技能描述持续变好的润滑剂。
4. 常见问题与排查技巧实录
技能库上线后,真正磨人的其实是各种边界问题。我把自己实际踩过的坑和排查思路整理成一份速查笔记,希望能帮你省掉一些不必要的加班。
4.1 我踩过的坑和排障过程
第一个坑是“技能描述与真实行为不一致”。我们曾经把一个搜索技能描述得很强大,说“支持全文检索、语义检索、关键词匹配”,但底层接口只接了关键词匹配。结果模型在用户问语义相近的问题时调用该技能,返回结果为空,然后傻乎乎地告诉用户“没有找到相关内容”。这类问题最大的隐患是模型不会主动识别技能本身的缺陷,它会用一套听起来很自然的话术掩盖底层服务的不足。后来我们定了一条规矩:技能描述只能基于真实能力来写,宁可写保守一点,也不要夸大。
第二个坑是“参数里藏着隐式转换”。早期查订单技能,输入参数是order_time,我们要求模型把用户说的时间转成时间戳。但不同时区的模型转换结果不一致,经常出现时间差 8 小时的问题。排查了很久才发现是模型在做隐式转换时采用了 UTC,而业务系统用的是北京时间。解决办法很简单:把输入参数定义改为 ISO 8601 字符串,并明确要求“不要转换时间戳,直接传原始时间字符串”,由技能执行层统一处理。这又一次验证了那句话:复杂度要从模型那里移除,放到代码里。
第三个坑是“多技能并发导致的竞态条件”。有一次做批量处理任务,Agent 同时调用三个写技能,结果两个技能同时更新了同一条记录,最后的数据状态是乱的。这个问题的根源是技能层没有做锁和幂等控制。后来我规定所有写技能都必须实现request_id参数,执行层根据request_id做幂等判断,同一请求重复执行只会生效一次。同时给高风险写操作加分布式锁,彻底解决了这个问题。
4.2 问题速查表
下面这张表,建议直接截图存起来,排查问题的时候对照着看:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 模型该调用技能却没调用 | 技能描述太窄,没有覆盖用户场景 | 检查描述里的触发场景,补充更多同义表达 |
| 模型调用了错误的技能 | 多个技能描述相似,边界不清晰 | 在描述中增加否定边界,明确“不要做某事” |
| 参数传错或格式不对 | 参数命名不够自然、缺少示例 | 参数命名贴近自然语言,在描述里给一个例子 |
| 技能调用成功但结果不符合预期 | 技能内部逻辑 bug 或下游服务异常 | 先看技能执行层日志,确认返回的status和error_message |
| 对话变慢 | 技能清单太长或系统 prompt 过大 | 改为动态加载,只暴露当前会话可能有用的技能 |
| 技能重复执行 | 没有幂等控制 | 引入request_id,同一请求只处理一次 |
| 用户无权限却调用成功 | 技能注册表未做用户级权限过滤 | 注册表按用户角色过滤技能,而不是只过滤系统级权限 |
| 模型自行编造技能结果 | 技能超时或返回异常后模型过度生成 | 给技能调用加超时熔断,返回错误消息强制模型如实回复 |
4.3 几个被低估的细节
最后分享三个容易被忽略但影响很大的细节。
第一个是技能版本管理。技能不是一成不变的,业务调整了,技能实现和描述都要升级。如果不做版本管理,排查问题时很可能看到模型还在用旧版本描述,而执行层已经换了新实现,两边不一致,各种奇怪问题都会出现。我的习惯是每个技能定义里都带version字段,并在日志中记录每次调用时模型看到的技能版本号,这样才能把“模型认知”和“实际执行”对齐。
第二个是技能灰度发布。技能描述的小改动可能影响模型的选择行为,甚至引发连锁反应。所以重要技能修改后,不要立刻全量上线,可以先让 5% 的流量走新版本,跑几天看指标没问题再全量。这个方法让我们避免了好几次上线事故。
第三个是技能的可解释性。用户有时候会质疑 Agent 的行为,比如“为什么没调用我买的 VIP 权益接口”。为了让用户信服,我每次技能调用都会生成一条调用记录,前端对话页面上可以展开查看当前轮次调用过哪些技能、传了什么参数、返回了什么结果。这个记录既是排查工具,也是用户权益的证明。透明永远比黑盒让人放心。
说实话,做了这么久的 Agent 相关项目,我觉得能把 agent-skills 这层做明白的人,做出来的智能体应用在稳定性、可维护性、安全性上都会明显高一个档次。技能层不是中间多出来的一道繁琐流程,而是把混乱变清晰的必经之路。你在实际落地时如果也遇到了什么怪问题,欢迎按上面的排查表先自查一遍,多数情况下,问题都藏在描述、参数、权限、幂等这几个环节里。