1. agent-skills 到底是什么,为什么现在就要开始做
1.1 从一个真实的尴尬场景说起
上周有个朋友给我看他的 Agent 项目,说是已经接入了大模型,也能跑通几个工具调用。代码里密密麻麻堆着几十个函数,什么get_weather、send_email、calc_price、fetch_user_info。乍一看功能齐全,但一跑起来就出问题:模型明明需要查库存,却调用了算价格的函数;同一个登录态逻辑在三个函数里各写了一遍;改了一下数据库字段名,所有工具函数全部报错。他最痛苦的是想加一个新能力,总得翻半天代码,生怕动一处崩一片。
这个问题本质上不是“工具不够多”,而是“技能没有体系化”。我在做 Agent 落地时也有过同样的经历,后来花了大力气把所有零散函数重构成一个可插拔的技能库,思路就是agent-skills这个方向。简单说,agent-skills不是某一个具体功能,而是一套面向 AI Agent 的“技能管理方案”——把原本散落在代码里的工具函数,升级成有规范、有描述、有输入输出协议、可独立注册和调用的技能单元,让模型能更准确地按需调用,让开发者能持续新增和迭代能力。
这篇文章不是给你讲某个开源框架的 API,而是把我自己做技能库时的完整思路、接口设计、实现过程和踩坑记录整理出来。无论你是在做聊天机器人、自动化助手还是复杂的多步骤 Agent,这套方法都能用得上。边看边抄,基本能避开我踩过的所有坑。
1.2 技能和工具到底有什么不一样
很多人会问:我直接用 function calling 不就行了吗,为什么还要单独搞一个“技能库”?这里有一个很重要的概念区分。
工具(Tool)通常指一个具体的函数或 API,比如search_documents(query),它解决的是“做某件事”。而技能(Skill)更像是“知道在什么情境下、用什么参数、按什么流程去做某件事”的能力封装。举个例子:get_weather(city)是一个工具,但“帮用户规划周末出行”就是一个技能——它会根据目的地天气、交通状况、用户偏好综合推荐,内部可能要调用多个工具,还要判断哪一步先做。
agent-skills的核心价值就是把工具调用从“碰运气”变成“有章法”。你不仅要告诉模型“有哪些函数可以用”,还要告诉它“这个技能适合什么场景、需要哪些上下文信息、输出应该是什么格式”。这样模型才能更加稳定地做出决策,不会在简单任务上反复横跳。
从我实际测试的经验看,没有技能库时,即便函数描述写得很清楚,模型仍然容易混淆参数和调用时机。有了统一的技能描述和强弱校验,成功率能提升不少。尤其是涉及多步骤任务时,技能之间的编排逻辑理顺了,整个 Agent 的行为可靠度会是两个量级。
2. 技能设计的关键思路:先定义接口,再考虑实现
2.1 给每个技能定义统一的“三件套”:能力描述、输入参数、输出协议
我刚开始做技能库时,犯过一个低级错误:每个函数都按自己的习惯去写注释,有的写一两句话,有的写了一整页文档,格式五花八门。结果模型一看就懵,根本不知道优先用谁。后来我强制自己遵循一套统一的技能描述规范,核心就是三件套:
- 能力描述(description):一句话说清这个技能是干什么的、适合在什么情况下调用。不要写“该函数用于获取数据”这种废话,而要写“当用户询问某城市当前或未来几天的天气情况时,调用此技能获取天气数据”。
- 输入参数(parameters):列出每个字段的名称、类型、必填性、取值范围以及示例。这一步特别重要,因为模型需要从用户对话里提取参数,如果字段定义不清楚,它很容易填错。
- 输出协议(output schema):明确返回的数据结构,至少要有状态码、业务数据、错误信息这三个字段。这样模型就能判断调用成功还是失败,决定下一步是继续还是换一种方式。
这三件套看似简单,但实际写起来有讲究。比如参数描述不能只写类型,还要写“取值逻辑”。我之前写过一个参数叫time_period,类型是 string,但没说明可取值是today、tomorrow、weekend,结果模型在调用时生成了next week这种乱七八糟的值。后来我把枚举直接列在描述里,错误率立刻降下来了。
2.2 技能分类:基础型、复合型、编排型
把技能都放在一个平面里,到后面一定会乱。我的经验是,先按复杂度分成三类,分别对待。
- 基础型技能:对应一个原子操作,比如查询数据库、发 HTTP 请求、读文件。这类技能要尽量“无状态”,同一个输入永远得到同一个输出,容易缓存和复用。
- 复合型技能:由多个基础技能组合而成,执行固定流程。比如“生成周报”,它会先拉取项目数据,再调用文本总结,最后格式化输出。复合型技能内部流程是确定的,模型只需要提供必要参数。
- 编排型技能:这类最灵活,它内部不是固定顺序,而是根据当前上下文动态决定调用哪些基础技能。比如“帮用户安排旅行计划”,需要根据城市、日期、预算去查天气、查酒店、查交通,每一步的结果都会影响下一步动作。
为什么要做这个分类?因为设计和测试的侧重点完全不同。基础型技能要重点测试边界条件,比如参数缺失、超时、返回异常;复合型技能要重点测试流程中的异常中断情况;编排型技能则要重点测试模型在中间决策点上的准确度——这一步往往会受提示词影响很大。
我建议你不管项目多小,先按这个分类把现有能力整理一遍。整理完后,你会发现哪些技能其实是重复的,哪些是伪需求,哪些结构上就很别扭,趁早拆掉重来。
2.3 设计技能接口时的三条硬性选择
在一次线上分享中,我总结过三条自己比较坚持的原则,算是从实际教训换来的。
第一,技能优先做成无状态。不要在技能内部保存用户会话数据,所有需要的数据都通过参数传入,或者从共享上下文中读取。否则技能之间容易互相“埋雷”,尤其当多个技能共享同一个会话时,一个技能修改了状态,另一个技能的行为就会变得不可预测。
第二,每个技能都要保证可重试。Agent 调用技能时经常遇到网络抖动、服务超时,如果技能不是幂等的,重试就会造成数据重复、费用翻倍。最简单的做法是给涉及写操作的技能增加一个request_id参数,服务端去重,这样即使重试也安全。
第三,技能的输出必须能支撑后续决策。光返回一行文本是不够的,至少要返回结构化的数据加一层“置信度”或“状态说明”。比如查询用户订单,如果查不到,应该返回status: "not_found",而不是抛出一个异常。原因很简单,模型需要根据状态来决定是道歉、追问还是换一种策略。如果所有失败都表现为异常,模型就没有办法做出合理应对。
这三条原则看起来会增加一点开发量,但对于一个长期维护的 Agent 项目来说,省下的麻烦远比投入多。
3. 从零到一实现一个可插拔技能库:实操记录
3.1 目录结构与技能元信息定义
我在项目里通常会建这样一个目录结构:
agent-skills/ ├── skills/ │ ├── weather/ │ │ ├── SKILL.md │ │ ├── impl.py │ │ └── tests/ │ ├── document_summary/ │ │ ├── SKILL.md │ │ ├── impl.py │ │ └── tests/ │ └── ... ├── loader.py ├── registry.py └── examples/每个技能单独一个文件夹,SKILL.md是技能的元信息和声明文件。这个文件建议用 Markdown 写,既方便人看,也可以让解析器读取。我用这样的格式:
--- name: weather_query description: 当用户询问某城市当前或未来几天天气时使用。支持城市名,可指定日期。 version: 1.0.0 author: your-name tags: [weather, query] --- ## input - city: string, required, 城市中文名,如“北京”。 - date: string, optional, 日期,格式 YYYY-MM-DD,默认今天。 ## output - code: int, 0表示成功,非0表示失败 - data: object, 包含 temperature, condition, humidity, wind - error: string, 错误信息,code非0时有效把技能元信息独立放到一个文件里,最大的好处是加载器不用去 import 每个 Python 模块就能知道技能的全貌。在做技能列表展示、权限控制、依赖分析时都特别方便。如果直接去读函数签名,不仅慢,还需要执行代码,容易触发副作用。
3.2 技能注册机制:如何让新增技能像插 U 盘一样简单
注册机制是我一开始就坚持要做的。因为我不希望每次新增技能都要去改一个大配置或者注册表,那样迟早会改出问题。我采用的做法是“按目录约定 + 自动扫描”:
- 启动时扫描
skills/下所有包含SKILL.md的文件夹。 - 解析
SKILL.md的元数据,校验必填字段是否完整。 - 动态加载对应实现模块(
impl.py),并将模块中标记了@skill_impl("skill_name")的函数绑定到技能名称上。 - 如果校验失败或加载时报错,就把该技能标记为
disabled,并记录错误原因,不影响其他技能加载。
使用装饰器绑定实现,代码写起来很舒服:
# impl.py from skill_sdk import skill_impl @skill_impl("weather_query") def run_weather_query(city: str, date: str = None) -> dict: # 实现逻辑 ...注册器会自动把这个函数关联到SKILL.md中的name上。这样,当我需要新增一个技能时,只要创建一个文件夹,写好SKILL.md,在impl.py里实现函数并打上装饰器就行。删除技能时,直接删掉文件夹,下次启动自动失效。
有一点要注意:自动扫描虽然方便,但也要做好“技能屏蔽”机制,有些技能暂时不想要了,不想删文件,就加一个enabled: false的字段。然后再明确约定:外部不要随便改这个字段,避免测试时误伤。
3.3 技能调用的统一入口:上下文、路由和执行
所有技能都通过一个统一的入口执行,类似agent_skills.execute(skill_name, params, context)。这里的context是全局上下文对象,里面会放一些通用信息,比如用户 id、会话 id、当前时间、环境标等。技能函数不需要也不应该自己解析这些信息,直接通过context读取即可。
执行时有个关键步骤叫路由选择。如果用户说“帮我查一下明天天气”,模型可能觉得需要调用某个技能,但实际技能库里并没有直接叫get_weather_tomorrow的技能。这时统一入口可以做一个别名映射:把常见的口语化名称映射到正式技能名。比如:
ALIASES = { "明天天气": "weather_query", "明日天气": "weather_query", "天气": "weather_query", }在模型调用之前,先做一遍别名解析,能避免很多因为一个名字没对上而导致的失败。
此外,统一入口里我还加了一个“前置拦截层”。天然适合做权限检查、参数预校验、流量控制。比如某些技能只允许管理员调用,某些技能需要强制刷新参数格式,都可以在这一层处理,不需要每个技能重复实现。这样做的好处是:即使将来换掉后端大模型,技能调用层不用跟着变。
3.4 实战示例:从零实现一个“文本摘要”技能
为了让你更有代入感,我完整记录一下我实现document_summary技能的过程。
首先,创建文件夹和SKILL.md:
--- name: document_summary description: 当用户需要将长文本、文章、会议纪要提炼成简短摘要时使用。适合超过500字的文本。 version: 1.1.0 tags: [text, summary, nlp] --- ## input - content: string, required, 待摘要文本 - max_length: int, optional, 摘要最大长度,默认200 - style: string, optional, 摘要风格,可选 concise(简洁)或 detailed(详细),默认 concise ## output - code: int, 0 成功,非0失败 - data.summary: string, 生成的摘要 - data.char_count: int, 摘要实际字符数 - error: string, 错误信息impl.py实现逻辑:
from skill_sdk import skill_impl @skill_impl("document_summary") async def summarize(content: str, max_length: int = 200, style: str = "concise", context=None): if not content or len(content.strip()) < 20: return {"code": 400, "data": None, "error": "content too short"} if style not in {"concise", "detailed"}: style = "concise" # 这里可以调用大模型 API,也可以调用本地模型 # 关键是要控制 prompt,并做好长度截断与去重 summary = await call_summary_api(content, max_length, style) return { "code": 0, "data": { "summary": summary, "char_count": len(summary), }, "error": None, }我特别想提醒的一点:不要只顾着写调用逻辑,一定要在技能里考虑“不可用”的情况。比如大模型 API 超时,你要捕获异常并返回一个可读的错误状态,而不是让技能直接崩溃。否则整个 Agent 可能会卡死,用户等半天得不到任何反馈。
注册之后,这项技能会自动出现在技能列表里。接着用一个小测试验证调用:
result = await execute("document_summary", { "content": "这里的正文足够长,可以用来测试摘要功能……", "max_length": 100, }) assert result["code"] == 0我在这个示例里故意把参数校验写得简单了点。实际生产环境建议加上更严格的内容审核、敏感信息检测以及成本控制。比如当文本过长时,要分块处理然后再合并摘要,而不是一次性丢给模型,否则 token 费用很惊人。
4. 技能评测与多场景适配:不能只看“它能跑”
4.1 为什么评测要前置,而且要持续做
有一段时间我盲目相信“技能能跑通”就等于“技能好用”,结果上线没几天就就出问题。问题集中在两类:一类是调用成功率还行,但返回结果质量不稳定;另一类是某些调用对应的事件缺失,模型误判成技能不可用。后来我吸取教训,专门建立了一个评测流程,每次技能变更都必须跑一轮数据,才允许发布。
评测前置的价值在于:你能在模型层受到影响之前,先发现技能层的问题。比如,某个技能在参数缺失时应该返回什么?模型会因为你的返回格式而决定要不要继续追问。如果你返回的是error: "invalid",模型可能直接道歉结束;如果返回的是error: "missing_parameter: city",模型大概率会追问用户城市信息。这个差异非常大,直接决定用户体验。
4.2 评测集怎么建:覆盖正常、边缘、恶意与噪声
我通常给每个技能准备三组评测用例:
- 正常用例:覆盖典型场景。比如天气查询,要有不同城市的请求、有日期变换、有切换城市后的查询。
- 边缘用例:覆盖参数缺失、参数类型错误、边界值(空字符串、超长文本、极小数)。
- 噪声用例:故意测试无关请求或者夹杂其他意图的话语,确保技能不会乱触发。
还可以引入一个“意图混淆”的测试:比如用户只是随口问“今天会不会下雨”,此时天气查询和“出行建议”技能都可能被触发,需要看模型到底选了哪个。如果经常选错,说明技能描述里的场景区分还不够明确,需要调整描述。
评测指标上,我一般不只看准确率,更看重三件事:技能调用成功率、技能执行完整率、语义混乱率。前两个好理解。第三个“语义混乱率”是指正常调用技能,但返回结果和用户需求明显不符的比例。比如用户问“明天天气”,技能返回了昨天的数据,虽然调用是成功的,但语义是错的。这种问题往往来自技能内部的日期计算逻辑,用普通成功率根本测试不出来。
4.3 多场景适配:同一技能如何服务不同领域
agent-skills还有一个很有意思的玩法:同一套技能框架,可以适配不同业务场景。比如我在公司内部推广这套方案时,电商团队做“订单查询”技能,医疗团队做“预约挂号”技能,但底层的注册、调用、权限校验、日志追踪都是同一套代码。
具体适配时,要额外保留“场景标识”这个字段。在context里带上scene,技能内部可以根据场景决定不同的策略。例如“天气查询”技能在出行场景下会返回进一步的城市建议,在农业场景下可能更关注降水概率和温湿度。但注意,不要让scene变成一个巨大的 if-else 分支,否则技能会变得笨重。我的建议是,每个场景只做少量差异化分支,其余逻辑共用。
多场景适配还有一层含义是指“模型在长上下文里的表现”。当 Agent 同时挂载了十几二十个技能时,有些模型会忽略部分技能,总挑熟悉的调用。针对这个问题,一是要精简技能描述字数,描述越短越容易被模型记住;二是可以对技能做“排序”,把用户可能更常用的排在前面。这些细节只能在多场景反复测试中慢慢调出来。
5. 常见问题与排查技巧:我在实际运行中遇到的坑
5.1 技能响应太慢,怎么定位瓶颈
技能慢的坑我掉过太多次了。一次是连接远程数据库每次请求都新建连接,导致每次查询要花2秒多。另一次是文本摘要技能里因为某个参数没设置超时,大模型接口卡了 30 秒才返回,整个对话像死掉一样。
排查思路我总结成一个四步法:
- 第一步,先看技能本身在内部耗时多少,绕过模型调用,直接用脚本调用技能实现函数。如果本身很快,瓶颈在模型决策层;如果本身很慢,继续往下找。
- 第二步,检查技能实现里有没有可以优化的连接复用、缓存、索引。比如数据库查询就加上索引;HTTP 请求就用连接池;重复计算就做结果缓存。
- 第三步,检查上下文内容是不是太大。很多技能其实不关心整个对话历史,但框架把所有历史都传进去了,导致大模型处理很慢。
- 第四步,看有没有“重复执行”问题。有些 Agent 框架会为了“保证成功”而自动重试技能调用,但失败后并不通知上层,结果一条请求实际执行了三次,每次都写入数据,用户看到结果晚,数据还重复了。
另外,我强烈建议给每个技能加上耗时日志,至少记录total_time和stage_times。这样线上出问题可以直接拉日志,而不是瞎猜。
5.2 技能输出不稳定,经常有遗漏字段怎么办
模型调技能时,一项常见故障是参数漏传。明明技能要求必填city,模型却经常只传date。这种情况问题基本不在模型,而是在技能描述本身不够“锁定”。
我的解决办法是两点:
- 在
SKILL.md里给每个必填参数加一句“如果用户没有提供该参数且没有合理的默认值,直接报错 missing_parameter,不要编造。”很多模型很会想象,不明确禁止时,它就会自己造一个出来。 - 在入口执行时,做严格的参数校验。凡必填缺失的,直接返回
missing_parameter:xxx这样格式明确的错误,而不是继续执行。这种做法看起来“不近人情”,实际对模型反而友好,因为它得到了清晰的反馈,可以立刻追问用户。
还有一个隐藏点:输出字段不稳定。比如摘要技能应该返回summary,偶尔返回了content,调用方拿不到正确字段。为此,入口层我会做“输出协议校验”,用轻量 JSON Schema 校验一下返回结构。如果不符合协议,可以选择自动修复某些字段名,或者记录错误并返回一个标准错误。这种提前校验能避免很多下游出现不可解释的KeyError。
5.3 技能之间存在依赖和冲突,怎么处理
技能多了以后,依赖问题会出现。比如“订单查询”和“用户信息查询”都要访问用户中心,如果用户中心的接口升级了,这两个技能都会受影响。我的做法是引入轻量级的依赖声明和检查,比如在SKILL.md里加入dependencies字段,列出需要的服务名或共享缓存。启动时统一检查这些依赖是否可用,如果不可用,技能标记为degraded,调用时给上层一个降级提示。
冲突的情况常见于“一个业务动作可以由多个技能来完成”。比如“给用户发优惠券”这个操作,可能同时有“优惠券系统技能”和“运营活动技能”两个都能做。为了不让模型纠结,我会在技能描述里明确边界:这个技能适合什么,不适合什么。必要时直接禁用另一个技能,只保留最合适的一个。
如果实在无法避免,也可以做一个“技能路由提示”组件,在调用前根据业务规则自动去掉不合适的技能。但这属于相对重型的方案,大多数项目建议从精简技能数量和描述入手。
5.4 关于 agent-skills 项目,我最后想分享的一点体会
做了这么久的技能库,我最大的体会是:它不是一个一劳永逸的方案,而是需要持续迭代的“活系统”。你今天定义的技能描述,明天可能因为模型升级就变得不再合适;你原本觉得完美的分类,跑了一段时间后可能又会发现新的边界情况。所以不用追求一开始就设计一个完美架构,先把几个核心技能用标准方式管起来,跑通流程,再慢慢扩展。
另外,有一个小技巧我特别想分享:每次给技能写描述时,都假设自己完全不了解业务,只凭描述去调用。如果描述里出现了“获取信息”这种说了等于没说的话,那就要重写。反之,如果一段描述让一个外行都能准确判断什么时候该用,那就说明它足够清晰了。这个习惯帮我避免了很多上线后才发现的问题。
agent-skills看起来只是一个小工具,但它背后反映的其实是 Agent 工程里最容易被忽视的部分:能力的封装和治理。如果你正被 Agent 的不可控、难扩展困扰,不妨从整理自己的技能库开始。相信我,这一步投入的收益,远超你的预期。