首页 / 文章

代理技能即代码:界定范围、公开信息、编写脚本并对其进行测试

为何 SKILL.md 是生产级逻辑:仅记录流程化知识,调整激活机制,逐步加载内容,将具体操作移至脚本中,构建评估体系并淘汰无效部分。

发布者:REACTAPP.TOP阅读约 10 分钟2207 词

来源: Agent Skills Are Code. Treat Them Like It.

SKILL.md 文件看起来像文档,但编辑它会改变智能体的行为方式。这使它更接近代码而非散文,因此应当据此进行设计、界定范围并开展测试。

技能并非文档

构建大语言模型技能时,人们通常的第一反应是写更多内容:解释架构、添加示例、描述边缘情况并说明背景,以便模型理解事物为何如此运作。但这种做法往往是错误的。

技能并非供学习该系统的开发者使用的手册,而是注入到正在努力完成任务的智能体中的上下文信息。这一差异决定了整个设计方向。关键问题不在于如何向模型解释系统,而在于智能体需要了解什么才能做出正确的下一步决策。

从这个角度出发,一些选择就会变得更容易:

  • 将说明以流程形式编写
  • 明确指定激活条件
  • 逐步加载信息
  • 将确定性任务放入脚本中
  • 在真实任务中测试技能

仅保留程序性知识

现代编程智能体已经能够处理大量常规的软件工程任务。它们熟悉 Git 和 npm,了解 Angular、React、TypeScript、SQL、REST API、测试框架以及 CI 配置。仅仅重复这些内容的技能几乎毫无价值:

When implementing an Angular component, use TypeScript.
Write maintainable code.
Follow Angular best practices.
Add tests.

它消耗上下文信息,却无法补充模型所缺乏的内容。真正有价值的技能能够掌握特定于你所处环境的知识:

When modifying an Angular application in this repository:

1. Locate the closest project.json belonging to the affected application.
2. Check whether the project uses Vitest or Karma before generating tests.
3. For applications under apps/trading/, do not import directly from apps/shared/.
4. Run the affected project's test target rather than the workspace-wide test command.
5. Before changing a public interface in libs/order-entry/, search for consumers in
   apps/trading-terminal and apps/blotter.tp

在这里,智能体学到了些它无法从 Angular 的公开文档中可靠推断出的内容,即某个特定代码库中的工作应如何开展。这正是技能的用武之地。

Agent Skills 格式将相关内容组织为一个目录,其中包含 SKILL.md 文件以及可选的脚本和资源。元数据可供查询,而完整的说明文件和支持文件则仅在需要时加载。文件格式并非关键所在,真正重要的是通用模型能力与你的环境必须提供的知识之间的界限。

上下文即预算

交给智能体的所有内容都会消耗上下文。工具描述、MCP工具架构、仓库使用说明、对话历史以及技能描述都会占用上下文,而已激活的技能其内容则会消耗更多。因此问题不在于某些信息是否有用,而在于它是否对当前任务有用。

假设这些技能已经安装:

angular-testing
angular-migration
jira-workflow
postgres-debugging
release-process
accessibility-review
storybook
incident-response
performance-profiling

当任务是修复出问题的 Angular 单元测试时,代理无需了解发布流程或 PostgreSQL 调试步骤。正因如此,发现能力几乎与内容本身一样重要。对比这两段描述:这一段的触发条件极其简单:

---
name: testing
description: Helps run tests in our repository.
---

这个要精确得多:

---
name: angular-unit-testing
description: >
  Run and debug Angular unit tests in projects using Karma or Vitest.
  Use for Angular unit-test failures and test creation.
  Do not use for Playwright E2E tests, React projects, or backend tests.
---

第二种描述不仅说明了该技能的功能,还划定了路由边界,将无关指令排除在工作上下文之外。一篇关于LLM智能体技能设计的文章很好地阐述了这一点:技能元数据并非单纯的装饰性内容。草率的激活规则会将有用指令引入错误的任务中,而过于严格的规则则会导致该技能根本无法被找到。

逐步披露优于庞大的SKILL.md文件

常见的错误是让SKILL.md逐渐变成一个小型内部维基。它最初只有二十行代码,随后有人添加了部署步骤、故障排除指南、示例以及配置选项。三个月后,智能体要修改一个文件就必须阅读1,500行内容。

应当像设计软件那样来组织结构:

angular-workflow/
├── SKILL.md
├── testing.md
├── migrations.md
├── architecture.md
└── scripts/
    ├── affected-projects.ts
    └── verify-boundaries.ts

SKILL.md 中仅包含路由逻辑:

# Angular repository workflow

When modifying an Angular project:
1. Identify the affected project.
2. Read `architecture.md` when changing dependencies between projects.
3. Read `testing.md` when creating or modifying tests.
4. Read `migrations.md` when changing Angular versions or framework APIs.
5. Run `scripts/verify-boundaries.ts` after changing imports between projects.

只有当任务涉及测试时,详细的测试说明才会被纳入上下文。Anthropic将此称为渐进式披露:智能体首先加载元数据,一旦确定相关技能有用才会读取正文,只有在必要时才会打开引用的文件。其目的并非提供尽可能多的知识,而是在恰当的时刻传递正确的知识。

让脚本执行其能够完成的任务

另一个界限有助于区分二者:指令用于决策,而脚本则用于确定性任务。想象这样一个技能的描述:

Find all applications under apps/.
Check their Angular versions.
Ignore archived applications.
Compare their versions with the root package.json.
Report applications that are behind.

代理可以完成这项任务,但必须通过反复检查目录、解析文件、判断什么内容属于归档文件、跟踪状态,以及可能进行大量工具调用才能实现。当某个操作能够被精确描述时,只需将其编写一次即可:

node scripts/check-angular-versions.mjs

该技能会明确告知何时运行它以及如何处理输出结果:

When checking Angular version consistency, run
`node scripts/check-angular-versions.mjs`.

If it reports mismatches, inspect only the projects listed in the output.

这在多个方面都有优势。该脚本可以进行单元测试,且每次运行结果一致。它需要的模型决策更少,调用的工具也往往更少。一旦出现故障,无需让大语言模型重复推理过程即可重现问题。

这并不意味着要用脚本取代智能体,而是意味着要选择合适的抽象方式。用模型来进行判断,比如确定架构变更可能影响哪些项目;用代码来实现精确的算法,比如列出哪些项目引用了某个特定包。智能体则负责协调这两者,这样的组合比用英语编写整个工作流程更为高效。

工具与技能解决不同的问题

工具为智能体提供操作指令,而技能则教会其在何时以及如何使用这些指令。命令行工具或许可以提供一种计算受影响项目的方法:

tr project affected --base main

该工具提供相应能力,而匹配技能则赋予组织所需的知识:

Before modifying a shared library:
1. Run `tr project affected --base main`.
2. If either `trading-terminal` or `blotter` is affected, run their integration suites.
3. If more than five applications are affected, stop and inspect the dependency change
   before proceeding.1

CLI 并不需要知道为何有五个受影响的应用程序会显得可疑,相关技能也不必了解依赖关系遍历的原理。这就是清晰的边界划分。无论能力是通过 shell 命令、Web API、MCP 服务器还是其他任何接口提供的,都应遵循同样的原则:除非政策确实属于低级工具的范畴,否则不要将其嵌入其中;除非必要,否则也不要用普通文本形式编写确定性算法。

更多的技能并不一定更好

在开发智能体时,一种诱人的做法是收集那些看起来有用的技能并全部安装,然后认为智能体的能力就此提升了。但事实表明,应当更加谨慎地选择技能。

SkillsBench涵盖了11个领域的86项任务。根据其论文的初始版本,经过筛选的技能使平均通过率提升了16.2个百分点,不同领域之间的提升幅度差异较大。软件工程领域的提升幅度仅为4.5个百分点,且在84项任务中有16项在加入这些技能后表现反而更差。由模型自行生成的技能则没有带来任何平均提升效果。

SWE-Skills-Bench研究了49项用于真实软件工程任务的公开技能。其中39项并未提升通过率,平均提升幅度仅为1.2个百分点。仅有7项高度专业化的技能使结果提升了多达30个百分点。另有3项技能则因指导内容与项目实际使用的版本不同,导致表现下降了多达10个百分点。

这符合该架构的预测结果。通用技能会与模型已有的知识产生竞争,而非常具体的技能则能恰好提供缺失的信息:

When our generated API clients change, regenerate schemas before running typecheck.

那个有价值。这个的价值则低得多:

Always think carefully before modifying APIs.

经验法则:不要因为存在某个任务就创建技能,而应针对智能体始终遗漏的流程知识来创建技能。

技能需要测试

这可能是最大的概念转变。由于修改 SKILL.md 会改变行为,其后果与代码修改类似:

  • 删除一条指令可能导致智能体跳过必要的验证步骤。
  • 如果描述过于宽泛,该技能可能在无关工作中被触发。
  • 添加大量参考文档可能会增加令牌使用量,但并不会提升效果。
  • 重写流程可能会导致成功率下降。

因此必须进行测试。对于用于管理 Angular 迁移的技能,评估集可以包含如下任务:

Upgrade application A from Angular 20 to Angular 21.

怀着这样的期望:

Expected:
- correct migration command executed
- generated migration changes preserved
- application tests executed
- unrelated applications untouched

第二个任务:

Upgrade a shared library consumed by applications A and B.

带着自身的期望:

Expected:
- consumers discovered
- both affected applications typechecked
- dependency constraints respected

第三点:

Modify a React application in the same monorepo.

其预期结果是不会发生激活:

Expected:
- Angular migration skill is NOT activated

那个最终案例与任何一次成功的运行具有同等重要性,因此需要测试两种情况:该技能应该在此处激活,而不应在那里激活。有一个名为SkillGrade的工具正是基于这一理念开发的。你可以定义任务,让智能体反复运行,然后通过精确的检查、基于记录会话的LLM评分标准,或两者结合来为每次运行打分。即便没有这个工具,你依然可以应用这一原则。关键在于将技能视为可能退化的存在。如果在无法判断新版本是否优于旧版本的情况下就修改技能,那就相当于在未经测试的情况下修改生产逻辑。

从失败开始,而非从技能市场开始

创建无用技能的最简单方法,就是询问编码智能体应该具备哪些技能。这样你就会得到一份清单,内容包括测试、调试、代码审查、文档编写、架构设计、Git工作流、无障碍性、安全性以及模型已经相当熟悉的另外二十多个主题。

更好的起点是代理自身的历史记录。寻找反复出现的故障。也许该代理存在以下问题:

  • 在庞大的单仓库中运行整个测试套件,而非针对受影响的项目进行处理
  • 不断违反仓库中的依赖边界规则
  • 选择了技术上可行的迁移路径,却忽略了内部的兼容性步骤
  • 修改的是生成的文件,而非生成这些文件的架构定义
  • 每次有服务出现故障时,都要花费五分钟重新执行相同的调试流程

这些都是需要提升的技能点。其培养流程如下:观察反复出现的故障,找出缺失的操作知识,编写最实用的最小化技能脚本,将确定的操作步骤放入脚本中,创建具有代表性的评估任务,进行测试,之后保留、改进或删除该技能。而技能的改进又会回到重新编写脚本的阶段。

注意最后一个选项,因为删除操作是可行的。那些无法改善行为的技能并非无害,它们会增加上下文复杂性、维护负担,还会带来可能与后续内容冲突的额外指令。

真正的抽象概念是受控上下文

人们常将技能视为扩展智能体的方式,这没错,但它掩盖了一个更有趣的工程问题:向智能体提供信息很容易,而决定在何种执行场景下哪些信息是相关的才是难点。

一个设计良好的智能体环境看起来就不像是一个庞大的提示语,而更像是一个具有多层结构的软件系统。其中始终存在通用模型知识以及一些全局性指令;只有当任务需要时,才会引入特定于任务的技能、条件加载的参考资料、确定性工具以及运行时存储库上下文。每一层都有其特定的功能。如果这些职责界限模糊,上下文就会过度膨胀,从而导致行为难以理解。将它们分开后,各项技能就会变得相当简单乏味,而这正是好事。

一个优秀的技能并非华而不实的提示语,而是一段简洁的程序化知识,在适当的情况下被调用,尽可能利用确定性工具,并根据其设计目的来检测可能出现的故障。