☰
Agent Skills不是长提示词:SKILL.md、按需加载与权限边界实战
2026/10/13 16:44:13 网站建设 项目流程

在把二十多个技能说明硬塞进系统提示词吃了一个多月苦头之后,我对“Agent Skills 就是长提示词”这个说法真的忍不了了。最初我也觉得,上下文窗口都这么大了,每个技能写一段说明丢进去,模型看到关键词自然会调用,何必搞什么SKILL.md、按需加载这些复杂机制?结果实践下来,这套“朴素方案”踩的坑一个比一个深:上下文开销暴涨、技能规则互相打架、改一个技能要全量回归,更麻烦的是权限完全没法控制——一个技能里的命令一旦被触发,Agent能执行的操作面就是模型能操作的全部,没有任何隔离。这篇内容我想系统聊聊,为什么Agent Skills不能只是“长提示词”,以及SKILL.md、按需加载、权限边界这三件事背后的核心逻辑和我自己的一些实操经验。

1. 为什么“提示词写长一点”看起来很省事,实际是死路一条

1.1 上下文成本:你不是在写提示词,而是在给每次请求交“常驻税”

先讲最容易被忽视的账。模型每次请求都要把全部输入token读完,系统提示词里每多一段固定的技能说明,它就会在每一次请求中出现。这不是写文档时“多写一段也无妨”的问题,而是一笔随请求次数线性增长的固定成本。

假设一个技能说明平均1000个token,20个技能就是2万token。无论用户问的是“帮我写一封邮件”还是“今天天气怎么样”,这2万token都会先被模型读完,再开始回答。按一天1000次请求算,一个月就是6亿token的纯浪费。这部分开销跟用户需求完全无关,纯粹是为了“让模型知道有这些技能存在”付出的常驻税。

除了钱,还有延迟。输入token越多,首个token的响应时间越长。我在自己的项目里观察到,技能说明从2000 token涨到2万token后,每次请求的首响应时间增加了大约30%到50%。这种延迟在交互式场景里体验很差。更隐蔽的问题是,多轮对话中历史不断累积,常驻技能会把真正需要的工作上下文挤到窗口边缘,严重的时候模型甚至会“遗忘”用户最开始的需求。

1.2 服从性衰减:规则堆得越多,模型越容易记混

第二个问题出在质量层面。模型对超长文本中每一条规则的注意力是不均衡的。一个提示词里塞20个技能,每个技能都以“当你需要XXX时,请使用YYY”开头,读到最后几条时模型已经记混了。更麻烦的是关键词重叠——“格式化”这个词会同时出现在JSON格式化、YAML格式化、SQL格式化三个技能说明里。用户说“把这段内容格式化一下”,模型很可能随机选一个,或者把多个技能的行为混在一起。

我实测遇到过一个非常典型的情况:某次用户让Agent“整理一下日志格式”,模型同时调用了JSON格式化技能和日志清洗技能,输出既不是JSON也不是日志表格,而是介于两者之间的东西。这种问题在长提示词模式下很难修,因为你没法定位是哪条规则出了问题——所有规则都混在一起,互相影响。拆成独立技能模块之后,被选中的技能正文才会进入上下文,没被选中的技能根本不对模型产生干扰。这就是“先选后读”的价值:与其让模型在20条规则里挑,不如先让它看一眼精简目录,再把完整说明呈现在它面前。

1.3 维护噩梦:改一个技能,要重新测试全部技能

第三个问题是维护维度。长提示词方案下,技能说明是拼在一整段提示词里的,技能之间是紧耦合状态。改一个技能的说法,模型对整段提示词的理解都可能发生变化,本来能正常触发的其他技能会突然失灵。你只能做全量回归,把所有技能的用户场景逐一测一遍,才敢上线。

SKILL.md模式下,每个技能是独立文件独立目录,可以单独修改、单独测试、单独回退。我现在的习惯是,改一个技能只跑这个技能的测试样例,再顺带测两三个描述相近的邻近技能,不需要把整个技能库重新过一遍。版本号还能帮你快速回滚到上一个可用版本,这在长提示词方案里是没法想象的。

维度长提示词方案SKILL.md + 按需加载
上下文开销所有技能说明常驻仅被选中的技能载入
技能隔离弱,规则互相干扰强,独立模块独立测试
测试与回滚全量回归单技能验证、版本回滚
权限控制无法按技能区分每个技能独立声明

2. SKILL.md:技能元信息与正文为什么要分离

长提示词方案最大的结构问题,是把“技能是什么”和“技能怎么用”揉在一起给模型看。SKILL.md要解决的,恰恰就是把这个混合体拆成两层:一层的元信息,供路由决策使用;一层是正文说明,供技能被选中后指导执行。

2.1 一个SKILL.md的骨架长什么样

一个标准的SKILL.md通常由两大部分组成:开头的YAML frontmatter元信息,以及后面的Markdown正文。字段的具体名称在不同框架里略有差异,但思路是一致的。

--- name: json_prettifier description: 当用户提供一段JSON文本,并要求美化缩进、按键排序、压缩为单行或统一格式风格时使用;不负责语法错误校验。 version: 1.2.0 permissions: filesystem: read: ["${WORKSPACE}/data/**"] write: ["${WORKSPACE}/output/**"] network: none commands: allow: [] dependencies: runtime: python3 --- # 美化JSON 处理步骤: 1. 使用 Python 内置 json 模块解析输入。 2. 根据用户要求设置缩进、排序或压缩参数。 3. 输出格式化结果,或写入输出目录。 注意事项: - 输入不是合法 JSON 时,返回解析错误信息,不尝试修复。 - 输出文件命名规则:原文件名加 `_formatted` 后缀。

frontmatter里的字段各有各的用途:name是机器唯一标识,description是给模型做路由判断的,version负责版本管理和回滚,permissions声明技能权限边界,dependencies声明运行时依赖。正文部分则只描述“怎么做”的流程。另外有一个容易踩的坑:路径变量不要写死。上面示例里的${WORKSPACE}是平台运行时注入的变量,替换成绝对路径换一台机器就废了。

2.2 description字段是给“路由器”看的,不是给人看的

description是SKILL.md里最重要的字段,没有之一。它在按需加载的路由阶段起到决定性作用。模型在决策时,只能看到所有技能的description列表,要靠这些短文本做初筛判断。这个判断只需要回答一个问题:当前用户请求有没有落在本技能的职责范围内。

所以description不能写成“该技能用于处理JSON”这样笼统的功能介绍,而应该像API函数的docstring:动词开头,写明触发场景,写明不适用边界。

错误的写法:“把JSON格式化。”——太短,没有边界。 正确的写法:“当用户提供一段JSON文本,并要求美化缩进、按键排序、压缩为单行或统一格式风格时使用。该技能不负责语法错误校验;若需要判断JSON是否合法,请使用JSON校验技能。”

差别在哪?正确写法里有“当…时使用”的触发条件,有具体的动作动词,有明确排除的边界。这些信息让模型即使面对两个description含相似词汇的技能,也能做出区分。

提示:description里的每一句话都应该服务于路由决策,而不是给人看的项目介绍。

2.3 正文只写“怎么做”,少写“是什么”和“为什么”

正文部分最常见的误区,是把背景知识写进去。“JSON是JavaScript Object Notation,一种轻量级数据交换格式”这种句子,对模型的帮助几乎为零。模型不缺背景知识,缺的是具体操作序列。

正文应当只包含能指导模型完成任务的操作知识:具体步骤、输入输出示例、边界条件、输出规范。示例的作用尤其大,模型擅长模仿示例,一个包含输入和预期输出的例子,胜过十句抽象描述。边界条件也要明确,比如空输入、非法输入、超大输入分别应该怎么处理,否则模型会在这些意外情况下自由发挥。

正文精炼还有一个现实原因:技能被选中后,正文会占用上下文token。精炼的正文在完成同样任务时消耗更少,模型执行时也更不容易中途迷路。长提示词方案要求“尽可能完整覆盖各种情况”,导致提示词越来越臃肿;SKILL.md因为能被按需加载,可以做到“最小足够”——只覆盖本技能的职责边界,其他情况交给别的技能或者通用能力。

3. 按需加载:让Agent决定“带哪几个技能出门”

3.1 加载机制:路由先行,正文后到

长提示词方案把所有技能一次性全部塞进上下文,让模型“看到”所有技能。按需加载则反过来:先把技能库变成注册表,注册表只保留轻量元信息,模型根据用户请求在元信息上做匹配,选中之后再读取对应技能的完整文件和资源。

这个机制拆开看有三个环节。注册层:框架扫描技能目录,把每个SKILL.md的name、description等元信息提取成索引。决策层:根据当前对话最近一轮内容,有时连同历史信息,在索引上计算匹配度,选出top-1或top-k技能。加载层:被选中技能的正文和依赖资源注入会话上下文,全程没被选中的技能不占任何上下文空间。

这个模式很像先看目录再翻页。目录很薄,翻起来很快,等确定要读哪一章之后,才把整章内容拿过来细读,而不是把整本书背在身上。

3.2 二十个技能以内怎么选,上百个技能又怎么选

技能库的规模不同,路由策略也不同。小于20个技能时,直接让模型自己选择就够了——把全部技能的description列出来,让模型当场决定。这个阶段最需要做好的,是把description写清楚,并且控制好词表重叠度。

超过20个技能之后,description列表本身就会变得很长,让模型逐条阅读的成本开始上升。这时候建议在路由层加一道向量检索:把每个技能的description预先embedding成向量,用户请求来了先做相似度召回,筛出前5到10个候选,再把这些候选的description交给模型做最终选择。相当于“初筛加终选”,向量负责快速缩小范围,模型负责在候选集中做精准判断。

3.3 实测收益:上下文开销省了多少,正确率提升在哪

我在自己的项目里做过一次对比。技能库共18个技能,如果全部塞进系统提示词,光技能部分就占约2.1万token。改成按需加载后,平均每轮实际命中2到3个技能,技能正文部分只占约3000到5000token,开销下降了七成以上。响应速度也跟着变快了,首token延迟明显缩短。

更重要的变化在正确率上。全量加载时,我频繁遇到该用JSON格式化技能却用了日志清洗技能这类误触发;按需加载后这类错误少了很多,因为没被选中的技能压根不在模型视野里,不存在“20个选项里挑一个”的困扰。每个技能的职责范围,在模型看来反而更清晰了。

3.4 按需加载不是免费的:路由也有盲区

按需加载的代价,是路由可能选错。一旦路由把某个技能漏掉了,后果比全量加载更严重——模型根本不知道这个技能存在,也就不会调用,用户会觉得“你好像变笨了”。在全量加载方案里反而不会有这个问题,所以工程上必须给按需加载留后路。

我的建议是,路由决策阶段一定要有“无技能可用”的兜底分支,让模型在无法判断时不要硬选,而是明确回答“没有找到匹配技能”或直接使用通用能力回答。另外可以把最常用的几个技能设为常驻,其他技能走按需加载,既保留上下文清爽,又能覆盖高频场景。最后,路由日志要记录下来——哪些技能频繁被选中、哪些从来不命中——这些数据既是优化description的依据,也是清理技能库的依据。

4. 权限边界:技能可以“知道”什么,又能“动”什么

4.1 长提示词方案的危险:提示词只是文本,动作共享一套权限

权限边界是“为什么不能只是长提示词”这个问题里最容易忽略、却最致命的一环。

长提示词模式下,所有技能说明只是文本。真正执行动作的是Agent的通用工具集:文件读写、命令执行、网络请求。模型在技能说明的指引下调用这些工具时,权限就是Agent主系统的权限。技能里写“当需要整理项目时,把临时目录清空”,模型照做时调用删除命令的权限和Agent主系统一样大,没有任何机制能拦一下。

这就像给了所有技能一把能开所有房间的钥匙,只是在一张纸条上写着“不到特殊情况别乱开门”。模型不是故意的,但长提示词的模糊表达、技能间的相互干扰,都可能让它做出超范围操作。权限边界解决的就是这个问题:把“模型说什么”和“系统允许做什么”分开,靠运行时强制约束,而不是靠模型自觉。

4.2 权限的最小单位:文件、命令、网络、凭据

一个技能应该声明四类资源的权限,缺一不可。

文件系统权限:声明可读路径和可写路径。比如只读工作区里的data目录,只写output目录。没有权限的路径,运行时直接拒绝。命令执行权限:默认拒绝一切命令,需要执行时走白名单机制,只允许技能声明的那几条命令。网络访问权限:默认关闭,需要访问外部API的技能单独声明允许的域名或URL前缀。凭据管理:技能需要第三方服务的key时,不要在SKILL.md里明文写key。技能只声明“需要xx服务的密钥”,由框架在运行时从安全存储注入,并且只在技能执行期间可访问。

正确示范:

permissions: filesystem: read: ["${WORKSPACE}/data/**"] write: ["${WORKSPACE}/output/**"] network: none commands: allow: []

反面示范:

permissions: filesystem: read: ["/**"] write: ["/**"] network: full commands: allow: ["*"]

如果你的技能只是处理data目录下的文件,前者就够了。后者等于把Agent的全部能力交给技能,跟长提示词方案没有本质区别。

4.3 权限审计与最小权限原则

权限边界的设计原则是“最小权限”:技能运行时只拥有完成本职任务所需的最少能力,其余功能一律关闭。

但“最小权限”要落地,必须有审计工具配合。框架应为每次技能调用记录审计日志:调用了哪些工具、读了哪些文件、写了哪些文件、有没有发起网络请求。一旦出现异常操作,可以通过日志快速定位是哪个技能在哪个时间点做了什么。长提示词方案连这种排查都做不了——技能只是提示词文本,日志里只能看到Agent的通用操作记录,没法追溯是哪段规则导致的操作。

注意:不要把API key或任何凭据写进SKILL.md文件,凭据必须由运行时统一注入。技能的知识和技能的权限是两回事,技能可以“知道”凭据存在,但不应该“拥有”凭空读取凭据的能力。

4.4 权限太严怎么办:别急着加宽,先用运行时授权

最小权限原则说起来容易,做起来会遇到实际问题:技能被限制得太死,用户指定的文件路径不在白名单里,直接报错,技能就废了。遇到这种情况,不要简单地在SKILL.md里把路径改成全量读,应该用运行时授权机制。

所谓运行时授权,就是技能保持声明最小权限不变,用户在对话中显式确认“允许该技能读取某个路径”后,框架把这份授权写入本次会话的临时权限表。技能访问该路径时不再被拒。这种动态授权既保持了技能默认最小权限,又让场景灵活很多。如果你发现某个技能频繁需要动态授权,那说明它默认声明的权限范围划分错了,应该调整默认边界,而不是每次都靠用户手动补授权。

5. 从零设计一个SKILL.md:日志格式化技能的全过程

5.1 场景与需求拆解

用一个我常做的技能当例子:应用日志清洗与格式化。用户经常丢来一大段应用日志文本,要求按时间排序、按级别过滤、统一时间戳格式。在长提示词方案下,我写过一段很长的提示词,效果一直不稳定,原因也很典型:不同用户表达需求的方式差别很大,“排序”“过滤”“清理”“整理”都可能是同一个需求的不同说法,提示词里描述稍有偏差,模型就不知道到底该不该触发。

用SKILL.md重做之后,第一步不是写正文,而是拆需求。先把输入形式定下来:支持粘贴文本,也支持文件路径。再列处理能力:按时间戳排序、按DEBUG/INFO/WARN/ERROR级别过滤、统一时间戳格式。然后是输出形式:支持直接返回文本,也支持写入输出目录。最后划边界:只处理纯文本日志,不抓远程日志服务,不处理二进制文件。

5.2 完整SKILL.md示例与逐段解读

需求拆清楚之后,写出来的SKILL.md大概是这样的:

--- name: log_formatter description: 当用户提供一段应用日志文本或日志文件路径,并要求按时间排序、按级别过滤、统一时间戳格式或整理成表格时使用。适合单次粘贴的短日志和日志文件批量处理;不适合抓取远程日志服务,也不处理二进制日志。 version: 1.0.0 permissions: filesystem: read: ["${WORKSPACE}/logs/**", "${INPUT_FILE}"] write: ["${WORKSPACE}/output/**"] network: none commands: allow: [] dependencies: runtime: python3 --- # 日志格式化技能 处理步骤: 1. 读取输入文本或文件,按行分割。 2. 正则提取每行开头的 ISO 时间戳;无法识别时间戳的行标记为 `NO_TS`。 3. 支持过滤级别:DEBUG / INFO / WARN / ERROR,默认全部保留。 4. 输出格式:先按时间升序排序,`NO_TS` 行放在末尾,用 Markdown 表格呈现或写回文件。 注意事项: - 日志行超过 5000 行时,只处理前 5000 行并提示用户。 - 若输入为空,直接返回提示信息,不执行任何写入。 - 写文件前必须确保输出目录存在,不存在则创建。

看description部分,它有触发场景,有支持的动作,也有明确排除的情况。全文没有“日志是应用程序输出的记录”这类背景知识,全是操作步骤和边界条件。再看权限声明,read只覆盖logs目录和输入文件,write只覆盖output目录,网络是none,命令是空列表——这个技能的全部工作就是把文本读进来,处理完写出去,完全不需要网络,不需要执行外部命令。

5.3 从“能用”到“好用”:五个优化经验

第一,版本号一定要写。技能改坏是常有的事,有version字段才能快速回滚。哪怕只是description里加了一个触发词,也应升一个版本。

第二,description里加“不适用场景”。很多技能互相干扰,就是因为只写了什么时候用,没写什么时候不用。加上“本技能不负责XX”之后,路由的区分度会明显提升。

第三,正文末尾放一个“本技能不做的事情”清单。这个清单在长提示词方案里很难加,因为堆在一起只会让提示词更长;但在SKILL.md正文里加一份负面清单,能有效防止模型把多个技能的行为混在一起。

第四,把复杂逻辑拆到参考脚本里。SKILL.md正文保留简短步骤,复杂解析逻辑放到scripts目录的脚本里,正文告诉模型“调用scripts/format_log.py即可”。这样正文短、token省、模型执行也稳定。

第五,建立回归测试样例集。为每个技能准备几组输入:正常输入、空输入、超大输入、格式错误输入,并记录对应的预期行为。技能改动后跑一遍样例,比人工反复聊天验证高效得多。

测试用例预期行为
正常日志10行排序并输出表格
空字符串提示输入为空,不写文件
4000行日志正常处理,给出处理提示
5010行日志只处理前5000行并提示用户
完全无时间戳的内容全部标记NO_TS并置底排序

6. 踩坑记录:技能不触发、互相抢占、权限过严的排查链路

6.1 现象一:技能从不触发

我遇到过最典型的问题,是技能写好了,请求打过来它却永远不出来。排查时不要先怀疑框架,第一件事是检查description。

先看description是不是写成了“这个技能可以处理XXX”的句式,而不是“当用户给出XXX并要求XXX时使用”的句式。前者没有触发钩子,模型很难判断什么时候该选它。再看是不是被其他技能抢占,另一个description里包含完全相同的关键词时,路由可能会优先选它。把问题技能单独隔离到临时目录再跑一遍,如果这样能正常触发,说明是路由竞争问题,不是技能本身的问题。最后检查兜底逻辑,如果用户请求的意图比较隐晦,模型可能直接走了通用能力分支,压根没进技能路由。

修改方向就是给description补上具体触发词和动作场景,在“不适用场景”里加上排除项,然后在最常用的场景下反复测试,直到触发稳定。

6.2 现象二:多个技能互相抢占

两个技能的description都含“格式化”三个字,模型经常调错。这种问题最好在上线前就预防:description里尽量避免使用与其他技能共用的宽泛动词,如果避免不了,就用“触发场景加边界”把两者区分开。

假设A是JSON格式化,B是日志格式化。A的description写“当用户要求美化JSON缩进、按键排序或压缩为单行时使用”,B的写“当用户要求按时间排序日志行、按级别过滤日志时使用”。两者虽然都会沾“格式化”,但动作对象和输入形态完全不同,模型基本能分清楚。如果框架支持优先级或score字段,可以在路由层微调权重,但别依赖它——description写清楚才是根本。

6.3 现象三:按需加载后技能偶尔“消失”

这是按需加载模式的典型副作用。用户在长提示词方案下从没见过技能“消失”的问题,因为技能说明一直在上下文里。切到按需加载后,用户问“你怎么不会这个了”,通常不是技能失效,而是路由没选中它。

排查时先看路由日志,确认这个技能最近的命中次数和触发条件。再检查它在描述里是否缺乏明确的触发信号。有一个实操技巧:给description加一行“调用方式示例”,写明“用户说出‘帮我整理日志’、‘按错误级别过滤’这类话时,优先使用本技能”。这相当于给路由器喂了一组标杆样例,命中率会提升不少。实在不行,就把这类高频技能设为常驻技能,别走按需加载了。

6.4 现象四:权限过严导致技能“半残”

技能能读不能写、或者写不了特定目录,这种情况先看权限审计日志。日志里会明确记录被拒绝的路径和操作,看到哪一步被拒就能对症下药。按4.4里说的运行时授权机制处理,别让技能默认权限膨胀。

路径问题是另一个高频坑。很多权限失败是因为路径写死了,没有用框架提供的路径变量。${WORKSPACE}在不同环境下指向不同目录,技能里写“/home/某用户/data”这种绝对路径,换一台机器就废了。规范做法是全部使用框架注入的路径变量,这样技能才能在不同环境间迁移。

技能库维护这块,我目前的习惯是每个技能目录放五个部分:SKILL.md、scripts目录、resources目录、tests目录、CHANGELOG文件。每次改技能,先补测试样例,再改正文或权限,最后更新变更记录。改完跑一遍测试集,十几分钟就能确认没有破坏其他技能。遇到需要回滚的情况,version字段加上变更记录能很快定位到上一个版本。这套流程跑下来,技能库从二十几个膨胀到五十几个也没再乱过。如果你也正在用“把所有技能说明堆进提示词”的方案,我建议抽一个周末,把其中一个技能拆成SKILL.md试试。按需加载和权限边界带来的收益,用一次就知道了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询