# Skills · 可复用的 prompt 包，还能按文件门控

> Skills 是 grok 复用 prompt 的一等公民：SKILL.md（frontmatter + 正文）。这一章看它的作用域优先级、防御式 frontmatter 解析、从不改 system prompt 的几条注入路径，以及一个精巧设计——glob 门控的技能，直到你碰到匹配文件才现身。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/skills
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# Skills · 可复用的 prompt 包，还能按文件门控

Skills 是把「反复用的一段指令/清单/流程」固化下来的单元——一个 `SKILL.md`(YAML frontmatter + Markdown 正文)。grok 的 skills 分在三处：`xai-grok-agent/prompt/skills.rs` 负责启动时的发现编排，`xai-grok-tools/.../skills/` 放解析、作用域类型和格式化，会话中的门控与列表在 `skill_discovery_tracker/`。

## 作用域：一个 Ord 定优先级

`SkillScope` 是个 `#[repr(u8)]` 枚举，**序数小 = 优先级高**：`Local`(0,`cwd/.grok/skills`)> `Repo`(1)> `User`(2,`~/.grok/skills`)> `Server`(3)> `Bundled`(4，内置)> `Plugin`(5)。优先级用一个 `sort_by_key(|s| s.scope)` + 首见去重就实现了——**把优先级编码成类型的 `Ord`**，简单又不易错。

去重不是一刀切：跨作用域同名是有意的遮蔽；同一作用域里重名(比如复制了技能目录却忘了改 `name`)则两个都留，后来者改用目录名作身份，frontmatter 的 `name` 只作显示名。内置技能现在放在 `~/.grok/bundled/skills/`，按 `Bundled` 作用域发现；旧版本解压进 `~/.grok/skills/` 的副本会遮住它们，所以每次启动都清理一遍——只删与已发布内容逐字节一致的，你改过的保留。

和 [AGENTS.md](https://daiw.org/manual/grok-build/agents-md) 一样，项目里的技能也要过目录信任：未受信时整条项目链(连同 workspace-user 覆盖层)都不扫；插件技能也只取已启用**且受信**的插件。

## 防御式解析：一个坏字段不该丢掉整个技能

frontmatter 解析(`parse_skill_frontmatter`)做了**三层容错**：① 先按无类型 `HashMap` 逐字段解；② 失败就把带 YAML 特殊字符的值加引号重试；③ 再不行就逐行**抢救** `name`/`description`/`when-to-use` 这几个关键字段。设计意图很明确：**模型或用户写的 SKILL.md frontmatter 常有小毛病，一个坏字段不该让整个技能消失**。

## 注入：一个信封，三条路

技能的「信封」是一对 XML 标签，见 `skill.rs` 里的 `build_skill_message`(第 42–47 行)：

```rust
pub fn build_skill_message(skill: &SkillInfo, content: &str) -> String {
    format!(
        "<skill name=\"{}\" description=\"{}\" path=\"{}\">\n{}\n</skill>",
        skill.name, skill.description, skill.path, content
    )
}
```

标签给模型一个清晰的身份与边界：里面是要遵循的额外指令，不是被调用的程序。但它对属性和正文都**不做转义**(有测试专门钉住这一点)，所以这是语义边界，不是防注入的安全边界。

它的注释仍自称「所有路径共用的格式化出口」，可在 HEAD 里真正调用它的只剩一处。技能正文实际有三条路进上下文：

- **模型调 `skill` 工具**：正文作为**工具结果**返回，用的是另一种信封 `<skill_content name="…">`，附技能目录和文件清单。模型也常直接用 `read_file` 读 SKILL.md——技能列表里每一项都给了绝对路径。
- **用户敲 `/技能名 参数`**：拼 prompt 时就地展开成 `<skill name="…" args="…">`，外面包一层 `<skill_information>`，追加在 `<user_query>` 之后，省掉一次工具往返。
- **agent 定义里的 `skills:` 预加载**：这才是 `build_skill_message` 的唯一调用方，拼好后接在该 agent 的提示词正文前面。

一个和 [system prompt](https://daiw.org/manual/grok-build/system-prompt) 呼应的设计：技能列表和激活通告都作为 `<system-reminder>` **用户消息**注入，`SkillUpdateEffects` 的注释写明「system prompt 从不因技能而改动」。唯一的例外是第三条：预加载的技能本就属于那个 agent 自己的提示词。

## glob 门控：碰到文件才现身

最精巧的是 `paths:` 门控(`skill_discovery_tracker/conditional.rs`)。一个技能若声明了 `paths:`(比如 `["**/*.proto"]`)，它**默认被藏起来**(`is_pending`)，不进技能列表——直到模型**碰到匹配的文件**(读/改一个 `.proto`),`SkillDiscoveryReminder` 在那次工具调用后 `activate_for_paths` 把它激活现身。匹配用 gitignore 风格，对**还没创建**的文件也管用。

算「碰到」的只有 `read_file`、`list_dir`、`search_replace` 和 `apply_patch`(多文件补丁里每个文件都算)，bash 和 grep 碰过的路径不算。还有个限制：concise 模式下这个 reminder 整体关闭，`paths:` 技能也就不会被激活。

<Callout type="info">
  这个门控解决一个真实痛点：一个大项目可能有几十个专用技能(protobuf 规范、前端组件约定……)，全塞进列表既费 token 又干扰模型。按「你正在碰的文件」**按需现身**，让技能列表始终小而相关。激活是会话级的，`/clear` 会重新收起。这是「上下文是稀缺资源」这条原则在技能系统里的又一次体现。
</Callout>

## 和《从 LLM 到 Coding Agent》对照

对照你熟悉的 [Claude Code 的 Skills](https://daiw.org/manual/claude-code/skills):grok 的实现几乎是它的源码版——作用域优先级、`SKILL.md` frontmatter、`<skill>` 注入、glob 门控。教学最小实现里这些都不存在；它们是「让一个 agent 可被大量复用 prompt 扩展、还不撑爆上下文」的工程。下一章看更重的扩展：MCP。

---

上一篇：[AGENTS.md 项目规则](https://daiw.org/manual/grok-build/agents-md) · 下一篇：[MCP 客户端](https://daiw.org/manual/grok-build/mcp-client)
