跳到正文

Coding Agent 原理与实践

11:Skills——Agent 怎样使用任务相关的“技能”?

作者:折腾客

约 3,104 字72 行代码

文章目录

1. 什么是 Skill,为什么需要按需加载?

Skill(技能)是一组围绕特定任务编写的说明和配套资源。 它告诉 Agent 这类任务应该怎样做,例如遵循哪些要求、采用什么步骤、参考哪份模板。按照 Agent Skills 的格式约定,一个 Skill 以 SKILL.md 为入口,还可以附带参考文档、脚本和模板。

Agent Skills 由 Anthropic 于 2025 年 10 月推出,随后在同年 12 月作为开放标准发布,便于采用该标准的不同 Agent 复用 Skill。它约定了说明文件和配套资源怎样组织,各个 Agent 则需要实现相应的发现与加载功能。官方介绍

例如,我们让 Agent:“根据这次代码改动,写一条 Git 提交信息。”假设团队有这样的要求:

  • 先查看代码差异,确认这次实际改了什么。
  • 标题采用“类型: 一句话说明”的格式,修复问题用 fix,新增功能用 feat。
  • 用中文说明具体改动,避免只写“更新代码”这样含糊的描述。

这些步骤、格式要求和示例,就可以写成一个生成提交信息的 Skill。Agent 处理这项任务时,先读取它,再查看改动、按规范生成提交信息,例如:fix: 修复登录失败时未显示错误提示的问题。用户不用每次都重新解释团队规范。

这些要求也可以写进 AGENTS.md。但其中的内容会作为项目约定提供给模型,即使当前任务用不上提交信息的规范,也会带着这部分内容。如果把代码审查、生成文档等任务的详细说明也都放进去,无关内容就会越来越多。

Agent 中的 Skill 管理模块将 “这个 Skill 适合做什么” 和 “具体应该怎样做” 分开提供给模型,让模型先根据用途选择,再读取详细说明。沿用生成提交信息的例子,具体过程如下:

  1. Agent 启动时,先准备可用 Skill 的清单。 清单包含每个 Skill 的名称、用途说明和文件位置。调用模型时,Agent 会把这份清单提供给它,暂时不提供各个 Skill 的完整正文。例如,其中一项说明“这个 Skill 用于按团队规范生成 Git 提交信息”,具体规范留到使用时再读取。
  2. 用到某个 Skill 时,再读取具体做法。 模型收到生成提交信息的任务后,可以根据用途说明选中这个 Skill,再请求读取它的 SKILL.md。这时,标题格式、类型选择规则等详细要求才进入模型的上下文。
  3. 需要更多细节时,再读取配套资料。 如果正文引用了另一份文件,里面有提交信息的模板和更多示例,Agent 可以在需要时继续读取。

这种按任务需要,逐步把更多细节加入模型上下文的方式,叫作渐进式披露(progressive disclosure)。下面看看 Ink 的 Skill 管理模块怎样实现这个过程。

2. Agent 怎样发现并使用 Skill?

第一节介绍了“先提供用途,需要时再读取做法”的思路。下面沿着同一个提交信息任务,先看可用 Skill 怎样通过提示词告诉模型,再看具体要求怎样进入模型上下文、指导后续工作。

我们把这个教学示例命名为 commit-message。示例中的说明文件由本文编写,管理模块的代码节选自 Ink 的 skills.ts 及其调用处。想看仓库提供的现成 Skill,也可以参考用于记录项目进展的 ledger。

2.1 怎样把可用 Skill 告诉模型?

启动时,Ink 的 loadSkills 会从约定或用户指定的路径查找 Skill。常用路径包括项目的 .ink/skills/ 和用户的 ~/.ink/skills/;也可以通过 --skill 指定路径,Ink 还兼容 .agents/skills/。

项目路径中的 Skill 需要先通过项目信任检查。

沿用生成提交信息的例子,假设用户已经把它放在项目的 .ink/skills/commit-message/ 文件夹中:

.ink/skills/commit-message/
├── SKILL.md
└── references/
    └── examples.md

先看这个例子的完整 SKILL.md,名称、用途说明和具体操作要求都写在这份文件里:

---
name: commit-message
description: 根据代码改动,按团队规范生成中文 Git 提交信息。
---

# 生成 Git 提交信息

1. 先查看代码差异,确认这次实际改了什么。
2. 标题采用“类型: 一句话说明”的格式;修复问题用 fix,新增功能用 feat。
3. 用中文说明具体改动,避免只写“更新代码”。

需要参考提交信息的模板和示例时,读取 references/examples.md。

这份文件分成两部分:

  • 名称和用途说明(元信息):两个 --- 分隔符之间的 name 和 description,分别说明“这个 Skill 叫什么”和“适合做什么”。这部分采用 YAML 格式,帮助模型判断是否需要使用这个 Skill。
  • 具体操作要求(正文):第二个 --- 分隔符之后的全部内容,包括“生成 Git 提交信息”的标题、三个步骤,以及读取参考示例的说明。它告诉模型“使用这个 Skill 时,应该怎样完成任务”。

找到文件后,loadSkillFromFile 读取它,parseSkillFile 将上面这两部分分开。Ink 从元信息中取得名称和用途说明,再记录文件位置,保存一份 Skill 记录:

// 文件:src/core/skills.ts
// 所在函数:loadSkillFromFile
// 省略变量声明和错误处理。
raw = readFileSync(filePath, "utf8"); // 读取文件
parsed = parseSkillFile(raw); // 分开名称和用途说明(元信息)与具体操作要求(正文)

const { frontmatter } = parsed; // 两个 --- 分隔符之间的 name、description 等字段
// 从 frontmatter 取得名称 name,校验用途说明后得到 descriptionText;过程省略。

return {
  skill: {
    name,
    description: descriptionText,
    filePath, // SKILL.md 的绝对路径
    // 省略配套资料的基准目录、来源及可见性配置。
  },
  // 省略诊断信息。
};

每找到一个有效的 Skill 文件,Ink 就得到一份这样的记录。loadSkills 收集这些记录,供后续生成清单;此时记录的是名称、用途说明和文件位置,不包含“先查看代码差异”等正文中的操作要求。

接下来,Ink 将这些记录整理成可用 Skill 清单,加入系统提示词(system)。

runner.ts 组装系统指令时,通过 formatSkillsForPrompt 将前面准备的 Skill 记录转换成文字清单:

// 文件:src/runner.ts
// 所在方法:RunnerImpl.assembleSystem,Skill 清单组装部分。
if (
  this.options.skills !== undefined &&
  this.tools.some(t => t.name === "read")
) {
  system += formatSkillsForPrompt(this.options.skills);
}

这里要求存在 read 工具,因为模型选中 Skill 后还需要读取文件。formatSkillsForPrompt 除了列出名称、用途说明和位置,还会告诉模型:任务与用途说明匹配时,使用 read 读取对应文件。

假设项目位于 /workspace/app,加入 system 的内容如下;这里省略了配套资料相对路径的处理说明,以及其他系统指令:

Use the read tool to load a skill's file when the task matches its description.

<available_skills>
  <skill>
    <name>commit-message</name>
    <description>根据代码改动,按团队规范生成中文 Git 提交信息。</description>
    <location>/workspace/app/.ink/skills/commit-message/SKILL.md</location>
  </skill>
</available_skills>

调用模型时,这段提示词随系统指令一同发送。模型看到用户要“根据这次改动写提交信息”,又看到 commit-message 的用途说明,就有了选择它的依据;location 告诉它去哪里读取具体规范,清单前的指令则告诉它通过 read 取得文件内容。

选择 Skill 的是模型,Skill 管理模块负责提供清单和读取入口。 Ink 没有另外用关键词匹配器决定本次必须加载哪一个。清单本身也占用上下文,只是完整正文和配套资料暂时没有加入。

2.2 Skill 正文怎样进入模型上下文,指导后续工作?

现在,可用 Skill 的名称、用途说明和文件位置已经进入模型上下文。模型因此知道 commit-message 适合生成提交信息,但 SKILL.md 正文中的标题格式、类型选择等具体要求,还没有进入上下文。

模型选中 Skill 后,会请求 read 工具读取对应的 SKILL.md。Ink 将读取结果加入对话历史;下一次调用模型时,Skill 正文就随历史进入上下文,模型能够据此继续工作。

下面对照读取正文前后的模型上下文:

读取 Skill 正文前后,模型上下文有什么变化?

沿用生成提交信息的教学示例,只展开与 Skill 有关的上下文。

  1. 读取正文前:上下文中有 Skill 清单

    用户要求生成提交信息,模型根据清单判断 commit-message 适合这项任务。

    system可用 Skill 清单

    commit-message · 按团队规范生成中文 Git 提交信息 · SKILL.md 的绝对路径

    messages用户任务
    • user根据这次代码改动,写一条 Git 提交信息。

    模型知道 Skill 的用途和文件位置,还没有看到具体操作要求。

  2. 读取正文后:具体要求进入上下文

    Agent 执行 read,把说明文件的内容作为工具结果写入 history。

    system可用 Skill 清单

    commit-message · 按团队规范生成中文 Git 提交信息 · SKILL.md 的绝对路径

    messages历史中新增的内容
    • assistant调用 read:commit-message/SKILL.md
    • toolResult先查看代码差异;标题用“类型: 一句话说明”,修复用 fix、新功能用 feat;用中文说明具体改动。

    下一次调用模型时,这些操作要求随历史进入上下文,模型可以据此继续工作。

读取后的状态只列新增消息,原来的用户任务仍在历史中。正文通过工具结果加入 messages,system 中保留 Skill 清单。图中文字为内容摘要,路径省略了项目目录。

这些要求会给模型后续工作提供依据。例如,正文要求“先查看代码差异”,模型可以提出 bash 调用,运行 git diff 查看改动;正文规定修复问题用 fix、用中文说明,模型就可以据此组织提交信息。假设本次改动修复了登录失败时不显示提示的问题,生成结果可以是:fix: 修复登录失败时未显示错误提示的问题。

2.3 用户指定 Skill 时,正文怎样进入上下文?

前一小节由模型根据清单选择 Skill。如果用户已经确定要使用 commit-message,可以在 Ink 的交互界面输入:

/skill:commit-message 根据这次代码改动,写一条 Git 提交信息。

Ink 收到这个命令后,会读取该 Skill 的正文,将它放在本次任务要求之前,组合成一条用户消息。模型处理这次任务时,就能直接看到具体操作要求,不必先根据清单选择、再请求读取正文。

命令处理函数通过 expandSkillBlock 生成这条消息。传入的 skill 是启动时保存的 Skill 记录,args 是命令后面的任务要求,也就是“根据这次代码改动,写一条 Git 提交信息”。核心处理如下:

// 文件:src/core/skills.ts
// 所在函数:expandSkillBlock
const content = readFileSync(skill.filePath, "utf8");
const body = parseSkillFile(content).body.trim(); // 取得第二个 --- 分隔符之后的正文

// 在正文外加上 Skill 名称、文件位置及参考资料路径说明。
const block =
  `<skill name="${skill.name}" location="${skill.filePath}">\n` +
  `References are relative to ${skill.baseDir}.\n\n` +
  `${body}\n</skill>`;

const trimmedArgs = args.trim();
return trimmedArgs === "" ? block : `${block}\n\n${trimmedArgs}`;

命令处理函数将返回的文本作为用户消息提交。沿用前面的 Skill 文件,消息内容如下:

<skill name="commit-message" location="/workspace/app/.ink/skills/commit-message/SKILL.md">
References are relative to /workspace/app/.ink/skills/commit-message.

# 生成 Git 提交信息

1. 先查看代码差异,确认这次实际改了什么。
2. 标题采用“类型: 一句话说明”的格式;修复问题用 fix,新增功能用 feat。
3. 用中文说明具体改动,避免只写“更新代码”。

需要参考提交信息的模板和示例时,读取 references/examples.md。
</skill>

根据这次代码改动,写一条 Git 提交信息。

这条消息进入对话历史后,Skill 正文与任务要求会一同加入本次模型调用的 messages。

两种使用方式都读取 SKILL.md,区别在于正文以哪种消息进入上下文:

使用方式正文以哪种消息进入上下文
模型根据清单选择文件读取的工具结果消息(toolResult)
用户通过 /skill:commit-message 指定包含正文与任务要求的用户消息(user)

3. Skill 为什么可能没有按预期生效?

3.1 为什么 Skill 没有出现在清单中?

如果文件已经放好了,模型却看不到这个 Skill,可以沿清单的准备过程检查:

  • 是否被发现。 检查文件是否放在扫描范围内。本例使用项目的 .ink/skills/ 文件夹,还需要项目通过信任检查。
  • 元信息是否有效。 SKILL.md 缺少非空的 description,或 YAML 解析失败时,Ink 会警告并跳过它。
  • 是否与其他 Skill 重名。 Ink 保留先加载的同名 Skill,并提示冲突。显式路径先于项目路径,项目路径先于用户路径,最后保留的可能是另一份同名文件。
  • 是否允许向模型展示。 设置 disable-model-invocation: true 会将 Skill 从模型可见的清单中移除;可用工具中没有 read 时,当前实现也不会提供 Skill 清单。

disable-model-invocation 适合只希望用户主动调用的 Skill,对应的 /skill:名称 命令仍可保留。它控制清单展示,不改变文件访问权限。

3.2 清单里有了,模型为什么仍没有选中?

模型根据用途说明判断 Skill 与任务是否相关。如果 description 只写“处理提交”,就不如“根据代码改动,按团队规范生成中文 Git 提交信息”明确。

用途说明写清楚了,也不保证模型每次都会选中它。已经确定要使用哪个 Skill 时,可以通过 /skill:名称 指定;命令成功读取正文并提交消息后,正文会直接进入上下文。模型能否正确完成正文要求的步骤,还取决于模型、工具和任务条件。

3.3 读取或调用失败时,怎样处理?

清单记录了文件位置,但启动后文件仍可能发生变化。如果 SKILL.md 或配套资料被删除、变得不可读,两条使用路径会在不同位置收到错误:

发生位置当前处理
模型调用 read 读取 Skill 或配套资料读取失败作为工具错误交回模型,模型可以据此调整后续操作。
用户通过 /skill:名称 读取正文命令处理函数捕获错误并向用户显示,本次不会向模型提交消息。

第二条路径的错误处理,在 2.3 节介绍的命令处理函数中,位于调用 expandSkillBlock 的外层:

// 文件:src/core/skills.ts
// 所在函数:buildSkillCommands,命令 run 回调的 catch 分支。
catch (err) {
  const message = err instanceof Error ? err.message.split("\n")[0] : "failed to read";
  ctx.renderer.error(`ink: /${name} failed to read ${skill.filePath}: ${message}`);
}

另外,Ink 当前不允许在 Agent 运行尚未结束时提交 Skill 命令:命令会被拒绝,用户需要等当前运行结束后重新提交。

4. Skill 怎样改变 Agent 的工作方式?

回到生成提交信息的任务:Skill 管理模块先提供清单,模型或用户选中 commit-message,详细规范和所需示例再进入上下文。Agent 随后用已有工具查看改动,按规范生成提交信息。渐进式披露让模型先知道有哪些 Skill 可用,再按任务需要取得细节。

Skill 补充的是做法和资料,所需的执行能力仍由工具提供。下一篇 MCP 将继续讨论,怎样把外部服务提供的工具接入 Agent。