1. 从“长提示词”到 Agent Skills:为什么堆提示词这条路走不通
如果你最近在折腾 Agent 相关的东西,大概率会经历这么一个阶段:一开始觉得提示词写得越长越详细,Agent 就越聪明。于是你把系统提示词从 500 字写到 2000 字,再写到 8000 字,把能想到的规则、示例、边界条件全塞进去。结果呢?模型该犯错还是犯错,该跑偏还是跑偏,而且每次调用都要把这一大坨提示词重新喂一遍,token 消耗肉眼可见地往上涨。
这就是我最初接触 Agent Skills 时的真实状态。后来看到 SKILL.md 这套机制,才意识到问题的本质:长提示词解决的是“一次性说清楚”的问题,而 Agent Skills 解决的是“按需加载、按权限执行”的问题。这两件事根本不在一个层面上。
打个比方。长提示词就像你出门前把家里所有东西都塞进一个行李箱,不管今天是要去爬山还是去开会,箱子重量都一样。而 Agent Skills 更像是你有一个工具墙,每个工具挂在固定的位置上,今天要拧螺丝就只拿螺丝刀,要锯木头就只拿锯子。SKILL.md 就是那个工具墙上的标签,告诉你“这个工具是干什么的、什么时候用、用了会有什么后果”。
这篇文章我想聊的就是这件事:为什么 Agent Skills 不能只是“长提示词”的另一种写法,SKILL.md 到底承载了什么,按需加载和权限边界这两个概念在实际工程里意味着什么。如果你正在做 Agent 相关的开发,或者正在设计一套多技能协作的系统,这些内容应该能帮你少走一些弯路。
2. SKILL.md 到底解决了什么问题:不只是“换个地方写提示词”
2.1 长提示词的三个死穴
先说说长提示词为什么在 Agent 场景下特别容易翻车。我总结下来主要是三个问题。
第一个是注意力稀释。你把 8000 字的提示词塞给模型,模型并不是均匀地关注每一句话。实际测试下来,中间部分的指令被遵循的概率明显低于开头和结尾。这就像你给一个人交代 50 件事,他大概率只能记住前 5 件和后 5 件,中间的全靠缘分。Agent 场景下这个问题更严重,因为 Agent 往往需要多轮调用,每一轮都要重新读一遍这 8000 字,每一轮的“缘分”还不一样。
第二个是上下文污染。长提示词里往往混杂着各种场景的规则。比如你写了一个“文件操作”的 Agent,提示词里既有“读取文件时要注意编码”的规则,又有“写入文件时要检查权限”的规则,还有“删除文件时要二次确认”的规则。当 Agent 只是在读取一个文件时,后面那些写入和删除的规则就是纯粹的噪音,但它们依然占着上下文窗口,依然在影响模型的判断。
第三个是维护成本爆炸。当你的 Agent 需要支持 20 种不同的操作时,长提示词会变成一个几千行的怪物。每次加一个新功能,你都要小心翼翼地往里面塞一段,还要担心会不会和已有的规则冲突。改完之后还得全量回归测试,因为谁也不知道这次改动会不会影响其他场景的行为。
2.2 SKILL.md 的核心思路:把“说明书”和“操作手册”分开
SKILL.md 的做法本质上是一次关注点分离。它不再把所有的规则都写在一个大提示词里,而是把每个技能拆成独立的文件,每个文件只描述一件事:这个技能是什么、什么时候用、怎么用、有什么限制。
我拿一个实际例子来说明。假设你要做一个“文档处理 Agent”,支持读取、摘要、翻译、格式转换四个技能。长提示词的写法可能是这样的:
你是一个文档处理助手。你可以读取文档、生成摘要、翻译文本、转换格式。 读取文档时,注意检查文件是否存在,注意编码格式,注意大文件要分块读取。 生成摘要时,注意保留关键信息,注意控制长度,注意不要改变原意。 翻译文本时,注意保持术语一致,注意处理专有名词,注意语言风格。 转换格式时,注意目标格式的兼容性,注意特殊字符的转义,注意保留原始结构。 ...而 SKILL.md 的写法是拆成四个文件,每个文件只关心自己的事。比如read-document/SKILL.md里只写读取相关的规则,summarize/SKILL.md里只写摘要相关的规则。Agent 在执行任务时,先看任务需要哪个技能,然后只加载对应的 SKILL.md。
这个区别看起来简单,但实际效果差别很大。我实测下来,同样是“读取一个 Markdown 文件并生成摘要”这个任务,长提示词方案下模型偶尔会去检查“写入权限”和“格式转换兼容性”,因为它被那些无关规则干扰了。而 SKILL.md 方案下,模型只看到读取和摘要两个技能的说明,行为干净很多。
2.3 渐进加载:不是所有技能都要一开始就加载
SKILL.md 还有一个关键设计是渐进加载。这个词听起来有点玄,其实逻辑很朴素:Agent 不需要在启动时就知道所有技能的细节,它只需要知道“有哪些技能可用”,然后在真正用到某个技能时再去加载详细说明。
这就像你去餐厅吃饭。你不需要在进门之前就把菜单上每道菜的做法都背下来,你只需要知道“这家店有川菜、粤菜、日料”,然后点菜的时候再去看具体那道菜的配料和做法。SKILL.md 的渐进加载就是这个逻辑:Agent 的系统提示词里只放一个技能列表,每个技能一行简介,真正的详细规则放在各自的 SKILL.md 里,用到才读。
这个设计带来的好处是上下文窗口的利用率大幅提升。假设你有 30 个技能,每个技能的详细说明平均 500 字,如果全部加载就是 15000 字。但实际一个任务通常只用到 2 到 3 个技能,渐进加载下只需要加载 1000 到 1500 字。剩下的上下文窗口可以留给实际的对话内容和工具返回结果,模型的表现会明显更稳定。
注意:渐进加载不是“懒加载”的简单翻版。懒加载解决的是“什么时候读文件”的问题,渐进加载解决的是“什么时候让模型知道这个技能存在”的问题。前者是工程优化,后者是认知优化。
3. 按需加载的工程实现:从技能注册到上下文注入
3.1 技能注册表的设计
要让按需加载跑起来,第一步是有一个技能注册表。这个注册表不需要很复杂,本质上就是一份清单,告诉 Agent 系统“现在有哪些技能可用”。我一般会用一个 JSON 或者 YAML 文件来维护,结构大概是这样:
skills: - name: read-document description: 读取本地文档内容,支持 txt、md、pdf 等格式 path: skills/read-document/SKILL.md triggers: - 读取 - 打开 - 查看文档 - name: summarize description: 对文本内容生成摘要,支持指定长度 path: skills/summarize/SKILL.md triggers: - 摘要 - 总结 - 概括这个注册表里最关键的是description和triggers两个字段。description是给模型看的,让它知道这个技能大概能干什么。triggers是给路由逻辑用的,当用户输入里出现这些关键词时,系统就知道可能需要加载对应的技能。
这里有个经验:description不要写太长,一行到两行就够了。写太长的坏处是,当你有几十个技能时,光是技能列表就占掉大量上下文。我一般控制在 30 字以内,只保留最核心的信息。
3.2 技能匹配与加载时机
技能匹配的逻辑有两种常见做法。一种是关键词匹配,就是上面说的triggers字段,用户输入里包含某个触发词就加载对应技能。这种做法简单直接,但不够灵活,用户换个说法可能就匹配不上了。
另一种是语义匹配,把用户输入和技能描述都转成向量,算相似度,超过阈值就加载。这种做法更鲁棒,但需要额外的向量计算,而且阈值不好调。我实际用下来,比较稳的方案是两者结合:先用关键词做粗筛,再用语义做精排。关键词匹配到的技能直接进候选,语义相似度高的也进候选,最后取前 3 到 5 个加载。
加载时机也很重要。我的做法是在每一轮对话开始时做一次匹配,而不是在任务开始时一次性匹配完。因为 Agent 的任务往往是多轮的,第一轮可能只需要读取技能,第二轮才需要摘要技能。如果一开始就把所有可能用到的技能都加载了,就失去了按需加载的意义。
3.3 上下文注入的具体操作
匹配到技能之后,下一步是把 SKILL.md 的内容注入到上下文里。这里有个细节:不要直接把整个 SKILL.md 原样塞进去,而是要做一层包装。我一般会用这样的格式:
当前可用技能: - read-document:读取本地文档内容,支持 txt、md、pdf 等格式 详细说明: [SKILL.md 的内容]这样包装的好处是,模型能清楚地知道“这段内容是某个技能的说明”,而不是把它当成普通的对话内容。实测下来,这种结构化的注入方式能让模型更准确地遵循技能规则。
还有一个坑要注意:SKILL.md 的内容要放在系统提示词之后、用户输入之前。如果放在用户输入之后,模型可能会把技能说明当成用户说的话,行为会很奇怪。这个顺序问题我踩过好几次,一开始没注意,模型总是把技能规则和用户指令混在一起理解。
4. 权限边界:Agent Skills 的安全底线
4.1 为什么权限边界不能靠提示词来保证
很多人觉得,在 SKILL.md 里写一句“不要删除文件”或者“不要访问敏感目录”,模型就会遵守。这个想法很危险。提示词层面的约束是软约束,模型可能遵守,也可能不遵守,尤其是在多轮对话或者复杂任务下,模型很容易“忘记”这些规则。
我做过一个测试:在一个文件操作 Agent 的 SKILL.md 里明确写了“禁止删除任何文件”,然后让 Agent 执行一个“清理临时文件”的任务。结果模型在第三轮的时候还是调用了删除操作,因为它觉得“清理”就等于“删除”。这就是软约束的不可靠性。
真正的权限边界必须是硬约束,也就是在工程层面限制 Agent 能做什么,而不是靠模型自觉。具体来说,就是在工具调用层做拦截:Agent 可以请求删除操作,但执行层会检查这个操作是否在允许范围内,不在就直接拒绝,并把拒绝结果返回给模型。
4.2 权限边界的三个层次
我一般把权限边界分成三个层次来设计。
第一个层次是技能级权限。每个 SKILL.md 里声明这个技能需要什么权限,比如read-document需要文件读取权限,write-document需要文件写入权限,delete-document需要文件删除权限。Agent 在加载技能时,系统会检查当前会话是否拥有对应权限,没有就不加载。
第二个层次是操作级权限。同一个技能下,不同操作的权限可能不一样。比如write-document技能里,追加内容可能只需要普通写入权限,覆盖整个文件可能需要更高权限。这个层次需要在 SKILL.md 里把操作细分,每个操作标注所需的权限级别。
第三个层次是资源级权限。同样是读取操作,读取/tmp下的文件和读取/etc下的文件,风险完全不同。资源级权限就是限制 Agent 能访问哪些路径、哪些域名、哪些数据源。这个层次通常不在 SKILL.md 里写,而是在系统的权限配置里统一管理。
| 权限层次 | 控制对象 | 实现位置 | 典型场景 |
|---|---|---|---|
| 技能级 | 整个技能是否可用 | 技能加载器 | 无删除权限时不加载删除技能 |
| 操作级 | 技能内的具体操作 | SKILL.md + 执行层 | 允许追加不允许覆盖 |
| 资源级 | 操作的目标资源 | 系统权限配置 | 限制可访问的目录范围 |
4.3 权限声明与检查的实操
在 SKILL.md 里声明权限,我一般用 frontmatter 的格式,放在文件开头:
--- name: write-document description: 向文档写入内容 permissions: - file:write - file:append resources: - path: /workspace/output/* ---然后在执行层,每次工具调用前都做一次检查。检查的逻辑大概是:先看这个操作需要什么权限,再看当前会话有没有这个权限,最后看目标资源是否在允许范围内。三个都通过才执行,任何一个不通过就拒绝。
这里有个经验:拒绝的时候要给模型明确的反馈。不要只说“权限不足”,要说“当前会话没有文件删除权限,无法执行删除操作,你可以尝试其他方式”。这样模型能理解为什么被拒绝,并且可能找到替代方案。如果只说“权限不足”,模型可能会反复尝试同一个操作,浪费轮次。
提示:权限检查一定要放在执行层,不要放在模型层。模型层可以做“建议性检查”,比如在 SKILL.md 里写“这个操作需要谨慎”,但真正的拦截必须在执行层。两者配合使用,效果最好。
5. 常见问题与排查技巧实录
5.1 技能加载了但模型不遵守
这是最常见的问题。SKILL.md 明明加载了,规则也写得很清楚,但模型就是不按规则来。排查下来通常是三个原因。
第一个原因是技能说明太长。SKILL.md 写了几千字,模型读到后面已经忘了前面。解决办法是精简,每个 SKILL.md 控制在 500 字以内,只保留最关键的规则。如果确实需要详细说明,可以拆成多个技能,或者把详细说明放到单独的文档里,SKILL.md 里只放摘要和引用。
第二个原因是技能说明和用户指令冲突。比如 SKILL.md 里写“输出格式必须是 JSON”,但用户说“用表格给我看”。模型会倾向于遵循用户指令,因为用户指令更“新”。解决办法是在 SKILL.md 里明确优先级,比如写“除非用户明确要求其他格式,否则输出 JSON”。但更好的做法是,在技能设计时就避免这种冲突,让技能规则和用户指令尽量不打架。
第三个原因是技能加载顺序不对。如果多个技能的规则有重叠,加载顺序会影响模型的理解。我一般把最具体的技能放在最后加载,这样它的规则会“覆盖”前面更通用的规则。这个逻辑和 CSS 的层叠类似,后加载的优先级更高。
5.2 按需加载导致技能遗漏
按需加载的另一个常见问题是该加载的技能没加载。用户说“帮我把这个文件转成 PDF”,但系统只匹配到了“读取”技能,没匹配到“格式转换”技能,因为用户没说“转换”这个词。
解决办法是扩大触发词范围。不要只写“转换”,还要写“转成”“导出”“生成”“变成”等近义词。另外,可以在技能注册表里加一个related字段,列出相关技能,当一个技能被加载时,把相关技能也加入候选。
还有一个技巧是在系统提示词里放一个技能总览。不是详细说明,就是一行一个技能名加一句话描述。这样模型在规划任务时,能看到所有可用技能,即使某个技能没被自动匹配到,模型也可能主动请求加载。这个总览的 token 开销很小,但能显著降低技能遗漏的概率。
5.3 权限检查误拦截正常操作
权限边界设计得太严,会把正常操作也拦掉。比如你限制了文件写入只能在/workspace/output下,但用户想写一个临时文件到/tmp,就被拦了。
解决办法是分级权限。不要只有“允许”和“拒绝”两种状态,而是设计成“自动允许”“需要确认”“禁止”三级。/workspace/output下的写入自动允许,/tmp下的写入需要确认,/etc下的写入直接禁止。这样既保证了安全,又不会把正常操作卡死。
| 问题现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 技能加载了但不遵守 | 说明太长/指令冲突/加载顺序 | 检查 SKILL.md 长度和优先级声明 | 精简说明、明确优先级、调整加载顺序 |
| 该加载的技能没加载 | 触发词覆盖不足 | 检查用户输入和触发词匹配情况 | 扩大触发词、增加 related 字段、加技能总览 |
| 权限误拦截 | 权限粒度过粗 | 检查权限配置的路径和操作范围 | 分级权限、增加确认机制、细化资源范围 |
| 技能之间规则冲突 | 多个技能规则重叠 | 检查各 SKILL.md 的规则是否有矛盾 | 明确技能边界、调整加载顺序、合并冲突技能 |
5.4 一个容易被忽略的坑:技能版本管理
当你的技能越来越多,SKILL.md 的修改会变得频繁。如果没有版本管理,很容易出现“昨天还能跑,今天就不行了”的情况。我的做法是给每个 SKILL.md 加一个版本号,在技能注册表里记录当前使用的版本。修改 SKILL.md 时,先升版本号,再改内容,这样出问题可以快速回滚。
另外,SKILL.md 的修改最好走代码审查流程。不要觉得“只是改个提示词”就随便改,实际上 SKILL.md 的改动对 Agent 行为的影响,比改代码还大。我见过好几次因为改了一句 SKILL.md 的描述,导致整个 Agent 的行为模式变了。
6. 从长提示词迁移到 Agent Skills 的实操建议
如果你现在手里有一个跑得还不错的长提示词 Agent,想迁移到 SKILL.md 架构,我的建议是不要一次性全迁。先挑一个独立的、边界清晰的技能迁,比如“读取文件”或者“发送邮件”。迁完之后对比行为,确认没问题再迁下一个。
迁移的时候,先把长提示词里和这个技能相关的规则全部摘出来,整理成 SKILL.md。整理的过程中你会发现,很多规则其实是重复的或者矛盾的,正好借这个机会清理一遍。我迁移的时候,长提示词从 6000 字精简到了 2500 字,剩下的都拆到了各个 SKILL.md 里,整体行为反而更稳定了。
还有一个建议是保留一个“兜底”技能。当所有技能都没匹配上时,加载这个兜底技能,里面放一些通用的规则和引导。这样即使按需加载出了问题,Agent 也不会完全失控。
最后说一个我自己的体会:Agent Skills 这套东西,核心不是技术有多复杂,而是思维方式要转变。从“把所有东西都告诉模型”变成“让模型在需要的时候自己找”。这个转变一开始会不习惯,总觉得“万一模型找不到怎么办”。但实际用下来,只要技能注册表和触发词设计得当,模型找技能的能力比我们想象的要强。而且即使偶尔找不到,也比被一堆无关规则干扰要好得多。