ClaudeCode 的执行机制:ReAct 与 Plan-and-Execute 在 TypeScript 源码中的实现

ClaudeCode 的执行机制:ReAct 与 Plan-and-Execute 在 TypeScript 源码中的实现

这篇文章继续沿着 claude-code 的 TypeScript 源码往下拆。

如果上一篇文章回答的是 ClaudeCode “怎么记住上下文”,那这篇文章回答的是另一个更核心的问题:

  1. ClaudeCode 到底是不是按 ReAct 在跑;
  2. 它说的 Plan-and-Execute,在源码里到底是怎么实现的;
  3. 什么时候会走默认的直接执行,什么时候会切到 plan mode;
  4. 它判断“该不该先规划”的依据到底是什么。

先说结论:

ClaudeCode 的默认执行内核是一个典型的 ReAct 式主循环:模型输出、发现 tool_use、执行工具、把 tool_result 追加回上下文、继续下一轮。

而所谓 Plan-and-Execute,并不是另一套完全独立的 agent runtime。它本质上是在同一个 ReAct 主循环上,再叠加一层 plan mode 权限模式 + plan file + 用户审批流 + 额外提示词/attachment

换句话说:

  • ReAct 是 ClaudeCode 的“基础发动机”;
  • Plan-and-Execute 是这个发动机上的“受控规划挡位”。

1. 先看大图:ClaudeCode 其实是“两层机制”

很多人第一次看 ClaudeCode,会以为:

  1. 简单任务走普通 agent;
  2. 复杂任务走另一个 planner executor 系统。

但源码里不是这么分的。

更准确地说,ClaudeCode 是:

  1. 永远有一个统一的 query() 主循环,负责采样模型、收工具调用、执行工具、继续下一轮;
  2. 是否处于 plan mode,只会改变这轮循环的约束、提示和权限,不会换掉底层循环本身。

所以它的真实结构更像这样:

用户请求
  -> query() 主循环
     -> 模型输出 assistant
     -> 如果出现 tool_use,则执行工具
     -> 把 tool_result 追加回上下文
     -> 继续下一轮

如果进入 plan mode:
  -> 还是同一个 query() 主循环
  -> 只是变成“只读探索 + 写 plan file + AskUserQuestion/ExitPlanMode”
  -> 计划获批后,退出 plan mode
  -> 再回到同一个 query() 主循环继续真正实施

这也是为什么,从工程实现上说,Plan-and-Execute 更像:

“ReAct loop + planning constraints + approval boundary”

而不是一个和普通执行完全分开的执行器。


2. ReAct:ClaudeCode 的默认执行内核

2.1 query() 就是主循环

ClaudeCode 最核心的执行循环在 src/query.ts

它先把当前运行态状态放进一个 state,然后直接进入一个无限循环:

let state: State = {
  messages: params.messages,
  toolUseContext: params.toolUseContext,
  ...
}

while (true) {
  let { toolUseContext } = state
  const { messages, turnCount, ... } = state
  ...
}

这段代码在 src/query.ts

这个结构本身就很像经典的 agent 主循环:

  1. 读取当前消息上下文;
  2. 调模型;
  3. 看模型有没有要用工具;
  4. 有工具就执行;
  5. 再带着新结果继续下一轮。

如果从模式上看,这就是一个非常标准的 ReAct runtime。


2.2 ClaudeCode 里的 ReAct,不是概念,而是“assistant -> tool_use -> tool_result -> next turn”

ReAct 的关键不是“模型会思考”,而是:

  1. 模型先给出当前轮响应;
  2. 如果响应里有工具调用,就进入行动;
  3. 把行动结果再喂回模型继续推理。

ClaudeCode 在流式消费模型输出时,会显式收集这轮 assistant 里出现的 tool_use block:

if (message.type === 'assistant') {
  assistantMessages.push(message)

  const msgToolUseBlocks = message.message.content.filter(
    content => content.type === 'tool_use',
  ) as ToolUseBlock[]

  if (msgToolUseBlocks.length > 0) {
    toolUseBlocks.push(...msgToolUseBlocks)
    needsFollowUp = true
  }
}

这里有两个很关键的点:

  1. 只要 assistant 消息里出现了 tool_use,就会把 needsFollowUp = true
  2. ClaudeCode 不依赖 API 返回的 stop_reason === 'tool_use',因为源码里明确写了这个值“不可靠”。

源码原注释非常直白:

// Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly.
// Set during streaming whenever a tool_use block arrives — the sole
// loop-exit signal.

这意味着 ClaudeCode 在工程上真正判断“这一轮是不是要继续 ReAct”的依据,不是 stop_reason,而是:

流里有没有真实到达 tool_use block。

这点很重要,因为它说明 ClaudeCode 的 ReAct continuation 是按消息内容判定的,不是按 API 元数据盲信的。


2.3 工具执行之后,结果会被重新塞回下一轮上下文

ClaudeCode 在拿到 toolUseBlocks 之后,会执行工具:

const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)

for await (const update of toolUpdates) {
  if (update.message) {
    yield update.message
    toolResults.push(
      ...normalizeMessagesForAPI(
        [update.message],
        toolUseContext.options.tools,
      ).filter(_ => _.type === 'user'),
    )
  }
}

这段做了两件事:

  1. 执行工具,产出工具结果;
  2. 把工具结果规范化成可以重新送回模型的消息,塞进 toolResults

然后在需要继续下一轮时,它会构造新的状态:

state = {
  ...state,
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  toolUseContext: updatedToolUseContext,
  turnCount: turnCount + 1,
  pendingToolUseSummary: nextPendingToolUseSummary,
}
continue

这就是最典型的 ReAct 闭环:

  1. messagesForQuery 是上一轮已有上下文;
  2. assistantMessages 是这轮模型刚刚输出的推理/工具调用;
  3. toolResults 是执行后的观察结果;
  4. 三者拼起来进入下一轮。

所以如果要用一句最工程化的话定义 ClaudeCode 的 ReAct:

ClaudeCode 把每一轮 assistant 产出的 tool_use 视作 continuation signal,执行后把 tool_result 追加回消息流,再进入下一轮 query()


2.4 什么时候会停下,不再继续 ReAct

如果这一轮没有出现工具调用,needsFollowUp 就会保持为 false

if (!needsFollowUp) {
  const lastMessage = assistantMessages.at(-1)
  ...
}

也就是说,ClaudeCode 默认会在下面这些情况下结束当前 ReAct 回合:

  1. 模型已经给出最终文本回答,没有再发 tool_use
  2. 达到了 maxTurns
  3. 被用户中断;
  4. 遇到模型错误、上下文溢出等异常恢复失败;
  5. 某些 hook 明确阻止 continuation。

所以 ClaudeCode 的 ReAct 不是“无限自动做事”,而是:

只要模型还在发工具调用,它就继续;一旦本轮没有工具调用,就把这轮当成完成。


2.5 ClaudeCode 什么时候会走 ReAct

答案其实很简单:

几乎所有正常的 ClaudeCode 任务,默认都在走 ReAct。

因为不管你是在:

  • 读代码;
  • 改代码;
  • 搜文件;
  • 跑命令;
  • 用 agent tool;

最后都还是走到同一个 query() 主循环里,由 assistant 消息决定要不要发工具调用。

所以 ReAct 不是 ClaudeCode 的“某种高级模式”,而是它的默认操作系统。


3. Plan-and-Execute:不是第二套引擎,而是 ReAct 上叠加的 planning 模式

3.1 Plan mode 的本质:切换 toolPermissionContext.mode

ClaudeCode 用 ToolPermissionContext 表示当前工具权限状态:

export type ToolPermissionContext = {
  mode: PermissionMode
  ...
  prePlanMode?: PermissionMode
}

这里的 prePlanMode 注释写得很清楚:

/** Stores the permission mode before model-initiated plan mode entry,
 * so it can be restored on exit */

也就是说,plan mode 不是一个抽象概念,而是一个真实的运行态权限模式:

  • 当前模式是 default / auto / bypassPermissions / plan 之一;
  • 一旦进入 plan,ClaudeCode 会记住进入前的模式;
  • 退出 plan mode 时再恢复回去。

这就是 Plan-and-Execute 的第一层实现基础。


3.2 手动进入 plan mode:/plan

用户可以直接通过 /plan 命令手动进入 plan mode。

src/commands/plan/plan.tsx 里的逻辑很直接:

if (currentMode !== 'plan') {
  handlePlanModeTransition(currentMode, 'plan')
  setAppState(prev => ({
    ...prev,
    toolPermissionContext: applyPermissionUpdate(
      prepareContextForPlanMode(prev.toolPermissionContext),
      {
        type: 'setMode',
        mode: 'plan',
        destination: 'session',
      },
    ),
  }))
  ...
}

这说明只要用户显式发 /plan

  1. 就会把当前 session 切到 plan
  2. 同时运行 prepareContextForPlanMode(),保存原模式并做必要的权限调整。

所以第一种进入 Plan-and-Execute 的方式非常明确:

用户显式要求规划。


3.3 模型主动进入 plan mode:EnterPlanModeTool

ClaudeCode 还允许模型自己调用 EnterPlanModeTool

src/tools/EnterPlanModeTool/EnterPlanModeTool.ts 的说明是:

return 'Requests permission to enter plan mode for complex tasks requiring exploration and design'

真正切模式的逻辑也是:

context.setAppState(prev => ({
  ...prev,
  toolPermissionContext: applyPermissionUpdate(
    prepareContextForPlanMode(prev.toolPermissionContext),
    { type: 'setMode', mode: 'plan', destination: 'session' },
  ),
}))

这个工具的返回结果还会进一步告诉模型:

Entered plan mode. You should now focus on exploring the codebase and
designing an implementation approach.

并且明确限制:

  1. 只读探索;
  2. 设计方案;
  3. 需要时用 AskUserQuestion
  4. 准备好后用 ExitPlanMode
  5. 不要先写代码或改文件

这说明第二种进入 Plan-and-Execute 的方式是:

模型认为当前任务足够复杂,主动请求进入 plan mode。


4. 模型什么时候会主动进入 plan mode

这部分是最关键的,因为用户真正关心的是:

ClaudeCode 凭什么判断“这次要先规划,不要直接干”?

答案其实直接写在 EnterPlanModeTool 的 prompt 里。

4.1 External 版本的判断标准:偏积极进入 plan mode

src/tools/EnterPlanModeTool/prompt.ts 里,external 版本写得很细。

它明确说:下面这些情况,任意一个成立 都应该优先考虑进入 plan mode:

  1. 新功能实现
  2. 存在多种合理实现路径
  3. 会修改现有行为或结构
  4. 涉及架构决策
  5. 很可能要跨 2-3 个以上文件
  6. 需求不清,需要先探索
  7. 用户偏好会影响实现选择

原文核心意思是:

Use this tool proactively when you're about to start a non-trivial
implementation task.

Prefer using EnterPlanMode for implementation tasks unless they're simple.
Use it when ANY of these conditions apply:
...

这相当于一种“宁可先对齐方案,也不要返工”的策略。


4.2 Ant 版本的判断标准:更保守,更偏“先做事”

同一个文件里,ant 版本明显更保守。

它强调的是:

  1. 只有当实现路径存在真正的重大歧义时才进入 plan mode;
  2. 如果可以合理推断出实现路径,就直接开始;
  3. 能问具体问题就别开完整 planning phase。

最关键的几句是:

Use this tool when a task has genuine ambiguity about the right approach...

Skip plan mode when you can reasonably infer the right approach:
- The task is straightforward even if it touches multiple files
- The user's request is specific enough that the implementation path is clear
...
When in doubt, prefer starting work and using AskUserQuestion
for specific questions over entering a full planning phase.

所以如果用一句话概括两种风格:

  • external:复杂实现任务,倾向先 plan;
  • ant:只有存在明显歧义和高返工风险时,才 plan;否则先做。

4.3 明确不该进入 plan mode 的情况

无论 external 还是 ant 版本,其实都明确列了“不该进入 plan mode”的任务类型:

  1. 小修小补;
  2. 明确、直接、实现路径显然的任务;
  3. 纯研究/纯阅读代码任务;
  4. 只是想知道某个实现位置,而不是要改代码。

比如 prompt 里直接举例:

  • Fix the typo in the README
  • Add a console.log to debug this function
  • What files handle routing?

这些都不应该触发 planning phase。

所以 ClaudeCode 的判断不是“复杂就一定 plan”,而是:

当任务的歧义、影响面、返工风险、用户偏好敏感度足够高时,才切 plan mode。


5. 进入 plan mode 后,模型实际会被怎样约束

5.1 plan mode 不只是一个 flag,还会注入额外 instructions

进入 plan mode 后,ClaudeCode 不是只把 mode 改成 plan 就完了。

它还会把 plan mode 指令注入到消息流里。

src/services/compact/compact.ts 里有一个很关键的函数:

export async function createPlanModeAttachmentIfNeeded(
  context: ToolUseContext,
): Promise {
  const appState = context.getAppState()
  if (appState.toolPermissionContext.mode !== 'plan') {
    return null
  }

  return createAttachmentMessage({
    type: 'plan_mode',
    reminderType: 'full',
    isSubAgent: !!context.agentId,
    planFilePath,
    planExists,
  })
}

注释也写得非常明确:

This ensures the model continues to operate in plan mode after compaction

也就是说,plan mode 不是只靠“之前模型还记得”来维持,而是:

ClaudeCode 会显式创建 plan_mode attachment,把规划约束重新灌给模型。


5.2 这些约束是怎么传给模型的

src/utils/messages.ts 会把 plan_mode attachment 变成真正发给模型的消息。

在 attachment 分发里:

case 'plan_mode': {
  return getPlanModeInstructions(attachment)
}

而这些 instructions 最终会被包装成:

createUserMessage({ content, isMeta: true })

并进一步包上:


...

对应工具函数是:

export function wrapInSystemReminder(content: string): string {
  return `\n${content}\n`
}

这说明在模型视角里,plan mode instruction 并不是一个神秘内部状态,而是:

  1. 一个 user 侧的 meta message;
  2. isMeta: true
  3. 内容被 <system-reminder> 包住;
  4. 明确告诉模型自己当前在 plan mode,哪些事能做,哪些事不能做。

5.3 plan mode 下实际要求模型怎么做

messages.ts 里,plan mode 的说明非常具体。

尤其是 interview 版 workflow,核心要求是:

Plan mode is active. The user indicated that they do not want you to
execute yet -- you MUST NOT make any edits (with the exception of the
plan file mentioned below), run any non-readonly tools ... or otherwise
make any changes to the system.

然后要求模型循环执行:

  1. Explore:只读地读代码;
  2. Update the plan file:把发现写进 plan file;
  3. Ask the user:当遇到无法仅靠代码决定的歧义时,问用户。

并且 turn 结束只能有两种方式:

  1. AskUserQuestion 继续澄清;
  2. ExitPlanMode 请求用户审批。

这段源码其实已经把 Plan-and-Execute 的“Plan”阶段定义得非常清楚了:

读代码、写计划、问用户,但不真正实施。


6. Plan-and-Execute 里的 “Execute” 是什么时候开始的

6.1 必须通过 ExitPlanMode

当计划准备好之后,模型必须调用 ExitPlanModeV2Tool

它的 prompt 直接写着:

Use this tool when you are in plan mode and have finished writing your
plan to the plan file and are ready for user approval.

并且特别强调:

  1. 研究任务不要调用;
  2. 只是理解代码不要调用;
  3. 只有在“已经写完计划,准备开始真正实施”时才调用。

这说明 ClaudeCode 的设计不是:

计划做完就自动开始执行

而是:

计划做完后,必须跨过一个明确的 approval boundary。


6.2 普通用户场景:需要本地确认

对普通非 teammate 场景,ExitPlanModeV2Tool 会要求用户确认:

requiresUserInteraction() {
  return true
}

checkPermissions(...) {
  return {
    behavior: 'ask',
    message: 'Exit plan mode?',
  }
}

也就是说:

  1. 模型先在 plan mode 里把方案写好;
  2. 通过 ExitPlanMode 请求退出;
  3. 用户确认后,ClaudeCode 才真正退出 planning phase。

6.3 teammate 场景:可能要求 team lead 审批

如果当前 session 是 teammate,并且带了 plan_mode_required,逻辑会更严格。

ExitPlanModeV2Tool.ts 里:

if (isTeammate() && isPlanModeRequired()) {
  ...
  const approvalRequest = {
    type: 'plan_approval_request',
    ...
  }
  await writeToMailbox('team-lead', ...)
  return {
    data: {
      awaitingLeaderApproval: true,
      ...
    },
  }
}

这说明对于 swarm/team 协作场景:

plan-and-execute 甚至可以不是“用户审批”,而是“leader 审批”。

--plan-mode-required 这个 CLI 参数,则是从运行时上强制该 teammate 必须先 plan 再 implement。

src/main.tsx 里也能看到这个选项:

program.addOption(
  new Option('--plan-mode-required', 'Require plan mode before implementation')
)

6.4 退出 plan mode 后,不是切到另一个 executor,而是回到原来的 ReAct 主循环

退出 plan mode 时,ClaudeCode 会恢复进入前的权限模式:

let restoreMode = prev.toolPermissionContext.prePlanMode ?? 'default'
...
toolPermissionContext: {
  ...baseContext,
  mode: restoreMode,
  prePlanMode: undefined,
}

同时还会注入一个 plan_mode_exit 提示:

You have exited plan mode. You can now make edits, run tools, and take actions.

所以 Execute 阶段本质上就是:

  1. 通过 ExitPlanMode 获得批准;
  2. 恢复到 default 或原来的 auto
  3. 再回到同一个 ReAct 主循环里继续正常做事。

这也是本文最重要的一个结论:

ClaudeCode 的 Plan-and-Execute 不是 “planner 线程 + executor 线程” 两套引擎,而是 “先在 ReAct 里受限规划,再在同一个 ReAct 里恢复执行”。


7. ClaudeCode 到底依据什么判断“该直接做,还是先 plan”

把前面的代码合起来,判断依据其实可以总结成四层。

7.1 第一层:显式用户意图

如果用户:

  • 手动敲了 /plan
  • 明确要求“先给方案”“先规划一下”“别急着改代码”;

那么 ClaudeCode 会直接进入 plan mode。

这是最硬的一层信号。


7.2 第二层:任务特征是否足够复杂/模糊

如果是模型主动判断,核心依据就是 EnterPlanModeTool prompt 里列出来的那几类任务特征:

  1. 新功能;
  2. 多种合理路径;
  3. 会改现有结构;
  4. 涉及架构抉择;
  5. 多文件改动;
  6. 需求不清,需要先探索;
  7. 用户偏好会显著影响实现方案。

也就是说,它不是拿“任务长度”判断,而是拿:

歧义度、影响面、结构性风险、返工成本

来判断。


7.3 第三层:当前模式本身也会影响判断

如果处于 auto mode,源码里还会额外注入一句很强的指导:

Prefer action over planning — Do not enter plan mode unless the user
explicitly asks. When in doubt, start coding.

这意味着在 auto mode 里,ClaudeCode 的默认倾向是:

  1. 先做;
  2. 少打断;
  3. 少开 plan mode;
  4. 除非用户明确要求,或者确实存在重大歧义。

所以“要不要 plan”不是静态判断,还会被当前 permission mode 改写。


7.4 第四层:协作/团队约束

如果是 teammate 且 plan_mode_required=true,那这个判断已经不是模型自由选择了,而是被运行环境强制:

必须先进 plan mode,拿到 approval 才能实施。

这种场景更像企业流程管控,而不是 agent 自己的自由判断。


8. ReAct 和 Plan-and-Execute 的关系,可以怎么理解

如果把这两套机制放在一起看,可以把 ClaudeCode 理解成下面这张图:

flowchart TD
    A["用户请求"] --> B["query() 主循环"]
    B --> C["模型输出 assistant"]
    C --> D{"是否出现 tool_use"}
    D -->|否| E["结束当前回合"]
    D -->|是| F["执行工具"]
    F --> G["把 tool_result 追加回 messages"]
    G --> B

    A --> H{"是否进入 plan mode"}
    H -->|否| B
    H -->|是| I["只读探索 + 写 plan file + AskUserQuestion"]
    I --> J["ExitPlanMode 请求审批"]
    J --> K{"审批通过?"}
    K -->|否| I
    K -->|是| B

这张图其实表达了一个很实用的工程事实:

  1. ReAct 是底层循环
  2. Plan-and-Execute 是这层循环上的 gating 和 workflow
  3. “Plan”阶段和“Execute”阶段,本质上都还是同一个 query() 在跑,只是上下文约束不同。

9. 扩展:ClaudeCode 里还有更重型的 planning 形态

除了本地 plan mode,ClaudeCode 还有一个更重型的 /ultraplan

src/commands/ultraplan.tsx 里直接写了:

Advanced multi-agent plan mode with our most powerful model

它会把 planning 放到 Claude Code on the web 侧远程跑,甚至支持更长时间、多 agent 的 planning。

不过从本文主题看,/ultraplan 更像是:

Plan-and-Execute 的远程增强版

而不是 ClaudeCode 日常默认的主执行机制。

日常主线还是本文前面分析的那套:

  • 普通请求:默认 ReAct;
  • 需要方案对齐:进入本地 plan mode;
  • 获批后:回到 ReAct 执行。

10. 最后总结

如果只记住三句话,我觉得最重要的是这三句:

  1. ClaudeCode 默认就是 ReAct。
    query.ts 的主循环本质上就是 assistant 产出 tool_use,系统执行工具,再把 tool_result 回灌给下一轮。

  2. Plan-and-Execute 不是第二套执行器。
    它是在同一个 ReAct runtime 上,加了一层 plan mode + plan file + approval flow + system reminder

  3. 什么时候进入 plan mode,取决于歧义、影响面、返工风险和显式用户意图。
    任务足够明确时,ClaudeCode 倾向直接做;任务存在重大路径分歧或需要先对齐方案时,ClaudeCode 才会切到 planning phase。

如果从 agent 工程角度看,这套设计其实很聪明:

  • 默认执行路径保持简单,永远只有一个主循环;
  • 复杂任务时,不需要切换到另一套“planner framework”;
  • 只要通过 permission mode 和 message injection,就能让同一个 runtime 同时支持“直接执行”和“先规划再执行”。

这也是 ClaudeCode 源码里很值得学的一点:

不要急着把 planning 和 execution 做成两套系统。很多时候,一个稳定的 ReAct 主循环,加上一层明确的 planning workflow,就已经足够强。


文章作者: 小风雷
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小风雷 !
评论
 本篇
ClaudeCode 的执行机制:ReAct 与 Plan-and-Execute 在 TypeScript 源码中的实现 ClaudeCode 的执行机制:ReAct 与 Plan-and-Execute 在 TypeScript 源码中的实现
基于 ClaudeCode TypeScript 源码,拆解它如何用 query 主循环实现 ReAct,如何通过 plan mode、plan file、approval flow 叠加出 Plan-and-Execute,以及这两套机制分别在什么条件下触发、判断依据是什么、如何持续传递给大模型。
2026-04-06
下一篇 
ClaudeCode 的记忆机制:短期记忆、长期记忆与模型输入全链路拆解 ClaudeCode 的记忆机制:短期记忆、长期记忆与模型输入全链路拆解
基于 ClaudeCode TypeScript 源码,系统拆解短期记忆、长期记忆、Session Memory、transcript JSONL、agent memory,以及这些记忆最终如何以 system prompt、meta user message、 等形态进入大模型。
2026-04-06
  目录