跳到主要内容

技能执行流程:从 /skill-name 到实际运行

🟡 进阶

完整流程概览

当用户输入 /pr-review 关注性能问题 时,这个简单的命令会触发一条复杂的执行链路。下面的流程图展示了从用户输入到 AI 最终响应的完整过程:

第一步:解析斜杠命令

用户输入首先进入 processSlashCommand(),调用 parseSlashCommand() 提取命令名和参数:

// source/src/utils/processUserInput/processSlashCommand.tsx,第 309-330 行
export async function processSlashCommand(
inputString: string,
...
): Promise<ProcessUserInputBaseResult> {
// 解析 "/pr-review 关注性能问题" → { commandName: 'pr-review', args: '关注性能问题' }
const parsed = parseSlashCommand(inputString)
if (!parsed) {
return { messages: [...], shouldQuery: false, resultText: 'Commands are in the form `/command [args]`' }
}

const { commandName, args: parsedArgs, isMcp } = parsed

// 检查命令是否存在
if (!hasCommand(commandName, context.options.commands)) {
// 如果像一个命令名(不是文件路径),报告未知技能错误
if (looksLikeCommand(commandName) && !isFilePath) {
return { ..., resultText: `Unknown skill: ${commandName}` }
}
// 否则当作普通输入处理(用户可能在输入 /path/to/file)
return { messages: [createUserMessage(inputString)], shouldQuery: true }
}

// 命令存在,继续处理
const { messages, ... } = await getMessagesForSlashCommand(commandName, parsedArgs, ...)
}

关键逻辑:如果命令名看起来像文件路径(/etc/hosts),系统不会报错,而是当作普通用户输入处理。只有当输入符合命令格式但命令不存在时,才报告"Unknown skill"。

第二步:命令查找与类型分发

findCommand() 在所有已注册的命令中查找匹配项。找到后,根据 command.type 分支处理:

// source/src/types/command.ts 中的命令类型定义
type Command = PromptCommand | LocalCommand | LocalJSXCommand
// ^技能类型 ^本地JS执行 ^带UI的本地命令

对于技能(type === 'prompt'),进入 getMessagesForPromptSlashCommand()

第三步:getPromptForCommand 获取提示词内容

这是技能执行的核心:

// source/src/utils/processUserInput/processSlashCommand.tsx,第 869 行
const result = await command.getPromptForCommand(args, context)

不同类型的技能,getPromptForCommand 的实现不同:

文件型技能loadedFrom === 'skills'):

async getPromptForCommand(args, toolUseContext) {
// 1. 前缀 Base directory 信息
let finalContent = baseDir
? `Base directory for this skill: ${baseDir}\n\n${markdownContent}`
: markdownContent

// 2. 参数替换
finalContent = substituteArguments(finalContent, args, true, argumentNames)

// 3. 替换特殊变量
finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir)
finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, getSessionId())

// 4. 执行内联 shell 命令(MCP 技能跳过此步)
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, ...)
}

return [{ type: 'text', text: finalContent }]
},

内置技能loadedFrom === 'bundled'):

async getPromptForCommand(args, context) {
// 可以访问运行时上下文:消息历史、会话状态等
const messages = context.getAppState().messages
const sessionMemory = await getSessionMemoryContent(...)
// 动态生成提示词
return [{ type: 'text', text: buildPrompt(args, messages, sessionMemory) }]
},

第四步:参数替换——$ARGUMENTS 占位符

substituteArguments() 支持多种参数占位符格式:

// source/src/utils/argumentSubstitution.ts

// 支持的占位符格式:
// $ARGUMENTS → 完整参数字符串
// $ARGUMENTS[0] → 第一个参数(按空格分隔)
// $0, $1, $2 → 短格式索引参数
// $name, $target → 命名参数(需在 frontmatter 定义 arguments: name target)

示例

技能 front matter 定义了命名参数:

---
arguments: env branch
description: Deploy to environment
---
Deploy $branch to $env environment.
If no environment specified, deploy to staging.

调用 /deploy production main 后,提示词变为:

Deploy main to production environment.
If no environment specified, deploy to staging.

如果技能文件没有任何 $ARGUMENTS 占位符,但用户提供了参数,系统会自动在提示词末尾追加:

ARGUMENTS: production main

这确保用户的参数不会被默默丢弃。

第五步:内联 Shell 命令执行

SKILL.md 支持通过 !`` `` 语法在提示词加载时执行 shell 命令,并将输出嵌入提示词中:

---
description: 代码审查
---

# 代码审查

当前分支: !`git branch --show-current`
最近提交: !`git log --oneline -5`
文件变更数: !`git diff --stat | tail -1`

请审查以下变更...

当技能被调用时,这些命令会立即执行,输出会替换对应的语法块。Claude 收到的提示词看起来是:

当前分支: feature/auth-improvement
最近提交: abc123 Add OAuth support
def456 Fix login bug
...
文件变更数: 12 files changed, 234 insertions(+), 45 deletions(-)

请审查以下变更...

注意:这一步仅对非 MCP 技能执行。MCP 技能因安全原因跳过内联命令执行。

第六步:构建对话消息

技能提示词最终被构建成一组结构化的消息,注入到对话上下文:

// source/src/utils/processUserInput/processSlashCommand.tsx,第 886-918 行

// 1. 元数据消息:标识这是一个技能调用
const metadata = formatCommandLoadingMetadata(command, args)
// → "<command-message>pr-review</command-message>\n<command-name>/pr-review</command-name>"

// 2. 主内容消息(技能提示词 + 用户图片等)
const mainMessageContent = [...imageContentBlocks, ...result]

// 3. @-mention 附件消息(技能内容中可能引用了 @file.ts)
const attachmentMessages = await toArray(getAttachmentMessages(...))

// 4. 工具权限消息:扩展当前会话的 allowedTools
const additionalAllowedTools = parseToolListFromCLI(command.allowedTools ?? [])

const messages = [
createUserMessage({ content: metadata }), // 元数据
createUserMessage({ content: mainMessageContent, isMeta: true }), // 提示词
...attachmentMessages, // 附件
createAttachmentMessage({ // 权限扩展
type: 'command_permissions',
allowedTools: additionalAllowedTools,
model: command.model,
}),
]

command_permissions 消息的作用是在这次技能调用期间临时扩展工具权限。例如一个 /deploy 技能声明了 allowed-tools: Bash,用户在不允许 Bash 的上下文中调用它时,这条消息会临时授权 Bash 工具的使用。

第七步:技能 hooks 注册

如果技能的 front matter 定义了 hooks,它们会在技能调用时被注册到当前会话:

// source/src/utils/processUserInput/processSlashCommand.tsx,第 871-878 行
const hooksAllowedForThisSkill =
!isRestrictedToPluginOnly('hooks') || isSourceAdminTrusted(command.source)

if (command.hooks && hooksAllowedForThisSkill) {
const sessionId = getSessionId()
registerSkillHooks(
context.setAppState,
sessionId,
command.hooks,
command.name,
command.skillRoot,
)
}

这允许技能声明它自己的 pre/post-tool hooks,例如在每次工具调用后自动记录日志,或在特定条件下中止操作。

技能执行 vs 普通对话的差异

维度普通对话技能调用
进入 query() 的消息结构单条 user message多条:元数据 + 提示词 + 附件 + 权限
allowedTools继承会话默认设置可通过 command_permissions 临时扩展
model继承会话设置可通过 front matter 覆盖(model: claude-opus-4-5
effort 级别继承会话设置可通过 front matter 覆盖
hooks无额外注册技能可声明专属 hooks
参数处理用户手动表达$ARGUMENTS 占位符自动替换
shell 预处理!`` `` 语法内联执行(本地技能)
技能记录addInvokedSkill() 记录到 compaction 系统

技能的 fork 执行模式

部分技能通过设置 context: fork子代理中运行,而非内联到当前对话:

---
description: 在子代理中运行的复杂任务
context: fork
agent: general-purpose
---

executionContext === 'fork' 时,技能不是将提示词注入当前对话,而是创建一个独立的子代理(通过 forkedAgent.ts),子代理有自己的对话历史、工具权限和 token 预算。执行完成后,结果会返回给父代理。

这种模式适合复杂、长耗时的任务,避免消耗父代理的 context window 预算。

📄source/src/utils/processUserInput/processSlashCommand.tsxL827-920查看源码 →