2829 字
7 分钟
Claude Code 内置子命令深度解析:/compact、/simplify 与 /goal
2026-07-25

本文基于 Claude Code Best 和 Open-ClaudeCode,并对照 Anthropic 的 2.1.63 CHANGELOG 及类型定义。核心 CLI 没有完整公开,内部调用链以这两个复刻仓库为准。

选这三个命令,是因为它们分别代表三种执行方式:/compact 把输入交给本地函数,/simplify 把工作步骤写进模型提示词,/goal 把目标保存成跨轮次状态。三者都经过命令发现器,随后进入 local、prompt 和 local-jsx 协议。

一、命令发现器只负责找到入口#

复刻实现用 Command 联合类型描述命令的加载方式:

export type LocalCommandResult =
| { type: 'text'; value: string }
| { type: 'compact'; compactionResult: CompactionResult }
| { type: 'skip' }
type LocalCommand = {
type: 'local'
supportsNonInteractive: boolean
load: () => Promise<LocalCommandModule>
}
type LocalJSXCommand = {
type: 'local-jsx'
load: () => Promise<LocalJSXCommandModule>
}
export type PromptCommand = {
type: 'prompt'
source: SettingSource | 'builtin' | 'plugin' | 'bundled'
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}

类型定义来自复刻实现的 src/types/command.ts。三个命令的注册信息如下:

命令类型入口进入执行阶段后做什么
/compactlocalsrc/commands/compact/index.ts调用压缩函数,生成摘要并替换当前上下文
/simplifypromptsrc/skills/bundled/simplify.ts返回提示词,由主 Agent 按提示词调用工具
/goallocal-jsxsrc/commands/goal/index.ts更新目标状态,并决定是否继续下一轮查询

用户输入先进入命令发现器,发现器按 Command.type 把请求交给不同的执行入口:

flowchart TD A["用户输入 /command"] --> B["命令发现与懒加载"] B --> C{"Command.type"} C -->|local| D["/compact.call()"] C -->|prompt| E["/simplify.getPromptForCommand()"] C -->|local-jsx| F["/goal.call()"] D --> G["重写消息历史"] E --> H["主 Agent 按提示词调用工具"] F --> I["GoalState、GoalTool、idle hook"] I --> J["下一轮自动查询"]

compact 直接加入 COMMANDS 数组,goal 只有在 GOAL feature flag 打开时才会加入。bundled Skill 通过 registerBundledSkill() 注册,getSkills() 再把它和文件型 Skill、插件 Skill 合并。发现器只决定“这个名字对应哪个入口”,压缩、审查和续跑分别在后面的执行层完成。

二、/compact:模型生成摘要,代码替换历史#

2.1 local 命令的懒加载#

/compact 的注册对象只包含元数据和动态导入入口:

src/commands/compact/index.ts
const compact = {
type: 'local',
name: 'compact',
description:
'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]',
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
supportsNonInteractive: true,
argumentHint: '<optional custom summarization instructions>',
load: () => import('./compact.js'),
} satisfies Command

启动时先加载这份注册信息。用户真正执行 /compact 后,load() 才导入压缩模块。DISABLE_COMPACT 控制手动命令是否可用,自动压缩还要经过自己的配置和 feature flag 判断。

2.2 call() 先确定要压缩的消息#

src/commands/compact/compact.ts 的 call() 可以概括成下面的路径:

/compact args
-> getMessagesAfterCompactBoundary(messages)
-> 没有自定义指令时,尝试 session-memory compaction
-> reactive-only 模式走 reactiveCompactOnPromptTooLong
-> microcompactMessages(messages)
-> compactConversation(messagesForCompact, customInstructions)
-> 返回 { type: "compact", compactionResult }

getMessagesAfterCompactBoundary() 只取最近一个压缩边界之后的消息。没有可压缩消息时,命令直接返回错误。

microcompactMessages() 会先处理工具结果。启用 cached microcompact 且模型支持时,代码扫描可压缩的 tool_result,按消息分组记录,再通过 cache edit 删除旧结果。消息对象可以保持不变,删除动作延迟到 API 层执行。

flowchart LR A["当前消息"] --> B{"有可用的 microcompact 路径吗?"} B -->|是| C["删除或标记旧工具结果"] B -->|否| D["保留原消息"] C --> E["compactConversation"] D --> E E --> F["生成摘要"]

2.3 压缩提示词只负责生成交接摘要#

compactConversation() 先估算 preCompactTokenCount,执行 executePreCompactHooks(),再把 Hook 返回的指令和用户在 /compact 后输入的指令合并。用户指令排在前面。

getCompactPrompt() 对模型输出有明确约束:只输出文本,不调用工具,并用 <analysis> 和 <summary> 区分内部草稿与最终摘要。摘要要保留用户目标、文件名、函数签名、代码修改、错误修复和安全约束:

<analysis>
为交接摘要整理当前任务状态
</analysis>
<summary>
用户目标
关键文件和函数
已完成的修改与错误修复
未完成步骤
需要继续遵守的约束
</summary>

formatCompactSummary() 会移除 <analysis> 草稿,只把 <summary> 写回会话。摘要请求复用主会话的工具 Schema 时,模型一旦发起工具调用,这次请求就不会得到可用的摘要文本。

2.4 生成摘要后如何安装回会话#

streamCompactSummary() 优先调用 runForkedAgent(),复用主会话的系统提示词、工具和缓存前缀。失败或开关关闭时,代码改用 queryModelWithStreaming(),并做几项输入清理:

  • 图片和文档替换成 [image]、[document] 标记。
  • 移除压缩后会重新注入的 Skill attachment。
  • 禁用 thinking,只保留文本摘要。
  • 只允许读取类工具,并拒绝工具调用。

历史过长时,摘要请求本身也可能超出上下文窗口。代码按 API round 丢弃最早的一组消息后重试;仍然失败时返回 Conversation too long to summarize,提示用户使用 /clear。

摘要生成后,代码保存 readFileState,清理读取缓存,并构造恢复附件。这些附件可以包含最近读取的少量文件、仍在运行的 Agent、计划文件、plan mode 指令、已经调用过的 Skill,以及工具、Agent、MCP 指令的 delta。复刻实现对单文件、单 Skill、总附件 token 数和恢复文件数量都设有限制。

createCompactBoundaryMessage() 会生成一个 system / compact_boundary 消息,记录触发方式、压缩前 token 数和最后一条旧消息的 UUID。新历史按下面的顺序组装:

boundaryMarker
summaryMessages
messagesToKeep
attachments
hookResults

QueryEngine 和 SDK 层识别这个边界。原始 transcript 仍然存在,当前 API 看到的是摘要、保留的近期消息和恢复附件。

手动压缩、自动压缩和 Reactive Compact 都调用 compactConversation(),触发入口不同:

触发方式入口触发条件
手动 /compactcommands/compact/compact.ts用户输入命令,可以附加摘要指令
自动压缩services/compact/autoCompact.tstoken 阈值和配置满足条件
Reactive CompactreactiveCompactOnPromptTooLong()API 返回 prompt-too-long,实验开关开启

官方 SDK 类型把 session.compact() 的 trigger 暴露为 plugin,并允许 Hook veto。cached microcompact、reactive 模式和具体阈值属于版本实现细节。

三、/simplify:把审查流程放进提示词#

3.1 prompt 命令返回一段文本#

Anthropic 的 2.1.63 CHANGELOG 将 /simplify 列为 bundled slash command。在复刻实现中,它注册成一个 prompt 命令:

src/skills/bundled/simplify.ts
export function registerSimplifySkill(): void {
registerBundledSkill({
name: 'simplify',
description:
'Review changed code for reuse, quality, and efficiency, then fix any issues found.',
userInvocable: true,
async getPromptForCommand(args) {
let prompt = SIMPLIFY_PROMPT
if (args) prompt += `\n\n## Additional Focus\n\n${args}`
return [{ type: 'text', text: prompt }]
},
})
}

getPromptForCommand() 返回的是 ContentBlockParam[]。命令本身没有执行审查,也没有独立的 AST 分析器;主 Agent 收到这段文本后,才按照提示词调用 git、Agent、Edit 和 Write 等工具。

提示词的实际拼接方式如下:

SIMPLIFY_PROMPT
## Additional Focus
用户在 /simplify 后提供的参数

没有附加参数时,模型只收到 SIMPLIFY_PROMPT。有参数时,参数被追加到 ## Additional Focus 下,用来限定性能、API 兼容性或目录范围。

SIMPLIFY_PROMPT 的主体围绕三个阶段展开。下面保留阶段和审查项,省略了源码中的工具名与补充说明:

# Simplify: Code Review and Cleanup
审查所有变更文件,处理代码复用、质量和效率问题。
## Phase 1: Identify Changes
运行 git diff。存在暂存内容时运行 git diff HEAD。
没有 diff 时,审查用户点名或本轮修改过的文件。
## Phase 2: Launch Three Review Agents in Parallel
并行启动三个审查 Agent:
- Code Reuse Review
- Code Quality Review
- Efficiency Review
把完整 diff 交给每个 Agent,并等待三份结果。
## Phase 3: Fix Issues
合并审查结果,跳过误报,修复确认后的问题,并报告跳过的建议。

3.2 主 Agent 如何执行审查#

SIMPLIFY_PROMPT 要求主 Agent 先确定变更集合:工作树有暂存内容时使用 git diff HEAD,否则使用 git diff;工作树没有变化时,审查用户提到或本轮编辑过的文件。

确定 diff 后,主 Agent 在同一条模型响应中并发调用三个 Agent:

Agent检查内容
Code Reuse Review搜索已有 helper,检查重复函数、手写路径处理和重复类型守卫
Code Quality Review检查冗余状态、参数膨胀、复制粘贴、泄漏抽象、字符串类型和无意义 JSX 嵌套
Efficiency Review检查重复计算、串行执行、热路径膨胀、内存泄漏和过宽读取

三个调用返回后,主 Agent 合并发现,选择需要处理的问题并修改代码。误报可以跳过,但需要在最终摘要中说明。整个过程使用普通的权限、工作树和工具调用协议。

sequenceDiagram participant U as 用户 participant S as bundled Skill participant M as 主 Agent participant A as 三个 Agent 调用 participant T as Edit/Write 工具 U->>S: /simplify [additional focus] S->>M: 返回 SIMPLIFY_PROMPT M->>A: 并行发起三类审查 A-->>M: reuse / quality / efficiency findings M->>T: 选择并修复问题 T-->>M: 工具结果和权限检查 M-->>U: 修复摘要

这里的提示词决定审查顺序和关注点,代码修改仍由主 Agent 的工具调用完成。是否采纳某条建议,取决于模型对 diff、仓库搜索结果和误报的判断。

四、/goal:状态保存目标,idle hook 负责续跑#

4.1 local-jsx 命令处理目标操作#

/goal 的注册对象使用 local-jsx:

src/commands/goal/index.ts
const goal = {
type: 'local-jsx',
name: 'goal',
description: 'Set or view a persistent goal that drives auto-continuation across turns',
argumentHint: '[<objective> | status | clear | pause | resume | complete]',
bridgeSafe: false,
load: () => import('./goal.js'),
} satisfies Command

goal.tsx 把输入转换成状态操作:

输入操作
/goal <objective>创建或替换目标
/goal 或 /goal status显示目标、状态、耗时、token 和续跑轮数
/goal pause / resume暂停或恢复自动续跑
/goal continue达到最大轮数后,由用户明确继续
/goal complete手动完成目标
/goal clear清除目标并写入 tombstone

命令完成回调可以设置 shouldQuery: true,也可以插入隐藏的 metaMessages。设置新目标时,源码会插入 <goal-objective-updated> 元消息,UI 显示截断后的摘要。

4.2 GoalState 保存跨轮次数据#

状态机位于复刻实现的 src/services/goal/goalState.ts,按 session ID 存在 Map<string, GoalState> 中:

type GoalState = {
objective: string
status: 'active' | 'paused' | 'blocked' | 'budget_limited'
| 'usage_limited' | 'max_turns' | 'complete'
tokenBudget: number | null
tokensUsed: number
startTime: number
pausedAt: number | null
accumulatedActiveMs: number
blockedAttempts: number
lastBlockReason: string | null
turnsExecuted: number
}

状态更新有几条规则:

  • pause 会把当前 active 时间累加到 accumulatedActiveMs,恢复时重新设置 startTime。
  • cost tracker 收到 API usage 后更新 token 计数,达到 tokenBudget 就进入 budget_limited。
  • 相同原因的 blocked 报告连续三次后才进入 blocked,原因变化会重置计数。
  • 自动续跑最多执行 MAX_GOAL_TURNS = 150 轮,达到上限后进入 max_turns,需要 /goal continue 才能继续。

goalStorage.ts 每次状态变化都会向当前会话的 JSONL transcript 写入 goal checkpoint。/goal clear 追加 goal-cleared tombstone,恢复会话时,读取器按 session ID 取最后一个状态。

4.3 GoalTool 报告完成和阻塞#

模型通过 GoalTool.ts 读取目标或报告状态,输入 schema 是:

packages/builtin-tools/src/tools/GoalTool/GoalTool.ts
z.strictObject({
action: z.enum(['get', 'update']).optional(),
status: z.enum(['complete', 'blocked']).optional(),
reason: z.string().optional(),
})

get 返回当前快照。update 只能把目标标记为 complete 或 blocked。完成时工具记录 token、有效耗时和续跑轮数,再调用 completeGoal();阻塞时调用 recordBlockedAttempt()。目标状态由应用代码保存,模型只能通过这个 schema 约束的工具修改它。

4.4 两层提示词把目标带入下一轮#

Goal 有两类模型上下文。

第一类是每轮都可以看到的目标摘要。它把状态、耗时、token、预算和轮数放进 XML:

<active-goal status="active" elapsed="4m 12s" tokens="18400" budget="50000" turns="3">
为 monorepo 添加可回滚的数据库迁移并补齐测试
</active-goal>

第二类是 idle hook 注入的 continuation meta message。buildContinuationPrompt() 要求模型保持原始目标范围,完成前逐项寻找测试、文件内容或命令结果等证据;遇到阻塞时继续尝试,只有相同原因连续三轮才标记 blocked;完成或确认阻塞后,通过 GoalTool 更新状态。

续跑提示词的主要内容如下:

<goal-steering type="continuation">
当前目标:{objective}
已用 token:{tokensUsed}
token 预算:{tokenBudget}
保持目标范围。
完成前执行 Completion Audit,寻找可验证证据。
对相同阻塞原因进行连续计数。
完成或确认阻塞后调用 GoalTool 更新状态。
</goal-steering>

目标文本会经过 escape_xml_text,防止 <、> 和 & 破坏 XML 结构。

4.5 idle hook 决定是否自动发起下一轮#

useGoalContinuation() 在 isLoading 从 true 变成 false 时检查续跑条件:

一轮查询结束
-> 检查用户队列、活动 JSX、plan mode 和 abort 状态
-> 读取 GoalState
-> budget_limited: 注入一次收尾提示
-> active 且未到 MAX_GOAL_TURNS: turnsExecuted += 1
-> enqueue(<goal-steering type="continuation">...</goal-steering>)
-> 下一轮正常 query

队列里有 /goal pause 或普通用户消息时,Hook 会跳过本次自动续跑。目标进入 complete、blocked、max_turns 或资源受限状态后,也不会继续创建实质性工作。

五、三个命令分别控制什么#

维度/compact/simplify/goal
Command 类型localpromptlocal-jsx
处理对象当前会话的消息历史当前任务的审查流程跨轮次目标和生命周期
主要执行者压缩函数和摘要模型主 Agent 与三个审查 AgentGoalState、GoalTool、idle hook
提示词作用规定摘要输出格式和保留内容规定审查顺序与检查项规定续跑条件和完成证据
状态写入compact boundary 和新历史普通文件修改JSONL goal checkpoint
停止条件摘要失败或上下文仍超限主 Agent 跳过误报或完成修改complete、blocked、预算和轮数限制

这三个实现展示了三种不同的扩展方式:

  • local 命令把用户输入交给确定的本地函数,函数可以改写会话历史。
  • prompt 命令只增加模型上下文,后续动作由模型通过普通工具完成。
  • local-jsx 命令维护应用状态,再用提示词和 idle hook 把状态带进下一轮。

命令菜单里的名字不能直接说明运行时行为。要判断一个子命令会做什么,需要继续看它注册的 Command.type、懒加载模块、提示词注入位置和状态写入点。

参考源码#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

赞助
Claude Code 内置子命令深度解析:/compact、/simplify 与 /goal
https://blog.souloss.cn/posts/ai/agents-decoded/claude-code/claude-code-built-in-commands-compact-simplify-goal/
作者
Souloss
发布于
2026-07-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时