内置技能包与技能检索
内置技能怎样用 Bun 宏打进产物并在首次运行时解压,同名覆盖与禁用开关,技能目录、skill_search 的 BM25 检索与中文别名,以及斜杠调用的注入。
上游 OpenCode 的 skill 机制很朴素:到 ~/.claude/skills 等目录找 SKILL.md,把清单放进系统提示词,模型再用 skill 工具按名字加载(见 Agent、命令与 Skills)。MiMo Code 在这套机制上加了三样东西:随二进制分发的技能包、按相关度找技能的 skill_search 工具,以及给中文界面准备的斜杠别名。
技能包分两份:packages/opencode/src/skill/builtin/.bundle/ 下 27 个面向用户的内置技能;packages/opencode/src/skill/compose/.bundle/ 下 14 个 compose:* 技能,只供 legacy Compose agent 使用(见 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 沙箱)。 - 覆盖内置技能:在
.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)。
打包:Bun 宏把目录编进产物
extract.ts 用两种方式导入同一个函数(packages/opencode/src/skill/builtin/extract.ts:9):
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 数据库(见历史轨迹) |
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):
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 和 /名称 读的则是实时列表。前缀快照见压缩、修剪与请求前缀。
skill_search:先精确匹配,再 BM25
skill_search(packages/opencode/src/tool/skill-search.ts:19)只有一个参数 query,参数说明要求模型把请求改写成动作、输入、期望产出、受众四个要素。执行时对 modelInvocable 的结果调用 searchSkills(packages/opencode/src/skill/search.ts:68):
- 排除 compose:名字以
compose:开头的不参与。 - 精确提及:查询里出现技能名、
aliases或中文别名即命中,得分为 1。含汉字的别名做子串匹配,其余要求前后都不是字母或数字(search.ts:20)。 - 分词:转小写,去掉停用词,其中包括
action、input、output、audience这四个模板标签;连续汉字切成相邻二字组,单字保留;长度大于 3、以s结尾但不以ss结尾的词去掉末尾的s(search.ts:49)。 - BM25 与混合打分:文档是名字、别名、中文别名与描述拼起来的文本。每个技能的 BM25 分除以本次最高分后乘 0.55,加上查询词覆盖率乘 0.35,保留 4 位小数。精确命中排在最前,去重后截取前 3 个。
这些参数写死在 Flag 对象里,并不读环境变量(flag.ts:222):
// 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 精简——用受限的命令行语法代替 JSON 调工具,以及 bash 输出怎样被压缩。