关键词:Code Agent / Claude Code / CLI / Bootstrap / QueryEngine / Agent 架构 / 工程设计
别把 Code Agent 当聊天机器人:先看懂 Claude Code 的总架构和启动链路
很多人分析 Code Agent,一上来就盯着模型调用,结果越看越碎。
真正的问题不是“它调了哪个模型”,而是这套系统怎么从一个 CLI 命令,变成一个能长期执行任务的 Agent。Claude Code 的前两章其实就在回答这件事:一个 Code Agent 的骨架到底长什么样,它又是怎么被启动起来的。
一、先把范式分清:Chatbot、Copilot、Agent 不是同一种东西
从工程上看,这三者的差异不在 UI,而在执行边界。
| 类型 | 能做什么 | 不能做什么 |
|---|---|---|
| Chatbot | 一问一答、生成文本 | 不主动行动 |
| Copilot | 读编辑器上下文、给建议 | 通常不闭环执行 |
| Code Agent | 调工具、看结果、继续推进 | 不能没有状态机 |
Code Agent 的本质不是“更强的聊天”,而是下面这条循环:
flowchart TD
U["用户输入"] --> C["组装上下文"]
C --> M["调用模型"]
M --> R{"返回类型"}
R --> |"最终回答"| END["结束"]
R --> |"tool_use"| T["调用工具"]
T --> TR["工具结果回写历史"]
TR --> M
只要系统进入这个闭环,它就不再是 Chatbot,而是执行器。
二、Claude Code 的总架构,其实是七块东西咬在一起
把源码抽掉细节,Claude Code 的主骨架是这样的:
CLI / Bootstrap
↓
QueryEngine / queryLoop
├─ Context Management
├─ Tool System
├─ Permissions / Hooks
├─ Skills / Plugins / MCP
└─ UI Layer (Ink/React)
这里真正的主干不是 UI,也不是模型 SDK,而是中间三层:
queryLoop:负责让任务一轮轮继续;- Context:负责让模型每一轮都知道自己处在什么状态;
- Tool System:负责把模型意图变成真实操作。
换句话说,Claude Code 不是“终端里包了一个 LLM”,而是“用 LLM 驱动的一套工具执行框架”。
三、CLI 启动的第一原则:快路径不能被慢路径拖累
Claude Code 在 cli.tsx 里先做了快速路径分流。像 --version 这种命令,根本不值得把主系统拉起来:
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
}
这个做法看起来普通,但它代表一个很成熟的 CLI 判断:
非主路径必须延迟加载,不能污染主路径的冷启动时间。
所以内部 MCP 模式、daemon worker、后台会话管理等路径,全部走动态 import()。
不需要的模块,不在普通交互场景里承担启动成本。
四、main.tsx 做得最好的地方,不是功能多,而是“等待重叠”
进入 main.tsx 后,Claude Code 立刻做三件事:
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();
import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();
这意味着:
- 入口开始时先打性能点;
- 企业配置读取立即启动;
- Keychain 中的 token / API key 预取立即启动;
- 后续模块加载继续往下跑。
sequenceDiagram
participant M as main.tsx
participant K as Keychain
participant L as 模块加载
M->>K: startKeychainPrefetch()
M->>L: 继续加载 Ink / Commander / React
par 并行
K-->>K: 读取凭证
L-->>L: 模块求值
end
它不是在做复杂优化,而是在贯彻一个很基本的工程原则:把等待和计算重叠起来。
CLI 工具的启动体验,往往就输赢在这种细节上。
五、执行模式必须尽早判断:交互式和无头模式根本不是一回事
Claude Code 很早就判断当前是不是非交互模式:
const hasPrintFlag = cliArgs.includes('-p') || cliArgs.includes('--print');
const isNonInteractive =
hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || !process.stdout.isTTY;
关键点不是 -p,而是这一句:
!process.stdout.isTTY
这意味着只要输出不是终端,比如:
- 被管道消费;
- 被重定向到文件;
- 跑在 CI 里;
它就自动转成非交互模式。
这才是一个能被脚本和流水线真正利用的 Agent CLI。
如果一个 Agent 只能服务“人在终端前手动敲命令”的场景,它的工程价值会被大幅限制。
六、参数解析不是装饰,它定义了系统有多少种工作姿态
main.tsx 里用 Commander.js 注册了大量参数。重要的不是“参数多”,而是参数直接映射系统姿态:
--print:无头执行;--bare:跳过 hooks、LSP、插件同步等附加能力;--permission-mode:切换权限模型;--model/--effort:改变推理策略;--allowed-tools/--disallowed-tools:收紧或放宽工具池;--add-dir:调整文件可访问范围。
这说明 Claude Code 不是只有一种运行方式。
它是同一套核心循环,在不同环境里切换不同外壳。
七、真正的入口不是 main(),而是 query()
CLI 解决的是“怎么启动”,真正决定 Agent 行为的是 src/query.ts 里的 query():
export async function* query(
params: QueryParams,
): AsyncGenerator<StreamEvent | RequestStartEvent | Message | ...> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}
最值得注意的是它为什么是 async function*。
因为 Agent 不是“跑完再返回”的程序,而是会在过程中持续产生事件:
- 模型输出;
- 工具调用;
- 工具执行进度;
- 工具结果;
- 结束原因。
如果不用 async generator,就很难同时做好实时 UI 和中间状态分发。
八、queryLoop() 是整套系统的心跳
真正执行循环的是 queryLoop()。它维护一份跨轮次状态:
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
turnCount: number
transition: Continue | undefined
}
这份状态告诉我们,Claude Code 从来不是“多调几次模型”。
它是一个明确的状态推进器。
每轮循环都做这几件事:
- 取当前消息;
- 检查是否需要压缩;
- 调模型;
- 看到
tool_use就派发工具; - 收集结果回写历史;
- 判断是否继续。
flowchart TD
A["准备消息"] --> B["必要时压缩上下文"]
B --> C["调用模型"]
C --> D{"出现 tool_use?"}
D --> |"是"| E["执行工具"]
E --> F["工具结果回写 messages"]
F --> A
D --> |"否"| G{"是否完成?"}
G --> |"是"| H["返回 Terminal"]
G --> |"否"| A
只要你理解了这一步,就会知道为什么很多 Agent Demo 看起来会动,但一进真实任务就塌:
它们没有把“任务推进”做成状态机,只是做成了多轮问答。
九、第一篇该记住的,不是某个函数,而是三条原则
Claude Code 的前两章合起来,核心其实只有三条:
1. Agent 不是聊天产品,而是执行系统
只要它进入“调用工具-观察结果-继续决策”的闭环,它就不是普通聊天。
2. 启动路径本身就是工程能力的一部分
快路径分流、并行预热、模式早判,这些都不是边角优化,而是主能力。
3. 一切最终都收敛到 queryLoop 这个状态推进器
CLI、参数、模式、上下文、工具,最后都只是为了让这条循环稳定工作。
最后
如果把 Claude Code 当成“会写代码的聊天机器人”,后面很多设计你都会看不懂。
只有把它当成一个长期运行的执行框架,你才会理解:
- 为什么启动要这么抠延迟;
- 为什么模式要这么早分流;
- 为什么
query()要做成 async generator; - 为什么整个系统要围绕
queryLoop组织。
这才是看 Claude Code 前两章最该拿到的东西。