博主头像
MoYan's Blog

前进,不择手段的前进!

Claude Code 核心 Agent Loop 与提示词构建分析报告

范围与结论先行

这份报告基于 restored-src 中已读源码,对两个核心问题做源码级分析:

  1. 主线程 Agent Loop 是如何运行的
  2. 系统提示词、工具提示词、上下文与缓存前缀是如何拼装出来的

最重要的结论有三个:

  • 真正的主循环在 src/query.tsqueryLoop(),不是在 main.tsx main.tsx 负责启动、装配和把参数送进 query()query() 本身只是包了一层生命周期收尾;真正跨轮维护状态、调用模型、执行工具、决定继续/终止的地方是 queryLoop()
  • 提示词不是单一字符串,而是一组有层次、可缓存、可覆写的 prompt parts。 src/constants/prompts.ts 负责构造默认系统提示词片段,src/utils/systemPrompt.ts 负责按优先级决定最终有效系统提示词,src/utils/queryContext.ts 负责把 systemPrompt + userContext + systemContext 作为 API 缓存前缀的核心三元组取出来。
  • 子代理不是另一套完全独立的架构,而是对同一个 query() 引擎的复用。 src/tools/AgentTool/runAgent.tsresumeAgent.tsforkSubagent.ts 都会重新准备自己的 messages/systemPrompt/userContext/systemContext/toolUseContext,然后再次调用 query()。所以 Claude Code 的核心 loop 是“一个主引擎 + 多种调用方”的结构,而不是每种 agent 各写一套 loop。

1. 架构总览:谁负责什么

1.1 src/main.tsx:启动、装配、入口编排

src/main.tsx 不是模型推理循环本身,而是主线程运行时装配器。它负责:

  • 加载命令与 agent 定义。
  • 解析 CLI 传入的 agent、model、system prompt、append prompt 等设置。
  • 决定主线程 agent 是谁,以及是否要把 agent 的 initialPromptgetSystemPrompt() 注入本轮。
  • 为交互式与非交互式会话准备不同的启动路径。

关键片段:

  • src/main.tsx:2025-2052:加载 commands 与 agent definitions。
  • src/main.tsx:2054-2065:解析主线程 agent 并写入 bootstrap state。
  • src/main.tsx:2082-2089:非交互模式下,如果指定了 custom agent,会直接采用 agent 的 system prompt。
  • src/main.tsx:2091-2103:如果 agent 带 initialPrompt,会被前置到用户输入前面。
  • src/main.tsx:2140-2169:teammate / swarm 场景会把 agent prompt 作为 appendSystemPrompt 附加进去。
  • src/main.tsx:2194-2209:proactive / kairos 模式会继续在 appendSystemPrompt 里追加模式专用补充段。

这解释了为什么 main.tsx 必须很重:它不是“做推理”,而是在第一次真正调用 query() 前,把模型运行所需的环境条件全部拼好

1.2 src/query.ts:真正的核心 Agent Loop

src/query.ts 里导出两个关键生成器:

  • query():外层包装器。
  • queryLoop():核心循环。

query() 只做一件额外的事:在 queryLoop() 正常返回后,为已消费的 slash command UUID 发出 completed 生命周期通知。

关键片段:

  • src/query.ts:219-239query() 包装 queryLoop()
  • src/query.ts:241-260 及后续:queryLoop() 真正开始。
  • src/query.ts:203-217:定义 loop 之间共享的 State,包括:

    • messages
    • toolUseContext
    • autoCompactTracking
    • maxOutputTokensRecoveryCount
    • hasAttemptedReactiveCompact
    • maxOutputTokensOverride
    • pendingToolUseSummary
    • stopHookActive
    • turnCount
    • transition

这说明 Claude Code 的主循环不是“请求一次 API 然后结束”,而是一个多轮状态机:每轮都可能追加消息、执行工具、压缩上下文、恢复 max token 错误、继续下一轮,直到进入 terminal state。

1.3 src/query/stopHooks.ts:每轮结束后的收尾与后台副作用层

handleStopHooks() 不是主循环本身,而是每轮 assistant 回合结束后的收尾器。它承担了三类责任:

  1. 保存 cache-safe params,供后续 side question / background summarization / fork 共享缓存前缀。
  2. 触发后台副作用,如 prompt suggestion、extract memories、auto-dream、computer-use cleanup。
  3. 执行 stop hooks,并根据 hook 结果决定是否阻止继续下一轮。

关键片段:

  • src/query/stopHooks.ts:65-81handleStopHooks() 签名。
  • src/query/stopHooks.ts:84-98:构造 stopHookContext,并在主线程 / SDK 查询时保存 cache-safe params。
  • src/query/stopHooks.ts:133-157:bare mode 之外的后台副作用:prompt suggestion、extract memories、auto-dream。
  • src/query/stopHooks.ts:174-189:读取当前 permission mode,启动 executeStopHooks()

因此,stopHooks 更像是turn finalizer,而不是 loop driver。把它和 queryLoop() 混为一谈会导致误解:前者是“这一轮结束后怎么办”,后者是“下一轮要不要继续”。

1.4 src/tools/AgentTool/*:子代理运行时

AgentTool 目录不是普通工具目录,它自己就是一个“小型运行时”。

其中最关键的是 runAgent.ts

  • src/tools/AgentTool/runAgent.ts:700-714:创建 subagent 的 ToolUseContext
  • src/tools/AgentTool/runAgent.ts:721-730:暴露 cache-safe params 给后台 summarization / prompt cache 共享。
  • src/tools/AgentTool/runAgent.ts:748-757:直接再次调用 query()
  • src/tools/AgentTool/runAgent.ts:816-858:finally 中执行收尾:MCP cleanup、session hooks 清理、prompt cache tracking cleanup、file state 释放、todo entry 清理、后台 shell task kill、monitor MCP tasks kill。

这说明子代理不是主线程的“特殊分支”,而是把同一个 query engine 再跑一遍,只是换了一套 messages、prompt、tools、permissions、session metadata。


2. 主线程 Agent Loop 的详细调用链

可以把主线程调用链抽象成:

main.tsx
  -> 准备 commands / agents / model / system prompt 附加项 / toolUseContext 所需依赖
  -> QueryEngine / REPL / print 等调用 query()
query()
  -> queryLoop()
     -> 准备本轮请求配置
     -> 调用 API / 处理流式输出
     -> 如 assistant 产生 tool_use,则 runTools()
     -> 将 tool_result 追加回 messages
     -> handleStopHooks()
     -> 根据 transition / token budget / max turns / stop conditions 决定继续或终止

几个关键点:

2.1 query() 只是包装器

src/query.ts:219-239 很清楚:query() 只是在 yield* queryLoop(...) 前后包了一层命令生命周期通知。不能把 query() 本身当成 loop 逻辑中心

2.2 状态跨轮持久化靠 State

State 是 loop 的“工作内存”,而不是 React state。它把多轮交互里必须持续存在的信息留在 query engine 里,例如:

  • 当前消息序列。
  • 工具执行后的 toolUseContext 更新。
  • auto-compact / reactive compact 的跟踪状态。
  • max token 错误恢复计数。
  • 上一轮为何继续(transition)。

设计动机很明显:Claude Code 要支持“assistant 调工具 -> 工具回结果 -> assistant 再推理 -> 再调工具”的多阶段 trajectory,所以不能只靠一次 API 请求的局部变量。

2.3 工具执行不在 query 文件内硬编码,而是下放给 runTools()

src/query.ts 会在 assistant 输出 tool_use blocks 后,把它们送到 runTools()。真正的并发 / 串行策略在:

  • src/services/tools/toolOrchestration.ts

关键实现:

  • runTools() 会先 partitionToolCalls(),把工具调用划分成:

    • 并发安全的只读批次
    • 必须串行的非只读批次
  • src/services/tools/toolOrchestration.ts:26-30:按并发安全性分批。
  • src/services/tools/toolOrchestration.ts:35-63:只读批次并发执行,并延迟应用 context modifiers。
  • src/services/tools/toolOrchestration.ts:65-80:非只读批次串行执行。

这背后的原理是:

  • 只读工具可以并发,提升吞吐。
  • 会修改上下文或外部状态的工具必须串行,否则 ToolUseContext 和外部副作用会乱序。
  • 即便并发执行,也不能立刻乱改共享 context,所以 context modifier 被收集后再按 tool_use 顺序应用。

这不是普通“Promise.all 跑工具”,而是受上下文一致性约束的 agentic tool scheduler

2.4 stop hooks 发生在每轮收尾,而不是每次工具调用后

handleStopHooks() 的输入是:

  • messagesForQuery
  • assistantMessages
  • systemPrompt
  • userContext
  • systemContext
  • toolUseContext

也就是说,它处理的是整轮结束后的上下文快照,而不是某个单独工具调用的局部事件。这也是为什么它适合做:

  • cache-safe params 快照
  • prompt suggestion
  • extract memories
  • auto-dream
  • stop hook 执行

因为这些逻辑需要看到“本轮完整 assistant trajectory”,而不是某个工具的局部结果。


3. 子代理 Loop:和主线程是同引擎复用,不是另一套系统

3.1 runAgent.ts 直接复用 query()

src/tools/AgentTool/runAgent.ts:748-757 是全报告最关键的证据之一:

子代理最终也是:

for await (const message of query({...}))

这意味着:

  • 主线程 agent
  • fork 出来的自己
  • fresh subagent
  • 某些 swarm / in-process teammate 场景

本质上都共享同一个 query engine。

3.2 子代理的差异在“上下文准备”,不是 loop 内核

子代理路径主要做的不是“重写一套 loop”,而是先准备:

  • 专属 ToolUseContext
  • agent-specific system prompt
  • agent-specific messages
  • agent-specific session / transcript / cleanup hooks

例如:

  • src/tools/AgentTool/runAgent.ts:700-714:创建 subagent context。
  • src/tools/AgentTool/runAgent.ts:721-730:共享 cache-safe params。
  • src/tools/AgentTool/runAgent.ts:732-742:记录 sidechain transcript 和 metadata。

这说明 AgentTool 的本质是:在 query 引擎外面包一层“agent runtime adapter”

3.3 子代理为什么需要独立 finally cleanup

runAgent.ts:816-858 的 finally 特别长,原因很重要:子代理会带来大量“会泄漏的跨轮资源”,例如:

  • MCP servers
  • session hooks
  • prompt cache tracking state
  • cloned file state cache
  • transcript subdir mapping
  • AppState.todos 中 agent key
  • agent spawn 的 background shell tasks

也就是说,Claude Code 不是只关心“模型是否回答出来”,它还必须把subagent runtime 的副作用和资源占用清干净,否则长会话会积累大量隐性状态。


4. 提示词构建主链:从默认 prompt 到最终 effective system prompt

4.1 src/constants/prompts.ts:默认系统提示词的内容工厂

src/constants/prompts.ts 负责生产“默认 Claude Code 系统提示词”的各个 section。

从已读片段可以确定它至少负责:

  • hooks section
  • system reminders section
  • language section
  • output style section
  • MCP instructions section
  • simple intro / system / doing tasks / actions with care 等 section
  • tool 相关说明
  • memory prompt / proactive / brief / skill search 等 feature-gated 内容

更关键的是,它定义了:

  • SYSTEM_PROMPT_DYNAMIC_BOUNDARY

src/constants/prompts.ts:105-116 注释写得非常明确:

  • boundary 之前的内容可以走更稳定的缓存策略。
  • boundary 之后的内容包含 user/session-specific 动态部分,不应该按同样方式缓存。

这说明默认 system prompt 的设计目标不是“尽量写全”,而是在表达能力和 prompt cache 命中率之间做工程平衡

4.2 src/utils/systemPrompt.ts:最终 system prompt 的优先级决策器

buildEffectiveSystemPrompt() 定义了最终 system prompt 的优先级顺序:

  1. overrideSystemPrompt
  2. coordinator mode system prompt
  3. agent system prompt
  4. custom system prompt / default system prompt
  5. appendSystemPrompt 永远最后追加(override 除外)

关键片段:

  • src/utils/systemPrompt.ts:28-40:优先级说明。
  • src/utils/systemPrompt.ts:56-58:override 直接替换全部。
  • src/utils/systemPrompt.ts:62-75:coordinator mode 用 coordinator prompt 替换默认 prompt。
  • src/utils/systemPrompt.ts:77-83:主线程 agent prompt 的获取逻辑,built-in 与 custom agent 不同。
  • src/utils/systemPrompt.ts:99-113:proactive 模式下,agent prompt 不是替换默认,而是附加在默认 prompt 后面。
  • src/utils/systemPrompt.ts:115-122:常规情况下,agent prompt > custom prompt > default prompt,再加 append。

这里有一个很关键的工程思想:

Claude Code 把“默认系统提示词内容”与“最终采用哪套提示词”的决策分成两层。

这避免了 constants/prompts.ts 变成一个巨大的条件分支地狱,也让 fork / agent / coordinator / proactive 等模式可以复用同一套底层 prompt 片段。

4.3 src/utils/queryContext.ts:把 prompt 变成可缓存查询前缀

fetchSystemPromptParts() 会取出三样关键东西:

  • defaultSystemPrompt
  • userContext
  • systemContext

代码在:

  • src/utils/queryContext.ts:44-74

而注释明确指出:

  • 这三样东西共同构成 query API cache-key prefix。
  • 如果设置了 customSystemPrompt,默认 prompt 与 getSystemContext() 会被跳过,因为 custom prompt 替代默认 prompt,systemContext 也不该再依附在一个不存在的默认 prompt 后面。

这是 Claude Code prompt 架构里最容易被忽略但非常重要的一点:

prompt 构建不是只为了“给模型看”,还是为了“让缓存策略可预测”。

4.4 side question / fork 为什么要复用这套前缀构建

buildSideQuestionFallbackParams()src/utils/queryContext.ts:88-179)的注释说明:

  • 在 stopHooks 还没来得及生成最新 snapshot 时,side question 也要重建近似一致的缓存前缀。
  • 它会尽量复刻 QueryEngine 里的 system prompt 组装方式,以维持 cache hit。

这进一步证明 Claude Code 的 prompt 不是一段散落在调用点的字符串,而是跨场景共享的结构化前缀协议


5. 工具提示词(Tool Prompt)如何嵌入整体提示词体系

5.1 工具提示词不是主系统提示词的一部分,而是工具 schema/description 的组成部分

src/tools/AgentTool/prompt.ts 为例,它负责生成 AgentTool 的描述文本。这里的 prompt 不是主 system prompt,而是给模型看的工具说明文本

关键实现:

  • src/tools/AgentTool/prompt.ts:66-70getPrompt(agentDefinitions, isCoordinator, allowedAgentTypes)
  • src/tools/AgentTool/prompt.ts:71-74:先按 allowedAgentTypes 过滤 agent 列表。
  • src/tools/AgentTool/prompt.ts:76-97:fork enabled 时加入 “When to fork” section。
  • src/tools/AgentTool/prompt.ts:99-113:如何写 agent prompt 的 section。
  • src/tools/AgentTool/prompt.ts:190-199:agent 列表可选择通过 attachment 注入,而不是直接内嵌进工具描述里。
  • src/tools/AgentTool/prompt.ts:190-194 的注释直接指出:动态 agent list 会让 tools-block prompt cache 失效,所以可切换为 attachment message 方案。

这背后的原理非常关键:

  • 主系统提示词负责定义整体行为边界与身份。
  • 工具提示词负责定义某个工具的使用契约。
  • 两者都影响模型行为,但缓存粒度和变更成本不同。

Claude Code 显然有意识地在控制“哪些动态信息可以进工具 description”,因为工具描述一旦变化,会影响 tools-block 缓存稳定性。

5.2 BashTool / AgentTool 这类工具尤其重要,因为它们的 prompt 近似“二级系统指令”

虽然本轮没有完整展开 BashTool/prompt.ts,但从 constants/prompts.ts 和已建立的 AGENTS 分析可以看出:

  • BashToolAgentTool 这种工具,它们的 prompt 不只是描述参数,而是承载了大量行为规范。
  • 这些工具 prompt 与主 system prompt 一起,构成了模型最终遵守的“多层合同”。

所以 Claude Code 的行为约束并不是只有一张 system prompt,而是:

系统提示词(全局行为)
 + 工具池及每个工具的描述/规则(局部行为)
 + 当前消息上下文
 + userContext / systemContext

6. 工具池是如何与提示词耦合的

6.1 assembleToolPool() 是工具可见性的源头之一

grep 结果显示:

  • src/tools.ts 导出 assembleToolPool()
  • AgentTool.tsxresumeAgent.tsREPL.tsxuseMergedTools.ts 都会调用它。

虽然本轮没有展开 src/tools.ts 的全文,但已有证据足够说明:

  • 工具池不是静态常量,而是根据权限上下文、MCP tools、feature gates、环境状态动态组装
  • 提示词构建必须与工具池同步,否则 system prompt 或 tool prompt 会告诉模型“你能做 X”,但 runtime 实际不给这个工具,造成 contract drift。

6.2 为什么 prompt 构建和 tool pool 必须联动

在 Claude Code 里,“提示词构建”和“工具池组装”不是两个独立问题,而是一致性问题:

  • getSystemPrompt() 会根据 tools、model、mcpClients 等构造系统提示词。
  • AgentTool/prompt.ts 会根据当前有效 agent 列表构造 tool description。
  • assembleToolPool() 决定哪些工具真的会暴露给模型。

所以整体原则是:

模型看到的描述,必须和 runtime 真正可执行的能力集尽量一致。

这也是为什么代码里到处都是 feature gates、dynamic import、cache boundary、attachment injection —— 都是在降低“模型可见契约”和“运行时真实能力”之间漂移的概率。


7. stop hooks、prompt suggestion、memory extraction 与主循环的关系

7.1 它们不是主循环的 driver,而是 turn-end side effects

handleStopHooks() 会:

  • 生成 stopHookContext
  • 保存 cache-safe params
  • 触发 prompt suggestion
  • 触发 extract memories
  • 触发 auto-dream
  • 执行 stop hooks
  • 做 computer-use cleanup

这些逻辑都发生在“assistant 一轮输出已经结束”之后。

因此必须明确区分:

  • 主循环 driverqueryLoop()
  • 收尾执行器handleStopHooks()
  • 后台副作用任务:prompt suggestion / extract memories / auto-dream / teammate idle / task completed hooks 等

如果把这些 side effects 说成“agent loop 的主逻辑”,就会误判系统行为:

  • 它们会影响下一轮上下文或用户体验。
  • 但它们不决定 assistant 如何流式生成当轮内容。

7.2 为什么 stopHooks 要保存 cache-safe params

src/query/stopHooks.ts:92-98 的设计非常关键:

  • 主线程和 SDK 查询会在 stopHooks 阶段保存 cache-safe params。
  • side question、btw、background summarization 等能力之后会读这个 snapshot。

这意味着 stopHooks 不只是“运行 hook”,它还是跨 turn / 跨调用方共享 prompt 前缀快照的检查点


8. 为什么 Claude Code 必须设计成这样

从源码能看出,这套架构不是风格偏好,而是被几个工程约束逼出来的。

8.1 启动性能压力

src/entrypoints/cli.tsxmain.tsx 大量使用 feature gate 与动态导入。原因很简单:

  • CLI 启动不能一上来把所有 agent/tool/service 都拖进来。
  • 但真正进入交互主循环时,又必须拿到完整能力集。

所以系统自然演化成:

  • 轻入口
  • 重装配
  • 统一 query engine

8.2 Prompt Cache 稳定性压力

SYSTEM_PROMPT_DYNAMIC_BOUNDARYqueryContext.ts 的 cache-key prefix、AgentTool/prompt.ts 的 agent-list attachment gate,都在说明同一个问题:

Prompt 一旦过于动态,缓存命中率就会被打穿。

因此 Claude Code 必须把:

  • 静态可缓存部分
  • 动态 session 部分
  • 工具描述中的高波动部分

尽量拆开。

8.3 多 agent / fork / teammate 复用压力

如果主线程和子代理各写一套 loop:

  • bug 修两份
  • compact / stop hook / tool orchestration / token budget 逻辑修两份
  • prompt cache 行为也更难保持一致

所以最合理的方式就是现在这样:

  • 一个 query engine
  • 多个 caller(main thread、subagent、fork、hook、swarm)

8.4 安全与权限边界压力

工具池、permission mode、BashTool、stop hooks、toolUseContext 都说明:

  • 这个系统不是“生成文本就完事”,而是能真实执行工具与副作用。
  • 因此模型可见 contract 与 runtime permission boundary 必须严格联动。

这也是为什么:

  • tool pool 要动态组装
  • tool prompt 要精细控制
  • stop hooks 要拿到 permission mode
  • subagent cleanup 要 kill background tasks

9. 关键文件索引

Loop 主链

  • src/main.tsx
  • src/query.ts
  • src/query/stopHooks.ts
  • src/services/tools/toolOrchestration.ts

Prompt 主链

  • src/constants/prompts.ts
  • src/utils/systemPrompt.ts
  • src/utils/queryContext.ts

子代理 / fork / agent runtime

  • src/tools/AgentTool/AgentTool.tsx
  • src/tools/AgentTool/runAgent.ts
  • src/tools/AgentTool/resumeAgent.ts
  • src/tools/AgentTool/forkSubagent.ts
  • src/tools/AgentTool/prompt.ts

其他 query() 调用方(证明 query engine 被复用)

grep 结果显示 query() 还会被以下位置调用:

  • src/QueryEngine.ts
  • src/utils/forkedAgent.ts
  • src/utils/hooks/execAgentHook.ts
  • src/utils/swarm/inProcessRunner.ts

这进一步印证:Claude Code 的核心 agentic runtime 不是“main.tsx 里的一段逻辑”,而是被多个调用方复用的统一 query engine


10. 最终总结

如果只用一句话概括 Claude Code 的核心实现原理,可以这样说:

Claude Code 把“多轮 agentic 推理 + 工具调度 + turn-end 副作用 + prompt/cache 前缀管理”收敛到了一个统一的 query() / queryLoop() 引擎里;而 main.tsxAgentTool、fork、swarm、SDK side-question 等只是围绕这个引擎准备不同的上下文、提示词与工具池。

再展开一点,就是四层:

  1. 入口装配层main.tsx
  2. 循环执行层query.ts
  3. 工具与回合收尾层toolOrchestration.ts + stopHooks.ts
  4. 提示词与缓存前缀层prompts.ts + systemPrompt.ts + queryContext.ts

而 Claude Code 之所以复杂,不是因为它只是个“聊天外壳”,而是因为它实际上在做三件彼此牵制的事:

  • 让 agent 足够自主
  • 让工具执行足够安全
  • 让 prompt/tool cache 足够稳定

这三件事同时成立,才逼出了今天这套架构。

Claude Code 核心 Agent Loop 与提示词构建分析报告
https://blog.moyanjdc.top/archives/20/
本文作者 MoYan
发布时间 2026-04-06
许可协议 CC BY-NC 4.0
发表新评论