如果你只打算看 Claude Code 的一个文件,看 src/query.ts。
这个文件 1729 行,包含了 AI 编程 Agent 的全部核心逻辑。这么说吧:你把 Claude Code 其他 50 万行代码全删了,只留 query.ts + QueryEngine.ts + 工具实现,它还是一个能跑的 Agent。其他一切——UI、命令、Skill、MCP——都是围绕这个核心循环搭建的外围设施。
剥掉错误处理、Feature Flag、日志、消融实验开关之后,核心循环长这样:
// src/query.ts (简化版,保留核心逻辑)
async function* queryLoop(params: QueryLoopParams) {
let state = {
messages: params.initialMessages,
turnCount: 1,
outputTokenRecoveryCount: 0,
}
while (true) {
// 1. 构造上下文
const systemPrompt = buildSystemPrompt(params)
const messagesForApi = maybeCompressHistory(state.messages)
// 2. 调 API(流式)
const stream = createMessageStream({
model: params.model,
system: systemPrompt,
messages: messagesForApi,
tools: params.toolDefinitions,
max_tokens: calculateMaxTokens(state),
})
// 3. 处理流式响应
const response = await processStreamEvents(stream)
// 4. 有工具调用?执行它们
if (response.toolUses.length > 0) {
const results = await executeToolCalls(response.toolUses, params.toolContext)
state.messages.push(response.assistantMessage)
state.messages.push(...results.map(toToolResultMessage))
state.turnCount++
continue // → 回到 while(true) 顶部
}
// 5. 没有工具调用 = LLM 说完了
state.messages.push(response.assistantMessage)
return // 退出循环
}
}这就是每一个 AI Agent 的核心模式:用户说话 → 调 LLM → LLM 要工具就执行 → 把结果喂回 LLM → 重复,直到 LLM 给出纯文本回复。
Claude Code、Cursor、Cline、Aider,底层都是这个循环。差别只在细节处理。而 Claude Code 在这个骨架上堆了一层又一层的防御性逻辑,读完之后我觉得这大概是目前工业界最健壮的 agentic loop 实现。
很多 Agent 框架的系统提示词是一个写死的字符串。Claude Code 的不是。
src/constants/prompts.ts 有 914 行,系统提示词在每次循环迭代时动态组装。它会根据当前环境拼接不同的模块:
// prompts.ts 的组装逻辑(概念性简化)
function buildSystemPrompt(context) {
const parts = []
parts.push(CORE_IDENTITY) // "你是 Claude Code,一个终端里的 AI 助手"
parts.push(getToolDescriptions()) // 当前可用的工具列表和使用建议
parts.push(getEnvironmentInfo()) // OS、工作目录、Git 状态、Python 版本
parts.push(getCWDInfo()) // 当前目录下的关键文件
parts.push(getMemoryFiles()) // CLAUDE.md 和 memory 文件的内容
parts.push(getSkillInstructions()) // 已加载 Skill 的指令
if (context.isResumedSession) {
parts.push(RESUMED_SESSION_NOTICE) // "这是从之前中断的会话恢复的"
}
// @[MODEL LAUNCH] TODO: 针对不同模型版本的行为修正
if (isCapybara(context.model)) {
parts.push(CAPYBARA_COMMENT_FIX) // "不要过度注释代码"
}
return parts.join('\n\n')
}这意味着同一个用户在不同目录下、不同时间点、用不同模型,看到的系统提示词是不一样的。这也是为什么 Claude Code 在不同项目里表现差别很大——不是 LLM 变了,是提示词变了。
有意思的一点:源码里有大量 @[MODEL LAUNCH] 注释,每一个都是"当某个新模型发布时需要修改的地方"。其中关于 Capybara 的有好几条,包括"v8 版本的虚假声明率 29-30%,需要在 prompt 层面修补"。说明新模型的行为矫正是在系统提示词层面做的,不是在模型层面。
CoreCoder 的 prompt.py 是这个机制的极简版(35 行):根据当前工作目录、OS 信息、可用工具列表动态拼接。没有模型特定修正和 Skill 加载,但核心思路一样。
这是整个源码里我最想研究的部分。独立文件 src/services/tools/StreamingToolExecutor.ts,530 行。
一般 Agent 框架的做法:等 LLM 的完整响应接收完毕 → 解析所有 tool_use 块 → 串行或并行执行。中间有一段等待时间。
Claude Code 的做法:LLM 还在生成后面的内容时,前面的工具已经开始跑了。
实现方式是监听 Anthropic API 的 Server-Sent Events 流。每个 content block(可能是文本块或 tool_use 块)有独立的 start / delta / stop 事件。当一个 tool_use 块的 stop 事件到达时,这个工具的输入 JSON 就完整了。不用等其他 content block。
// StreamingToolExecutor.ts 的事件处理逻辑(简化)
class StreamingToolExecutor {
private pendingBlocks = new Map<number, PartialToolUse>()
private runningTools: Promise<ToolResult>[] = []
onStreamEvent(event: StreamEvent) {
switch (event.type) {
case 'content_block_start':
if (event.content_block.type === 'tool_use') {
this.pendingBlocks.set(event.index, {
id: event.content_block.id,
name: event.content_block.name,
inputJson: '',
})
}
break
case 'content_block_delta':
if (event.delta.type === 'input_json_delta') {
// 工具输入 JSON 的碎片陆续到来,拼接
const block = this.pendingBlocks.get(event.index)!
block.inputJson += event.delta.partial_json
}
break
case 'content_block_stop': {
const block = this.pendingBlocks.get(event.index)
if (block) {
// 输入完整了,解析 JSON,**立即**提交执行
const input = JSON.parse(block.inputJson)
const promise = this.executeToolWithPermissionCheck(block, input)
this.runningTools.push(promise)
this.pendingBlocks.delete(event.index)
}
break
}
}
}
}还有一个关键设计:并发安全标记。每个工具有一个 isConcurrencySafe 属性。读文件、grep、glob 这些只读操作标记为 safe,可以并行。写文件、bash 这些有副作用的需要独占执行。StreamingToolExecutor 维护了一个调度队列来保证这个约束。
效果:假设 LLM 一次返回三个工具调用——读文件 A(200ms)、读文件 B(150ms)、跑测试(2s)。串行执行总共 2350ms。StreamingToolExecutor 下,三个工具在 LLM 还在生成的时候就已经开始跑了,实际延迟接近于 max(200, 150, 2000) = 2000ms,甚至更短(因为工具执行时间被 LLM 生成时间"吸收"了)。
CoreCoder 用了一个折中方案:不做流式解析,但当 LLM 一次返回多个 tool_calls 时,用 ThreadPoolExecutor 并行执行。损失了"在 LLM 生成期间就开始执行"的优势,但保留了"多工具并行"的收益。大概十几行代码。
生产级 Agent 不能因为一次 API 超时就崩溃。query.ts 里的错误处理写得很细致,我数了一下大致有这些分支:
API 429(限流):指数退避重试,最多 5 次。每次等待时间翻倍。
API 400/413(请求体太大):说明上下文超了。自动触发压缩,裁掉旧消息,然后用压缩后的消息重新调 API,而不是报错退出。
API 529(服务过载):切换到配置里的 fallbackModel。比如主力模型 Opus 不可用,fallback 到 Sonnet。
输出触顶(max_output_tokens):这个处理最有意思。模型的回复被截断了——可能一个工具调用的 JSON 只输出了一半。大多数框架会报错。Claude Code 的做法是"扣留"这个不完整的响应,悄悄重试,最多 3 次。代码上面有段中世纪巫师口吻的注释:
// Heed these rules well, young wizard. For they are the rules of thinking,
// and the rules of thinking are the rules of the universe. If ye does not
// heed these rules, ye will be punished with an entire day of debugging
// and hair pulling.
维护这段代码的人显然被坑过不少次。
用户中断(Ctrl+C):正在跑的工具通过 AbortController 取消。已经完成的工具结果保留在消息历史中。下次用户输入时,LLM 可以看到"你之前做了 A 和 B,C 被用户取消了"。
工具执行异常:这是最关键的一条。异常不会传播到外层。Claude Code 把异常信息包装成一条 tool_result,角色设为 error,然后喂回 LLM,让 LLM 自己决定如何处理。LLM 可能会换一种方式重试,或者告诉用户"这个方案行不通,我试试另一个"。
这才是"agentic"的核心含义:Agent 遇到问题时自己想办法,而不是转头问人。
CoreCoder 的 agent.py 实现了工具异常捕获喂回 LLM 和 max_rounds 限制,但没有做 API 层面的重试和 fallback(这些在不同 LLM provider 之间差异太大,放在上层处理更合适)。
QueryEngine 管理两种预算:
轮次预算(maxTurns)。每完成一轮工具调用算一个 turn。超过上限就强制停止。主要防 LLM 陷入循环(比如反复读同一个文件但每次都"没找到"它要的东西)。
美元预算(maxBudgetUsd)。每次 API 调用的 token 消耗换算成费用。累计超过预算就停。在 SDK 模式下特别有用:你可以限制一个自动化任务最多花 0.5 美元。
// QueryEngine.ts
type QueryEngineConfig = {
maxTurns?: number // 最大轮次
maxBudgetUsd?: number // USD 预算上限
taskBudget?: { total: number } // API 侧 token 预算
// ...
}CoreCoder 用 max_rounds=50 做了轮次限制,没做美元预算(多 provider 环境下费率不统一,不好算)。
AppState 里有个字段叫 speculationState,追踪每一轮的结束方式:bash 命令、文件编辑、正常文本回复、权限拒绝。系统用这个来预判下一步操作。
如果上一轮 LLM 编辑了一个文件,系统会提前准备好 diff 渲染组件。如果上一轮跑了 bash 命令,系统会提前分配一个终端 buffer。
用过 Claude Code 的人应该有感觉——它"想"完之后开始干活的速度很快。投机执行是原因之一。
如果你在做自己的 Agent 产品,从 query.ts 可以带走的最重要的设计:
- 工具异常喂回 LLM,不要替 Agent 做决定
- 上下文超限时压缩重试,不要报错退出
- 工具并行执行,用户感知延迟可以大幅降低
- 轮次预算,防止 LLM 无限循环
- 系统提示词动态拼装,同一个 Agent 在不同环境下有不同行为
这些设计模式我在 CoreCoder 里都做了最小实现(agent.py 110 行)。如果你想看它们在生产代码里长什么样,query.ts 是最好的教材。
本文是 Claude Code 源码导读 系列第 2 篇。配套实现:CoreCoder