Claude Code 源码解析(一):整体架构与主链路

Claude Code 是 Anthropic 官方的终端 Coding Agent。本文基于 claude-code-main 源码快照,梳理其整体架构、主交互链路与 Message 类型系统,为后续深入 query 循环、Tool 实现、上下文压缩等专题打基础。

仓库概览

仓库约 1900 个文件,核心逻辑集中在 src/

目录/文件职责
entrypoints/cli.tsxmain.tsxCLI 入口
screens/REPL.tsx主交互界面(Read-Eval-Print-Loop)
query.ts / QueryEngine.tsAgent 主循环(API → Tool → 再 API)
utils/messages.tsMessage 构造、流式事件处理
services/api/claude.tsAnthropic API 流式调用
tools/各 Tool 实现(Bash、Edit、Agent 等)
components/Ink 终端 UI 组件
types/message.jsMessage 联合类型定义(快照中可能缺失,但被全仓库引用)

启动链路:

1
cli.tsx → main.tsx → replLauncher → App + REPL

用户输入经 handlePromptSubmitprocessUserInputonQueryquery() 进入 Agent 循环。


REPL:一切交互的枢纽

REPL.tsx 是 Claude Code 的「大脑皮层」——负责 UI 渲染、用户输入、状态管理,以及把用户动作桥接到无 UI 的 query/tool 层。

关键函数关系:

1
2
3
4
5
handlePromptSubmit
└─ processUserInput // 路由:文本 / slash / bash / skill
└─ onQuery // 防并发(queryGuard),更新 messages
└─ onQueryImpl // 组装 context、system prompt,调用 query()
└─ query() // async generator,Agent 主循环

onQueryonQueryImpl

  • onQuery(约 2855 行):对外入口。先把 newMessages append 到 state,再通过 queryGuard 防止并发 query;委托 onQueryImpl 执行。
  • onQueryImpl(约 2661 行):真正跑 query 前的准备:
    • 写入 slash-command 的 allowedTools 到 store
    • 调用 getToolUseContext 组装上下文
    • 拉取 systemPrompt / userContext / systemContext
    • 调用 query() 并用 onQueryEvent 消费流式事件

onQueryEventhandleMessageFromStream

onQueryEvent(约 2584 行)是薄封装,核心逻辑在 utils/messages.tshandleMessageFromStream(约 2930 行):

  • stream_event:更新 streamingTextstreamingToolUses、spinner 状态,不落库
  • 完整 messageassistant / user / system 等):onMessage 写入 messages 列表
  • tombstone:删除流式 fallback 产生的半条 assistant message
  • stream_request_start:切换 spinner 为 requesting

Agent 主循环:query() / queryLoop

query.ts 中的 query() 是一个 async generator,实现经典的 Agent 循环:

1
2
3
4
5
6
7
while (needsFollowUp) {
1. 上下文预处理(snip → microcompact → context collapse → autocompact)
2. 调用 API(流式)
3. 解析 assistant message(text / thinking / tool_use)
4. 执行 tool,收集 tool_result
5. 若有 tool_use → needsFollowUp = true,回到步骤 1
}

每一步通过 yield 向外推送 Messagestream_event,REPL 的 for await 消费这些事件更新 UI。

上下文管理(三步 + 兜底)

在每次 API 调用前,query 会对 messages 做阶梯式压缩:

  1. Snip:删除整条 message(如过长的 tool 结果)
  2. Microcompact:清空 tool 结果内容 / cache edit 标记
  3. Context collapse:对 span 做摘要,UI 与 API 各持一份视图
  4. Autocompact(兜底):整段对话 compact,插入 compact boundary

queryCheckpoint

utils/queryProfiler.ts 中的性能 profiling 标记。设置环境变量 CLAUDE_CODE_PROFILE_QUERY=1 启用,用于分析 query 各阶段耗时。


systemPrompt 与 systemContext 的区别

概念来源作用
systemPromptconstants/prompts.tsgetSystemPrompt()给模型的指令(你是谁、怎么用工具等)
systemContextcontext.tsgetSystemContext()环境信息(git 状态、目录结构等),append 到 system
userContextcontext.tsgetUserContext()CLAUDE.md 等用户规则,走 messages 而非 system

三者最终在 onQueryImpl 中组装,传入 query()


Message 类型系统

定义位置

Message 类型在 src/types/message.js 中定义(TypeScript 联合类型)。全仓库大量 import type { Message, UserMessage, AssistantMessage, ... } from '../types/message.js'

对外 SDK 侧还有平行定义:src/entrypoints/sdk/coreSchemas.tsSDKMessageSchematype: z.literal('assistant') / 'user' 等)。

核心结论:Message.type 不是 LLM 输出的

LLM/API 给的是 role + content blocks;Claude Code 在代码里推断/构造成带 typeMessage 联合类型,再写入 REPL state 和 transcript。

Message.type来源说明
assistant代码收到 API 响应/流式 block 完成后包装
user代码真人输入、tool_result、meta 系统消息等
attachment纯代码IDE 上下文、queued command、memory 等
progress纯代码tool 执行进度(Bash/Sleep/Agent)
system纯代码警告、compact 边界、bridge 状态等
stream_event纯代码API SSE 中间事件,不是最终 message
tombstone纯代码流式 fallback 时标记删除半条 assistant

两层 type 不要混淆

层级字段来源
外层 envelopeMessage.type = 'user' / 'assistant' / 'system'Claude Code 代码
API messagemessage.message.type = 'message'Anthropic SDK 的 BetaMessage
Content blockcontent[].type = 'text' / 'tool_use' / 'thinking'LLM/API 生成

构造示例

assistant — 流式 API 在 content_block_stop 时包装(services/api/claude.ts):

1
2
3
4
5
6
7
const m: AssistantMessage = {
message: { ...partialMessage, content: normalizeContentFromAPI(...) },
type: 'assistant',
uuid: randomUUID(),
timestamp: new Date().toISOString(),
}
yield m

usercreateUserMessage()utils/messages.ts):

1
2
3
4
5
6
const m: UserMessage = {
type: 'user',
message: { role: 'user', content },
isMeta, // true → UI 隐藏、模型可见
toolUseResult, // 存在时语义上是 tool result,type 仍是 'user'
}

stream_event — 流式中间态,不落库:

1
yield { type: 'stream_event', event: part, ttftMs }

assistant ≠ thinking

一条 type: 'assistant' 的 message 可包含多种 content block:textthinkingtool_use 等。thinking 是 block 级别类型,不是 message 级别。

UI 渲染在 components/Message.tsx 中按外层 message.type 分支,再按 content block 类型细分组件。


getToolUseContext:上下文工厂

REPL.tsx 约 2392 行的 getToolUseContext 是 REPL 的上下文工厂函数——每次跑 query、执行 slash command、弹权限对话框时,都调用它组装 ProcessUserInputContext,把「当前会话状态 + 可调用的能力 + UI 回调」打包传给下游。

签名

1
2
3
4
5
6
getToolUseContext(
messages: MessageType[],
newMessages: MessageType[],
abortController: AbortController,
mainLoopModel: string,
): ProcessUserInputContext

返回类型 ProcessUserInputContext = ToolUseContext & LocalJSXCommandContext,贯穿 processUserInput → query() → runTools → slash commands / 权限 UI

核心设计:从 store 读最新状态

不从 useAppState() 闭包拿值,而是 store.getState() 实时读

1
2
3
4
5
6
7
const computeTools = () => {
const state = store.getState()
const assembled = assembleToolPool(state.toolPermissionContext, state.mcp.tools)
const merged = mergeAndFilterTools(combinedInitialTools, assembled, state.toolPermissionContext.mode)
if (!mainThreadAgentDefinition) return merged
return resolveAgentTools(mainThreadAgentDefinition, merged, false, true).resolvedTools
}

原因:

  1. MCP 异步连接:render 之后 MCP server 才连上,闭包里的 tools 会过期
  2. query 中途刷新options.refreshTools: computeTools 让 query 循环可再拉最新 tools
  3. 为 headless 循环预留:不依赖 React render 周期

返回对象结构

1. options — query / tool 运行配置

commands、tools、MCP clients/resources、thinkingConfig、agentDefinitions、customSystemPrompt 等。

2. 状态读写

  • getAppState: () => store.getState() — 纯读
  • setAppState — 改全局 app state
  • messages / setMessages — 当前 transcript
  • updateFileHistoryState / updateAttributionState — 带引用相等优化

3. UI / 交互回调

字段用途
setToolJSX本地 JSX command 弹窗
setStreamModespinner 状态
onCompactProgresscompact 时改 spinner
setInProgressToolUseIDs跟踪进行中的 tool
appendSystemMessage追加 UI-only system message
requestPrompthook 交互式 prompt

在链路中的位置

1
2
3
4
5
handlePromptSubmit → processUserInput → onQuery → onQueryImpl

getToolUseContext

query() / runTools

一句话:getToolUseContext 不是业务逻辑本身,而是 REPL 把 React/store/UI 能力注入到无 UI 的 query/tool 层的适配器


其他值得知道的细节

Agent Swarms 与排队

setMemberActive 仅标记 teammate active;不会因其他队友 busy 而排队。排队只看本实例的 queryGuard.isActive

setHaikuTitle

用 Haiku 小模型根据首条用户消息生成终端标题。会跳过 slash command 输出、bash 输入等合成 breadcrumb,等待真实用户 prose。

isMeta 用户消息

isMeta: true 的 user message:UI 隐藏、模型可见。用于系统注入、tool 调用记录等不需要展示给用户的内容。

Haiku 的用途

Haiku 是小模型,用于标题生成等轻量任务,不走主 Agent 循环。


数据流总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flowchart TB
Input[用户输入] --> Submit[handlePromptSubmit]
Submit --> Process[processUserInput]
Process --> OnQuery[onQuery]
OnQuery --> OnQueryImpl[onQueryImpl]
OnQueryImpl --> GetCtx[getToolUseContext]
GetCtx --> Query[query / queryLoop]
Query --> API[Anthropic API 流式]
API --> StreamEvent[stream_event 中间态]
StreamEvent --> Handle[handleMessageFromStream]
API --> Assistant[type: assistant 最终消息]
Assistant --> Handle
Query --> Tools[runTools]
Tools --> ToolResult[createUserMessage toolUseResult]
ToolResult --> Query
Handle --> REPL[REPL messages state]
REPL --> UI[Ink 终端渲染]

Message 构造流:

1
2
3
4
5
6
用户输入 ──→ createUserMessage() ──→ type: 'user'
API SSE ──→ stream_event (中间) ──→ handleMessageFromStream 更新 UI
──→ content_block_stop ──→ type: 'assistant' (最终)
Tool 执行 ──→ createUserMessage({ toolUseResult }) ──→ type: 'user'
──→ createProgressMessage() ──→ type: 'progress'
上下文注入 ──→ createAttachmentMessage() ──→ type: 'attachment'

后续阅读建议

  1. handleMessageFromStream 全文messages.ts:2930)— stream_event 如何转成 streamingText / 最终 message
  2. queryLoop + runTools — 一个具体 Tool 从 tool_use 到 tool_result 的完整路径
  3. services/api/claude.ts — SSE 解析与 content_block_stop 时机
  4. 上下文压缩 — snip / microcompact / context collapse / autocompact 的实现细节

小结

Claude Code 的架构可以概括为:

  • REPL 负责交互与状态;query 负责 Agent 循环;messages.ts 负责类型转换与流式处理
  • Message.type 是 Claude Code 自己的 transcript 类型,100% 由代码赋值;LLM 只生成 content block
  • getToolUseContext 是 REPL 与 query/tool 层之间的适配器,保证 tools/MCP/state 始终 fresh
  • 主链路就是:输入 → processUserInput → onQuery → query(API ↔ Tool 循环) → handleMessageFromStream → UI

下一篇可以深入 queryLoop 内部、Tool 注册与执行、以及上下文压缩的具体算法。