先看大图景:Claude Code 是“模型 + 本地编排层 + 安全约束”的组合体
如果你刚接触这个项目,或者你已经打开了很多源码文件但不知道它们彼此是什么关系,先读这一章。
这章的目标只有一个:让你先建立 Claude Code 的整体心智模型,再去看后面的 Prompt、工具、权限和上下文细节。
注:本章当前仍属于 Wave 1 draft。主链和关键锚点已经比较稳定,但 evidence 与段落映射还在继续补细。
如果要用一句话概括 Claude Code 的架构,可以这样理解:
一个持续运行的 agent loop,外面包着 prompt 组装、工具系统、权限系统、上下文管理和扩展层。
但这句话容易让人误解。很多人会以为"核心 loop 简单,所以整个系统也简单"——恰恰相反。
真正的情况是:
核心循环确实可以保持直接(调模型 → 执行工具 → 回传结果 → 继续),但正是因为它保持简单,外围的工程化设施才必须足够厚实。这些外围层不是"可有可无的包装",而是让这个简单循环能在真实环境里稳定运行的必要条件。
打个比方:一台汽车的核心是发动机,但如果没有刹车、方向盘、安全气囊、冷却系统,这台车就只是一个会爆炸的铁盒子。Claude Code 的设计哲学也是如此——让核心保持可理解,让约束在外围层生效。
模型负责理解需求、决定下一步做什么、生成文字或工具调用请求。
Claude Code 负责把模型放进一个可控的本地运行环境里。它会准备 prompt、提供工具、执行权限检查、管理上下文、渲染终端 UI。
文件系统、Shell、Git、MCP 服务器、IDE、网络等能力都在本地环境里。模型本身不能直接碰这些能力,必须通过 Claude Code 暴露出来的工具间接访问。
用户输入
↓
Claude Code 收集上下文
↓
Claude Code 组装 prompt 和工具列表
↓
模型返回:
- 直接文字
- 或工具调用请求
↓
Claude Code 判断能不能执行该工具
↓
执行工具,把结果回送给模型
↓
重复,直到模型返回最终文本
所以它不是“模型一次性回答”,而是“模型和本地运行时之间的多轮协作”。
终端 UI 层
React + Ink
负责输入、输出、流式渲染、权限弹窗、状态展示
CLI 与启动层
src/main.tsx
负责参数解析、初始化配置、装配运行时
对话编排层
src/QueryEngine.ts
负责一次会话中每一轮请求的状态管理和参数准备
执行循环层
src/query.ts
负责真正的模型调用、工具执行、结果回传循环
基础能力层
Prompt、Tools、Permissions、Context、Compaction、MCP
集成服务层
API、OAuth、LSP、Analytics、Remote、Plugins、Skills
| 文件 | 作用 | 为什么值得先看 |
|---|---|---|
src/main.tsx |
程序入口与启动装配 | 看懂 CLI 从哪开始把系统拼起来 |
src/QueryEngine.ts |
一轮会话请求的组织者 | 看懂一次 turn 是怎么被准备出来的 |
src/query.ts |
真正的 agent loop | 看懂“模型 -> 工具 -> 模型”的循环 |
src/constants/prompts.ts |
System Prompt 组装 | 看懂模型为什么会被约束成现在这样 |
src/tools.ts |
工具注册表 | 看懂 Claude Code 给模型开放了哪些能力 |
src/utils/permissions/* |
权限与安全控制 | 看懂为什么它不会直接乱执行 |
src/services/compact/* |
上下文压缩 | 看懂长对话为什么还能继续 |
src/services/mcp/* |
MCP 集成 | 看懂外部能力如何接进来 |
很多人第一次看 Agent 系统时,会以为流程是这样的:
用户输入 → 模型思考 → 返回结果 → 结束
但 Claude Code 的真实流程要复杂得多,也有趣得多。让我们跟随一次真实请求,看看它到底经历了什么。
当你第一次启动 Claude Code 时,src/main.tsx 做的不是”等你输入后再慢慢想”,而是立刻开始一系列并行准备工作。
为什么要这样设计?
因为很多依赖项的初始化非常耗时——读取配置文件、连接 MCP 服务器、初始化权限上下文、加载工具定义。如果等到用户输入后再做这些事,每次响应都会有明显延迟。
所以 Claude Code 的策略是:在用户还在思考要问什么的时候,系统已经把后续需要的东西准备好了。
具体来说,启动阶段会做这些事:
- 并行预热关键 I/O:MDM、keychain 这类系统级依赖会被提前触发,避免后续阻塞
- 裁定权限模式:不是等到真正执行工具时才临时决定,而是在启动时就根据 CLI 参数、配置文件、危险标志位归一化出最终的 permission mode
- 构建工具池:内置工具、Feature Flag 控制的实验性工具、MCP 工具,都在这个阶段完成注册
- 初始化外围能力:UI 渲染、Telemetry、LSP、Analytics 等模块也会在这时装配好
这不是过度优化,而是产品级 CLI 的必要准备。用户感知到的”启动即可用”,背后是这套并行预热机制在支撑。
当你输入一条消息后,系统不会直接把这句话扔给模型。src/QueryEngine.ts 会先做一轮精心的准备工作。
为什么需要这一层?
因为模型需要的不只是”用户说了什么”,还需要知道:
- 现在应该遵守什么规则(System Prompt)
- 当前项目有什么特殊要求(userContext)
- 运行环境是什么状态(systemContext)
- 有哪些工具可以调用(ToolUseContext)
- 之前的对话历史是什么
这些信息不是”顺手拼一下”就行的,而是需要明确的分层和优先级。
QueryEngine 做的关键决策:
- 确定本轮模型:用户显式指定的模型 > 会话级模型 > 默认模型
- 装配三类上下文:并发获取
systemPrompt、userContext、systemContext,而不是串行等待 - 包装权限检查:把
canUseTool包一层,变成wrappedCanUseTool,不只是判断允许/拒绝,还要记录哪些工具被拒绝了、参数是什么 - 维护会话状态:消息历史、文件状态缓存、token 用量、权限拒绝记录,这些都是会话级的持久状态
这一层的存在,让后续的核心循环可以专注于”调模型、执行工具、回传结果”,而不用操心”上下文从哪来、权限怎么判、状态怎么存”。
src/query.ts 是整个系统真正意义上的核心。但这里有一个很容易读错的细节:
query() 本身不是那条长期运转的循环,queryLoop() 才是。
这不是命名细节,而是理解整套系统的关键。query() 更像外层包装,负责暴露生成器接口、处理中断、管理流式输出。真正反复推进的是 queryLoop() 里的 while (true)。
循环的核心逻辑:
1. 整理消息
不是把内存里的所有消息原样发出去
而是先做上下文裁切、压缩视图整理、API 协议归一化
2. 调用模型
流式接收 assistant 输出
不是等整条响应结束才处理,而是边收边解析
3. 识别 tool_use
不是靠 API 的 stop_reason,而是直接扫描 assistant content block
这样更具体,也更稳定
4. 执行工具
找到对应工具定义 → 检查权限 → 真正执行 → 生成 tool_result
这不是隐式发生的,而是本地显式重建成下一轮的 user-side message
5. 回流结果
把 tool_result 追加回消息历史
再次调用模型,让它基于新信息继续决策
6. 判断继续/停止/压缩
不是简单的二叉判断,而是多层关卡串起来的结果
可能继续、可能停止、也可能触发上下文压缩后继续
为什么这套 loop 很关键?
因为 Claude Code 的绝大多数聪明行为,都不是 CLI 自己规划出来的,而是模型在每一轮拿到新上下文后临时做的决定。
所以这套 loop 的首要任务不是”替模型思考”,而是:
- 准确传递上下文
- 正确执行工具
- 稳定记录状态
- 在每一轮之间保持一致性
这一层你可以把它理解成三道护栏,它们不是”循环结束后才检查”,而是在循环的每一步都在发挥作用:
第一道护栏:工具系统
决定”能做什么”。模型看到的不是”电脑上所有能力”,而是一组经过精心设计的工具接口。每个工具都有:
- 清晰的描述(告诉模型什么时候该用它)
- 参数约束(限制输入形状)
- 权限语义(标记这是读操作还是写操作)
第二道护栏:权限系统
决定”现在允不允许做”。不是简单的 allow/deny,而是多层判定:
- 先看当前权限模式(plan/default/auto)
- 再看规则(deny/ask/allow)
- 再分析参数(路径是否安全、命令是否危险)
- 最后才决定是自动允许、提示确认,还是直接拒绝
第三道护栏:上下文系统
决定”模型这次能看到什么”。不是”把所有历史都发过去”,而是:
- 裁掉 compact 边界之前不需要的历史
- 处理过长的 tool result
- 结合 snip / microcompact / context collapse 等机制
- 在窗口接近上限前主动压缩
这三道护栏不是”循环外的附加模块”,而是循环内部持续参与的运行时机制。
表面上看,主流程像这样:
while (模型还在调工具) {
执行工具
把结果发回模型
}
但产品级 CLI 不能只停在这一步,因为现实里还要解决:
- 工具执行前是否有权限
- 命令是否危险
- 文件路径是否合法
- 上下文是否快溢出
- Prompt 是否会打爆缓存
- MCP 服务器会不会动态变化
- 多 Agent 怎么隔离上下文
- UI 如何实时显示每一步状态
所以 Claude Code 不是“算法复杂”,而是“工程边界条件复杂”。
再往细看一步,至少还有这些真正会改变主链行为的实现细节:
tool_use的识别不只靠 API 的stop_reason- 工具池不是简单等于
getTools() - permission mode 是启动时先裁定,再进入后续执行链
- auto compact 阈值会先预留 summary 输出空间,再决定何时主动压缩
Claude Code 没有一个很重的外部任务规划器去替模型做决策。它更像是在给模型搭一个受控工作台。
模型负责决策,不代表它拥有无限权力。Prompt、权限、沙箱、路径验证、模式切换,这些都由工程层硬性约束。
query.ts 的主循环仍然保持相对直接,这是一个非常重要的取舍。它让系统更容易调试,也更容易扩展。
这点在 Prompt Cache、上下文压缩、MCP 动态信息和工具池构建里都很明显。
Claude Code 不是先把结构写死,再让优化来将就;它会反过来调整信息放置位置,让系统更稳定。
如果你已经准备打开源码,建议按这个顺序:
src/main.tsxsrc/QueryEngine.tssrc/query.tssrc/constants/prompts.tssrc/tools.tssrc/utils/permissions/PermissionMode.tssrc/services/compact/autoCompact.tssrc/services/mcp/client.ts
不要一上来就钻进 utils/permissions 或 services/mcp 的深层文件,那样很容易迷路。
先把 Claude Code 看成一层“本地编排系统”,后面的章节就容易理解很多:
- Prompt 决定模型怎么想
- Tool 决定模型能做什么
- Permission 决定工具是否能执行
- Context 决定模型这次看到了什么
- MCP 决定系统可以扩展到什么边界
而且从这轮补进去的源码证据看,更准确的整体印象应该是:
- 启动阶段会先把 permission mode、权限上下文和部分外部依赖预热起来
- 主循环真正的本体是
queryLoop(),不是外层包装函数 - 工具、权限、上下文、压缩都不是外挂模块,而是主链中持续参与的运行时机制
- 01 — System Prompt 分层设计:看懂模型行为是怎么被一步步塑形的
- 02 — Agent Loop 核心循环:看懂一次请求从输入到输出到底经历了什么