1. agent-skills解决的不是"能不能",而是"贵不贵"
如果你搞过一阵子Agent开发,大概率遇到过这么一种情况:想让模型完成某个具体动作,比如"从一篇文章里抽出正文、去掉导航和广告、再概括成三句话"。硬靠提示词去堆,也能跑通,但代价是提示词越来越长、越来越脆弱,换一个网站就崩,改一个需求就要重新调一版prompt。
我第一次接触到agent-skills这个项目时,正好被这类问题折腾得够呛。回头看,这个项目的核心思路其实很简单:不要把所有能力都揉进提示词里,而是把能力拆成一个个可以独立加载的"技能包",让Agent在需要的时候才去装载对应的技能。
1.1 所有的工具类Agent,最终都会遇到同一个瓶颈
先说一个反直觉的观察。很多人以为Agent的能力上限由模型决定,模型强就万事大吉。但实际跑过才发现,真正卡住项目的往往是上下文窗口和指令冲突。
举个例子。假设你要做一个能处理各种文档的助手,早期做法很直接:把"如何解析PDF""如何提取表格""如何识别图片中的文字"这些说明全部塞进system prompt。结果每个任务开始之前,模型都要先"读完"这几千字的说明书,再开始干活。
这里有两个问题。第一个是费用和延迟,每轮对话都在为那些用不到的说明文字付费;第二个更麻烦,指令越多,模型越容易产生注意力漂移。技能说明和用户需求混在一起,模型偶尔会分不清哪条指令才是当前的最高优先级的那个,输出质量波动很大。
agent-skills的解法是换个角度:模型不需要在每轮对话中都具备所有能力。它只需要知道"我有哪些技能可用,每个技能是干什么的",等真正碰到对应任务时,再把详细的技能说明和脚本注入进去。
1.2 技能包和工具调用的区别在哪
有人可能会说,这不就是function calling吗?其实不完全一样。
工具调用(function calling)是一个很薄的接口层,你给模型暴露一个函数名、参数列表,模型决定什么时候调用、传什么参数,具体逻辑在你的代码里执行。而agent-skills更像一个完整的操作手册加执行脚本的组合体。
一个技能包,除了提供可执行的脚本,还带着一份给模型看的说明书。说明书里写清楚了:这个技能适合处理什么任务、在什么场景下用它、有哪些边界限制、完成任务的步骤是什么。模型下载技能包之后,不只是"调一个函数",而是"理解了一套做事的方法,然后用脚本去执行"。
这一点差别在复杂任务上非常明显。工具调用适合"执行单个原子操作",但技能包适合"完成一整个需要多步判断的任务"。
1.3 它是给谁用的
简单梳理一下适合用agent-skills的人群。
- 如果你在用Claude、DeepSeek这类支持长上下文的模型做自动化任务,目前主要靠提示词堆功能,且已经堆到维护困难的程度。
- 如果你维护着多个Agent,希望不同任务之间共享一套能力,而不是每个Agent都copy一份prompt。
- 如果你做的是文档处理、信息抓取、数据分析这类"目标明确但过程经常变"的任务,静态提示词很难覆盖各种情况。
如果你只是做一次性的脚本,不需要工程化,那这个东西对你来说可能有点重。但如果你想给自己的Agent搭一套可持续维护的能力体系,它提供了一个挺完整的范式。
2. 一个技能包的真实内部结构拆解
agent-skills里,一个技能不是一段字符串,而是一个目录。我拿自己复刻过的项目结构来说,典型的技能包长这样:
my-skill/ ├── SKILL.md ├── scripts/ │ ├── run.py │ └── requirements.txt └── resources/ └── reference.pdf乍看很简单,但每一层的设计都有讲究。搞懂这个结构,你自己写技能包时就能少走很多弯路。
2.1 SKILL.md:写给模型看的操作手册
SKILL.md是整个技能包的核心,也是和普通工具函数最大的区别所在。
它用的是Markdown格式,开头有一段YAML frontmatter,里面是元数据:
--- name: web-content-extractor description: 从网页URL中提取干净的正文内容,移除导航栏、广告、评论等噪音元素,返回纯文本或结构化HTML。适用于文章阅读、内容聚合、资料归档等需要正文的场景。 when_to_use: 当输入是一个网页链接,且目标是获取该页面的主体内容时使用。不适合需要登录、动态渲染极重的单页应用页面。 ---description和when_to_use这两个字段非常关键。它们就是模型决定"要不要加载这个技能"的依据。在实际运行时,系统只会把这两个字段提供给模型做筛选,只有当模型判断需要这个技能时,完整SKILL.md才会被加载进上下文。
这一点很像搜索引擎的索引页与正文页的关系。前者负责被检索和匹配,后者负责在被需要时提供完整信息。
frontmatter下面就是正文。正文的写法有几个原则,我后面单独讲,这里先记住一个核心:它是写给人(模型)看的操作流程,不是写给机器看的API文档。
2.2 scripts:真正干活的执行层
脚本目录放的是具体的执行逻辑。这个设计有个很务实的意图:让模型通过SKILL.md理解任务目标和步骤,再通过"调用脚本获得中间结果"来逐步完成任务。
比如上面的web-content-extractor,流程是这样的:
- 模型拿到SKILL.md,知道"我要提取正文,有脚本可以用"。
- 模型调用
scripts/run.py,传入URL参数。 - 脚本返回处理后的文本。
- 模型根据结果继续后续操作(比如写摘要、翻译、归档)。
比较关键的一点是,脚本不应该试图取代模型的判断能力,而应该提供模型做不到的"脏活累活"能力。比如网络请求、HTML解析、图像处理、文件格式转换。模型负责的是"决定怎么做",脚本负责"具体执行"。
2.3 resources:技能相关的外部支撑材料
resources目录是用来放参考材料的。这个目录更灵活,可以是PDF、图片、数据字典、示例文件,甚至是模板。
为什么要单独安排一个resources目录?因为不是所有参考资料都需要被脚本读取,有些是给模型看的。比如你做一个涉及特定行业术语的技能,可以在resources里放一份术语表,SKILL.md中指引模型在需要时去查阅。
这种"分离存放、按需加载"的思路,贯穿整个技能的运行流程,目的都是减少无谓的token消耗。
3. 手写一个"网页正文提取"技能的完整过程
前面拆得再清楚,都不如亲自动手写一个技能包来得直接。我以自己实际做过的一个"网页正文提取"技能为例,完整走一遍流程。
3.1 先想清楚边界,再动笔写SKILL.md
写技能包最容易犯的错误是一上来就写代码。其实第一步应该是定义技能的边界。
我当时给自己的技能定了几条规则:
- 输入:一个可以公开访问的URL。
- 输出:干净的正文纯文本,保留标题和段落结构。
- 不做的事:不处理需要登录的页面,不渲染复杂JavaScript生成的内容,不处理PDF(那是另一个技能)。
- 失败时的行为:如果页面提取不到正文内容,返回明确错误信息,不要返回整个HTML。
定义完边界,SKILL.md的正文就好写了。核心是给模型一个清晰的执行流程:
# Web Content Extractor ## 任务目标 从给定的网页URL中提取主要内容,去除导航、侧边栏、广告、页脚等无关信息。 ## 执行步骤 1. 使用 `scripts/run.py` 请求目标URL,参数为 `--url <目标地址>`。 2. 如果脚本返回错误,尝试更换URL格式(如添加https://前缀)后重试。 3. 如果脚本成功,将返回的正文内容整理后提供给用户。 4. 整理时保留原文的段落层级,删除多余空行。 ## 注意事项 - 脚本依赖目标网站结构,不同新闻网站的表现差异较大,输出异常时尝试更换来源URL。 - 提取结果仅为纯文本,不包含图片和链接。 - 如果页面内容过短(少于300字),认定为提取失败。注意这里面的"执行步骤"是个非常重要的设计。它是在告诉模型"先做什么,遇到问题怎么办,合格结果的标准是什么"。模型拿到这套步骤之后,会根据实际情况灵活调整,但它不再需要凭空猜测了。
3.2 编写脚本:遵循"一次调用、标准IO"原则
脚本部分我用的Python,加上requests和BeautifulSoup两个库:
#!/usr/bin/env python3 import argparse import re import sys import requests from bs4 import BeautifulSoup def extract_main_content(html: str) -> str: soup = BeautifulSoup(html, "html.parser") # 先移除明显不属于正文的模块 for tag in soup.find_all(["nav", "header", "footer", "aside", "script", "style"]): tag.decompose() article = soup.find("article") or soup.find("main") or soup.body if not article: return "" # 只保留文本段落,去掉深层无用嵌套 paragraphs = article.find_all("p") text_blocks = [p.get_text(strip=True) for p in paragraphs] text_blocks = [b for b in text_blocks if len(b) > 20] # 过滤短碎片 return "\n\n".join(text_blocks) def main(): parser = argparse.ArgumentParser(description="Extract main content from URL") parser.add_argument("--url", required=True) args = parser.parse_args() try: resp = requests.get(args.url, timeout=10, headers={ "User-Agent": "Mozilla/5.0 (compatible; agent-skills/1.0)" }) resp.raise_for_status() content = extract_main_content(resp.text) if len(content) < 300: print("ERROR: extracted content is too short, the page may be protected or non-standard.", file=sys.stderr) sys.exit(1) print(content) except Exception as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()脚本设计上有个原则值得说一句:脚本只做"确定性"的部分,把"判断"留给模型。比如我不写任何摘要逻辑,因为摘要是模型擅长的事;我只负责把正文从HTML里拎出来,这是模型不擅长且容易出错的事。
另外,脚本的输出必须简洁、干净。最好只输出正文内容,或者加一个简单的错误格式,不要把调试日志、进度信息混进来。模型需要从输出中快速判断下一步操作,噪音太多会干扰它的决策。
3.3 测试时用命令而不是口头描述去验证
写完技能包后,最关键的一步是用真实命令验证技能是否可用。这里说的"可用"包含两层意思:
- 脚本本身能不能跑通,输出质量如何。
- 模型读到SKILL.md之后,能不能正确理解并调用脚本。
我当时拿了三个不同类型的网站在测试,结果发现了不少问题。比如某个网站结构特殊,正文内容不在<article>里,而是散落在多个<div>中;又比如某个网站加了Cloudflare的校验,直接请求会返回403。这些情况我在SKILL.md里补充了说明,模型遇到403时,会尝试换UA头,或者直接告诉用户"该页面需要额外的访问验证"。
这个"实测-补充文档-再实测"的循环非常关键。SKILL.md不只是一份静态说明,它会随着真实场景的反馈不断迭代。这也提醒了我在写技能包时,尽量预留一个"常见异常及处理"的段落,让模型遇到问题时能参考。
4. Agent的技能调度机制:命中、加载与上下文管理
技能包本身写好了,接下来就要看Agent怎么知道在什么时候加载它了。这部分是整个agent-skills项目里最容易被人忽视、但对实际体验影响最大的环节。
4.1 description是技能的唯一"门面"
在大多数实现里,Agent启动时只会拿到所有技能包的元数据列表,通常是name、description、when_to_use这几个字段。
模型会根据当前用户输入的任务类型,在这个列表里做匹配,有点像大脑快速扫一眼备忘录用的小标签。如果description写得不好,技能包写得再好,模型也不会去加载它。
所以description的写作有个核心原则:用"用户会说的人话"来写,并且明确写出触发条件。
举个例子。假设你的技能是用来处理Excel的,description你写"Advanced spreadsheet manipulation utilities for data processing operations"——听起来很专业,但模型看到的效果可能一般。你改成"当用户提到Excel表格、xlsx、csv、表格数据清洗或转换时使用。支持读取、筛选、合并、拆分工作表"——效果会好得多。原因在于,模型匹配的是用户问题里的语义和你description里的语义,你的描述越接近用户可能说的原话,匹配成功率越高。
这也解释了为什么技能包设计里,when_to_use会被单独拆出来。它的作用就是逼着技能编写者想清楚一件事:"什么场景下用我这个技能?"想得越具体,模型判断时就越不容易迷茫。
4.2 按需加载的上下文压缩效果
按需加载带来的一个直接好处是上下文窗口压力大幅下降。我拿自己的使用情况对比过:
| 方案 | 每轮基础token消耗 | 加载技能后token消耗 | 说明 |
|---|---|---|---|
| 全部技能写进system prompt | 约5k | 恒定5k | 每个任务都背着所有技能说明 |
| agent-skills按需加载 | 约500 | 约2k~3k | 大部分任务不需要加载任何技能 |
在对话中,多出来的这部分token是省不掉的,但关键收益在于:大量无关技能说明不再占用每轮上下文,模型在推理时的注意力明显更集中。
4.3 加载与卸载的时机
技能加载并不是"一次加载,永久生效"。我见过不少爱好者自主实现的方案,加载逻辑写得非常简单:只要某个技能被用到过,就一直留在上下文里,这会导致一个问题——对话主题切换到别的方向后,之前的技能说明依然在上下文里占地方,干扰后续推理。
合理的做法应该是:技能在任务切换后自动卸载。更精确地讲,是Agent判断当前任务不涉及某个技能时,要从上下文中移除或折叠对应的技能说明。当后续再遇到需要该技能的任务时,重新加载。
这种"用则载、不用则卸"的机制,就是agent-skills项目在实践中最有区分度的地方。它不是简单的工具集,而是带着一套有意识的上下文管理策略。
4.4 技能冲突时的优先决策
还有一种情况:多个技能看起来都能解决当前问题。比如"网页正文提取"和"网页标题批量提取",输入都是一个URL列表,用户说的却是"帮我看看这个页面讲了啥"。这时候模型会倾向于选择范围更贴近的"正文提取"技能。
但如果两个技能的description都写得太宽泛,模型就会犹豫。我在实战中的经验是,尽量让技能的任务范围正交,不要重叠。如果一个新技能和现有技能有大面积重合,优先考虑扩展旧技能,而不是新建一个。
5. 实测中的数据表现与常见坑
这一节说一些我实际跑agent-skills项目时遇到的真实问题,也是社区里反馈最多的地方。每个坑背后都对应一条教训。
5.1 我踩过的三个典型坑
第一是脚本输出格式不规范。早期我写过一个生成报告的技能,脚本直接打印了一段模板文本,没有明确的成功/失败标记。结果模型在后续处理时把模板里的占位符错当成了真实内容,折腾了很久。后来我把所有技能脚本的输出统一成"成功就输出业务内容,失败就输出ERROR:开头的信息",模型处理起来省心很多。
第二是SKILL.md写得过度详细。有些技能包作者担心模型理解不了,把操作步骤写了十几条,还加了大量背景介绍,结果加载进去之后占了一堆token,模型反而抓不住重点。现在我写SKILL.md,正文控制在600字以内,最多800字,只保留流程、步骤、注意事项三个部分。
第三是依赖环境不一致。这个问题在多人协作或迁移时特别明显。脚本用到的Python库没写进requirements.txt,换环境跑就直接崩。后来我给自己定了一条规矩:任何技能包的scripts目录下都必须有requirements.txt,哪怕只有一个依赖也要显式声明。
5.2 延迟表现需要注意的点
技能加载确实会增加单次请求的处理时间。首次加载一个技能包,要把SKILL.md和元数据注入上下文,这会导致首token延迟增加几百毫秒到一两秒,取决于技能说明的长度和模型服务的算力。
但在长对话里,这个成本会被摊薄。因为技能一旦加载,后续任务就不再需要重复注入说明。如果你的Agent是"一次性任务"模式,比如每次请求都是独立的新对话,那技能加载的固定开销会占比较大比例。这种情况下,可以考虑把最常用的技能预先放进系统提示词里,把次常用的保留按需加载。
5.3 如何调试"模型就是不加载技能"的问题
如果你发现模型明明遇到了对应任务,却始终不触发技能加载,不要急着怪模型。按照下面这个链路排查:
- 先看技能列表里这个技能的
name和description是不是被正确传给了模型。 - 再看description里有没有出现关键词歧义。比如技能名叫
url-fetcher,但用户说的是"抓取这个网页",你的description里却写的是"fetch URL data",匹配不到很合理。 - 然后跑一次带日志的会话,把模型每次的选择过程打印出来,看看它在候选技能里做了什么决策。
- 最后,如果一切正常但还是不加载,可以在SKILL.md正文前加一行提示:"当用户需要获取网页内容时,必须先使用此技能。"
预算一个排查链路听起来很基础,但真的能解决80%的问题。
6. 搭建自己的技能库:从单技能到技能工厂
单个技能跑通只是第一步。等到你的Agent需要五六个甚至十几个技能的时候,怎么管理这些技能包就成了新的问题。下面是我自己在维护过程中总结的一些管理思路。
6.1 技能命名与目录规范
技能包的命名要符合两个标准:一是自解释,二是无歧义。web-content-extractor比extract好,pdf-table-parser比pdf-tool好。
另外目录层级建议保持扁平,不要做太深的嵌套,因为技能加载逻辑通常只是按文件名找SKILL.md,目录嵌套太深会导致加载失败或路径出错。一个技能包一个文件夹,所有文件平铺,这是最简单的管理方式。
6.2 技能测试:用固定样本做回归
为了确认改动不影响已有能力,我维护了一套简单的测试样本,包含:
- 每个技能的样例输入和期望输出。
- 一两个典型的"负例",确保技能在错误场景下不会误触发。
每次改完SKILL.md或脚本,就跑一遍这套样本。这里给个非常实践的技巧:把测试结果也放进一个独立的日志文件里,不要只看"通过/失败",还要记录"当时的加载耗时、返回内容长度"。因为技能包的一个隐性指标是上下文消耗,如果某次改动导致SKILL.md膨胀了30%,即使功能没坏,长期来看成本也不划算。
6.3 如何借鉴社区项目做自己的体系
agent-skills在开源社区里不是一个孤立的项目,业界有非常多的借鉴思路。如果你打算构建自己的技能库,比较推荐的做法是:
- 先列一个清单,写下你的Agent最高频处理的十类任务。
- 把每类任务拆成"决策步骤"和"执行动作"两部分。决策步骤留给模型,执行动作判断是否需要脚本辅助。
- 两类情况不需要做成技能包:一种是系统prompt就能稳定搞定的简单任务;另一种是高度依赖外部系统、需要复杂鉴权和状态管理的集成任务,这类更适合用传统的function calling去做。
- 把这十类任务里剩余的3-5个核心任务,按前面讲的方法逐步做成技能包。
6.4 公共技能与私有技能的拆分
最后说一个容易被忽略的细节:技能包也要区分公共和私有。
像"网页正文提取""PDF转文本"这种通用能力,适合做成公共技能库,团队内共享,持续打磨。而像"公司内部数据库查询""特定业务报表生成"这类包含业务逻辑甚至密钥信息的技能,一定要做成私有技能,和公共技能分开放置。我见过一些团队把API密钥直接放在技能包的脚本里传给模型,这是非常有风险的做法。技能包的本质是一段"可被模型读取和执行的代码",凡是会出现在模型上下文里的东西,都要假设它可能被"说出来"。
所以,涉及密钥的操作,应该把凭证放在服务端环境变量里,脚本从环境变量读,模型上下文里只出现"使用环境变量中的凭证"这类描述。
7. 一点个人总结:技能包思维改变的不只是Agent能力
做agent-skills项目这段时间,我最深的体会是:它改变的其实不只是Agent能做什么,而是我怎么描述Agent的能力。
过去我总觉得,Agent的能力来自模型本身——模型越强,Agent能做的事就越多。但技能包让我意识到,真正的瓶颈在于如何把模型的通用智能稳定地嫁接到特定的领域任务上。SKILL.md本质上是在用人类可以理解的表达方式,给模型搭建一座从"理解"到"执行"的桥梁。
这个过程很像带实习生。你不能指望实习生第一天就能独立处理所有任务,你得给他一本操作手册,告诉他在什么场景下找哪个工具,遇到问题先看哪一章,哪些操作是红线。等你带过几批实习生,你会发现,最省力气的做法不是事事都亲力亲为,而是把操作手册写清楚、把工具环境配好,然后放手让他干。
agent-skills就是这个思路在Agent世界的实践。它让我把对模型的"不放心"转化成了对技能包的持续迭代,也让Agent的行为模式越来越稳定、越来越可预期。如果你也正在被提示词维护问题困扰,不妨试试这套方法,从第一个技能包开始搭起。