这是「Claude Code 第一性原理拆解」的第 2 篇。
如果说上一篇在回答"Claude Code 到底在解决什么",那这一篇就只盯住一个核心问题:为什么 Agent 一定会长出主循环。
摘要
这一篇重点不是介绍 query.ts 的所有细节,而是先把主循环的语义抓稳:
- 对聊天系统来说,一轮常常是一次生成;对 Agent 来说,一轮通常要等动作链条耗尽才结束。
- Claude Code 的主循环本质上在维护"消息流 + 工具执行 + 状态回流"的统一闭环。
QueryEngine和query的关系,决定了它为什么不是一次性工具调用器。
读完这一篇你应该能回答
- 为什么"有 tool call 就继续 loop"不是实现细节,而是 Agent 语义本身。
- 为什么工具结果必须回到同一条消息历史。
- 为什么工作流图、LangGraph 和显式 query loop 在语义上能互相映射。
如果我要给 Claude Code 选一个最不该被忽视的模块,我会选主循环。
因为我觉得很多人对 Agent 的理解都有一个根深蒂固的误区:
觉得一次用户请求,就是一次模型调用。
而 Claude Code 最值得学的一点,恰恰是它明确反对这个误区。
在它的世界里,一次用户请求更像一个"回合":
- 用户说了一次意图
- 模型可能先回答一段话
- 然后发起工具调用
- 工具执行以后,结果又进入同一条消息流
- 模型继续判断下一步
- 直到不再需要工具,这一回合才真正结束
这和普通聊天系统差别非常大。
如果一句话概括,我会说:
对聊天系统来说,一轮是"问一次,答一次";对 Agent 系统来说,一轮是"直到动作链条消耗完为止"。
一、为什么主循环是第一性原理上的必需品
从最底层看,Agent 和聊天系统的差异不在文案,而在控制流。
聊天系统的最小控制流
python
def chat(user_input):
prompt = build_prompt(user_input)
return llm(prompt)
Agent 系统的最小控制流
python
def agent_turn(user_input):
messages.append(user_input)
while True:
assistant = llm(messages)
messages.append(assistant)
if not assistant.tool_calls:
return assistant
results = run_tools(assistant.tool_calls)
messages.extend(results)
真正的变化就在这里:
- 有没有循环
- 工具结果是不是回到同一条状态链里
- 回合结束条件是谁定义的
一旦这三件事变了,系统性质就完全变了。
二、源码主线:Claude Code 是怎么把一轮请求做成回合的
如果我要顺着源码看这条主线,我会抓这几个文件:
src/QueryEngine.tssrc/query.tssrc/constants/prompts.tssrc/utils/systemPrompt.ts
这四个文件一起看,才能把完整回合看明白。
1. QueryEngine.ts 解决的是"会话级控制"
QueryEngine 在我眼里不像一个简单的 wrapper,而更像一个:
- conversation controller
- turn lifecycle owner
它维护的东西不是单次调用参数,而是整段会话的可持续状态,比如:
mutableMessagesreadFileStateabortControllerpermissionDenialstotalUsagediscoveredSkillNames
这说明作者一开始就把系统想成:
一个会持续积累消息、状态、使用量和边界条件的会话对象。
2. query.ts 解决的是"单回合推进"
query() 和 queryLoop() 才是回合发动机。
它不是"调完模型就结束"的函数,而是一个生成器式循环。
里面会反复处理:
- 模型输出
- 工具调用
- 工具结果回填
- token budget
- compact
- 异常恢复
所以我会把 query.ts 看成:
Agent Turn Executor
3. prompts.ts 和 systemPrompt.ts 解决的是"每回合怎么构造系统语义"
很多人把 system prompt 想成一个静态长字符串。
但 Claude Code 这里明显不是。
它会根据不同模式、不同 agent、不同配置动态构造"本次真正要给模型的系统提示"。
这很关键,因为它说明:
system prompt 在这里不是写作模板,而是运行时策略对象。
三、我会怎么把 Claude Code 的回合控制翻译成 Python 伪代码
我会先写成最粗的骨架:
python
class QueryEngine:
def __init__(self, config):
self.config = config
self.messages = config.initial_messages or []
self.read_file_cache = config.read_file_cache
self.abort_controller = config.abort_controller
def submit_message(self, prompt):
self.messages.append(normalize_user_message(prompt))
system_prompt = build_effective_system_prompt(
default_prompt=self.config.default_prompt,
custom_prompt=self.config.custom_prompt,
agent_definition=self.config.agent_definition,
)
for event in query_loop(
messages=self.messages,
system_prompt=system_prompt,
tool_context=self.build_tool_context(),
):
yield event
再往里走一层,query_loop() 在我脑子里大概长这样:
python
def query_loop(messages, system_prompt, tool_context):
while True:
response = stream_model(messages, system_prompt)
yield response.events
assistant_msg = response.final_message
messages.append(assistant_msg)
if not assistant_msg.tool_calls:
return
tool_updates = run_tools(
tool_calls=assistant_msg.tool_calls,
tool_context=tool_context,
)
for update in tool_updates:
messages.extend(update.messages)
tool_context = update.new_context
if should_compact(messages):
messages = compact_messages(messages)
如果我读源码时一直把逻辑压回这两段伪代码,我会非常清楚:
- 哪些东西是骨架
- 哪些东西只是骨架上的增强
四、为什么"回合结束条件"是 Agent 系统最容易写错的地方
我觉得这是一个特别实际的问题。
很多人第一次做 Agent,会自然地写成这种结构:
- 调一次模型
- 如果有 tool call,就执行
- 再调一次模型
- 返回
这看起来很简洁,但很容易错。
因为真实情况往往是:
- 第二次模型输出里还会继续 tool call
- 某次 tool result 会触发 compact
- 某次调用可能需要 ask user
- 某次恢复后模型可能还要继续行动
所以如果系统把"第二次模型调用结束"误当成"本轮结束",后面很多状态都会错。
这也是为什么我越来越认为:
回合的结束条件必须绑定在"当前 assistant 是否还请求动作"上,而不是绑定在"你已经调用了几次模型"上。
五、为什么我觉得生成器式流式循环特别适合这种系统
Claude Code 这里用 async generator / yield 这套方式,我觉得非常自然。
因为 Agent 回合本来就不是一个"先算完再返回"的事情。
它天然包含多种事件:
- request started
- stream delta
- tool use
- tool result
- summary
- terminal state
如果所有东西都等最后一次性返回,会有几个问题:
- UI 体验很差
- 中途状态不可见
- 恢复点难定位
- 观测粒度不够细
所以从第一性原理看,一个行动型 Agent 更适合被组织成:
- 长时段循环
- 流式事件
- 明确状态落点
这点和很多大模型应用最大的差别就在这里。
六、system prompt 在主循环里到底扮演什么角色
我觉得很多人容易把主循环和 system prompt 分开看,好像:
- 主循环是 runtime 的事
- prompt 是 prompt engineering 的事
但在 Claude Code 里,它们是绑在一起的。
因为每一轮循环真正调用模型之前,都必须确定:
- 这次系统身份是什么
- 当前模式是什么
- 当前允许哪些行为
- 当前用户偏好是什么
- 当前 agent 有没有额外指令
这说明 prompt 在这里已经不是"调优模型风格"的文本,而是:
主循环每次进入模型之前的行为约束快照
这也是为什么我特别重视 buildEffectiveSystemPrompt() 这类逻辑。
七、用一张图把"请求"与"回合"分开
我会用下面这张图帮助自己区分两件事:
这张图最想强调的是:
- "用户请求"只是回合起点
- "有没有 tool calls"才是回合是否继续的关键
八、这件事和 LangGraph / 工作流框架怎么对应
如果我把 Claude Code 的主循环思路映射到 LangGraph,我不会做字面对照,而是做语义对照。
LangGraph 里更像是:
- 一个 model node
- 一个 conditional edge
- 一个 tool execution node
- 一个回跳 edge
Claude Code 里则是:
- 一个显式
while循环 - 循环中手动处理这些状态迁移
二者最大的区别不是能力,而是表达方式:
- LangGraph 用图显式表达状态迁移
- Claude Code 用代码控制流表达状态迁移
所以我不会说谁更高级。
我只会说:
如果我要做高度交互式 Agent,Claude Code 这种显式 loop 很值得读;
如果我要做稳定工作流,LangGraph 的显式图更适合审计和协作。
九、我的个人判断:主循环是最能暴露你有没有真正理解 Agent 的地方
我越来越觉得,判断一个人有没有真的理解 Agent,不是看他会不会讲:
- prompt
- RAG
- tools
而是看他能不能清楚回答:
- 一轮是怎么定义的
- 状态怎么持续
- 工具结果怎么回到主链路
- 什么时候真正结束
Claude Code 的主循环之所以值得看,就是因为它逼着我正面回答这些问题。
十、这一篇我想留下的结论
如果只留一句话,我会写:
对 Agent 来说,主循环不是实现细节,而是系统骨架。Claude Code 真正让我学到的,不是"怎么把模型接进终端",而是"怎么把一次用户请求变成一个可持续推进、可中断、可恢复、直到动作耗尽才结束的回合"。
而这恰恰是很多大模型应用在往智能体演进时,最先该补的那一课。