SOURCE NOTE · SKILL SYSTEM
Lesson 6: Skills 子系统
Skill 是 Markdown + YAML frontmatter。系统 prompt 只放清单;正文按需加载——模型走 skill 工具,用户走 /skill-name。
学完本课,你能回答:Skill 从哪几个来源加载、谁覆盖谁?frontmatter 有哪些字段、各控制什么?模型和用户分别通过哪两条路径触发 skill?想给项目注入领域知识时文件该放哪?
一句话概括
Skill 是一个 Markdown 文件 + YAML frontmatter。系统 prompt 里只放 name + description 的清单(省 token);模型判断匹配时才用 skill 工具把全文拉进上下文——这就是「按需加载」。
启动 / 触发时:load_skill_registry()
bundled → user → extra dirs → project → plugin
(后注册的同名片段覆盖先注册的)
|
v
系统 prompt 注入清单:name + description(不含正文)
|
┌────────┴────────┐
v v
模型路径 用户路径
skill 工具 /skill-name
返回全文 全文渲染成 prompt 提交
下面的 Skill Lab 不是逐步 trace,而是一张可调实验台:选定覆盖来源 → 拨两扇门禁 → 分别用模型路径或用户路径触发,直接看清单是否可见、正文是否进入上下文。
SKILL LAB · LOCAL SIMULATION
清单常驻,正文按需
先选定生效来源与两扇门禁,再分别用「模型」或「用户」触发。组件不读真实磁盘,也不调用模型。
谁覆盖谁
点击最晚注册、实际生效的那一层。项目领域知识优先放 project。
两扇门禁
frontmatter 把「广告位」和「谁能拉正文」拆开。description 只影响模型是否想点名。
模型默认只看到这些
系统 prompt 注入 name + description,不含正文。
deploy-checklist发布前检查清单,用户说「准备发布」时使用
≈ 一行广告位 · 低 token两条路径,两种回灌方式
尚未触发
默认上下文只有清单(若允许模型看见)。点下方按钮,看正文何时进入。
system prompt catalog
- deploy-checklist · 发布前检查清单,用户说「准备发布」时使用- 生效来源
project - 家目录
.openharness/skills/deploy-checklist - 清单
visible
核心文件
skills/types.py:SkillDefinition,skill 的 frozen dataclass。skills/_frontmatter.py:YAML frontmatter 解析与 fallback 链。skills/bundled/__init__.py:内置 skill,从content/*.md平铺加载。skills/loader.py:多来源加载、项目目录向上发现、路径安全校验。skills/registry.py:SkillRegistry,多键注册与去重。tools/skill_tool.py:模型路径,skill 工具按名取全文。prompts/context.py:把 skill 清单写进系统 prompt。commands/registry.py:用户路径,/skill-name斜杠命令与变量替换。
加载来源与覆盖顺序
load_skill_registry() 严格按此顺序注册,同名 key 后者覆盖前者(dict 赋值语义):
- bundled:
skills/bundled/content/*.md,平铺xx.md。 - user:
~/.openharness/skills,兼容~/.claude/skills、~/.agents/skills;布局为dir/skill/SKILL.md。 - extra dirs:运行时传入(如 ohmo workspace),布局同上。
- project:
.openharness/skills、.agents/skills、.claude/skills,布局同上。 - plugin:已启用插件携带的 skills,插件内部路径。
frontmatter:五个控制字段
---
name: deploy-checklist
description: 发布前检查清单,用户说"准备发布"时使用
user-invocable: true
disable-model-invocation: false
model: claude-opus-5
argument-hint: "<env>"
---
正文:模型/用户触发后拿到的完整指令……name:主键;缺省用目录名(bundled 用文件名)。description:写进系统 prompt 的一行——模型决定是否调用的唯一依据。user-invocable: false:从斜杠命令隐藏,只留给模型。disable-model-invocation: true:不进系统 prompt 清单;skill 工具也会拒绝,只能用户手动触发。model:用户路径用submit_model让这条 prompt 跑在指定模型上。argument-hint:参数提示,纯展示用(可选)。
没有 frontmatter 也能加载:fallback 链是 ① YAML frontmatter → ② 第一个 # 标题 + 首个正文段落(description 截断 200 字符)→ ③ "Skill: {name}" 模板。但缺 description 的 skill 模型几乎不会主动调用——description 就是 skill 的广告位。
Registry:一个 skill,多个键
register() 把同一个 skill 挂到多个键上:name、command_name、display_name、aliases。所以用户敲 /plan 和模型查 "plan" 命中的是同一份定义。list_skills() 再按 (source, path) 去重,保证清单里每个文件只出现一次。
两条触发路径的分工
模型路径
- 入口:系统 prompt 清单 →
skill(name=...)工具。 - 返回:原文
content作为工具结果。 - 变量替换:无。
- 门禁:
disable-model-invocation时拒绝。 - 模型指定:无(当前会话模型)。
用户路径
- 入口:
/skill-name args...。 - 返回:渲染后作为新 prompt 提交给模型。
- 变量替换:
ARGUMENTS、CLAUDE_SKILL_DIR、CLAUDE_SESSION_ID等占位符(写作${…}形式)。 - 门禁:
user-invocable: false时拒绝。 - 模型指定:
submit_model(frontmattermodel)。
用户路径的渲染逻辑(_render_skill_command_prompt):
prompt = f"Base directory for this skill: {base_dir}\n\n" + content
prompt = prompt.replace("${CLAUDE_SKILL_DIR}", base_dir)
prompt = prompt.replace("${ARGUMENTS}", args)
# 内容里没写 ${ARGUMENTS} 但用户传了参数 → 末尾自动追加 "Arguments: ..."
return CommandResult(submit_prompt=prompt, submit_model=skill.model)为什么用户路径要 prepend base_dir?skill 正文常引用同目录的辅助文件(脚本、模板)。告诉模型「这个 skill 的家在哪」,它才能用 read_file 等工具去取——你在 Claude Code 里看到的 Base directory for this skill: ... 就是这么来的。
懒加载 + 热加载
SkillTool.execute 和斜杠命令 handler 每次调用都重新 load_skill_registry()——没有缓存。代价是每次重新扫目录、解析 YAML;收益是改完 SKILL.md 立刻生效,不用重启会话。这和 Hooks 热重载(每条消息前重建 registry)是同一哲学:配置即改即生效,用微小的 IO 换迭代速度。
最小 SKILL.md 模板
---
name: my-check
description: 一句话说清做什么 + 什么时机该调用我
---
# my-check
## When to use
(触发条件,写给模型看的)
## Workflow
1. 步骤一
2. 步骤二
## Rules
- 硬约束落地建议:
- 项目知识放 project 目录,不要只放在个人
~/.openharness/skills。 - 把触发条件写进 description,否则清单再全,模型也难主动点名。
- 需要用户手搓启动、不希望模型抢跑时,设
disable-model-invocation: true。 - 需要模型内化流程、不希望污染
/补全时,设user-invocable: false。 - 辅助脚本与模板和 SKILL.md 同目录,依赖用户路径注入的 base_dir,或在正文里写清相对路径。
和前几课的边界
- Tools:通用能力接口(读文件、跑命令)。Skill 不是新工具类型,而是可按需注入的流程说明;模型仍通过已有工具完成动作。
- Permissions / Hooks:安全闸门与生命周期扩展。Skill 改变的是「模型接下来按什么剧本想问题」,不替代执行前鉴权。
- Context / Compact:清单常驻系统 prompt,正文按需进入对话;过长 skill 正文同样会占用上下文预算,仍可能触发 compact。
一句话:Tools 给手,Skills 给剧本;Permissions 与 Hooks 决定剧本里的动作能不能真的落地。
推荐精读(一手源码)
主读:src/openharness/skills/loader.py —— 来源顺序、项目发现与路径安全。
接着看:
src/openharness/skills/registry.py、types.py—— 多键注册与定义形状;src/openharness/skills/_frontmatter.py—— 字段与 fallback;src/openharness/tools/skill_tool.py—— 模型路径与disable-model-invocation;src/openharness/commands/registry.py—— 用户斜杠路径与变量渲染;src/openharness/prompts/context.py—— 清单如何进入系统 prompt。