在前面的文章中,我们已经逐一解析了 Claude Code 的核心工具链——从文件读写的 FileReadTool/FileEditTool,到 Shell 执行的 BashTool,再到网络搜索的 WebSearchTool。这些工具解决了"做什么"的问题,但尚未回答"怎么做最好"。Skills 正是 Claude Code 对这一问题的答案:将最佳实践封装为可复用的提示词工作流,让模型在特定场景下自动遵循预定义的专家模式。
而 SkillTool 则是 Skills 系统的执行引擎——它负责验证模型发起的 Skill 调用、路由到正确的执行路径、管理权限与上下文。本文将深入 SkillTool 的源码实现,剖析 Skill 从磁盘加载到最终执行的完整链路。
一、SkillTool 架构概览
SkillTool 位于 src/tools/SkillTool/ 目录下,是 Claude Code 37 个内置工具之一。与 BashTool、FileReadTool 等"原子能力"不同,SkillTool 是一个元工具(meta-tool):它不直接操作文件系统或网络,而是调用其他预定义的提示词模板(即 Skill)来扩展模型的行为。
src/tools/SkillTool/
├── SkillTool.ts # 工具定义、验证、权限、执行(核心 ~500 行)
├── prompt.ts # 工具提示词、Skill 列表格式化、预算控制
└── constants.ts # SkillTool 名称常量等SkillTool 的输入 Schema 极为简洁(src/tools/SkillTool/SkillTool.ts:331-340):
export const SkillTool = buildTool({
name: 'Skill',
inputSchema: z.object({
skill: z.string(), // 技能名称
args: z.string().optional(), // 可选参数(如 "-m 'Fix bug'")
}),
outputSchema: z.union([inlineOutputSchema, forkedOutputSchema]),
})模型看到的工具描述(src/tools/SkillTool/prompt.ts:173-196)会动态注入当前可用的 Skill 列表:
export const getPrompt = memoize(async (_cwd: string): Promise<string> => {
return `Execute a skill within the main conversation
When users ask you to perform tasks, check if any of the available skills match.
How to invoke:
- Use this tool with the skill name and optional arguments
- Examples:
- skill: "pdf"
- skill: "commit", args: "-m 'Fix bug'"
Important:
- Available skills are listed in system-reminder messages
- When a skill matches, invoke BEFORE generating any other response
- NEVER mention a skill without calling this tool
- Do not invoke a skill that is already running
`
})这种设计的精妙之处在于:Skill 列表不硬编码在工具描述中,而是通过 system-reminder 消息动态注入。这使得 Skills 的发现与加载完全独立于工具 Schema,新增 Skill 无需修改 SkillTool 的 JSON Schema。
Skills 的数据来源分布在多个位置:
| 来源 | 位置 | 优先级 |
|---|---|---|
| Bundled Skills | src/skills/bundled/(打入二进制) | 最高 |
| 内置插件 Skills | 插件 manifest 声明 | 次高 |
| 目录 Skills | ~/.claude/skills/ + 项目 .claude/skills/ | 中等 |
| MCP Skills | MCP Server 提供的 prompts | 动态 |
| 插件 Skills | 第三方插件 | 较低 |
| Slash 命令 | 内置 / 命令 | 最低 |
二、Skill 的发现与加载
2.1 加载入口:commands.ts
所有 Skills 的聚合发生在 src/commands.ts:351-396 的 getSkills() 函数中:
async function getSkills(cwd: string) {
const [skillDirCommands, pluginSkills] = await Promise.all([
getSkillDirCommands(cwd), // 目录 Skills(managed/user/project)
getPluginSkills(), // 插件 Skills
])
const bundledSkills = getBundledSkills() // 内置 Skills
const builtinPluginSkills = getBuiltinPluginSkillCommands() // 内置插件 Skills
return { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills }
}最终所有命令通过 loadAllCommands() 聚合并按优先级排序(src/commands.ts:447-467):
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
return [
...bundledSkills, // 1. 内置 Skills
...builtinPluginSkills, // 2. 内置插件 Skills
...skillDirCommands, // 3. 目录 Skills
...workflowCommands, // 4. Workflow 命令
...pluginCommands, // 5. 插件命令
...pluginSkills, // 6. 插件 Skills
...COMMANDS(), // 7. 内建 slash 命令
]
})关键设计:memoize 缓存避免重复磁盘 I/O。一旦 Skills 加载完成,后续调用直接返回缓存结果,直到缓存被显式清除。
2.2 目录 Skill 加载:loadSkillsDir.ts
src/skills/loadSkillsDir.ts(约 34KB)是 Skills 系统中最复杂的模块之一,负责从文件系统发现和加载用户自定义 Skills。其核心函数 getSkillDirCommands() 扫描 6 种来源的目录:
getSkillDirCommands(cwd)
├─ 确定加载路径
│ ├─ managed: ${MANAGED_PATH}/.claude/skills/
│ ├─ user: ~/.claude/skills/
│ ├─ project: .claude/skills/(向上遍历到 HOME)
│ └─ additional: --add-dir 指定的路径
│
├─ 并行加载(Promise.all)
│ ├─ loadSkillsFromSkillsDir(managedDir, 'policySettings')
│ ├─ loadSkillsFromSkillsDir(userDir, 'userSettings')
│ ├─ loadSkillsFromSkillsDir(projectDirs, 'projectSettings')
│ ├─ loadSkillsFromSkillsDir(additionalDirs, 'projectSettings')
│ └─ loadSkillsFromCommandsDir(cwd) ← 兼容遗留 /commands/ 格式
│
├─ 去重(按 realpath)
│ └─ getFileIdentity(filePath) → realpath 解析符号链接
│ └─ seenFileIds Map,首次出现者胜出
│
└─ 分离条件 Skills
├─ 无 paths → unconditionalSkills(立即可用)
└─ 有 paths → conditionalSkills Map(等待激活)去重机制(src/skills/loadSkillsDir.ts:725-763)使用 realpath() 解析符号链接,确保同一 Skill 的多个软链接只保留一个:
const fileIds = await Promise.all(
allSkillsWithPaths.map(({ skill, filePath }) =>
skill.type === 'prompt'
? getFileIdentity(filePath) // realpath() 解析符号链接
: Promise.resolve(null),
),
)
const seenFileIds = new Map<string, SettingSource>()
for (const entry of allSkillsWithPaths) {
const fileId = fileIds[i]
const existingSource = seenFileIds.get(fileId)
if (existingSource !== undefined) continue // 跳过重复
seenFileIds.set(fileId, skill.source)
deduplicatedSkills.push(skill)
}目录格式要求:仅支持 skill-name/SKILL.md 的目录结构,每个 Skill 必须包含一个 SKILL.md 文件,顶部附带 YAML Frontmatter。
2.3 Frontmatter 解析
SKILL.md 的 Frontmatter 定义了 Skill 的元数据和行为。解析流程(src/utils/frontmatterParser.ts:10-59):
export type FrontmatterData = {
'allowed-tools'?: string | string[] | null
description?: string | null
'argument-hint'?: string | null
when_to_use?: string | null
version?: string | null
model?: string | null // haiku, sonnet, opus, inherit
'user-invocable'?: string | null
'disable-model-invocation'?: string | null
hooks?: HooksSettings | null
effort?: string | null // low, medium, high, max
context?: 'inline' | 'fork' | null
agent?: string | null
paths?: string | string[] | null
shell?: string | null // bash, powershell
[key: string]: unknown
}解析后的 Frontmatter 通过 createSkillCommand()(src/skills/loadSkillsDir.ts:270-401)构建为 Command 对象。每个 Command 对象包含一个关键的 getPromptForCommand 闭包,在调用时执行参数替换和 Shell 命令:
async getPromptForCommand(args, toolUseContext) {
// 1. 添加基目录头
let finalContent = baseDir
? `Base directory for this skill: ${baseDir}\n\n${markdownContent}`
: markdownContent
// 2. 参数替换($ARGUMENTS, ${argName})
finalContent = substituteArguments(finalContent, args, true, argumentNames)
// 3. 技能目录变量替换
if (baseDir) {
finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir)
}
// 4. 会话 ID 替换
finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, getSessionId())
// 5. 执行内联 Shell 命令(MCP Skills 跳过此步)
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, context)
}
return [{ type: 'text', text: finalContent }]
}2.4 Bundled Skill 注册:bundledSkills.ts
内置 Skills 使用完全不同的注册路径(src/skills/bundledSkills.ts:53-100)。BundledSkillDefinition 类型定义如下:
export type BundledSkillDefinition = {
name: string
description: string
files?: Record<string, string> // 首次调用时解压到临时目录
getPromptForCommand: (args, context) => Promise<ContentBlockParam[]>
}注册函数 registerBundledSkill() 的核心逻辑:
export function registerBundledSkill(definition: BundledSkillDefinition): void {
// 如果有 files,创建提取目录和延迟提取逻辑
if (files && Object.keys(files).length > 0) {
skillRoot = getBundledSkillExtractDir(definition.name)
// 首次调用时提取文件到磁盘
getPromptForCommand = async (args, ctx) => {
extractionPromise ??= extractBundledSkillFiles(name, files)
const extractedDir = await extractionPromise
const blocks = await inner(args, ctx)
return prependBaseDir(blocks, extractedDir)
}
}
const command: Command = {
type: 'prompt',
source: 'bundled',
loadedFrom: 'bundled',
// ...其他字段
}
bundledSkills.push(command)
}关键设计:延迟提取。Bundled Skills 可以携带 files 字段(将文件内容打包在二进制中),首次调用时才解压到临时目录。这让模型能通过 Read/Grep 工具访问 Skill 附带的资源文件(如模板、配置示例等)。
启动注册入口在 src/skills/bundled/index.ts:13-58:
export function initBundledSkills(): void {
require('./verify.js').registerVerifySkill()
require('./debug.js').registerDebugSkill()
require('./remember.js').registerRememberSkill()
// ...更多内置 Skill
if (feature('AGENT_TRIGGERS')) {
require('./loop.js').registerLoopSkill() // 特性门控
}
}当前 Claude Code 内置 16 个 Bundled Skills:
| Skill | 用途 |
|---|---|
batch | 跨多文件的批处理操作 |
claudeApi | 直接与 Anthropic API 交互 |
claudeInChrome | Chrome 扩展集成 |
debug | 调试工作流 |
keybindings | 快捷键配置 |
loop | 迭代优化循环 |
loremIpsum | 生成占位文本 |
remember | 持久化信息到记忆 |
scheduleRemoteAgents | 调度远程 Agent |
simplify | 简化复杂代码 |
skillify | 从工作流创建新 Skill |
stuck | 卡顿时获取帮助 |
updateConfig | 程序化修改配置 |
verify / verifyContent | 验证代码正确性 |
2.5 MCP Skill 构建:mcpSkillBuilders.ts
src/skills/mcpSkillBuilders.ts 负责将 MCP(Model Context Protocol)Server 提供的 prompts 转换为 Claude Code 的 Skill 命令。这是 Claude Code 与外部 MCP 生态集成的关键桥梁。
转换逻辑位于 src/services/mcp/client.ts:2030-2102:
async function fetchCommandsForClient(client) {
const prompts = await client.listPrompts()
return prompts.map(prompt => ({
type: 'prompt',
name: `mcp__${normalizeNameForMCP(serverName)}__${prompt.name}`,
source: 'mcp',
loadedFrom: 'mcp',
// getPromptForCommand 调用 MCP 服务器获取内容
}))
}MCP Skills 的名称采用命名空间格式:mcp__<serverName>__<promptName>,避免与本地 Skills 冲突。特性门控 feature('MCP_SKILLS') 控制该功能是否启用。
三、Skill 的执行流程
3.1 完整执行链路
以下是 SkillTool 从模型调用到最终结果的完整执行流程:
flowchart TD
A[模型调用 SkillTool
{skill, args}] --> B[validateInput
验证格式 & 查找 Command]
B --> C[checkPermissions
权限规则匹配]
C --> D{context === 'fork'?}
D -->|YES| E[executeForkedSkill]
D -->|NO| F[processPromptSlashCommand
inline 执行]
E --> G[prepareForkedCommandContext]
G --> H[runAgent
子 Agent 运行]
H --> I[extractResultText
提取结果]
F --> J[getPromptForCommand
展开 Skill 内容]
J --> K[registerSkillHooks
注册 Hook]
K --> L[contextModifier
更新 tools/model/effort]
I --> M[返回 tool_result]
L --> M3.2 验证与权限
validateInput()(src/tools/SkillTool/SkillTool.ts:354-430)执行多层校验:
async validateInput({ skill }, context) {
// 1. 格式检查 — 非空
// 2. 标准化 — 去除前导 /
// 3. 远程 Skill 检查 — _canonical_ 前缀
// 4. 查找 — findCommand() 在 getAllCommands() 中
// 5. 禁用检查 — disableModelInvocation
// 6. 类型检查 — 必须是 'prompt' 类型
}错误码定义:
| errorCode | 含义 |
|---|---|
| 1 | 格式无效(空技能名) |
| 2 | 未知技能 |
| 4 | 模型调用被禁用 |
| 5 | 非 prompt 类型 |
| 6 | 远程技能未发现 |
权限检查(checkPermissions)遵循优先级规则:
- Deny 规则(最高优先级):精确匹配或前缀匹配(如
"review:*") - 远程 Skill 自动允许:
_canonical_<slug>前缀的 Skill - Allow 规则:用户显式允许的规则
- 安全属性白名单:若 Skill 仅包含
SAFE_SKILL_PROPERTIES中的属性(无 hooks、无 allowedTools、非 fork),自动允许 - 默认询问用户
3.3 Inline 执行:注入当前对话
默认情况下,Skill 以 inline 模式执行——将 Skill 内容展开为文本块,注入到当前对话中:
// processPromptSlashCommand() 执行路径
command.getPromptForCommand(args, context)
→ 参数替换 + Shell 命令执行
→ registerSkillHooks() // 注册会话级 Hook
→ addInvokedSkill() // 记录调用(压缩后恢复)
→ formatCommandLoadingMetadata() // 生成 <command-name> 标签
→ 创建消息 + tagMessagesWithToolUseID()Inline 返回结构:
{
data: {
success: true,
commandName: 'commit',
allowedTools: ['Bash', 'Read'],
model: 'sonnet',
status: 'inline',
},
newMessages: [...], // 注入到对话的新消息
contextModifier: (ctx) => { // 修改上下文:工具白名单、模型、effort
ctx.allowedTools = command.allowedTools
ctx.model = command.model
ctx.effort = command.effort
},
}3.4 Fork 执行:子 Agent 隔离
当 Skill 的 Frontmatter 中声明 context: fork 时,SkillTool 会创建一个子 Agent 来执行 Skill,与主对话隔离。这是 SkillTool 与 AgentTool 的关键交汇点。
sequenceDiagram
participant Main as 主 Agent
participant ST as SkillTool
participant FA as forkedAgent.ts
participant Agent as 子 Agent
participant API as Claude API
Main->>ST: call({skill: "verify", args: "..."})
ST->>ST: validateInput & checkPermissions
ST->>FA: executeForkedSkill()
FA->>FA: prepareForkedCommandContext()
FA->>FA: getPromptForCommand()
展开 Skill 内容
FA->>FA: parseToolListFromCLI()
解析工具白名单
FA->>FA: createGetAppStateWithAllowedTools()
修改 AppState
FA->>Agent: runAgent({
agentDefinition,
promptMessages,
model: command.model
})
loop 子 Agent Loop
Agent->>API: callModel()
API-->>Agent: 流式响应
Agent->>Agent: 执行工具(Read/Bash/...)
end
Agent-->>FA: 完成消息
FA->>FA: extractResultText()
提取最后 assistant 消息
FA-->>ST: {status: "forked", result: "..."}
ST-->>Main: tool_resultFork 执行的实现(src/utils/forkedAgent.ts:191-232):
export async function prepareForkedCommandContext(
command: PromptCommand,
args: string,
context: ToolUseContext,
): Promise<PreparedForkedContext> {
// 获取技能内容(含参数替换和 shell 执行)
const skillPrompt = await command.getPromptForCommand(args, context)
const skillContent = skillPrompt.map(b => b.type === 'text' ? b.text : '').join('\n')
// 构建工具白名单
const allowedTools = parseToolListFromCLI(command.allowedTools ?? [])
const modifiedGetAppState = createGetAppStateWithAllowedTools(
context.getAppState, allowedTools,
)
// 选择代理类型
const agentTypeName = command.agent ?? 'general-purpose'
const baseAgent = agents.find(a => a.agentType === agentTypeName)
// 构建提示消息
const promptMessages = [createUserMessage({ content: skillContent })]
return { skillContent, modifiedGetAppState, baseAgent, promptMessages }
}Fork 执行的优势在于隔离性:子 Agent 拥有独立的工具白名单、模型配置和 effort 设置,其执行过程不会污染主对话的上下文。 Skill 执行完毕后,只有最终结果(extractResultText() 提取的最后一条 assistant 消息文本)被返回给主 Agent。
3.5 上下文预算控制
Skills 列表通过 system-reminder 消息注入对话,但需要控制其占用的上下文预算,避免挤占用户消息的空间。预算控制逻辑位于 src/tools/SkillTool/prompt.ts:21-29:
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01 // 上下文窗口的 1%
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000 // 200k × 4 × 1% 兜底
export const MAX_LISTING_DESC_CHARS = 250 // 每条描述上限截断策略(formatCommandsWithinBudget):
- 计算总预算 =
contextWindowTokens × 4 × 1% - 尝试全量描述:若总字符 ≤ 预算,全部输出
- 分区保留:Bundled Skills 始终保留完整描述(不可截断),其余 Skills 平分剩余预算
- 截断描述至
maxDescLen字符;若maxDescLen < 20,非 Bundled 只显示名称
这种设计确保了核心内置 Skills 始终对模型可见,而用户自定义 Skills 在上下文紧张时被优雅降级。
四、条件激活与动态发现
4.1 条件 Skills
Skill 可以在 Frontmatter 中声明 paths 字段,实现按需激活:
---
name: React Component Review
description: Review React components for best practices
paths: ["src/components/**/*.tsx", "src/app/**/*.tsx"]
---带 paths 的 Skills 在启动时不会暴露给模型,而是存入 conditionalSkills Map。当用户编辑的文件路径匹配 paths 中的 glob 模式时,Skill 被自动激活(src/skills/loadSkillsDir.ts 后半段):
运行时(文件操作触发)
├─ activateConditionalSkillsForPaths(filePaths, cwd)
│ ├─ 遍历 conditionalSkills Map
│ ├─ 用 ignore 库匹配 paths 模式
│ │ └─ filePath 转为 cwd 相对路径后匹配
│ ├─ 匹配成功:
│ │ ├─ 移入 dynamicSkills Map
│ │ ├─ 从 conditionalSkills 删除
│ │ ├─ 加入 activatedConditionalSkillNames Set
│ │ └─ 记录遥测 tengu_dynamic_skills_changed
│ └─ 一旦激活,会话内持续有效
└─ 通知缓存失效 → skillsLoaded.emit()4.2 动态目录发现
当操作深层目录文件时,系统还会自动发现新的 Skills 目录(src/skills/loadSkillsDir.ts:861-915):
export async function discoverSkillDirsForPaths(
filePaths: string[],
cwd: string,
): Promise<string[]> {
for (const filePath of filePaths) {
let currentDir = dirname(filePath)
// 从文件所在目录向上遍历到 cwd(不含 cwd 本身)
while (currentDir.startsWith(resolvedCwd + pathSep)) {
const skillDir = join(currentDir, '.claude', 'skills')
if (!dynamicSkillDirs.has(skillDir)) {
dynamicSkillDirs.add(skillDir)
await fs.stat(skillDir) // 检查是否存在
// 检查是否被 .gitignore 忽略
if (await isPathGitignored(currentDir, resolvedCwd)) continue
newDirs.push(skillDir)
}
currentDir = dirname(currentDir)
}
}
// 按深度排序(最深优先),确保就近 Skill 优先级更高
return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length)
}按深度排序是关键设计:深层目录的 Skill 优先级高于浅层目录,确保项目局部的 Skill 覆盖全局的 Skill。
五、与相邻系统的关系
5.1 SkillTool vs AgentTool
SkillTool 与 AgentTool 的关系可以从两个维度理解:
| 维度 | SkillTool | AgentTool |
|---|---|---|
| 本质 | 调用预定义提示词模板 | 创建通用子 Agent |
| 输入 | Skill 名称 + 参数 | Agent 类型 + 任务描述 |
| 上下文 | Inline(注入主对话)或 Fork(子 Agent) | 始终 Fork |
| 工具限制 | 可限制 allowedTools | 可限制 allowedTools |
| 使用场景 | 可复用的专家工作流 | 一次性任务委托 |
当 Skill 声明 context: fork 时,SkillTool 内部调用 runAgent()(与 AgentTool 共享同一套子 Agent 机制),形成工具层面的嵌套调用。这是 Claude Code 实现"分而治之"策略的核心:主 Agent 负责决策,Skill/子 Agent 负责执行专业任务。
5.2 与插件系统的集成
插件可以声明自己的 Skills 目录(manifest.skillsPath 和 manifest.skillsPaths[]),通过 loadSkillsFromDirectory() 加载。插件 Skills 的命名空间格式为 {pluginName}:{namespace}:{skillName}(如 superpowers:code-reviewer),避免命名冲突。
插件加载路径(src/utils/plugins/loadPluginCommands.ts):
loadAllPluginsCacheOnly()
└─ 获取所有已启用插件
每个插件:
├─ 读取 manifest.skillsPath → 默认 Skills 目录
├─ 读取 manifest.skillsPaths[] → 额外 Skills 目录
└─ loadSkillsFromDirectory() 加载 SKILL.md
变量替换:
├─ ${CLAUDE_PLUGIN_ROOT} → 插件根目录
├─ ${CLAUDE_PLUGIN_DATA} → 插件数据目录
├─ ${CLAUDE_SKILL_DIR} → 技能目录
└─ ${user_config.X} → 用户配置值5.3 与工具池的协作
SkillTool 本身注册在工具池中(src/tools.ts),通过 getAllBaseTools() 暴露给模型。但 SkillTool 的可用 Skill 列表是动态的——每轮对话通过 getSkillToolCommands() 过滤出模型可调用的 Skills,并格式化为 system-reminder 消息注入。
工具池组装流程中的 Skill 相关环节:
assembleToolPool()
├─ built-in tools(含 SkillTool)
├─ AppState.mcp.mcpTools(MCP 工具)
└─ refreshTools() 每轮取最新快照
getSkillToolCommands()
├─ getCommands() → 所有命令
├─ 过滤: type === 'prompt' && !disableModelInvocation
├─ 去除 userInvocable === false 的(仅 slash 可用)
└─ memoize 缓存六、Skill 内容持久化与压缩恢复
Inline Skills 的内容通过 addInvokedSkill() 记录到会话状态中,确保在上下文压缩后仍可恢复(src/utils/hooks/registerSkillHooks.ts 及相关模块):
addInvokedSkill(name, path, content, agentId)
↓
存储在 session state 中
↓
压缩时 → buildPostCompactMessages()
↓
按 agentId 作用域恢复(防止跨 Agent 泄漏)这一机制解决了关键问题:当 Skill 内容被压缩算法移除后,模型如何在后续轮次中继续引用 Skill 的指令? 答案是:Claude Code 在压缩后主动将已调用的 Skills 重新注入对话,确保 Skill 的约束(如 allowedTools、model)在整个会话期间持续生效。
七、总结
SkillTool 是 Claude Code 中连接"原子工具"与"专家工作流"的枢纽。它的设计体现了几个核心思想:
声明式配置优于命令式编程:通过 YAML Frontmatter 声明 Skill 的行为(工具白名单、模型、执行模式),而非在代码中硬编码逻辑。
渐进式暴露:条件激活和上下文预算控制确保模型只看到当前相关的 Skills,避免信息过载。
执行模式可选:Inline 模式适合轻量级提示词注入,Fork 模式适合隔离性要求高的复杂任务,两者共用同一套 Skill 定义。
生态系统集成:Skills 可以从磁盘、插件、MCP Server 多种来源加载,形成开放的扩展生态。
理解 SkillTool 的实现,有助于我们设计自己的 Agent 扩展机制——无论是为内部团队构建标准化的代码审查流程,还是为开源项目提供可复用的调试工作流,Skills 系统都提供了一个经过生产验证的参考架构。