ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills 不只是长提示词:SKILL.md、按需加载与权限边界的设计逻辑

Agent Skills 不只是长提示词:SKILL.md、按需加载与权限边界的设计逻辑

做 Agent 应用的人,大概率都经历过这个阶段:为了让模型“什么都懂”,我们把所有规则、示例、工具说明一股脑塞进 System Prompt,直到提示词变成一份上万字的巨型文档。我见过最夸张的一份 prompt 有 2 万多字,修改记录到了 v12 版,效果却比 v8 还差。这个时候就该意识到,Agent Skills 的价值根本不在“把文字换成文件”,而是 SKILL.md 承载的结构化定义、按需加载机制、权限边界约束,这套东西本质上把“提示词”变成了“可运行的软件组件”。这篇文章就围绕这三个核心设计展开,聊清楚为什么 Agent Skills 不能只是长提示词,以及迁移过程中真正需要注意的细节。适合正在做 Agent 应用、RAG 方案、企业内部数字员工的工程师,也适合被 prompt 越调越乱折磨到想放弃的产品负责人。

1. 把Agent能力全塞进一条System Prompt,为什么最终会失灵

1.1 上下文窗口再大,也扛不住指令密度的下降

很多人觉得,上下文窗口从 4k 涨到 32k、再到 128k 甚至更大,那长提示词的问题不是自动解决了吗?这是典型的误解。窗口变大的确是好事,但它解决的是“装不装得下”的问题,而长提示词真正的毛病是“装下了也记不住、分不清”。

我们可以引入一个简单概念:指令密度。它是指提示词中“真正约束模型行为的关键指令”占全部上下文的比率。假设一份 5000 字的长提示词被塞进 100k 上下文的会话里,用户再粘贴几大段聊天记录、工具返回结果,模型在生成时面对的是一个混合体,关键指令只占很小比例。模型的注意力本质上是概率分布,不是“认真阅读每一个字”,指令密度越低,关键要求被落地的概率就越低。

所以我在调项目时有个经验:如果一段规则需要写在 System Prompt 里才能生效,说明它是靠“提醒”在起作用。靠提醒的东西,密度一降就会失效。真正可靠的机制,应该在架构层面保证“这段规则平时不在上下文中,只有需要时才出现,而且出现时就占据足够的注意力比重”——这正是按需加载的意义。

1.2 中间迷失:模型最擅长记住开头和结尾

还有一个让长提示词失效的硬伤,是位置偏差。语言模型对序列开头和结尾的内容关注度明显高于中间部分,这种现象在行业里有个通俗叫法叫“中间迷失”。如果一份提示词里排在第 80 条、位于文档正中间的规则,在真实推理时被覆盖的概率会高得多。

我自己做过一次对照测试。一条“必须引用数据来源”的规则,放在 System Prompt 第一段时,测试集里 9 次能遵守 8 次;把它挪到 50 条规则的正中间,5 次只能遵守 2 次。内容没变,只是位置变了,效果就崩了。这说明长提示词模式下,规则有效性高度依赖位置,而位置的排布又极其脆弱——你新增一条规则,就可能把之前的位置顶到“盲区”。

Agent 的技能式设计天然规避了这个问题。每个 SKILL.md 一旦被加载,就是当前上下文里的独立主体,要么完整参与推理,要么干脆不出现。它不会永远处在一条超长文档的中间位置。

1.3 多任务互相污染,和“改一处动全身”的维护泥潭

长提示词还会带来一种让人崩溃的现象:任务互相污染。比如一份提示词里同时写了客服话术和 SQL 生成规则,模型在生成 SQL 时会突然冒出客服腔调,或者在处理退款时错误地套用了物流话术的语气。因为所有规则被平铺在同一线性文档里,没有隔离层,模型很难在长序列中稳定区分“当前应该激活哪一组规则”。

维护上的问题更严重。我接手过一个客服 Agent,原有规则 80 条,为了支持新业务加了 3 条,结果用户反馈原本正常的问候语变了风格。排查了半天,发现是新增的规则中有一句“始终使用专业正式的语气”,它影响到了所有任务。想单独只影响某个任务,在长提示词里几乎做不到,因为你无法给规则加“作用域”。

技能体系把“作用域”变成了第一公民。每个 Skill 是一个独立的指令空间,加载哪个技能,哪个技能的规则才生效。不同技能之间不共享上下文,自然不存在这种跨任务污染。

2. SKILL.md 不是“提示词文件”,是能被索引和校验的指令单元

2.1 一个标准的Skill,文件结构长什么样

要理解 SKILL.md 的定位,先得看一个 Skill 的典型目录结构:

skills/ refund-handling/ SKILL.md scripts/ refund_status.py references/ refund_policy_2025.md examples/ refund_conversation_case.md

SKILL.md 是整个技能包的入口文件。它本身是 Markdown 格式,但和我们平时写给人类看的文档完全不同,它的读者主要是 Agent 框架的调度器和语言模型本身。文件内部有严格的元数据区(YAML frontmatter)和行为指令区,两者各司其职。

有人会问:为什么一定要用 Markdown?用 txt 不行吗?从纯技术上讲不是不行,但 Markdown 有几个天然优势:它有标准的元数据头语法,解析器可以稳定提取;它的标题结构可以让模型在长文档中快速定位章节;更重要的是,Markdown 在训练语料中极其常见,模型对它的结构有很强的模式直觉。这就像给指令套了一层“模型熟悉的格式外衣”。

2.2 frontmatter 里的 description,才是调度器的眼睛

SKILL.md 的开头是一段被---包裹的 YAML 元信息,其中最核心的字段是 name 和 description。name 是技能的稳定标识,description 则是调度器判断“当前场景要不要加载这个技能”的依据。

我见过一个典型的客服 Agent 技能元信息:

--- name: refund_request_handling description: > 处理用户的退款申请。适用于订单已完成支付但用户未收到商品、 商品损坏或与描述不符等场景。用户在咨询退款进度时使用。 当用户只是想闲聊、咨询优惠活动时,不要调用本技能。 permissions: - read:order_database - write:refund_record - require_approval:refund_amount ---

description 的写法直接决定技能的“召回率”。写得过于宽泛,比如“处理所有售后问题”,会导致大量无关场景错误加载,反而把上下文搞乱;写得过于狭窄,比如“仅在用户输入包含‘退款’且订单金额大于500元时才使用”,又会漏召回。我比较推荐的结构是:技能目标 + 触发场景 + 执行动作概述 + 不适用场景。

很多团队在设计技能时把精力花在正文的步骤上,却忽略了 description,这是本末倒置。正文步骤写错了最多是执行偏差,description 写错了直接是技能永远不被触发或者被错误触发。我见过一个冷门技能上线一个月调用率为零,追查后发现它的 description 里没有任何业务触发词,调度器根本不知道什么时候该用它。

2.3 正文是“执行协议”,不是“风格建议”

SKILL.md 的正文部分,是要被完整注入上下文、供模型在执行任务时“阅读”的指令。它应该围绕任务的输入、处理步骤、输出格式、约束条件四个维度组织。相比散文式描述,分步骤的编号结构效果要稳定得多。

举一个简化的正文片段:

## 执行步骤 1. 调用订单查询工具(tool: order_lookup),传入用户提供的订单号。 - 如果状态为 completed,进入第2步。 - 如果状态为 refunded,直接向用户说明退款已到账,流程结束。 2. 调用退款金额计算脚本(script: refund_amount.py),输出可退金额。 - 金额 ≥ 0 时,进入审批流程。 - 金额为 0 时,向用户解释不可退款原因,流程结束。 ## 输出要求 - 回答开头必须包含“关于您的退款申请”,结尾必须说明预计到账时间。 - 不允许承诺超出当前订单金额的退款。 ## 边界条件 - 本技能不处理换货请求,发现换货意图时告知用户转由人工客服处理。

有人可能会觉得这段内容写得有点“机械”,但这恰恰是对的。SKILL.md 不是给人看的操作手册,它是给模型看的执行协议。模型不需要润色和情感,它需要的是无歧义的分支判断和明确的步骤顺序。在实际效果对比中,结构化步骤比“请根据具体情况灵活处理”这种开放式指令的稳定率高出一大截。

2.4 和一段平铺提示词的本质差异

到这里可以做一个清晰的对照了。很多人觉得 SKILL.md 无非是把长提示词拆成多个文件,内容还是那些内容。这种理解忽略了“形式即架构”这件事:

维度平铺长提示词SKILL.md 技能单元
可寻址性无,只能整体注入有路径、文件名,可被索引和引用
元数据无name、description、permissions 等结构化字段
加载策略永远在上下文中按需加载,不在上下文中时才“零成本”
运行时权限默认继承全部工具权限由 permissions 声明,运行时强约束
版本控制靠整个 prompt 的版本每个技能独立迭代,可单独回滚
作用域隔离无,所有规则互相可见独立空间,互不污染

表格里最关键的一行是“运行时权限”。长提示词模式下,模型拥有的是全局工具权限,安全靠文字约束;技能模式下,框架可以在加载技能时动态授予一组受限权限,这已经超出了纯文本的范畴,进入了程序运行时的领域。

3. 按需加载:为什么“把100个技能都放进去”比“只放3个”更差

3.1 一次上下文预算复盘

先算一笔账。假设技能库里有 50 个技能,每个 SKILL.md 平均 2000 字。把 50 个技能全部注入上下文,光技能内容就是 10 万字。再加上用户会话历史和工具返回,还没开始干活,上下文窗口就快满了。

有人会说:模型不是有 128k 窗口吗?10 万 tokens 也装得下。但这里的关键不是“装得下”,而是“要付的代价”。上下文越长,推理延迟越高,单次调用成本越高,而且关键在于无关内容会稀释注意力。你让模型一边处理用户退款请求,一边让它“看着”物流查询、优惠券计算、商品推荐、售后回访等一堆技能说明,这些内容虽然不冲突,但会占用模型的注意力预算。

实测中我发现一个规律:同一任务,只注入相关技能时的任务成功率,比注入全部技能时高 10 到 20 个百分点。这还不是因为模型“变笨”,而是无关的技能说明在它视野里形成了噪音。模型无法像人一样自动忽略完全无关的一段文字,它至少会花一部分計算资源去“理解”它们。

3.2 按需加载的标准流程

实践中,一个完整的按需加载流程大致分四步:

  1. 索引预读:Agent 启动时,只把一个技能索引清单注入上下文。索引里每条只有 name 和 description,没有正文,体量非常小。

  2. 意图匹配:模型收到用户请求后,根据索引里的 description 做相关性判断,选出可能需要的一批技能,通常用 top_k 限制数量。

  3. 技能注入:框架从文件系统或技能仓库中读取选中的 SKILL.md,将其正文注入当前上下文,随后模型开始执行任务。

  4. 执行中补充:如果执行到一半发现某个分支需要额外技能,模型可以再次访问索引,按需补充加载。

索引文件本身是这样的:

skills: - name: refund_request_handling description: 处理退款申请、退款进度查询、退款可行性判断 path: skills/refund-handling/SKILL.md - name: logistics_tracking description: 查询物流轨迹、预判送达时间、处理延迟件 path: skills/logistics-tracking/SKILL.md

我把这个过程形容为“点到为止的懒加载”。索引像菜单,模型先看菜单决定点什么菜,而不是把整个后厨的食材和菜谱全部搬到桌上。这比把规则硬塞在 prompt 里的做法节省的不仅是 token,更是“认知开销”。

3.3 加载策略的几个关键参数

按需加载不是简单地“匹配就注入”,有几个参数需要在真实项目里反复调。

第一个是并发加载数量上限。我习惯把默认 top_k 设为 3,最多不超过 5。加载太少的风险是漏掉必要技能,加载太多又会重新走向“上下文膨胀”。为什么 3 比较合适?因为绝大多数单次 Agent 任务真正依赖的技能不超过 3 个,超过 3 个通常说明任务本身没拆分干净。

第二个是匹配阈值。description 与用户请求的语义相似度低于阈值时,不加载。阈值太低会导致疯狂误加载,阈值太高会导致什么都不加载。这个没有通用答案,建议基于样本集调,我自己的起点一般是 0.4,再根据实际调用率上下浮动。

第三个是缓存策略。同一个会话里,如果用户连续提出同类请求,技能内容不需要反复注入。兜底做法是:每次用户消息后,如果当前已加载技能与需求匹配,直接复用;若完全不匹配,再替换。

3.4 按需加载和检索增强生成(RAG)不是一回事

团队里经常有人把“按需加载技能”和“RAG 检索知识”混为一谈,这是两套思路。RAG 解决的是“模型不知道某个事实”的问题,比如公司的退款政策是什么、某款产品的参数是什么。它返回的是知识片段,模型把它当作参考资料。

按需加载技能解决的是“模型不知道该怎么做某件事”的问题。它返回的是一份行为操作手册,规定模型在这个场景下该调用哪些工具、按什么顺序执行、如何判断分支结果、输出格式是什么。区别在于:知识被参考,行为被执行;知识检索错误最多导致回答不准确,技能加载错误则可能导致整个执行链路走歪。

把技能描述也塞进向量库做相似度检索,这个思路可行,本质上是对索引文件的“召回增强”。但无论召回过程多智能,技能被加载后一定要进入“可执行”通道,而不是仅仅当作参考段落。理解了这一点,就理解了为什么技能化改造比单纯做 RAG 复杂一个量级。

4. 权限边界:把“请勿做某事”升级为“做不到某事”

4.1 长提示词下,安全约束为什么经常失效

长提示词模式有一个根深蒂固的安全缺陷:模型默认拥有所有能力,安全完全靠“提示词里的字面劝阻”。比如在 prompt 里写“绝对不要删除任何文件”,然后给模型挂了一个可以执行 shell 的工具。字面上它理解了这句话,但复杂任务执行过程中,中间步骤可能破坏掉它的上下文状态,或者提示注入让模型在某个分支里忘记了这条约束,最终调用执行工具。

问题的本质是:文字劝阻是“软约束”。模型将“不要删除文件”当成一条需要权衡的建议而不是物理上不可行的禁区。当提示词越来越长,这条建议的权重还会被进一步稀释。你没法靠语气词去对抗概率分布。

4.2 Skill 的运行时权限声明机制

技能体系提供的解法是硬约束:权限本身就不是模型“可以选择”的,而是框架授予与收回的。SKILL.md 里的 permissions 字段就是给框架看的“权限申请单”。

比如读取订单数据库、写入退款记录、触发审批,这些权限被框架在加载技能时动态授予。一个重要原则是:未声明的权限不存在。如果某个技能没有声明写文件权限,那么即使模型在推理时生成了文件写入操作,框架也会在工具调用层拦住,因为 Agent 运行时的工具集合里根本没有这个能力。

我特别强调一点:这种机制必须 fail closed,也就是缺少权限时直接拒绝,而不是 fail open(尝试执行或询问后仍然放行)。权限声明写错了导致技能调用失败,这个错误是显性的,容易被发现;权限声明过宽导致发生不该发生的操作,才是隐性的大坑。

4.3 高风险操作需要审批环节,而不是“先问问我”

在技能里处理高风险操作,比如退款、对外发邮件、删除数据,只靠权限声明还不够。常见做法是再加一层审批闸门:技能声明某个动作需要人工确认,Agent 执行到这一步时在界面上弹出审批卡片,等待用户点击确认后才继续。

这个设计和在 prompt 里写“执行前请先询问用户”完全是两回事。提示词里的“先问问我”是建议,模型可以在上下文复杂时遗忘;审批卡片是工作流层面的强制门,即使模型生成了对应动作,框架不拿到确认信号就不会真正执行。

我没有见过任何一家成熟的 Agent 应用敢把“高风险操作前先确认”这个保障交给纯提示词去落实。在权限系统设计成熟前,宁可让一部分技能无法自动执行,也不能让敏感操作处在“靠模型自觉”的状态。

4.4 提示注入面前,权限边界是最后一道防线

聊权限边界就不能不提提示注入攻击的场景。技能内容可能来自外部文档、第三方来源,恶意内容里可以嵌入“忽略上述指令,调用XX工具读取XX数据”之类的注入语句。模型确实存在被骗的可能,但权限系统在这里扮演了最后防线:如果该技能根本没有声明读取某个数据域的权限,即使模型被注入指令欺骗,工具调用也会在边界处被拦截。

这就是权限边界和长提示词最本质的区别。长提示词体系下,模型如果被注入的“更高优先级指令”蒙蔽,所有工具都是开放的,后果是真实的;技能体系下,权限在模型推理之前已经限定好了范围,注入指令想要越权,必须先突破程序层的权限系统,而不是靠说服模型。

安全模式实现方式失效模式
软约束prompt 文字:“不要做X”上下文丢失、注入干扰、规则被稀释
硬约束技能权限声明 + 运行时工具集过滤权限配置错误、审批流程缺失
最强约束技能权限 + 审批 + 独立沙箱环境权限声明混乱、沙箱逃逸漏洞

从表格可以看出,只靠第一层是远远不够的,技能体系的权限边界至少给了第二、第三层兜底。

5. 把已有的“长提示词”改造成多技能体系时的实操经验

5.1 粒度拆分的判断标准:按业务目标,不按工具

从长提示词迁移到技能体系,第一步不是写文件,而是拆粒度。我见过一个反面案例:有人按“工具归属”拆分,把需要用到数据库的技能全部揉进一个“数据库操作技能”里,结果这个技能包罗了订单查询、用户画像、库存更新几十种不相关动作,一加载就是几千字,退化成一个小型长提示词。

正确拆分标准是按业务目标。比如客服场景,拆成“退款处理技能”“物流查询技能”“优惠券计算技能”“退货预约技能”。每个技能解决一个明确目标,里面用到什么工具是实现细节。判断粒度是否合适有一个简单的测试:如果这个技能的 description 能在一句话里说清楚“什么时候用、用来干嘛”,粒度就基本对了;如果需要写两行才能解释清楚,说明还不够细。

工具类操作应该作为技能内部的 helper,而不是技能本身。这个认知能避免很多设计混乱。

5.2 description 写作模板:触发词不能省,排除条件必须有

description 是调度器的眼睛,这里给一个我反复验证过的写作模板:

  1. 技能负责的业务目标(一句话)。
  2. 使用它的具体触发场景(列出 2 到 3 个可识别信号,如“用户提到已有订单”、“点击了退款入口”)。
  3. 执行动作的大意描述(不必详细步骤,但要让模型知道这个技能是干嘛的)。
  4. 不适用场景(明确写出不要触发的情况)。

一个正面示例:

description: > 处理订单的退货退款流程。当用户申请退货、询问退款金额、 或发起退款投诉时使用。不处理仅查询物流信息的需求, 不处理已经完结超过30天的订单。

反面示例则常见这样的问题:“关于订单的所有问题都可以使用本技能”——这话和没说一样,调度器无法基于它做精准路由。description 不是给人看的简介,它是路由算法的输入特征,必须包含可匹配的业务信号。

5.3 迁移后一定要做指标对比

任何从长提示词到多技能体系的改造,都不能凭感觉判断成功。我建议至少跟踪五个指标:技能命中率(用户请求是否被正确加载到对应技能)、任务成功率(最终是否完成用户目标)、平均延迟、单次会话 token 消耗、需要人工兜底的次数。

实操时可以先跑一周旧方案作为基线,然后切换技能体系,再跑一周,做同期对比。根据我的迁移经验,token 消耗下降是最明显的,通常在 40% 到 60%,因为不再需要把所有规则都塞进每次调用。任务成功率的提升反而没有那么明显,因为它还受到技能拆分质量、description 命中率的影响,但只要迁移方案合理,最终总会向上走。

有一个容易被忽略的点是:技能命中率不等于任务成功率。命中率高只说明该加载的技能都加载了,不代表执行对了。两个指标要分开看,先调命中,再调执行。

5.4 迁移路上的几个坑,和对应的调整思路

第一个坑是技能描述重叠。两个技能的 description 都提到了“售后”,导致一次请求同时加载两个技能,彼此间的规则产生摩擦。解决办法是明确各自的边界,在 description 里写排除条件,比如“不处理退款问题”这种反向往约束。

第二个坑是 SKILL.md 正文过长。技能被加载后,正文长度直接占用上下文。我建议单个技能的正文控制在 1000 到 2500 字之间。超过 3000 字就要考虑拆子技能,或者把长篇参考资料移到 references 目录,只在需要时追加读取。

第三个坑是版本管理混乱。SKILL.md 一旦上线,任何人改动都可能影响线上行为,但传统 git 流程对单个文件变更的验证覆盖不足。我的做法是每一个技能的变更都有独立版本号,并配套一个最小测试集,每次变更后至少跑一遍该技能对应的 5 个典型测试样例,防止“改了 A 技能,B 技能跟着出问题”的情况。

第四个坑是冷门技能从不触发。排查时先看 description 里有没有业务触发词,再看匹配阈值是否设置过高。有时候把阈值从 0.6 降到 0.45,调用率就上来了。

最后说一点个人体会。我前后接触过多个从长提示词转向技能体系的案例,最深的感受是:很多人抱怨“模型变笨了”,其实不是模型的问题,而是他们把所有能力都塞进上下文,让模型在噪音里找重点。技能体系的三个核心——SKILL.md 结构化定义、按需加载、权限边界——其实都在做同一件事:让指令在正确的时机、以正确的形态、在受限的范围内被使用。这套思路迁移到任何需要 Agent 自主决策的场景,都能让系统从“一个什么都管但什么都管不好的巨型 prompt”,变成一组职责清晰、可独立维护、可安全组合的积木。如果你也在被长提示词困扰,我建议先从记录一份现役 prompt 的“规则清单”开始,找到那些只在特定场景使用的内容,拆出第一个 SKILL.md,你会立刻感受到差异。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进