# 内置技能包与技能检索

> 内置技能怎样用 Bun 宏打进产物并在首次运行时解压，同名覆盖与禁用开关，技能目录、skill_search 的 BM25 检索与中文别名，以及斜杠调用的注入。

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

上游 OpenCode 的 skill 机制很朴素：到 `~/.claude/skills` 等目录找 `SKILL.md`，把清单放进系统提示词，模型再用 `skill` 工具按名字加载（见 [Agent、命令与 Skills](https://daiw.org/manual/opencode/agents-and-commands)）。MiMo Code 在这套机制上加了三样东西：随二进制分发的技能包、按相关度找技能的 `skill_search` 工具，以及给中文界面准备的斜杠别名。

技能包分两份：`packages/opencode/src/skill/builtin/.bundle/` 下 27 个面向用户的内置技能；`packages/opencode/src/skill/compose/.bundle/` 下 14 个 `compose:*` 技能，只供 legacy Compose agent 使用（见 [Compose](https://daiw.org/manual/mimo-code/compose)）。各技能的内容本篇不展开。

## 怎么用

- TUI 里输入 `/` 看补全，`/skills` 打开选择框，`/<技能名>` 直接调用。界面语言是简体或繁体中文时，内置技能还有中文别名，例如 `/深度研究` 在提交前被解析成 `/deep-research`（`packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:1244`）。
- 一条消息里用 `/名称` 提到多个技能，前 3 个的正文自动注入，并附一段多技能编排提醒。
- 消息附带 PDF、Word、Excel（含 CSV、TSV）或 PPT 文件时，会插入一段提醒，列出对应 official 技能的 `SKILL.md` 路径。
- 模型这一侧：系统提示词尾部有技能目录，`skill_search` 按描述检索，`skill` 按名加载。GPT/Codex 工具集下这两个工具不在顶层，要在 `exec` 里以 `tools.skill(...)` 形式调用（见 [exec 与 QuickJS 沙箱](https://daiw.org/manual/mimo-code/exec-sandbox)）。
- 覆盖内置技能：在 `.mimocode/skills/<name>/SKILL.md`、`~/.claude/skills/` 等位置放一个同名技能。
- frontmatter 比上游多两个可选键：`aliases`（字符串数组，参与检索）和 `disable-model-invocation`（为真时模型看不见也加载不了，用户仍可 `/名称`）。v0.1.14 的内置技能都没有用到这两个键。

环境变量（取值 `true` 或 `1`）：

| 变量 | 效果 |
| --- | --- |
| `MIMOCODE_DISABLE_BUILTIN_SKILLS` | 不解压、不加载整个内置包，附件触发的文档技能提醒也随之关闭 |
| `MIMOCODE_DISABLE_OFFICIAL_SKILLS` | 只去掉 `docx-official`、`pdf-official`、`pptx-official`、`xlsx-official`、`html-to-video-pipeline` |
| `MIMOCODE_DISABLE_COMPOSE_SKILLS` | 不加载 `compose:*` 技能（README 没有列出） |
| `MIMOCODE_DISABLE_SLASH_SKILLS` | 只从 `/` 补全里隐藏技能，模型照常可用 |
| `MIMOCODE_DISABLE_EXTERNAL_SKILLS` | 不扫描 `.claude`、`.agents`、`.codex`、`.opencode` 下的技能 |

最后一项还能逐个关：`MIMOCODE_DISABLE_CLAUDE_CODE_SKILLS`、`MIMOCODE_DISABLE_AGENTS_SKILLS`、`MIMOCODE_DISABLE_CODEX_SKILLS`、`MIMOCODE_DISABLE_OPENCODE_SKILLS`；`MIMOCODE_MIMO_ONLY` 与 `MIMOCODE_DISABLE_CLAUDE_CODE` 会连带关掉 `.claude` 下的技能（`packages/opencode/src/flag/flag.ts:50`）。

```mermaid
flowchart LR
  B["内置包与 compose 包（Bun 宏内联）"] -- "首次运行解压" --> S["Skill 服务：扫描、去重、覆盖"]
  U["用户目录、项目目录、skills.paths 与 skills.urls"] --> S
  S -- "modelInvocable" --> C["system 尾部技能目录"]
  S -- "modelInvocable" --> Q["skill_search"]
  S --> T["skill 工具"]
  S -- "available" --> M["用户 /名称 注入"]
  Q -- "第一名得分不低于 0.85" --> L["自动加载正文"]
```

## 打包：Bun 宏把目录编进产物

`extract.ts` 用两种方式导入同一个函数（`packages/opencode/src/skill/builtin/extract.ts:9`）：

```ts
import { loadBuiltinBundle } from "./bundle.macro" with { type: "macro" }
import { loadBuiltinBundle as loadBuiltinBundleDev } from "./bundle.macro"
```

带 `with { type: "macro" }` 的是 Bun 宏：构建时执行 `loadBuiltinBundle()`（`packages/opencode/src/skill/builtin/bundle.macro.ts:19`），递归读出 `.bundle` 下每个技能目录的全部文件（跳过 `.DS_Store`、`Thumbs.db`、`__pycache__` 与 `.pyc`），把“技能名、相对路径、文件内容”组成的对象作为字面量内联进产物。compose 包的同名文件解释了为什么还要第二个导入：

> Bun macros only resolve in the static import graph of an entry point. In dynamic import() chains (e.g. plugin tests), the macro is unavailable — fall back to a normal runtime import of the same function.

（`packages/opencode/src/skill/compose/extract.ts:10`）

所以 `safeLoadBuiltinBundle` 捕获 `ReferenceError` 后改走普通调用（`builtin/extract.ts:91`）。文件一律按 UTF-8 文本读取（`builtin/bundle.macro.ts:14`），包里唯一的二进制文件 `playwright/assets/playwright.png` 经过一读一写，字节不会与原图一致；它只被该技能的 `agents/openai.yaml` 当图标引用。

## 首次运行：解压到数据目录

`extractBuiltinBundle`（`builtin/extract.ts:105`）在 Skill 服务第一次发现技能时运行：

- 目标是 `<data>/builtin_skills/<版本号>/skills/<技能名>`（`builtin/extract.ts:71`）。`<data>` 默认为 XDG 数据目录下的 `mimocode`（Linux 上是 `~/.local/share/mimocode`），设置 `MIMOCODE_HOME` 后为 `$MIMOCODE_HOME/data`（`packages/shared/src/global.ts:26`）。compose 包解到 `<data>/compose/<版本号>/skills`（`compose/extract.ts:32`）。
- 标记文件 `.extracted` 存的是版本号加本次启用技能名列表的 JSON，内容完全一致才跳过（`builtin/extract.ts:118`）。因此切换 `MIMOCODE_DISABLE_OFFICIAL_SKILLS`、新装了 `claude` 命令，都会触发重新解压；compose 包只检查标记文件是否存在。开发构建（渠道为 `local`）每次启动都重写。
- `claude-code`、`codex` 两个技能要求 PATH 上分别有 `claude`、`codex`（`builtin/extract.ts:20`），否则不解压，已有目录会被删除；被禁用的 official 技能同样删目录，`index.ts` 扫描之后还会按名字再滤一遍（`packages/opencode/src/skill/index.ts:190`）。
- 版本号是路径的一部分，升级后旧版本目录留在磁盘上，代码里没有找到清理逻辑。

README 的数据目录表把“技能”写在 cache 目录下（`README.zh.md:387`），而内置技能实际在 data 目录；cache 下的 `skills/` 放的是配置 `skills.urls` 拉取的远程技能（`packages/opencode/src/skill/discovery.ts:35`）。

README 的内置技能表列了 23 个（`README.zh.md:217`），`.bundle` 里实际有 27 个，下面 4 个没写进 README：

| 技能 | frontmatter 描述大意 |
| --- | --- |
| `grok-build` | Grok Build CLI（`grok`）的参考与工作流，只在用户明确点名时使用 |
| `mate` | 为 MiMo Desktop 的桌面宠物 Mate 生成精灵图与 manifest |
| `memory-search` | 内置 memory、history 工具不够用时，直接查询轨迹 SQLite 数据库（见[历史轨迹](https://daiw.org/manual/mimo-code/history)） |
| `playwright` | 用 `playwright-cli` 在终端驱动真实浏览器 |

compose 包的 14 个目录与 README 说的“十四个内置技能”一致（`README.zh.md:192`）。

## 发现顺序与同名覆盖

发现分两段。与项目无关的部分由 `discoverStableSkills`（`index.ts:176`）完成，用 `Effect.cachedInvalidateWithTTL` 以无限期 TTL 缓存（`index.ts:317`）；与目录相关的部分由 `discoverSkills`（`index.ts:239`）按实例计算：

| 顺序 | 来源 | 模式 |
| --- | --- | --- |
| 1 | 解压后的内置包 | `skills/*/SKILL.md` |
| 2 | 解压后的 compose 包 | `**/SKILL.md` |
| 3 | `~/.claude`、`~/.agents`、`~/.codex`、`~/.opencode` | `skills/**/SKILL.md` |
| 4 | 从工作目录逐级向上直到工作树根的同名目录 | `skills/**/SKILL.md` |
| 5 | 各配置目录（全局配置目录、各级 `.mimocode`、`~/.mimocode`） | `{skill,skills}/**/SKILL.md` |
| 6 | 配置 `skills.paths` | `**/SKILL.md` |
| 7 | 配置 `skills.urls` | 下载到 cache 后 `**/SKILL.md` |

同名时的处理（`packages/opencode/src/skill/index.ts:117`）：

```ts
  const isBundled = bundledRoots.some((root) => match.startsWith(root))
  const existing = state.skills[parsed.data.name]

  if (existing) {
    // User overrides always win: bundled must not overwrite non-bundled
    if (isBundled && !existing.bundled) return
    if (!isBundled && existing.bundled) {
      log.info("user skill overrides bundled", { name: parsed.data.name, location: match })
    } else {
      log.warn("duplicate skill name", {
        name: parsed.data.name,
        existing: existing.location,
        duplicate: match,
      })
    }
  }
```

包内技能永远写不过用户技能；两边同为用户技能（或同为包内技能）时，后加载的覆盖先加载的，只记一条警告。为了让“后加载”有确定含义，每个根目录的匹配结果先排序（`index.ts:170`），加载改为串行（`index.ts:300`）。v1.14.19 既不排序，又以 `"unbounded"` 并发解析，同名时谁赢取决于谁先解析完。

与上游相比还有几处不同：

- 外部目录多了 `.codex`、`.opencode`（`index.ts:23`），`Info` 多了 `aliases`、`disable_model_invocation`、`bundled` 三个字段（`index.ts:29`）。
- 远程技能沿用 v1.14.19 的下载逻辑：目标文件已存在就不再下载（`discovery.ts:38`），不会像 OpenCode 专栏所写的新版那样按 `version` 更新。
- write、edit 工具改动了 `.mimocode/skills/` 或 `.mimocode/tools/` 下的文件后，会调用 `ToolRegistry.reload`，进而让两段发现缓存一起失效（`packages/opencode/src/session/prompt.ts:1696`、`packages/opencode/src/tool/registry.ts:517`）。判断用的正则只认正斜杠（`prompt.ts:395`）。

## 三个集合：谁看得见、谁调得动

`Skill` 服务给出三种列表（`index.ts:339` 起）：

| 方法 | 过滤条件 | 用在哪里 |
| --- | --- | --- |
| `all()` | 不过滤 | 注册为斜杠命令（`packages/opencode/src/command/index.ts:264`）、`GET /skill` |
| `available(agent)` | 去掉 `skill` 权限求值为 deny 的 | 用户 `/名称` 注入 |
| `modelInvocable(agent)` | 再去掉 `disable_model_invocation` 为真的 | 技能目录、`skill_search`、`skill` 工具的报错提示 |

默认规则里 `skill` 是 `"*": "allow"` 加 `"compose:*": "deny"`（`packages/opencode/src/agent/agent.ts:112`），只有 compose agent 放行 `compose:*`（`agent.ts:221`）。deny 意味着谁都用不了：在 build agent 里输入 `/compose:plan` 不会注入任何内容。

## 技能目录放在 system 尾部

`SystemPrompt.skills`（`packages/opencode/src/session/system.ts:177`）在 agent 没有整体禁用 `skill` 时，输出一行 `Skills available in this session:`，后接 `Skill.fmt` 生成的 `<available_skills>` 详单，列表取自 `modelInvocable`。与 v1.14.19 比有两处变化：

- 上游还把一份简版清单拼进 `skill` 工具描述（registry 里的 `describeSkill`）；MiMo 删掉了这段，`skill` 工具描述固定为 `skill.txt`，不随技能集合变化。
- 技能目录曾以 synthetic 文本部件写进 user 消息，标记常量还留在 `packages/opencode/src/session/skill-catalog.ts:1`。转换历史时，这类旧部件会被跳过（`packages/opencode/src/session/message-v2.ts:856`）：

> Skill catalogs moved to the system tail. Suppress snapshots persisted by older versions so resumed sessions do not receive a duplicate user-side catalog.

同一会话在模型、agent、权限等不变时，system 内容随请求前缀快照冻结（`prompt.ts:4441`），工具集哈希变化才重算（`prompt.ts:4505`），所以会话中途新增的技能不一定马上出现在目录里；`skill_search` 和 `/名称` 读的则是实时列表。前缀快照见[压缩、修剪与请求前缀](https://daiw.org/manual/mimo-code/compaction-and-prune)。

## skill_search：先精确匹配，再 BM25

`skill_search`（`packages/opencode/src/tool/skill-search.ts:19`）只有一个参数 `query`，参数说明要求模型把请求改写成动作、输入、期望产出、受众四个要素。执行时对 `modelInvocable` 的结果调用 `searchSkills`（`packages/opencode/src/skill/search.ts:68`）：

1. **排除 compose**：名字以 `compose:` 开头的不参与。
2. **精确提及**：查询里出现技能名、`aliases` 或中文别名即命中，得分为 1。含汉字的别名做子串匹配，其余要求前后都不是字母或数字（`search.ts:20`）。
3. **分词**：转小写，去掉停用词，其中包括 `action`、`input`、`output`、`audience` 这四个模板标签；连续汉字切成相邻二字组，单字保留；长度大于 3、以 `s` 结尾但不以 `ss` 结尾的词去掉末尾的 `s`（`search.ts:49`）。
4. **BM25 与混合打分**：文档是名字、别名、中文别名与描述拼起来的文本。每个技能的 BM25 分除以本次最高分后乘 0.55，加上查询词覆盖率乘 0.35，保留 4 位小数。精确命中排在最前，去重后截取前 3 个。

这些参数写死在 `Flag` 对象里，并不读环境变量（`flag.ts:222`）：

```ts
  // Skill-search ranking and loading policy. Exact mentions stay above BM25;
  // the BM25/coverage blend has a 0.90 ceiling, and near-max results auto-load.
  MIMOCODE_SKILL_SEARCH_EXACT_SCORE: 1,
  MIMOCODE_SKILL_SEARCH_BM25_K1: 1.5,
  MIMOCODE_SKILL_SEARCH_BM25_LENGTH_NORMALIZATION: 0.75,
  MIMOCODE_SKILL_SEARCH_BM25_IDF_SMOOTHING: 0.5,
  MIMOCODE_SKILL_SEARCH_BM25_SCORE_WEIGHT: 0.55,
  MIMOCODE_SKILL_SEARCH_QUERY_COVERAGE_WEIGHT: 0.35,
  MIMOCODE_SKILL_SEARCH_AUTO_LOAD_THRESHOLD: 0.85,
  MIMOCODE_SKILL_SEARCH_SCORE_PRECISION: 4,
  MIMOCODE_SKILL_SEARCH_MAX_RESULTS: 3,
  MIMOCODE_SKILL_SEARCH_STEM_MIN_LENGTH: 3,
  MIMOCODE_SKILL_SEARCH_FILE_SAMPLE_LIMIT: 10,
```

第一名得分不低于 0.85 时自动加载（`skill-search.ts:45`）：照常询问 `skill` 权限，再把正文接在 JSON 结果后面返回；否则只返回候选列表，由模型决定。按混合公式，BM25 结果要过线，得是本次最高分且覆盖约 86% 以上的查询词，或者覆盖全部查询词且分数达到最高分的约 91%。

中文别名来自 TUI 简繁中文词条里的 `tui.skill.<名字>.slash`，多个别名以 `|` 分隔，只对内置的非 compose 技能生效（`packages/opencode/src/skill/localized-alias.ts:7`）。于是查询里出现 `Word文档` 就能精确命中 `docx-official`。有一处对不上：`mimocode-docs` 的词条键写成了 `tui.skill.mimocode.slash`（`packages/opencode/src/cli/cmd/tui/i18n/zh.ts:240`），按技能名查不到，它的中文别名与中文描述都不会生效。

## 加载：三条注入路径

- **`skill` 工具**（`packages/opencode/src/tool/skill.ts:15`）：名字查不到时，如果它其实是内置 workflow，就提示改用 `workflow` 工具；否则只列出 `modelInvocable` 里的名字，避免把隐藏技能泄露给模型。技能设了 `disable-model-invocation` 就拒绝，并让模型转告用户输入 `/名称`。通过后询问 `skill` 权限，由 `renderSkillContent`（`packages/opencode/src/tool/skill-content.ts:9`）输出 `<skill_content>` 块：正文、技能目录的 `file://` 地址、最多 10 个用 ripgrep 取样的附属文件路径。这段输出与上游 `skill.ts` 相同，只是抽成函数给 `skill_search` 复用。
- **用户提及**（`prompt.ts:1243`）：去掉代码块和行内代码后用正则找 `/名称`，只认 `available` 里的技能；前 3 个（`MAX_AUTOLOAD`）把原始正文包进 `<system-reminder>` 写进本条 user 消息（不带文件清单），其余只提示通过工具加载。提到 2 个及以上时再追加编排提醒，要求先读完每个 `SKILL.md`，判断是流水线、并行还是约束叠加，并声明冲突时哪个技能优先。TUI 里选中技能命令，服务端只写入 `/名称 参数` 这段可见文本（`prompt.ts:5202`），正文同样由这里注入。
- **附件触发**（`prompt.ts:1217`）：按 MIME 或扩展名匹配四个 official 文档技能（`builtin/extract.ts:30`），插入提醒列出各自 `SKILL.md` 的绝对路径。这张表不经过 `available` 或 `modelInvocable`。

下一篇：[命令行式工具调用与 Token 精简](https://daiw.org/manual/mimo-code/token-efficient-shell)——用受限的命令行语法代替 JSON 调工具，以及 bash 输出怎样被压缩。
