Claude Code 技能发现机制深度解析

基于源码逆向分析,详解 EXPERIMENTAL_SKILL_SEARCH 特性标志控制下的完整技能发现与加载系统。


目录

  1. 系统概览
  2. Feature Flag 门控机制
  3. 三层发现架构
  4. skill_discovery Attachment 数据结构
  5. SKILL.md 扫描与本地加载
  6. SkillTool 三条执行路径
  7. 遥测:was_discovered 字段
  8. 完整调用流程图
  9. 关键源码位置速查表

系统概览

Claude Code 的技能发现系统是一个独立于 Proactive 模式的子系统,普通对话模式同样可用。整个系统由编译期 Feature Flag EXPERIMENTAL_SKILL_SEARCH 控制:

  • 外部构建(npm 包):Flag 为 false,相关代码在打包阶段被死代码消除(DCE),连 'skill_discovery' 字符串字面量都不会出现在产物中。
  • Anthropic 内部构建:Flag 为 true,完整系统生效。部分功能还需额外满足 process.env.USER_TYPE === 'ant'

系统的核心职责:在每轮对话中,自动将与当前任务相关的技能推送给模型,让模型无需记住所有技能名称,按需调用即可。


Feature Flag 门控机制

Feature Flag 是 Bun 编译期 feature() 调用,不是运行时环境变量。以下是三个最核心的门控位置:

src/tools/SkillTool/SkillTool.ts 第 108–115 行 — 远程技能模块加载:

1
2
3
4
5
6
7
8
const remoteSkillModules = feature('EXPERIMENTAL_SKILL_SEARCH')
? {
...(require('../../services/skillSearch/remoteSkillState.js')),
...(require('../../services/skillSearch/remoteSkillLoader.js')),
...(require('../../services/skillSearch/telemetry.js')),
...(require('../../services/skillSearch/featureCheck.js')),
}
: null

src/query.ts 第 66–68 行 — prefetch 模块加载:

1
2
3
const skillPrefetch = feature('EXPERIMENTAL_SKILL_SEARCH')
? (require('./services/skillSearch/prefetch.js'))
: null

src/utils/attachments.ts 第 95–102 行 — attachment 组装时的门控:

1
2
3
4
5
6
const skillSearchModules = feature('EXPERIMENTAL_SKILL_SEARCH')
? {
featureCheck: require('../services/skillSearch/featureCheck.js'),
prefetch: require('../services/skillSearch/prefetch.js'),
}
: null

除编译期 Flag 外,还有一个运行时副门控:远程技能和 Canonical 技能的调用路径大量检查 process.env.USER_TYPE === 'ant'(位于 SkillTool.ts 第 378、494、606 行),确保远程技能体系只对 Anthropic 员工开放。


三层发现架构

层一:Turn-0 自动发现

入口: src/utils/attachments.tsgetAttachmentMessages() 函数,第 801–812 行:

1
2
3
4
5
6
7
8
9
10
11
12
13
...(feature('EXPERIMENTAL_SKILL_SEARCH') &&
skillSearchModules &&
!options?.skipSkillDiscovery
? [
maybe('skill_discovery', () =>
skillSearchModules.prefetch.getTurnZeroSkillDiscovery(
input, // 用户消息文本,作为搜索信号
messages ?? [],
context,
),
),
]
: []),

这个调用是阻塞式的 —— 发生在第 0 轮(Turn-0),此时模型还没有开始输出,没有其他工作可以用来掩盖延迟。getTurnZeroSkillDiscovery 内部会针对用户消息执行 AKI 向量搜索或关键词匹配,返回相关技能列表。

skipSkillDiscovery 防护是关键设计:当 SkillTool 加载一个技能的 SKILL.md 内容(可能长达 110KB)并将其作为 input 传入时,必须跳过发现流程,否则会对每次技能调用触发 ~3.3 秒的 AKI 查询。

轮间异步 Prefetch: 在后续轮次中,系统改用异步 prefetch 掩盖延迟:

1
2
3
4
5
6
7
src/query.ts 第 331 行(模型开始流式输出前):
startSkillDiscoveryPrefetch(null, messages, toolUseContext)
[与模型流式输出并发执行]

src/query.ts 第 1617–1628 行(工具执行完毕后):
collectSkillDiscoveryPrefetch(pendingSkillPrefetch)
[收集结果,注入下一轮 attachment]

Prefetch 还受写操作检测门控(write-pivot guard):只在预计会产生写操作的轮次触发,避免在纯读查询上浪费 AKI 调用。


层二:主动搜索工具 DiscoverSkillsTool

这是给模型主动调用的工具,用于”中途转向”或”非典型工作流”场景。其 prompt 模块位于 src/tools/DiscoverSkillsTool/prompt.js(Anthropic 内部,源码中不可见)。

系统提示中注入的引导语(src/constants/prompts.ts 第 333–341 行):

1
2
3
4
5
6
7
8
9
10
function getDiscoverSkillsGuidance(): string | null {
if (feature('EXPERIMENTAL_SKILL_SEARCH') && DISCOVER_SKILLS_TOOL_NAME !== null) {
return `Relevant skills are automatically surfaced each turn as "Skills relevant to your task:" reminders.
If you're about to do something those don't cover — a mid-task pivot, an unusual workflow,
a multi-step plan — call ${DISCOVER_SKILLS_TOOL_NAME} with a specific description of what you're doing.
Skills already visible or loaded are filtered automatically.
Skip this if the surfaced skills already cover your next action.`
}
return null
}

关键设计:已展示过的技能自动过滤,不重复推荐。DiscoverSkillsTool 的工具名常量通过条件 require 加载(第 86–91 行):

1
2
3
const DISCOVER_SKILLS_TOOL_NAME: string | null = feature('EXPERIMENTAL_SKILL_SEARCH')
? (require('../tools/DiscoverSkillsTool/prompt.js')).DISCOVER_SKILLS_TOOL_NAME
: null

层三:远程技能系统

远程技能使用 _canonical_<slug> 命名格式,模型通过 Skill("_canonical_some-skill") 调用。

两个核心内部模块(仅 Anthropic 构建可见):

模块 职责
remoteSkillState.js 维护本 session 内已发现的远程技能元数据(slug → URL 映射),提供 getDiscoveredRemoteSkill(slug)stripCanonicalPrefix(name)
remoteSkillLoader.js 从 GCS/AKI 拉取 SKILL.md 文件(带本地缓存),提供 loadRemoteSkill(slug, url)

远程技能工作流:

  1. 发现阶段(DiscoverSkills 工具):AKI 搜索返回 { slug, url } 元组,写入 remoteSkillState
  2. 执行阶段(SkillTool):检测到 _canonical_ 前缀后,从 state 取 URL,调用 loadRemoteSkill 拉取内容
  3. 缓存命中字段 remote_cache_hitremote_load_latency_ms 会记录到遥测

skill_discovery Attachment 数据结构

Attachment 的类型定义(src/utils/attachments.ts 第 537–542 行):

1
2
3
4
5
6
| {
type: 'skill_discovery'
skills: { name: string; description: string; shortId?: string }[]
signal: DiscoverySignal // 搜索信号,类型来自内部 signals.js
source: 'native' | 'aki' | 'both' // 来源:本地搜索 / AKI向量搜索 / 两者
}

渲染为模型消息src/utils/messages.ts 第 3506–3519 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
if (attachment.type === 'skill_discovery') {
if (attachment.skills.length === 0) return []
const lines = attachment.skills.map(s => `- ${s.name}: ${s.description}`)
return wrapMessagesInSystemReminder([
createUserMessage({
content:
`Skills relevant to your task:\n\n${lines.join('\n')}\n\n` +
`These skills encode project-specific conventions. ` +
`Invoke via Skill("<name>") for complete instructions.`,
isMeta: true,
}),
])
}

UI 渲染src/components/messages/AttachmentMessage.tsx 第 108–122 行):

1
2
3
4
5
6
7
8
9
10
11
12
if (attachment.type === 'skill_discovery') {
const names = attachment.skills
.map(s => s.shortId ? `${s.name} [${s.shortId}]` : s.name)
.join(', ')
// ant 用户额外显示反馈指令
const hint = isAntUser && firstId
? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]`
: ''
return <Line>
<Text bold>{attachment.skills.length}</Text> relevant skill(s): {names}{hint}
</Line>
}

压缩处理src/services/compact/compact.ts 第 211–223 行):

1
2
3
4
5
6
7
8
9
10
export function stripReinjectedAttachments(messages: Message[]): Message[] {
if (feature('EXPERIMENTAL_SKILL_SEARCH')) {
return messages.filter(
m => !(m.type === 'attachment' &&
(m.attachment.type === 'skill_discovery' ||
m.attachment.type === 'skill_listing'))
)
}
return messages
}

skill_discoveryskill_listing 在送入压缩摘要器前被剥离 —— 它们压缩后无论如何都会重新注入,留着只会浪费 token。


SKILL.md 扫描与本地加载

目录扫描顺序

src/skills/loadSkillsDir.tsgetSkillDirCommands() 函数(第 638–803 行)按以下顺序扫描:

  1. <managed-policy-path>/.claude/skills/(托管策略)
  2. ~/.claude/skills/(用户全局)
  3. 当前工作目录及所有父目录(直到 home)下的 .claude/skills/(项目级)
  4. --add-dir 参数指定目录下的 .claude/skills/
  5. 向后兼容:以上所有目录的 .claude/commands/(旧版路径)

文件结构要求

必须是 目录 + SKILL.md 的结构,不接受直接的 .md 平铺文件:

1
2
3
4
.claude/skills/
└── commit/
└── SKILL.md ✅
└── commit.md ❌ (被忽略)

Frontmatter 字段

parseFrontmatter() 解析的字段(第 185–265 行):

字段 用途
description 技能简介,用于发现系统匹配
when_to_use 触发时机提示
model 覆盖模型(如强制使用 opus)
effort 努力等级覆盖
allowed-tools 白名单工具列表
argument-hint 参数提示
arguments 参数定义
user-invocable 是否允许用户直接调用
context inline(默认)或 fork(子 Agent)
paths 条件触发路径(仅当匹配文件被修改时激活)
hooks 关联的 Hook 事件
shell 执行前运行的 Shell 命令
version 技能版本

变量替换

技能内容加载时(getPromptForCommand(),第 359–369 行)会替换两个占位符:

1
2
${CLAUDE_SKILL_DIR}  →  技能所在目录的绝对路径(用于引用捆绑脚本)
${CLAUDE_SESSION_ID} → 当前会话 ID

动态发现

discoverSkillDirsForPaths()(第 861–896 行)在文件被触碰时动态向上查找 .claude/skills/ 目录。新找到的目录会经过 git check-ignore 验证,被 gitignore 的目录跳过。结合 paths: frontmatter 字段,可以实现条件技能:只有当特定文件被修改时,该技能才会出现在列表中。


SkillTool 三条执行路径

SkillTool.call() 根据技能名称和配置分发到三条路径:

路径 A:远程 Canonical 技能(ant 专属)

触发条件:技能名带 _canonical_ 前缀 + USER_TYPE === 'ant'

1
2
3
4
5
6
7
// SkillTool.ts 第 605–613 行
if (feature('EXPERIMENTAL_SKILL_SEARCH') && process.env.USER_TYPE === 'ant') {
const slug = remoteSkillModules!.stripCanonicalPrefix(commandName)
if (slug !== null) {
return executeRemoteSkill(slug, commandName, parentMessage, context)
}
}

executeRemoteSkill()(第 969–1108 行)执行流程:

  1. getDiscoveredRemoteSkill(slug) → 取本 session 缓存的 URL
  2. loadRemoteSkill(slug, url) → 从 GCS/AKI 拉取内容(带缓存)
  3. 剥离 YAML frontmatter
  4. 替换 ${CLAUDE_SKILL_DIR}${CLAUDE_SESSION_ID}
  5. addInvokedSkill() 注册,防止压缩时丢失
  6. 返回 createUserMessage({ content, isMeta: true })

路径 B:Fork 子 Agent 技能

触发条件:frontmatter 中 context: fork

1
2
3
4
// 调用 executeForkedSkill()
// → prepareForkedCommandContext() 构建 Agent 定义
// → runAgent() 在完全隔离的子 Agent 中执行
// → extractResultText() 提取最终输出

路径 C:Inline 技能(默认)

触发条件:其余所有情况

1
2
3
4
5
6
7
// 调用 processPromptSlashCommand()
// → command.getPromptForCommand(args, context)
// → 读取内存中已加载的 SKILL.md 内容
// → 替换参数、变量
// → 执行 shell: 字段命令(非 MCP 技能)
// 返回 newMessages(携带 toolUseID)+ contextModifier
// contextModifier 会 patch: allowed-tools、model override、effort level

遥测:was_discovered 字段

was_discovered 是技能发现系统最核心的转化率指标,追踪”被发现的技能”是否真正被调用。

Session 级追踪

src/screens/REPL.tsx 第 1958–1963 行:

1
2
// 跨 ToolUseContext 重建持久存在,整个 Session 共享一个 Set
const discoveredSkillNamesRef = useRef(new Set<string>())

src/QueryEngine.ts 第 192–197 行:

1
2
// SDK 模式:每次 submitMessage 清空,防止跨 Turn 累积
private discoveredSkillNames = new Set<string>()

src/utils/forkedAgent.ts 第 385–386 行:

1
2
// 子 Agent 独立追踪
discoveredSkillNames: new Set<string>()

遥测事件

事件名:'tengu_skill_tool_invocation'

完整字段列表:

字段 类型 说明
command_name string 脱敏名称(自定义技能统一显示为 'custom'
_PROTO_skill_name string 未脱敏名称(PII 标记,路由到特权 BigQuery 列)
execution_context 'inline' | 'fork' | 'remote' 执行路径
invocation_trigger 'claude-proactive' | 'nested-skill' 触发来源
query_depth number 嵌套深度
was_discovered boolean 核心指标:是否经过发现系统
is_remote boolean 是否为远程技能
remote_cache_hit boolean 远程技能缓存命中(remote 路径专属)
remote_load_latency_ms number 远程加载延迟(remote 路径专属)

特殊规则:远程技能固定设置 was_discovered: true(第 1047 行),注释解释:

“Remote skills are always model-discovered (never in static skill_listing), so was_discovered is always true.”


完整调用流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
用户发送消息


getAttachmentMessages() [src/utils/attachments.ts]

├─ [EXPERIMENTAL_SKILL_SEARCH && !skipSkillDiscovery]
│ └─ getTurnZeroSkillDiscovery(input, messages, context)
│ ├─ 阻塞式 AKI 向量搜索 / 本地关键词匹配
│ └─ 返回 skill_discovery attachment
│ └─ discoveredSkillNames.add(skillName)

└─ skill_listing attachment(所有可用技能列表)
(启用搜索时仅包含 bundled+MCP,否则包含全部)



query.ts 启动模型流式输出

├─ startSkillDiscoveryPrefetch(...) [异步,与流式输出并发]
│ └─ 仅在 write-pivot 轮次触发

└─ 模型接收到的上下文:
├─ 系统提示:SkillTool 使用指南 + DiscoverSkillsTool 引导语
├─ skill_listing: "- skill-name: description\n..."
└─ skill_discovery: "Skills relevant to your task:\n- xxx: xxx"



模型决定调用 Skill("commit")


SkillTool.validateInput()
├─ _canonical_ 前缀?→ 查 remoteSkillState
└─ 否则 → findCommand() 查本地技能索引



SkillTool.checkPermissions()
├─ deny 规则?→ 拒绝
├─ _canonical_ 前缀?→ 自动允许(Curated 技能)
├─ allow 规则?→ 允许
├─ skillHasOnlySafeProperties()?→ 自动允许
└─ 否则 → 询问用户



SkillTool.call() 分发到三条路径:

├─ [_canonical_ + ant] → executeRemoteSkill()
│ ├─ loadRemoteSkill(slug, url) [GCS/AKI,带缓存]
│ ├─ 替换变量,注册 addInvokedSkill()
│ └─ 遥测: was_discovered=true, is_remote=true

├─ [context: fork] → executeForkedSkill()
│ ├─ runAgent() [隔离子 Agent]
│ └─ 遥测: execution_context='fork', was_discovered=?

└─ [默认 inline] → processPromptSlashCommand()
├─ 展开 SKILL.md,替换参数和变量
├─ 返回 newMessages + contextModifier
│ contextModifier: patch allowed-tools, model, effort
└─ 遥测: execution_context='inline', was_discovered=?



工具执行完毕后:
collectSkillDiscoveryPrefetch() [收集异步 prefetch 结果]
└─ 注入 skill_discovery attachments 到下一轮对话
hidden_by_main_turn 指标:目标 >98% 的情况下
prefetch 已在主轮次完成前解析完毕

关键源码位置速查表

功能 文件 行号
Feature Flag 门控(SkillTool) src/tools/SkillTool/SkillTool.ts 108–115
Feature Flag 门控(query) src/query.ts 66–68
Feature Flag 门控(attachments) src/utils/attachments.ts 95–102
Turn-0 发现调用 src/utils/attachments.ts 801–812
异步 prefetch 启动 src/query.ts 331–335
异步 prefetch 收集 src/query.ts 1617–1628
skill_discovery attachment 类型定义 src/utils/attachments.ts 537–542
skill_discovery → 模型消息转换 src/utils/messages.ts 3506–3519
skill_discovery UI 渲染 src/components/messages/AttachmentMessage.tsx 108–122
压缩前剥离 skill attachments src/services/compact/compact.ts 211–223
DiscoverSkillsTool 名称加载 src/constants/prompts.ts 86–91
DiscoverSkillsTool 系统提示引导语 src/constants/prompts.ts 333–341
SKILL.md 目录扫描 src/skills/loadSkillsDir.ts 638–803
动态技能目录发现 src/skills/loadSkillsDir.ts 861–896
Frontmatter 解析 src/skills/loadSkillsDir.ts 185–265
变量替换(SKILL_DIR, SESSION_ID) src/skills/loadSkillsDir.ts 359–369
Remote 技能执行入口 src/tools/SkillTool/SkillTool.ts 605–613
executeRemoteSkill 完整实现 src/tools/SkillTool/SkillTool.ts 969–1108
was_discovered(inline 路径) src/tools/SkillTool/SkillTool.ts 661–668
was_discovered(fork 路径) src/tools/SkillTool/SkillTool.ts 139–146
was_discovered(remote 固定为 true) src/tools/SkillTool/SkillTool.ts 1047
discoveredSkillNames(REPL Session) src/screens/REPL.tsx 1958–1963
discoveredSkillNames(SDK) src/QueryEngine.ts 192–197
discoveredSkillNames(子 Agent) src/utils/forkedAgent.ts 385–386
discoveredSkillNames(ToolUseContext 定义) src/Tool.ts 224–225
SkillTool 工具描述 src/tools/SkillTool/prompt.ts 173–195
SkillTool 工具名常量 src/tools/SkillTool/constants.ts 1

小结

Claude Code 的技能发现系统是一个精心设计的三层渐进式架构

  1. 被动发现(Turn-0):每轮对话自动运行,阻塞式确保模型第一时间看到相关技能
  2. 主动搜索(DiscoverSkillsTool):应对中途转向,已展示技能自动去重
  3. 远程技能(Canonical Skills):Anthropic 策划的企业级技能库,必须先经发现再执行

整个系统通过 EXPERIMENTAL_SKILL_SEARCH 编译期 Flag 完全隔离在外部构建之外,并通过 was_discovered 遥测字段持续度量”发现→调用”的转化率,驱动系统迭代优化。