SkillTool:Skills 执行器

📑 目录

在前面的文章中,我们已经逐一解析了 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 Skillssrc/skills/bundled/(打入二进制)最高
内置插件 Skills插件 manifest 声明次高
目录 Skills~/.claude/skills/ + 项目 .claude/skills/中等
MCP SkillsMCP Server 提供的 prompts动态
插件 Skills第三方插件较低
Slash 命令内置 / 命令最低

二、Skill 的发现与加载

2.1 加载入口:commands.ts

所有 Skills 的聚合发生在 src/commands.ts:351-396getSkills() 函数中:

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 交互
claudeInChromeChrome 扩展集成
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 --> M

3.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)遵循优先级规则:

  1. Deny 规则(最高优先级):精确匹配或前缀匹配(如 "review:*"
  2. 远程 Skill 自动允许_canonical_<slug> 前缀的 Skill
  3. Allow 规则:用户显式允许的规则
  4. 安全属性白名单:若 Skill 仅包含 SAFE_SKILL_PROPERTIES 中的属性(无 hooks、无 allowedTools、非 fork),自动允许
  5. 默认询问用户

3.3 Inline 执行:注入当前对话

默认情况下,Skill 以 inline 模式执行——将 Skill 内容展开为文本块,注入到当前对话中:

// processPromptSlashCommand() 执行路径
command.getPromptForCommand(args, context)
  → 参数替换 + Shell 命令执行
  → registerSkillHooks()          // 注册会话级 HookaddInvokedSkill()             // 记录调用(压缩后恢复)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_result

Fork 执行的实现(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):

  1. 计算总预算 = contextWindowTokens × 4 × 1%
  2. 尝试全量描述:若总字符 ≤ 预算,全部输出
  3. 分区保留:Bundled Skills 始终保留完整描述(不可截断),其余 Skills 平分剩余预算
  4. 截断描述至 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 的关系可以从两个维度理解:

维度SkillToolAgentTool
本质调用预定义提示词模板创建通用子 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.skillsPathmanifest.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 中连接"原子工具"与"专家工作流"的枢纽。它的设计体现了几个核心思想:

  1. 声明式配置优于命令式编程:通过 YAML Frontmatter 声明 Skill 的行为(工具白名单、模型、执行模式),而非在代码中硬编码逻辑。

  2. 渐进式暴露:条件激活和上下文预算控制确保模型只看到当前相关的 Skills,避免信息过载。

  3. 执行模式可选:Inline 模式适合轻量级提示词注入,Fork 模式适合隔离性要求高的复杂任务,两者共用同一套 Skill 定义。

  4. 生态系统集成:Skills 可以从磁盘、插件、MCP Server 多种来源加载,形成开放的扩展生态。

理解 SkillTool 的实现,有助于我们设计自己的 Agent 扩展机制——无论是为内部团队构建标准化的代码审查流程,还是为开源项目提供可复用的调试工作流,Skills 系统都提供了一个经过生产验证的参考架构。