做智能体(Agent)做到第二年,我越来越觉得,圈子里最被低估的开源项目,大概就是agent-skills这种看起来不起眼的技能库方案。模型本身会推理、会对话,但你让它去操作真实系统时,它连“把文件按日期归档”都做不了——不是模型笨,而是它手上没有“工具说明书”。agent-skills解决的正是这个问题:把可复用的脚本、API、命令行操作,统一封装成带元数据、校验规则和自然语言说明的“技能包”,让大模型在需要时按描述自动挑选并调用。
这篇文章我不会去复述官方 README,而是从工程落地角度,把agent-skills的设计思路、接入姿势、生产环境里踩过的坑,以及我维护这套技能库半年后的真实心得,一次讲透。不管你是刚接触智能体开发的新手,还是已经被“工具满天飞、维护一地鸡毛”折磨过的老手,这篇应该都能给你一点可抄作业的东西。
1. 先搞清楚:agent-skills 到底在解决什么问题
1.1 智能体落地的最大阻碍不是模型,是“手和脚”
很多人第一次接触智能体时都会有个错觉:模型强了,Agent 就强了。真做项目就会发现,模型能力在单次对话里确实够用,但一旦要“做事”,比如操作数据库、读取本地文件、调用内部系统接口,模型立刻变成只会说话的旁观者。传统做法是给模型塞一堆 JSON Schema,告诉它哪个函数能干什么,这叫 function calling。但这个方案在规模小的时候很爽,规模一大就崩:
- 几十个工具的 JSON Schema 全部塞进上下文,一次请求光工具描述就吃掉几千甚至上万 token;
- 工具之间的边界模糊,模型经常选错;
- 同一个脚本在三个项目里被复制了三份,改了 A 忘了 B,行为漂移。
agent-skills的思路是把“工具调用”从即时定义变成“技能资产”。一个技能包不仅包含可执行代码,还包含一段写给模型看的自然语言说明、参数校验规则、测试用例,以及适用条件和禁用条件。模型看到的是一个高度结构化的“技能清单”,而不是一堆冰冷的函数签名。我在某个内部自动化 Demo 里验证过,引入技能库后,模型选错工具的概率下降了大概六成,不是说它变聪明了,而是技能描述把“什么时候不该用”这件事写清楚了。
1.2 没有技能库时的三种狼狈现场
光说理论不过瘾,讲三个我亲自见过的真实现场。
第一个现场:工具描述越写越长。某项目的系统提示词里,工具说明从 6000 字膨胀到 20000 字,每次调用都要携带,响应速度肉眼可见地变慢。没人敢删,因为删了某个工具,模型就真的不会用了。
第二个现场:格式地狱。模型对 function calling 参数的格式理解经常“差一点”,比如日期传成字符串、数组传成逗号分隔文本。每修一次,就得在代码里加一段临时解析逻辑,越补越乱。后来发现,真正需要的不是更聪明的解析,而是每个工具入口有严格 schema 校验,参数不合法直接拒绝,把错误暴露在最初环节。
第三个现场:技能孤岛。A 同事写了一个“PDF 转文本”脚本,B 同事不知道,于是又写了一个参数风格完全不同的版本。做集成时才发现两边输出还不一致。
在agent-skills的框架下,这三个问题被归并成一件事:技能包必须是声明式的、带文档的、可版本化的。它不是某个模型的专属工具,而是团队共用的技能资产。
1.3 它适合谁,不适合谁
我在技术分享时经常被问:“这东西和 MCP 是什么关系?”“是不是所有 agent 都要上技能库?”我一般用一张表回:
| 方案 | 核心思路 | 上下文开销 | 动态发现 | 适用规模 |
|---|---|---|---|---|
| 裸用 function calling | 每个工具都塞 JSON Schema | 高,随工具数线性增长 | 无 | 工具少于 10 个 |
| MCP | 工具通过协议暴露,客户端按需获取 | 中,仍需把工具描述发给模型 | 支持远程发现 | 跨进程、跨语言 |
| agent-skills | 技能包带文档和校验,可检索可注入 | 低,可按需检索技能再注入 | 支持本地注册 | 工具多、复用强 |
| 自研 registry | 自己写一套注册中心 | 看实现 | 看实现 | 团队有大后端能力 |
agent-skills不是来淘汰 MCP 的。MCP 解决的是“工具如何传输”,agent-skills 解决的是“技能如何被描述、选择、复用”。实际项目里两者经常配合:技能包内部用 Python 实现,对外通过 MCP Server 暴露给主 Agent。
那它不适合谁?如果你只是给一个聊天机器人加两个接口查询能力,直接注册两个 function 就完了,上技能库属于过度设计;如果你完全没有测试习惯,只想“能跑就行”,技能库反而会增加你的维护负担。它的甜区,是场景复杂、技能数量超过 20 个、且同一个技能要被多个 Agent 复用的工程化场景。
2. 核心设计拆解:那个不起眼的技能包为什么这么好用
2.1 技能包的真实目录结构长什么样
我在把内部脚本迁移到agent-skills时,最先做的是理解它的目录约定。一个标准技能包大概长这样:
skills/ batch_rename_files/ SKILL.md execute.py requirements.txt tests/ test_execute.py assets/ sample_input.txtSKILL.md是整个技能包的灵魂,它负责和模型对话;execute.py是技能真正的执行体;tests是保障技能质量的回归测试;assets里放测试用的样例数据。
很多人第一眼会觉得“这不就是个带说明文档的脚本目录吗?”对,但厉害的点在于SKILL.md里的结构。它必须包含 front matter 元数据,有点像博客文章的 YAML 头:
--- name: batch_rename_files description: 按规则批量重命名指定目录下的文件。 when_to_use: 当用户有一批文件需要统一命名、添加前缀/后缀、或修改扩展名时。 when_not_to_use: 当用户只是询问某个文件是否存在或叫什么名字时,请使用 list_files。 parameters: directory: type: string required: true description: 待处理目录的绝对路径 pattern: type: string required: false default: "{index:03d}_{name}" description: 命名模板,支持 {index} 和 {name} 占位符 dry_run: type: boolean required: false default: true description: 为 true 时只输出改动预览,不实际执行 ---这套结构把“工具描述”从代码里彻底抽离出来了。之前我写的工具函数,注释里也有一点说明,但零散且不可校验。现在SKILL.md是唯一的信息源,解析它,就能生成模型所需的 tool schema;校验它,就能保证描述和实现不脱节。
2.2 description 才是灵魂:怎么让模型一眼选中你的技能
如果说agent-skills有“黑魔法”,那一定藏在description里。我见过太多人把技能描述写成功能清单:“本技能提供批量重命名、添加前缀、修改扩展名等功能。”这种描述不是给模型看的,是给 README 写手看的。模型做技能选择时,输入的是一段自然语言任务,它需要匹配的不是“功能多不多”,而是“这个任务是否命中技能的适用条件”。
所以我在项目里对描述有个硬性要求:必须写清 when_to_use 和 when_not_to_use,而且要写得像“决策树的分支条件”。比如一个search_files技能,我不写“搜索文件”,而是写:
当用户想按文件名、扩展名、内容关键字查找文件时使用;当用户已经确定文件路径、只是想读取内容时不要用,应使用 read_file;当用户想搜索的是代码仓库里的符号定义时不要用,应使用 code_search。
这样写之后,模型选错的概率会明显下降。我不止一次对比过:同一批技能,描述改得越“决策化”,评测集的工具选择准确率越高。写技能描述本质上是在写提示词,只不过这个提示词是给“技能选择器”看的。
2.3 参数 schema 是安全感的来源
模型接口调用时参数格式不可控,这是老问题。agent-skills的做法是通过 JSON Schema 对参数做运行时校验,不允许的字段直接报错,并把“如何修正”的提示返回给模型。
我自己的经验是,设计技能参数时有三条原则:
- 参数尽量扁平。能平铺的字段不要做嵌套对象,模型维护嵌套结构的出错率高。
- 能用枚举的别用自由文本。比如“执行模式”这种参数,最好定义成
dry_run / execute两个枚举,而不是“是否执行”这种语义模糊的字符串。 - 每个参数必须有默认值且默认值必须安全。我所有涉及删除、移动、覆盖操作的技能,默认都是
dry_run=true,模型不显式确认,就不会真正改动文件。这个设计救过我很多次。
校验不通过时,返回信息也要规范化。我见过不少实现直接把 Python traceback 整个丢给模型,模型反而被异常信息带偏。正确做法是把异常翻译成模型能理解的“参数错误提示”,再附上正确示例。
3. 实操:5 分钟写一个自己的技能包并跑通
3.1 准备最小环境
动手永远是理解项目最快的方式。先准备一个干净的 Python 环境:
python -m venv .venv source .venv/bin/activate pip install agent-skills如果不想装 SDK,也可以直接把agent-skills仓库里skills/目录作为技能根目录,然后把技能根目录路径写在配置里。我自己更推荐用 SDK,因为它会把“技能注册”“运行时校验”“模型接口适配”这几个环节串起来,后面接入 Agent 时会省很多事。
配置里最关键的是skills_root指向哪个目录。我习惯按领域分子目录:
skills/ files/ # 文件与目录操作 web/ # 网页抓取与搜索 data/ # 数据处理 system/ # 系统命令封装这样目录即命名空间,技能多了以后好管理,也方便给不同领域的技能设置不同的权限。
3.2 写第一个真实技能:批量重命名文件
在skills/files/batch_rename_files/下面创建SKILL.md,内容就用 2.1 那个例子。再创建execute.py:
#!/usr/bin/env python3 """skill: batch_rename_files""" import argparse import json import re from pathlib import Path def validate(directory: str, pattern: str = None, dry_run: bool = True): base = Path(directory) if not base.exists(): return {"ok": False, "error": f"目录不存在: {directory}"} if pattern is not None and not isinstance(pattern, str): return {"ok": False, "error": "pattern 必须是字符串"} return {"ok": True, "directory": str(base), "files": len(list(base.iterdir()))} def run(directory: str, pattern: str = "{index:03d}_{name}", dry_run: bool = True): base = Path(directory) if not base.exists(): return {"ok": False, "error": f"目录不存在: {directory}"} changes = [] for index, f in enumerate(sorted(base.iterdir())): if f.is_dir(): continue new_name = pattern.format(index=index, name=f.stem, ext=f.suffix) target = base / f"{new_name}{f.suffix}" if target.exists() and target.resolve() != f.resolve(): continue changes.append({"from": str(f.name), "to": str(target.name)}) if dry_run: return {"ok": True, "dry_run": True, "changes": changes} for item in changes: (base / item["from"]).rename(base / item["to"]) return {"ok": True, "dry_run": False, "changes": changes} if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--directory", required=True) parser.add_argument("--pattern", default="{index:03d}_{name}") parser.add_argument("--execute", action="store_true", help="默认是 dry_run") args = parser.parse_args() result = run(args.directory, args.pattern, not args.execute) print(json.dumps(result, ensure_ascii=False, indent=2))这段代码刻意保持简单,但已经把技能库的关键特点体现出来了:有validate做前置校验,有run做核心逻辑,而且默认不真执行。写技能时我强烈建议保留命令行入口,这样不用组装 Agent 也能先手动调试。
注册技能时,可以用 SDK 做个简单调用:
from agent_skills import SkillRegistry, ExecutionContext skills = SkillRegistry(skills_root="./skills") ctx = ExecutionContext(timeout=10, sandbox=True) result = skills.execute( "batch_rename_files", {"directory": "./downloads", "pattern": "photo_{index:03d}", "dry_run": True}, context=ctx, ) print(result.output)第一次跑通的时候,你会看到一条完整的调用链路:技能被找到、参数被校验、dry_run 结果被返回。这个链路就是agent-skills在真实 Agent 里的核心调用路径。
3.3 本地验证清单:跑通三步
写完技能别急着接 Agent,先按下面三步自测:
| 测试项 | 操作 | 通过标准 |
|---|---|---|
| 单测 | pytest skills/files/batch_rename_files/tests | 全部通过 |
| 命令行 | python execute.py --directory ./tmp --pattern "x_{index:03d}" | 输出 JSON 变更预览 |
| SDK 调用 | 调用skills.execute | 校验拦截生效、返回结构化结果 |
这三步看着简单,但能拦住绝大部分低级问题。最容易被漏掉的是第二步,因为它模拟的是不经过模型、直接调用技能的真实执行路径,很多“模型参数传歪了”的问题,最终都能在命令行复现。
4. 接入你自己的 Agent:三种主流姿势详解
4.1 姿势 A:作为工具直接注册
如果你的 Agent 用的是模型原生的 function calling / tool calling,最直接的姿势就是把agent-skills里的每个技能动态转成一个工具:
{ "type": "function", "function": { "name": "batch_rename_files", "description": "按规则批量重命名指定目录下的文件。当用户需要批量添加前缀后缀时使用;当用户只是询问文件信息时不要用。", "parameters": { "type": "object", "properties": { "directory": {"type": "string", "description": "目录路径"}, "pattern": {"type": "string", "default": "{index:03d}_{name}"}, "dry_run": {"type": "boolean", "default": true} } } } }这种方式的好处是链路最短、延迟最低,特别适合技能数量还不多、单机部署的场景。缺点也明显:所有技能描述都要一次性传给模型,技能一多,上下文开销就上涨。所以我一般建议,技能数超过 15 个时就不要全量注册了,改走“先检索再注入”的方式。
我实际做的一个内部方案是:所有技能元数据存在本地索引里,收到用户请求后,先用一个轻量级语义检索挑出候选技能(Top 5),再把候选技能的描述转成 tool schema 传给模型。这样上下文里永远只有候选技能的描述,而不是全部。
4.2 姿势 B:通过 MCP Server 暴露
当 Agent 和技能执行端不在同一个进程,甚至不在同一台机器时,MCP 就是更合适的传输层。agent-skills里每个技能可以看作一个天然的标准工具,写一个很薄的适配层就能把它们暴露成 MCP 工具。
一个典型的 MCP 工具定义会包含:
- 工具名称,对应技能名;
- 输入 schema,直接读
SKILL.md里的parameters; - 执行入口,调用技能包的
run; - 错误处理,把异常翻译为协议层的错误返回。
MCP 带来的最大价值是动态发现:Agent 可以通过协议查询“服务器上有哪些工具”,并按需获取工具描述,而不是把全部描述预置在提示词里。对大型组织来说,这意味着多个团队可以独立发布技能服务,Agent 像浏览应用商店一样挑选技能。
但要说句实话,MCP 也有成本——协议多一层,问题排查链路就长一层。我建议前端小团队先不要上 MCP,等技能被两个以上消费方复用时再考虑。
4.3 姿势 C:把技能描述注入系统提示词
还有一类轻量场景,不想引 SDK,也不想起 MCP 服务,那就把技能描述直接拼进系统提示词里。适合技能只有三五个、且都是稳定操作的场景。
你是一个自动化助手,可以使用以下技能: 1. batch_rename_files:按规则批量重命名文件。参数:directory、pattern、dry_run。 2. search_files:按文件名和内容搜索文件。参数:query、directory。 ...其余技能请忽略...这种方式简单直观,但没有运行时参数校验,也没有技能版本控制,技能一多体验会迅速恶化。
三种姿势没有绝对的优劣,放一起对比更清晰:
| 接入方式 | 动态发现 | 上下文开销 | 运维复杂度 | 推荐场景 |
|---|---|---|---|---|
| 工具注册 | 差 | 高 | 低 | 技能少、单体 Agent |
| MCP Server | 好 | 中 | 中 | 多团队、跨进程 |
| 提示词注入 | 无 | 最低 | 最低 | 三五个技能、验证想法 |
我的习惯是:先按姿势 A 验证核心链路,等技能数量膨胀或需要多进程复用时,切换到姿势 B;姿势 C 只在原型阶段用,不放进生产。
5. 生产环境必须盯住的五个暗坑
5.1 技能本质是代码执行,安全边界千万别省
这是我想强调的、也是事故率最高的一环。你可能已经看出来了,技能包就是一堆 Python 脚本,模型会按照自然语言任务来选技能、传参数。这意味着:一个用户完全可能在对话里诱导模型调用一个“执行系统命令”的技能,然后传一个危险参数。
我的安全底线是四件事:
- 默认沙箱:技能执行在受限进程里,不能访问系统关键环境变量以外的文件路径;
- 超时终止:每个技能有执行超时,超过即刻终止,防止死循环;
- 参数白名单:涉及路径操作的参数,做真实路径解析,禁止
..跨出允许目录; - 危险操作默认拒绝:删除、覆盖、移动等操作,除了
dry_run=true,还要在技能代码里二次确认。
有朋友会问,这不是增加了开发成本吗?对,是增加了一点,但你想一下:模型生成的参数是不可穷举的测试样本,你再怎么测,也测不完恶意输入。把安全边界做进技能框架里,是唯一可靠的方法。
5.2 技能漂移:模型升级后,技能说明书会过期
技能漂移这个词是我自己总结的。现象是:模型升级后,原本能正确调用的技能突然开始选不出来、参数传不对了。原因通常是模型对 description 的理解方式变了,或者我们写描述时隐含的措辞不再被新模型偏好。
怎么治?答案是建立回归集。我会为每个关键技能准备 5~10 个典型用户请求,比如“把下载文件夹里的 jpg 统一加上前缀 trip_”,然后固定模型和温度,定期跑一遍“请求 → 技能选择 → 参数提取”的评测,记录准确率。准确率一旦下降,立刻检查技能描述是否有歧义,或者是否需要针对新模型调整措辞。这不只是新模型的问题,我们自己修改 description 之后也必须跑回归。
5.3 上下文被技能描述吃光
不算复杂的算术:假如你有 50 个技能,每个技能的描述平均 800 个 token,全量注入一次就是 40000 token。任务还没开始,钱已经花了。所以生产环境里,技能数量一多,“选择哪些技能进入上下文”这件事本身就是一个检索问题。
我用的方案很朴素:先做过滤,再做排序。过滤靠 metadata 里的领域标签,比如用户问题里提到“文件”,就只保留 files 领域的技能;排序靠语义相似度,取 Top 5。这样上下文里的技能描述总开销能控制在 4000 token 左右,效果和全量注入差不多。
5.4 技能冲突:两个技能都“能”做同一件事
冲突有两种:一种是真冲突,两个技能都声明“可以批量重命名文件”;另一种是伪冲突,一个技能叫list_files一个叫search_files,都能搜到文件,但行为细节不同。模型很容易选错。
解决思路不是删掉一个,而是明确分工。我在agent-skills实践里学到的绝招是:在 when_not_to_use 里直接点名另一个技能。比如list_files写清楚“需要按关键字过滤时请使用 search_files”,search_files写清楚“只需要列出目录下所有文件名时优先使用 list_files”。把二义性写进描述,模型就不太会犹豫。
此外,注册表层我还会做一个简单的冲突检测:同一领域下 description 中同时命中相同动词短语的,构建时输出警告。
5.5 可观测性:让每次技能调用都能被复盘
Agent 失败后最痛苦的是不知道发生了什么。模型说“我调用了技能”,但技能到底执行了什么?参数是什么?返回了什么?超时了吗?这些信息没有日志,就只能靠猜。
我统一为每次技能调用打印结构化 JSON 日志:
{ "trace_id": "abc123", "skill": "batch_rename_files", "params": {"directory": "./downloads", "dry_run": true}, "model_choice_seconds": 1.2, "execution_seconds": 0.4, "ok": true, "output_preview": "rename 3 files", "error": null }有了 trace_id,就可以把模型请求、技能调用、最终回复串联起来复盘。哪一步错了,一眼就能定位。这个习惯帮我排查过好几个“看起来是模型幻觉、其实是参数没传对”的诡异问题。
6. 复盘:我用了半年之后的一些真实心得
6.1 技能库最值钱的部分不是工具注册,而是“技能即文档”
过去我们写内部工具,文档永远滞后,等工具稳定了,文档已经没人维护。agent-skills把文档变成了技能包的头等公民:没有SKILL.md的技能不会被注册,没有when_to_use的描述在检索时会被降权。这相当于把“写文档”这个动作嵌入了开发流程,而且是强制的。
我做过一个对比实验:把同一个内部接口分别写成普通 function 和完整技能包,然后让一个刚入职的新同事去接入。用普通 function 时,他反复来问参数含义;用技能包时,他直接读SKILL.md就自己跑通了。这就是“技能即文档”的价值。
6.2 维护一个内部技能市场,比维护代码库更考验治理能力
半年里我把二十多个内部脚本收敛成技能包,发现最难的并不是写代码,而是治理:
- 每个技能必须指定 owner,否则出了问题没人负责;
- 新技能合入前要有测试和 review,否则质量参差不齐;
- 技能描述里的术语要和团队统一,否则同义词太多导致检索混乱。
我现在会在技能根目录放一个GUIDE.md,写清楚新增技能的流程、命名规范、测试门槛。这个文件比任何技术实现都重要。
6.3 下一步想扩展的方向
目前我在尝试做三件事:一是给技能包加版本回滚,模型调错参数造成数据损坏时能快速恢复到上一个安全版本;二是让多个 Agent 共享同一个技能存储,技能一次发布、多处消费,消费方只读不写;三是尝试让模型根据历史调用记录自动生成或修正description,缓解技能漂移。
说实话,agent-skills这类项目离“完美”还很远,文档不算全,边界情况不少,社区也还在快速演进。但它提前帮我验证了一个重要判断:智能体要规模化落地,一定不是把所有逻辑塞进一个超长提示词,而是把能力拆成可复用、可治理、可观测的技能资产。如果你也在做 Agent 项目,与其继续在提示词里堆工具描述,不如花一个下午试试技能库的路子——大概率你也会像我一样,用完就回不去了。