这两年做AI应用,最头疼的问题不是模型不够聪明,而是模型空有一身本事,却缺一套趁手的工具。我自己在几个真实的业务项目里反复碰壁之后,才慢慢摸到一条比较靠谱的路:不要指望一个巨型System Prompt搞定所有事情,而是给代理(Agent)配一套组织良好的技能(Agent Skills)。这篇文章就把我从零开始构建技能体系的完整过程,包括设计思路、目录结构、代码模板、测试方法和排坑记录,一次性讲清楚。适合正在做Agent应用开发的工程师,也适合刚接触AI编程、想给助手加自定义能力的朋友。
先说一个必须承认的事实:模型本身的推理能力进步很快,但它在很多具体任务上依然表现得很挣扎。让它写一段总结没问题,让它从某个URL里提取正文并转成干净的Markdown,它就容易胡说八道,或者输出一大堆没用的导航文本。这时候你需要的不是换一个更强的模型,而是给它一个明确的、可复用的"技能"——把不确定性极高的任务,变成确定性极高的流程。
这套思路的本质,就是把Agent当成一个会看说明书、会调用工具的实习生。你给它一套写好的技能手册,它按需翻阅、按步骤执行。下面我按自己实际落地的路径,把这个体系一点点拆开讲。
1. 先搞清楚:Agent Skills到底是什么
网上关于Agent Skills的说法很多,但真正动手做的时候你会发现,很多人把它和插件、函数调用、工具混为一谈。我自己的理解是:Skill是一个“带了说明书的能力单元”,它至少包含三个部分——描述文件、实现脚本、运行说明。Agent在运行过程中,根据用户请求的意图,动态决定是否加载某个Skill,然后按照说明书去执行。
如果你用过早年间的命令行工具,会发现这思路非常熟:每个命令都有自己的--help,你不需要记住所有参数,需要的时候翻一下就行。Agent Skills就是把这个逻辑搬到了大模型时代。它不是预先把所有能力塞进模型上下文,而是按需挂载,模型看到SKILL.md里的描述之后,才知道该调用哪个脚本、按什么流程做、最后输出什么格式。
1.1 为什么单个Agent会“卡壳”
我刚开始做Agent的时候,也走过弯路:把所有指令、规则、示例都塞进一个巨大的系统提示词里。结果上下文很快被撑满,模型开始注意力涣散,任务一多就互相干扰。有一次我在同一个Promot里既让它处理数据清洗,又让它生成图表,结果它把数据清洗规则用在了生成标题上,整个输出完全没法看。
这个现象的本质是:模型的能力上限由上下文长度决定,但注意力的分配质量不由上下文长度决定。指令越多,模型越容易在关键时刻抓错重点。Agent Skills把大段操作指南从上下文中拆出去,只在需要的那一步读取对应的技能说明,这样做从源头上解决了指令互相污染的问题。
一句话:单线程的Prompt是给简单任务用的,Agent Skills是给组合任务用的。
1.2 Skill与Tool的分工逻辑
很多朋友会问:我有Function Calling了,为什么还要Skill?这个问题问得很关键。
Tool是原子操作,比如"搜索天气""发送邮件""执行SQL"。它接收一组参数,返回一个结果,通常由模型根据函数定义自动决定是否调用。Skill则是复合能力,它内部可以包含多个Tool、多段脚本、甚至几轮自我检查流程。一个"周报生成"Skill,可能要调用数据查询Tool、文本总结模型、Markdown渲染脚本,最后还要按公司格式模板输出。
更直白的区分方式:Tool是零件,Skill是装配好的部件。零件放在那里,模型知道怎么用;部件则需要额外的说明书,告诉模型在什么场景下组装、按什么顺序使用、输出结果要符合什么约束。Agent Skills的核心价值就在这里——把一组零件打包成一个开箱即用的整体,大大降低模型自由发挥的空间。
1.3 一套技能体系到底包含什么
一个完整可用的Agent技能体系,至少要涵盖三层:
第一层是技能库,也就是一堆SKILL.md文件加上配套脚本,按目录组织好,放在Agent可以访问的位置。第二层是注册表,不管你是用JSON清单、数据库记录还是简单的目录扫描,Agent需要能知道当前环境下有哪些技能可选。第三层是运行环境,脚本运行时的依赖怎么管理、权限怎么控制、日志怎么收集,这些决定了技能能不能在生产环境稳定跑起来。
我自己在实践中的经验是:初期别追求庞大,先保证有3到5个技能跑通全流程,再逐步扩充。技能体系设计得好不好,在规模小的时候看不出差距,等技能上到几十上百个的时候,你会发现描述质量、命名规范和版本管理才是真正的胜负手。
2. 构建技能体系前,先想清楚这四件事
很多教程一上来就教你写代码,但实际踩坑之后我发现:代码反而是最简单的一环,真正难的是设计。技能体系就像搭积木,积木本身的尺寸标准如果不一致,后面怎么搭都会塌。
2.1 技能粒度:多大算一个技能
这个是我被问过最多的问题,也是最难回答的问题。技能太大,Agent一加载就要读取大量说明,推理负担重,效果还不好;技能太小,Agent频繁切换技能、跨技能传递数据,又容易在中间环节丢失信息。
我个人的判断标准是:如果一个技能说明需要超过500行才能讲清楚,说明它拆小了;如果一个技能的一句话描述说不清它能干什么,说明它拆大了。做"网页正文转Markdown"这个技能时,我一开始把抓取、清洗、下载图片、生成摘要全放在一个技能里,结果SKILL.md写了快两百行,Agent加载后经常忽略后面的步骤。后来拆成"内容清洗"和"图片处理"两个独立技能,各自配合起来反而稳定很多。
另一个实用经验:技能应该对应一个"模型需要学习才能正确完成"的任务。如果一段操作不需要模型理解、直接写死脚本就能完成,那它应该是一个Tool或普通脚本,而不是一个Skill。
2.2 边界清晰:一个技能只做一件事
这条听起来像废话,但执行起来非常反直觉。我见过很多同事写技能时,喜欢在说明书里加各种"顺便""另外""如果用户需要,也可以"之类的扩展职责。结果就是模型在调用时犹豫不决,不知道当前这个技能到底主用在哪,甚至把一个报表生成技能用在了文档翻译上。
给技能划定边界,写SKILL.md时就要把所有职责限制在第一屏里。描述区只写清楚:这个技能解决什么问题、不解决什么问题、在什么触发条件下会被调用。你的"不做什么"写得越明确,模型的误用概率越低。比如我的文件整理技能,开头就会写清楚:本技能仅用于文件批量重命名和归档,不负责文件内容修改。
2.3 描述即接口:让Agent理解你的技能
这是整套体系里最容易被低估的地方。很多人把精力花在脚本实现上,SKILL.md草草写几句"这个脚本能把HTML转成Markdown"就完事。结果Agent根本不会主动调用,或者调用了却传错参数。
我需要反复强调:SKILL.md的description字段就是接口,接口写不好,再强壮的程序也发挥不出来。写description有一个很实用的技巧:第一句话直接写触发场景,而不是功能概述。"在用户提供一个URL并要求提取正文内容时使用"比"网页内容提取工具"好用得多。前者让模型在做意图匹配的瞬间就能锁定技能,后者则让它还要再想一下"这个功能到底适不适合当前场景"。
描述中还要明确说明输入参数类型和输出格式,最好直接给一个小例子。因为目前的Agent系统多数不支持复杂的函数签名校验,技能之间传递数据全靠模型阅读理解。一个小例子能帮模型极大降低格式猜测成本。
2.4 目录与命名规范
技能多了之后,目录命名就是检索效率的关键。我见过有人用中文拼音、功能缩写、项目代号做目录名的,到最后连自己都分不清。推荐直接用技能名-主语言-版本号的规则,比如web_cleaner_python_v2。在SKILL.md中用完整可读的名称作为name字段,目录名与name字段保持一致。
如果团队协作,我建议再维护一个技能索引文件,记录技能名、版本、负责人、依赖关系、更新日期。这不是形式主义,当你面临"某个技能脚本突然不工作了"的排查时,索引能帮你快速定位上一次改动是谁做的、动了哪些文件。
3. 动手实操:搭建第一个技能
拿一个真实场景练手:做一个"网页正文提取与清洗"技能。这个技能在我日常工作中使用率极高——抓竞品资料、整理资讯、喂给RAG系统,都要用到。我会把从文件结构到注册调用的完整过程放出来。
3.1 技能目录的最小结构
一个最小可用的技能目录大概长这样:
skills/ └── web_content_cleaner/ ├── SKILL.md ├── scripts/ │ └── clean_page.py └── assets/ └── stopwords.txtSKILL.md是给模型看的说明书;scripts目录放可执行脚本;assets放脚本依赖的静态资源。我的习惯是:任何非代码的资源文件都放assets,避免和脚本文件混在一起。这样打包、迁移技能时也能快速定位哪些是执行文件、哪些是数据文件。
如果你用的是某个AI编程框架或自定义Agent平台,技能目录的读取方式可能有自己的约定。不过一般都会兼容这种"描述文件+脚本"的模式,差异只在于注册方式。先按这个结构把技能写明白,再适配到具体平台,是成本最低的路径。
3.2 编写SKILL.md的正确姿势
一份好用的SKILL.md,最好控制在60到100行左右。太短说明信息不足,模型猜不到执行细节;太长模型加载慢,还会干扰关键信息提取。它的核心结构非常固定:YAML头部、触发描述、使用方法、注意事项、示例调用。
--- name: web_content_cleaner description: 当用户提供一个网页URL并要求提取正文、去除广告/导航等无用信息、将内容转换为Markdown格式时使用。不适用于PDF和图片内容。 --- # 网页正文提取与清洗 ## 使用方法 1. 使用脚本 scripts/clean_page.py 处理HTML内容。 2. 输入参数通过STDIN传递JSON,格式为 {"url": "..."} 或 {"html": "..."}。 3. 脚本输出JSON,格式为 {"title": "...", "content_markdown": "..."}。 4. 若需要下载图片,另行调用 image_downloader 技能。 ## 注意事项 - 脚本不处理动态渲染页面,必要时先用浏览器抓取渲染后的HTML再传入。 - 网页编码混乱时,优先检查HTML中的charset声明。 - 输出内容保留原文段落层级,不要擅自改写正文。这段文档的写法是刻意为之的。开头就把"触发条件"和"不适用范围"交代清楚,减少模型误用;使用方法用编号步骤写,模型执行时更容易按顺序走;示例调用和注意事项放在后半部分,保证主体步骤不被干扰。我强烈建议你按照这个模板先套一套,等熟悉了再按需调整。
3.3 脚本层的输入输出协议
脚本是实现层面最需要标准化的一环。无论你用什么语言,我建议统一采用"STDIN收JSON、STDOUT出JSON、STDERR记日志"的方式。这样模型侧不需要管理临时文件路径,也不容易踩编码坑。
# scripts/clean_page.py import json import sys from html.parser import HTMLParser class TextExtractor(HTMLParser): def __init__(self): super().__init__() self.skip_tags = {"script", "style", "nav", "footer", "aside"} self.in_skip = False self.parts = [] def handle_starttag(self, tag, attrs): if tag in self.skip_tags: self.in_skip = True if tag in ("p", "div", "br", "li", "h1", "h2", "h3", "h4"): self.parts.append("\n") def handle_endtag(self, tag): if tag in self.skip_tags: self.in_skip = False def handle_data(self, data): if not self.in_skip: self.parts.append(data) def clean_html(html): parser = TextExtractor() parser.feed(html) lines = [line.strip() for line in "".join(parser.parts).splitlines()] lines = [line for line in lines if line] return "\n\n".join(lines) if __name__ == "__main__": payload = json.load(sys.stdin) html = payload.get("html", "") if not html and payload.get("url"): # 这里留一个requests抓取入口,生产环境请加超时与重试 try: import requests resp = requests.get(payload["url"], timeout=10) html = resp.text except Exception as exc: print(json.dumps({"error": str(exc)}), file=sys.stderr) sys.exit(1) result = {"content_markdown": clean_html(html)} print(json.dumps(result, ensure_ascii=False))注意几点:输出JSON永远使用ensure_ascii=False,避免中文被转成\uXXXX,不然模型读起来非常痛苦,后续处理也容易出乱码。错误信息给到STDERR,并且返回非零退出码,便于Agent判断调用失败。生产环境里,抓取逻辑一定要加超时和重试机制,我之前有个技能因为没加超时,一个无法访问的URL卡了90秒,直接把Agent的响应时间拖垮了。
3.4 技能注册与触发测试
技能文件写好了,脚本也跑通了,接下来就是让Agent能看到、能调用。最简单的注册方式是写一个技能清单文件,在Agent启动时加载:
[ { "name": "web_content_cleaner", "path": "skills/web_content_cleaner", "version": "1.2.0", "enabled": true } ]启动加载时,系统会把每个技能的name和description字段做一个轻量索引,放进Agent系统消息的"可用技能"区。模型看到"web_content_cleaner:在用户提供URL并要求提取正文时使用",就可能在合适的时候请求读取完整SKILL.md并调用脚本。
触发测试是整个流程的关键。我通常会准备一批典型输入,反复检验同一个技能在不同说法下的触发稳定性。比如"把这个网页整理成笔记""帮我抓一下这个链接的内容""把这个页面的正文存到文件里"——这三句话说的是同一件事,但措辞差异很大,只有description写得足够贴近用户真实语言,技能才会被稳定触发。这一步靠拍脑袋是测不出来的,必须积累足够的样本反复测。
4. 避坑指南:技能调用不生效的排查清单
技能体系跑久了,你会发现大部分问题都集中在几个固定的环节里。我把高频问题和排查思路整理成了一份清单,每次技能出问题就按这个顺序查,基本都能快速定位。
4.1 描述问题:为什么就是不触发
不触发有很多种原因。我的排查顺序是:先看description有没有写"不适用范围";再看第一句话是否包含了用户最常用到的动词和名词。比如用户通常说"抓取"而不是"提取",你的description里如果只写了"提取",那模型匹配不到也很正常。
另一个隐蔽的坑是:多个技能的description互相重叠。我有一次做了"网页摘要"和"网页正文提取"两个技能,description里都没写清楚区别,结果模型随机二选一,内部测试有一半的命中率。后来我在"网页摘要"的前面加了"仅生成不超过300字的总结,不输出全文",在"网页正文提取"里加了"输出完整正文,不进行总结",两个技能从此各司其职。
4.2 输出问题:模型解析技能结果失败
技能脚本输出很规范,但模型就是"看不懂"——这种情况一般出在输出结构过于复杂。比如输出JSON嵌了三层嵌套字段,模型在往回复里转述时,容易漏字段或改字段名。
解决方式是在SKILL.md里给出一个"输出转换示意图",告诉模型最终面向用户时应该如何组织回复,直接写清楚映射关系。我之前在数据统计技能里加了一行:{"total_count": 128} → 向用户输出:共找到128条记录。就这一句话,回复质量提升非常明显。模型不是不懂JSON,而是不知道你在当前场景下希望它用什么形式呈现结果。
4.3 环境问题:脚本就是跑不起来
这个问题在本地调试时很少出现,一部署就集中爆发。最常见的两个原因:一是脚本依赖的环境变量没设置,二是Python版本不兼容。我有次排查一个技能报错,日志显示某个第三方库的API变了,再看SKILL.md里写的运行环境,压根没标注Python版本要求。后来所有技能SKILL.md里都会多一行"# 运行环境":Python 3.10+ 或 Node 18+,以及必要的依赖安装命令。
更稳妥的做法是给每个技能配独立的依赖声明文件,部署时用虚拟环境隔离。虽然笨,但能让你在升级主程序环境时不会被某个陈年技能拖累。如果条件允许,把技能的运行wrapper化,统一用沙箱执行,对权限控制和资源限制都更友好。
5. 把技能体系推向团队:版本管理与共享
自己做技能和一个团队维护几十个技能,完全是两个世界。单人场景,技能目录乱七八糟也能硬扛;团队场景,没有规范和工具支持,技能库很快会腐烂成一堆没人敢动的脚本。这一章谈谈我在这方面趟出来的经验。
5.1 技能仓库的组织方式
我推荐把技能当成代码来管理,独立建仓或者单独建目录,与主应用代码分离。每个技能一个子目录,里面挂自己的版本号和更新日志。主应用通过索引文件或子模块的方式引用技能版本,而不是直接把代码拷来拷去。
版本号建议直接套语义化版本规则。主版本号对应破坏性变更,比如输入协议改了、输出结构变了;次版本号对应兼容性功能新增;补丁号对应修复。这套规则在多人协作时价值极大,特别是有同事问你"这个技能还能不能安全升级"的时候,你只需要看一眼版本号就能回答。
5.2 跨项目复用的兼容策略
跨项目复用的最大障碍是技能对特定环境的隐性依赖。比如某个技能内部写死了数据目录路径,换一个项目就找不到文件。我的经验是:把所有环境相关配置挪到技能外部注入,SKILL.md只保留逻辑,运行时通过环境变量或配置文件传入路径、密钥、URL等。
还有些技能依赖主应用的能力,比如需要调用某个内部接口。这类技能我会在SKILL.md的"依赖"部分明确标注接口名和版本约束,避免换一个环境后静默地访问到不存在的接口。把依赖关系显式化,是技能能否被大家放心复用的关键分水岭。
5.3 技能评审:像Review代码一样Review技能
技能评审非常重要。一个写得不好的技能,不仅仅不工作,还会在Agent需要执行任务时被错误加载,进而污染整个对话的后续输出。
我们团队的评审清单包括这几项:description触发条件是否明确;SKILL.md是否存在超过两层以上的嵌套指令;脚本输入输出是否符合统一JSON协议;错误处理是否完备;依赖是否声明清楚;有没有做边界测试。每项打个勾或说明问题,评审通过后才允许合并进正式技能库。这套流程跑起来后,"技能乱写导致Agent行为失控"的事故明显少了很多。
6. 进阶玩法:从技能库到技能编排
技能体系做到后面,单个技能已经不是重点,重点是多个技能如何配合、Agent如何知道自己该用哪些技能、流程中间状态如何传递。这是最考验体系设计能力的部分。
6.1 技能链与主技能
所谓技能链,就是用一个"主技能"来串联多个子技能。比如我做一个"市场调研报告生成"技能,它的流程不是一步到位,而是先调用"网页内容清洗"抓取竞品信息,再调用"结构化数据提取"提炼关键指标,最后调用"Markdown排版"生成报告。主技能本身不处理具体内容,它更像是一个指挥者,负责规划步骤、传递中间结果、决定各子技能的调用顺序。
写主技能SKILL.md时有几个要点要说清楚:整个流程的步骤数;每步使用哪个子技能;每步输入从哪来、输出去哪;哪一步可以失败不影响整体;什么情况下应该中止流程并请用户补充信息。经过这种编排,Agent从"单点能力"进化成"组合能力",这是技能体系相对插件体系最有优势的地方。
6.2 给技能加上可观测性
技能跑没跑、跑得好不好,不能总靠感觉。我习惯在技能调用时记录三类日志:调用日志、结果日志、异常日志。调用日志记录哪个技能、哪个步骤、处理的输入摘要、耗时多少;结果日志记录输出是否成功、输出体量多大;异常日志记录错误类型和可能的原因。
有了这些日志,就可以做简单的质量分析。比如某个技能调用成功率一直低于80%,那大概率是description触发判断有问题,或者脚本对某些输入格式没处理好。再比如某个技能平均耗时特别高,就要考虑是不是抓取逻辑里忘了加页面大小限制。可观测性不是为了好看,它是你持续迭代技能体系的地图。
6.3 基于反馈的技能自优化
更高阶的玩法是让Agent自己反馈技能的使用体验。在技能调用链最后加一个"使用体验"步骤,让模型简要回答:这次执行是否顺利;哪个环节最不确定;有没有参数不知道该填什么。把这些反馈聚合成结构化数据,定期汇总分析。
我这套机制跑了几个星期后发现,很多"我觉得写得挺清楚"的技能,在真实使用中都会暴露出模型理解偏差。比如某技能description里有一个专业术语,模型总是误解其含义,后来我把术语换成了日常说法,触发准确率立刻提升。这种来自真实反馈的优化远比坐在电脑前反复调整措辞高效。
最后总结一句个人体会:构建Agent Skills体系这件事,看起来是在写代码,本质上是在做知识工程和流程设计。一份好的SKILL.md胜过上千行复杂代码,一个精准的description能让技能调用的准确率翻倍。与其花大力气训练模型,不如先把手头这套技能体系打磨清楚。这可能是投入产出比最高的优化方向。希望这篇内容能帮你少走一些我踩过的弯路。