Claude Code 技能发现机制深度解析
Claude Code 技能发现机制深度解析
基于源码逆向分析,详解
EXPERIMENTAL_SKILL_SEARCH特性标志控制下的完整技能发现与加载系统。
目录
- 系统概览
- Feature Flag 门控机制
- 三层发现架构
- skill_discovery Attachment 数据结构
- SKILL.md 扫描与本地加载
- SkillTool 三条执行路径
- 遥测:was_discovered 字段
- 完整调用流程图
- 关键源码位置速查表
系统概览
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 | const remoteSkillModules = feature('EXPERIMENTAL_SKILL_SEARCH') |
src/query.ts 第 66–68 行 — prefetch 模块加载:
1 | const skillPrefetch = feature('EXPERIMENTAL_SKILL_SEARCH') |
src/utils/attachments.ts 第 95–102 行 — attachment 组装时的门控:
1 | const skillSearchModules = feature('EXPERIMENTAL_SKILL_SEARCH') |
除编译期 Flag 外,还有一个运行时副门控:远程技能和 Canonical 技能的调用路径大量检查 process.env.USER_TYPE === 'ant'(位于 SkillTool.ts 第 378、494、606 行),确保远程技能体系只对 Anthropic 员工开放。
三层发现架构
层一:Turn-0 自动发现
入口: src/utils/attachments.ts 的 getAttachmentMessages() 函数,第 801–812 行:
1 | ...(feature('EXPERIMENTAL_SKILL_SEARCH') && |
这个调用是阻塞式的 —— 发生在第 0 轮(Turn-0),此时模型还没有开始输出,没有其他工作可以用来掩盖延迟。getTurnZeroSkillDiscovery 内部会针对用户消息执行 AKI 向量搜索或关键词匹配,返回相关技能列表。
skipSkillDiscovery 防护是关键设计:当 SkillTool 加载一个技能的 SKILL.md 内容(可能长达 110KB)并将其作为 input 传入时,必须跳过发现流程,否则会对每次技能调用触发 ~3.3 秒的 AKI 查询。
轮间异步 Prefetch: 在后续轮次中,系统改用异步 prefetch 掩盖延迟:
1 | src/query.ts 第 331 行(模型开始流式输出前): |
Prefetch 还受写操作检测门控(write-pivot guard):只在预计会产生写操作的轮次触发,避免在纯读查询上浪费 AKI 调用。
层二:主动搜索工具 DiscoverSkillsTool
这是给模型主动调用的工具,用于”中途转向”或”非典型工作流”场景。其 prompt 模块位于 src/tools/DiscoverSkillsTool/prompt.js(Anthropic 内部,源码中不可见)。
系统提示中注入的引导语(src/constants/prompts.ts 第 333–341 行):
1 | function getDiscoverSkillsGuidance(): string | null { |
关键设计:已展示过的技能自动过滤,不重复推荐。DiscoverSkillsTool 的工具名常量通过条件 require 加载(第 86–91 行):
1 | const DISCOVER_SKILLS_TOOL_NAME: string | null = feature('EXPERIMENTAL_SKILL_SEARCH') |
层三:远程技能系统
远程技能使用 _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) |
远程技能工作流:
- 发现阶段(
DiscoverSkills工具):AKI 搜索返回{ slug, url }元组,写入remoteSkillState - 执行阶段(
SkillTool):检测到_canonical_前缀后,从 state 取 URL,调用loadRemoteSkill拉取内容 - 缓存命中字段
remote_cache_hit和remote_load_latency_ms会记录到遥测
skill_discovery Attachment 数据结构
Attachment 的类型定义(src/utils/attachments.ts 第 537–542 行):
1 | | { |
渲染为模型消息(src/utils/messages.ts 第 3506–3519 行):
1 | if (attachment.type === 'skill_discovery') { |
UI 渲染(src/components/messages/AttachmentMessage.tsx 第 108–122 行):
1 | if (attachment.type === 'skill_discovery') { |
压缩处理(src/services/compact/compact.ts 第 211–223 行):
1 | export function stripReinjectedAttachments(messages: Message[]): Message[] { |
skill_discovery 和 skill_listing 在送入压缩摘要器前被剥离 —— 它们压缩后无论如何都会重新注入,留着只会浪费 token。
SKILL.md 扫描与本地加载
目录扫描顺序
src/skills/loadSkillsDir.ts 的 getSkillDirCommands() 函数(第 638–803 行)按以下顺序扫描:
<managed-policy-path>/.claude/skills/(托管策略)~/.claude/skills/(用户全局)- 当前工作目录及所有父目录(直到 home)下的
.claude/skills/(项目级) --add-dir参数指定目录下的.claude/skills/- 向后兼容:以上所有目录的
.claude/commands/(旧版路径)
文件结构要求
必须是 目录 + SKILL.md 的结构,不接受直接的 .md 平铺文件:
1 | .claude/skills/ |
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 | ${CLAUDE_SKILL_DIR} → 技能所在目录的绝对路径(用于引用捆绑脚本) |
动态发现
discoverSkillDirsForPaths()(第 861–896 行)在文件被触碰时动态向上查找 .claude/skills/ 目录。新找到的目录会经过 git check-ignore 验证,被 gitignore 的目录跳过。结合 paths: frontmatter 字段,可以实现条件技能:只有当特定文件被修改时,该技能才会出现在列表中。
SkillTool 三条执行路径
SkillTool.call() 根据技能名称和配置分发到三条路径:
路径 A:远程 Canonical 技能(ant 专属)
触发条件:技能名带 _canonical_ 前缀 + USER_TYPE === 'ant'
1 | // SkillTool.ts 第 605–613 行 |
executeRemoteSkill()(第 969–1108 行)执行流程:
getDiscoveredRemoteSkill(slug)→ 取本 session 缓存的 URLloadRemoteSkill(slug, url)→ 从 GCS/AKI 拉取内容(带缓存)- 剥离 YAML frontmatter
- 替换
${CLAUDE_SKILL_DIR}、${CLAUDE_SESSION_ID} addInvokedSkill()注册,防止压缩时丢失- 返回
createUserMessage({ content, isMeta: true })
路径 B:Fork 子 Agent 技能
触发条件:frontmatter 中 context: fork
1 | // 调用 executeForkedSkill() |
路径 C:Inline 技能(默认)
触发条件:其余所有情况
1 | // 调用 processPromptSlashCommand() |
遥测:was_discovered 字段
was_discovered 是技能发现系统最核心的转化率指标,追踪”被发现的技能”是否真正被调用。
Session 级追踪
src/screens/REPL.tsx 第 1958–1963 行:
1 | // 跨 ToolUseContext 重建持久存在,整个 Session 共享一个 Set |
src/QueryEngine.ts 第 192–197 行:
1 | // SDK 模式:每次 submitMessage 清空,防止跨 Turn 累积 |
src/utils/forkedAgent.ts 第 385–386 行:
1 | // 子 Agent 独立追踪 |
遥测事件
事件名:'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 | 用户发送消息 |
关键源码位置速查表
| 功能 | 文件 | 行号 |
|---|---|---|
| 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 的技能发现系统是一个精心设计的三层渐进式架构:
- 被动发现(Turn-0):每轮对话自动运行,阻塞式确保模型第一时间看到相关技能
- 主动搜索(DiscoverSkillsTool):应对中途转向,已展示技能自动去重
- 远程技能(Canonical Skills):Anthropic 策划的企业级技能库,必须先经发现再执行
整个系统通过 EXPERIMENTAL_SKILL_SEARCH 编译期 Flag 完全隔离在外部构建之外,并通过 was_discovered 遥测字段持续度量”发现→调用”的转化率,驱动系统迭代优化。