Agent Skills这个说法,我第一次看到时以为是给Agent写的一套“技能树”,后来真在项目里用起来才发现,它其实是一个非常朴素的工程抽象:把Agent经常要做的重复事情打包成一个带说明、带脚本、带依赖的单元,随用随取。这阵子我一直在折腾这玩意儿,最大的感受就是“工具调用”那套老办法在复杂任务面前越来越别扭,而Skills把Agent从“什么都会一点但什么都不精”拉回到了“按需装配、开箱即用”的轨道上。这篇文章不聊PPT层面的概念,直接从实际落地的角度拆解Agent Skills到底是什么、怎么设计一个能用的Skills包、写SKILL.md有哪些坑、以及多模型环境下如何保证它能被稳定触发。适合正在做Agent应用、被Tool Calling和长上下文折磨过的朋友们参考。
1. 为什么需要Agent Skills:从工具调用到技能封装
1.1 Tool Calling的痛点
先说一个我踩了很长时间的坑。早期给Agent接能力,最常规的做法就是Tool Calling,把所有函数定义一股脑塞给模型。函数少了还好,一旦超过二十个,问题就来了。
第一是上下文污染。每个工具定义动辄几百token,二十个就是几千token,即便用GPT-4级别的大模型能“理解”,但这部分内容每轮都要跟着对话历史一起送进去,实际开销远比想象中大。第二是维护成本。工具的参数变化,Prompt里的描述就得同步改,稍微不一致模型就开始幻觉出根本不存在的参数。第三是粒度的尴尬。工具越细,Agent的组合负担越大。比如让它做个“周报汇总”,我需要分别去調“读取文件”“解析格式”“计算增量”“生成Markdown”四个工具,中间任何一步描述不清楚,结果就开始跑偏。
最难受的是,模型并不真的“会”用这些工具。它只是知道有这个函数、参数怎么填,至于函数背后是调了外部API还是跑了一段本地脚本,它完全没概念。这种“工具即函数”的模型,本质上没有把操作流程沉淀下来,每次都是现猜现编,稳定性可想而知。
1.2 Agent Skills的分层定位
后来接触到Agent Skills,才慢慢理顺了Tools、MCP、Skills和Prompt之间的关系。我习惯用一套分层去理解:
| 层级 | 举例 | 作用 |
|---|---|---|
| 基础能力 | LLM、多模态模型 | 理解、推理、生成 |
| 工具调用 | 函数、API、MCP Server | 让模型获得操纵外部世界的能力 |
| 技能封装 | Agent Skills | 把工具+流程+示例+说明打包成可复用单元 |
| 工作流 | Prompt编排、任务规划 | 把多个技能按序组合,跑通端到端任务 |
Skills不是替代Tools,而是站在Tools上面一层。它的核心是把“怎么做某件事”这件事本身打包起来:包含一份人类可读的说明文档(SKILL.md)、若干脚本、可能还有hooks和示例数据。模型在需要的时候再加载这个包,而不是一开始就把所有细节全塞进上下文里。
这也带出了Agent Skills最重要的设计思想:渐进式披露(progressive disclosure)。系统只把技能的名称、触发条件、一句话描述放在Agent可见的清单里,等Agent判断需要调用时,才完整读取技能里的使用文档。好用的Skills一定不是上来就倒垃圾信息的,而是像图书馆先给书目,再给具体书页。
1.3 适用场景分析
用了这一段时间,我觉得Agent Skills最适合下面几类任务:
- 重复性高但流程固化的工作:周报汇总、日报生成、会议纪要整理、发票信息抽取。这些流程几乎不变,把它固化成Skill,Agent不用每次重新编排。
- 依赖特定脚本或工具链的任务:比如本地解析PDF、调用内部接口生成报表。这类任务需要精确的代码执行,靠模型凭空推理不现实。
- 需要领域知识沉淀的场景:比如医疗文本脱敏、法律条款摘要。把专业领域的规则和脚本封装进Skill里,模型调用时直接获得这套领域逻辑。
反过来,聊天陪伴、创意头脑风暴、高度依赖用户实时反馈的场景,就不太适合硬做成Skill。Skills本质上是把“会做的事情”提前固化,自由聊天的随机性太高,封装反而是浪费。我也不建议把特别重的业务逻辑塞进Skill里,它更适合做“轻流程+标准输入输出”,核心业务逻辑应该留在其他服务里。
2. 理解一个Skill的结构:目录、描述、脚本与依赖
2.1 目录骨架:SKILL.md、scripts与hooks怎么摆
一个标准的Agent Skills包,我常用的目录结构长这样:
my-skill/ ├── SKILL.md ├── scripts/ │ ├── main.py │ └── utils.py ├── assets/ │ ├── sample_input.json │ └── sample_output.json ├── hooks/ │ └── pre_run.py ├── requirements.txt ├── README.md └── .env.example每个文件的角色很明确:
- SKILL.md:技能说明文档,也是Agent最先生成和读取的文件。里面写清楚技能的用途、调用方式、注意事项。
- scripts/:技能要执行的具体脚本,通常用Python或Node.js写,入口脚本支持标准输入输出。
- assets/:示例数据文件,方便模型理解输入输出格式,也方便开发者测试。
- hooks/:生命周期钩子,比如运行前的参数校验、清理临时文件等。
- requirements.txt:Python依赖清单,环境初始化时自动安装。
- .env.example:环境变量样例,避免把密钥硬编码进技能包。
一开始我也不理解为什么非得把SKILL.md和README分开,后来发现这是给两个读者看的:SKILL.md是给Agent用的说明书,README是给人类开发者用的使用文档。两者的语气和内容粒度完全不同,混在一起反而两都顾不好。
2.2 SKILL.md是灵魂:它决定了Agent能不能正确调用你
SKILL.md是整个技能包的重中之重。我见过太多技能包写得像给人看的API文档,Agent根本摸不着头脑。好的SKILL.md需要具备三个要素:
一是清晰的YAML头信息。包括name、description、license,部分实现还支持allowed-tools字段,用于限制技能内部能调用哪些工具,这其实是个很好的安全边界。
二是正文要有明确的“触发场景+操作步骤+输入输出规范”。不能只说“这个技能用于生成周报”,要告诉Agent什么时候该用、什么时候不该用、输入数据长什么样、输出结果用什么格式。描述里最好带一两个具体的触发示例句,比如“用户说‘帮我汇总本周的工作’时优先使用此技能”。
三是长度控制。SKILL.md的理想长度在1500到2500字之间。太短说明不清楚,太长会撑爆渐进式披露带来的上下文收益。我实测下来,超过3000字之后,Agent对技能内容的理解就开始出现“只记住开头和结尾”的情况。
2.3 依赖、钩子与运行环境
技术实现上,Skills通常跑在受控的沙箱环境里,所以依赖管理要做干净。我一般约定三条规则:
- 入口脚本必须支持命令行参数或标准输入传入JSON,输出JSON到标准输出。这能保证Agent调用时和人类调试时走的是同一条路径。
- 依赖尽量精简,写在requirements.txt里,环境初始化时统一安装。不要用“动态下载”的骚操作,很多沙箱根本没有外网权限,运行时报错排查起来非常痛苦。
- hooks里的预执行脚本只做轻量校验,比如检查输入字段是否存在,不要放重逻辑。我把pre_run放在脚本自身内部,反而比hooks更直观,减少一层抽象。
再补一句权限隔离的问题。技能包里的脚本能访问什么、不能访问什么,最好在SKILL.md里明文声明,并且在执行环境中给出对应限制。比如“本技能只能读取/tmp/reports目录下的文件”,这个声明既是给Agent看的,也是给后端安全策略看的。
3. 手把手构建一个Skills包:周报生成技能实战
3.1 场景选择与目标拆解
我第一个完整的Skills包是做周报汇总,场景是这样的:团队成员每天都把工作记录写到统一的JSON文件里,格式是{"date":"2025-06-03","author":"张三","tasks":["修复登录页bug","评审支付接口"]}。老板每周五要一份按人聚合的周报。以前用工具调用,我需要让Agent调用“读取文件”“遍历目录”“聚合数据”“渲染Markdown”多个函数,每次生成的格式还飘忽不定。现在干脆做成一个skill,一个调用搞定全部。
目标拆解下来很简单:
- 输入:一个时间范围或一个目录路径
- 输出:按人分组的Markdown周报
- 处理逻辑:遍历目录下的JSON文件,筛选目标日期,按作者聚合任务,渲染成周报
整个过程没有引入任何外部API,纯粹是文件处理和字符串模板,非常适合做第一个实验。
3.2 编写SKILL.md与描述词
这个环节最考验“站在Agent视角思考”的能力。一开始我写的description是“Generate weekly report”,装进去后Agent根本不调用。后来我把描述改成带场景触发条件的版本,效果好多了。一个完整的SKILL.md长这样:
--- name: weekly-report-generator description: 用于生成团队周报。当用户要求汇总一周工作、生成周报/汇报文档、按人员整理任务时使用此技能。不适用于查询历史周报或修改已有文档。 allowed-tools: - file_read - file_list license: MIT metadata: version: 1.0.0 author: team-ai --- # 周报生成技能 ## 触发条件 - 用户说“帮我汇总本周工作”“生成周报”“按人整理一下这周的任务” - 输入包含目录路径或日期范围 - 不适用于处理非周报类的文档 ## 输入规范 接收JSON格式参数: { "directory": "/tmp/tasks", "start_date": "2025-06-01", "end_date": "2025-06-07" } ## 执行步骤 1. 读取directory下的所有.json文件 2. 过滤date字段在[start_date, end_date]范围内的记录 3. 按author字段分组,聚合tasks列表 4. 渲染为Markdown格式,输出分组标题为“## 姓名” ## 输出规范 输出JSON,包含scheme字段: { "weekly_report": "## 张三\n- 修复登录页bug\n- 评审支付接口\n", "task_count": 12 } ## 注意事项 - 日期格式使用YYYY-MM-DD - 如果目录为空,输出task_count=0,不要生成空标题 - 不要修改原始文件这个文件里,精心设计的触发条件说明、输入规范、注意事项,比脚本本身更重要。Agent调用技能的决策依据全在这份文档里,它写得越具体,模型越少自由发挥空间。
3.3 实现脚本与联调
脚本本身我用Python,几分钟就能写好:
#!/usr/bin/env python3 """weekly_report_generator.py - 生成周报""" import json import sys from pathlib import Path from datetime import datetime def load_tasks(directory: Path): tasks = [] for f in directory.glob("*.json"): with open(f, encoding="utf-8") as fp: data = json.load(fp) tasks.extend(data if isinstance(data, list) else [data]) return tasks def filter_by_date(tasks, start, end): fmt = "%Y-%m-%d" s, e = datetime.strptime(start, fmt), datetime.strptime(end, fmt) return [t for t in tasks if s <= datetime.strptime(t["date"], fmt) <= e] def render_report(tasks): by_author = {} for t in tasks: by_author.setdefault(t["author"], []).extend(t["tasks"]) lines = [] for author, task_list in by_author.items(): lines.append(f"## {author}") lines.extend(f"- {task}" for task in task_list) return "\n".join(lines) if __name__ == "__main__": try: params = json.load(sys.stdin) report = render_report(filter_by_date( load_tasks(Path(params["directory"])), params["start_date"], params["end_date"] )) print(json.dumps({"weekly_report": report, "task_count": len(report.splitlines())})) except Exception as e: print(json.dumps({"error": str(e)}), file=sys.stderr) sys.exit(1)联调时最重要的一个细节:先把脚本当成普通CLI测。echo '{"directory":"./tmp","start_date":"2025-06-01","end_date":"2025-06-07"}' | python scripts/weekly_report_generator.py,确认输出JSON没问题,再拿到Agent环境里去调用。如果直接塞给Agent调,出了问题根本分不清是Agent没触发还是脚本炸了。
我还会放一个sample_input.json到assets目录,目的是给Agent一个“参照物”,当它拿不准输入格式时可以参考。实测这个做法对降低误调用率很有效。
4. 工程化落地:版本管理、加载策略与团队协作
4.1 通用工作区:把skills作为仓库的一部分
Skills包多了之后,第一个问题是怎么组织。我目前的习惯是在项目仓库里单独建一个skills目录,每个子目录一个技能。目录名用短横线分隔的小写命名,比如weekly-report-generator、invoice-extractor。不要把技能脚本散布在项目各处,否则Agent能找到的技能列表就不一致。
这里还有一个小细节:技能包的版本管理直接复用Git。每次修改SKILL.md或脚本,至少要更新metadata里的version字段,并在commit信息里标注。否则多个模型实例部署后,加载到的技能版本不一致,排查问题时会非常痛苦。
4.2 加载策略与上下文预算
Agent能感知到的技能数量不是无限的。我一开始图省事,把十五六个技能全部放到Agent的可见清单里,结果上下文预算直接崩了。
做个粗略的估算:如果每个技能名称加description平均按300token算,15个技能就是4500token,这部分还能接受。但如果按“把所有SKILL.md都全文加载”的策略,每个技能2000字折合约1500到2500token,15个就是2.25万到3.75万token,直接占掉32K上下文的大半,这还没算对话历史。所以我强烈建议按需加载:
- Agent上下文里只保留技能的name和description。
- Agent根据用户请求判断需要调用某个技能时,再从技能仓库读取SKILL.md全文。
- 同一会话内,调用过的技能可缓存;未调用的技能不加载正文。
技能排序也能影响成功率。我一般把高频率使用、短小精悍的技能排在列表前面,冷门且文档长的往后放。大多数模型做工具选择时,对靠前的条目有明显的倾向性,这个经验不算科学,但实测有效。
4.3 团队维护流程
但凡超过三个人协作维护技能包,就必须定一下流程,否则很快变成一堆互相看不懂的脚本堆积。
我们目前在用的流程是:
- 新增技能时先提交SKILL.md草稿,大家评审的是“触发描述清不清楚”“依赖是否合理”,不是等写完了才看。
- 每个技能包必须有样例输入输出,没有sample数据的技能不接受合入。
- 改动技能逻辑必须在CHANGELOG里记录,版本号递增,老的版本通过git tag留存。
- 每周跑一次回归测试,用固定的问题集验证每个技能是否还能触发。
回归测试是最容易被忽视的。Agent模型版本升级后,同一个描述词的触发成功率可能会变,我在GPT-4o和Claude上测试过,同一个Skill描述,触发率有明显差异。所以技能描述写完不是一劳永逸,得跟着模型走。
5. 常见问题与排查实录
5.1 模型就是不调用Skill
我遇到过最典型的问题是Agent对技能看都不看一眼,直接自己硬写答案。排查顺序一般是这样:
- 检查技能描述是否包含触发场景。描述写“生成周报”,用户说“帮我汇总一下这周干了啥”,模型当然不会联想到。
- 检查技能名称是否与描述语义一致。名称是
weekly-report但描述里讲的是“会议纪要整理”,这会让模型的调用决策非常困惑。 - 在描述里加“不要做什么”反而有效率。比如“不适用于查询历史周报”“不适用于翻译文档”,负面约束能有效减少错误调用。
- 最后再检查技能清单是否被加载。很多框架要求Agent重启后才会刷新技能列表,改了SKILL.md不重启,测试结果当然是没变化。
5.2 上下文被截断
技能脚本的输出如果太大,最直接的结果就是对话上下文被截断,模型会把技能执行结果忘掉,接着开始胡编。
我的对策是:技能脚本的输出严格控制大小。周报生成的输出如果超过200行,就自动分页或只返回摘要。实在要返回大文件,把文件写到指定目录,输出JSON里只放一个文件路径,而不是全文。反正Agent端也有文件读取工具,它需要细节时自己再读。
另外就是合理利用progressive disclosure策略,这条我在4.2节已经强调过。技能正文一多,所有SKILL.md一股脑塞进去是灾难,等到需要调用的那一刻再读全文,是性能和稳定性的平衡点。
5.3 脚本和外部依赖报错
技能脚本跑起来报错,错误信息还不留给Agent,这个小问题能让你排查到怀疑人生。
我的经验是技能脚本的错误处理必须“向外抛”:
- try-catch之后再往标准错误流打印一段结构化的错误信息,比如
{"error": "file not found", "path": "/tmp/x.json"}。 - 不要只输出内部Python堆栈,Agent理解不了Traceback。把错误信息翻译成自然语言描述。
- 依赖安装不进沙箱的时候,不要擅自改用内置库硬编码数据,网络受限就接受受限,换一个不需要外部依赖的实现方案。
我自己还遇到过路径问题:技能的默认工作目录通常不是技能包所在目录。脚本里如果用了相对路径读数据,会读不到。所以脚本内部一定要基于传入的绝对路径去定位文件,或显式声明“默认目录是用户指定的目录,不是脚本所在目录”。
5.4 多Agent并行时的状态隔离
当多个Agent会话同时调用同一个技能时,临时文件和工作目录会产生冲突。我遇到过两个会话同时写入/tmp/report.md,导致互相覆盖。教训就是:技能脚本不要依赖固定路径的临时文件。
解决办法是在入口脚本里用tempfile.mkdtemp()或uuid生成唯一子目录,用完即删。同时,脚本内部绝不修改全局状态,所有数据都通过输入输出传递。如果技能里涉及加锁,尽量用进程级锁而不是文件锁,跨平台行为更可控。
另外,如果技能里面有外部API调用,记得在SKILL.md里写明“调用频率建议”和“超时设置”。否则多个会话一开,外部接口直接被打满,报错的时候你会在监控面板前面发很久的呆。
我个人的体会是,Agent Skills这套东西,真正的门槛不在写脚本,而在“给模型写说明书”。脚本写得再漂亮,SKILL.md没说清楚,Agent照样不会用或者用错。反过来,只要描述写得精准、流程拆得清晰,哪怕脚本本身粗糙一点,整体效果也差不到哪去。以后我可以顺着这个方向,把技能包自动测试、触发率监控这些内容继续完善,让每个技能包都能在项目里稳定扛住真实流量。