Claude Code源码剖析 - Phase1

Claude Code 源码解析笔记

前言

阅读Claude Code源码,并不是说要将它50w+行代码全部消化理解,我们所要学习的是它的中心思维,它的核心中的核心,所以这次阅读源码的目标是: 读懂 Claude Code 里真正的 AI 应用核心,并最终用 Python 做出一个简化版的cc(美好愿景,后续不知道能不能做到哈哈哈)

重点不放在前端 UI,而放在它的模型编排,tool的调用,内部的核心机制等等,相信学习源码过后,自己的AI应用开发能力会更上一层楼!!!


Phase 1:Agent 最小闭环

1. 阅读目标

Phase 1 先只追这条主线:

text 复制代码
state.messages
-> messagesForQuery
-> deps.callModel(...)
-> assistantMessages
-> toolUseBlocks
-> runTools(...)
-> toolResults
-> state.messages = messagesForQuery + assistantMessages + toolResults
-> continue 下一轮

最重要的理解:

text 复制代码
模型不会真的执行工具。
模型只会输出 tool_use。
本地 runtime 看到 tool_use 后执行工具。
工具结果变成 tool_result。
tool_result 再作为下一轮 messages 的一部分发给模型。

2. src/query.ts 文件结构

src/query.ts 的顶层结构大致是:

text 复制代码
src/query.ts:129-168    yieldMissingToolResultBlocks() 辅助函数
src/query.ts:181-185    isWithheldMaxOutputTokens() 辅助函数
src/query.ts:187-205    QueryParams 类型
src/query.ts:210-223    State 类型
src/query.ts:225-245    query() 外层包装器
src/query.ts:247-1741   queryLoop() 主循环

也就是说,query.ts 的主体几乎全是 queryLoop()

阅读策略:

text 复制代码
不要逐行硬啃 queryLoop 的 1500 行。
先按 agent loop 主线分段读。
支线如 compact、fallback、hook、token budget 先知道位置和作用,后续再深挖。

3. QueryParams:启动 Agent Loop 的参数包

位置:src/query.ts:187-205

ts 复制代码
export type QueryParams = {
  messages: Message[]
  systemPrompt: SystemPrompt
  userContext: { [k: string]: string }
  systemContext: { [k: string]: string }
  canUseTool: CanUseToolFn
  toolUseContext: ToolUseContext
  fallbackModel?: string
  querySource: QuerySource
  maxOutputTokensOverride?: number
  maxTurns?: number
  skipCacheWrite?: boolean
  taskBudget?: { total: number }
  deps?: QueryDeps
}

QueryParams 不是用户最新输入的一句话,而是启动一次 agent loop 需要的完整参数包。

最关键的三个字段:

  • messages:当前对话历史,不是单条 prompt。
  • canUseTool:工具权限判断函数。模型只能请求工具,是否允许执行由本地 runtime 决定。
  • toolUseContext:工具执行上下文,包含工具列表、权限状态、abortController、agentId 等运行环境。

用 C++ 类比:

cpp 复制代码
struct QueryParams {
    std::vector<Message> messages;
    SystemPrompt systemPrompt;
    std::unordered_map<std::string, std::string> userContext;
    std::unordered_map<std::string, std::string> systemContext;

    CanUseToolFn canUseTool;
    ToolUseContext toolUseContext;

    std::optional<std::string> fallbackModel;
    QuerySource querySource;
    std::optional<int> maxOutputTokensOverride;
    std::optional<int> maxTurns;
    std::optional<bool> skipCacheWrite;
    std::optional<TaskBudget> taskBudget;
    std::optional<QueryDeps> deps;
};

为什么不能只传用户的一句话?

因为 agent 要继续工作时,除了用户输入,还要知道:

text 复制代码
之前聊了什么?
当前 system prompt 是什么?
当前有哪些工具?
工具权限是什么?
是不是 subagent?
最多还能跑几轮?
有没有 fallback model?
有没有 compact / token budget 状态?

所以更像:

cpp 复制代码
QueryParams params = {
    .messages = 当前完整对话历史,
    .systemPrompt = 系统提示词,
    .userContext = 用户环境信息,
    .systemContext = 系统环境信息,
    .canUseTool = 工具权限判断函数,
    .toolUseContext = 工具运行环境,
    .querySource = 这次 query 来自哪里,
    .maxTurns = 最大循环轮数,
};
query(params);

一句话总结:

text 复制代码
QueryParams = 让 agent loop 能从当前状态继续跑下去的一整包输入。

4. State:每一轮循环之间传递的状态

位置:src/query.ts:210-223

ts 复制代码
type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
  stopHookActive: boolean | undefined
  turnCount: number
  transition: Continue | undefined
}

QueryParams 是外部传入的初始参数。

StatequeryLoop() 内部 while(true) 每一轮之间传递的可变状态。

Phase 1 最重要的是:

  • messages
  • toolUseContext

因为 agent loop 每一轮最核心的变化就是:

text 复制代码
messages 变长,或者被 compact 后替换;
toolUseContext 可能因为工具执行而更新。

例子:

text 复制代码
第一轮:
messages = [
  user: "读一下 src/query.ts"
]

模型返回:
assistant: tool_use Read(src/query.ts)

本地工具执行:
user/tool_result: "文件内容是..."

第二轮:
messages = [
  user: "读一下 src/query.ts",
  assistant: tool_use Read(src/query.ts),
  user/tool_result: "文件内容是..."
]

C++ 简化版:

cpp 复制代码
State state;

while (true) {
    auto messages = state.messages;
    auto toolUseContext = state.toolUseContext;

    Response response = callModel(messages);

    if (response.isText()) {
        return Completed;
    }

    if (response.isToolUse()) {
        ToolResult result = runTool(response.toolUse);
        state.messages.push_back(response.assistantMessage);
        state.messages.push_back(result.toMessage());
        continue;
    }
}

5. query():外层包装器

位置:src/query.ts:225-245

ts 复制代码
export async function* query(
  params: QueryParams,
): AsyncGenerator<
  | StreamEvent
  | RequestStartEvent
  | Message
  | TombstoneMessage
  | ToolUseSummaryMessage,
  Terminal
> {
  const consumedCommandUuids: string[] = []
  const terminal = yield* queryLoop(params, consumedCommandUuids)

  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed')
  }
  return terminal
}

query() 是异步生成器函数。

它运行过程中可以多次 yield 中间事件,最后 return terminal

这里的 Terminal 不是终端窗口,而是"这次 query 为什么结束",例如:

text 复制代码
completed
model_error
aborted_tools
max_turns

核心代码:

ts 复制代码
const terminal = yield* queryLoop(params, consumedCommandUuids)

意思是:

text 复制代码
query() 把 queryLoop() 产生的所有中间事件原样转发出去。
queryLoop() 正常结束后,query() 拿到它的 return 值 terminal。

C++ 协程类比:

cpp 复制代码
using QueryEvent = std::variant<
    StreamEvent,
    RequestStartEvent,
    Message,
    TombstoneMessage,
    ToolUseSummaryMessage
>;

AsyncGenerator<QueryEvent, Terminal> query(QueryParams params) {
    std::vector<std::string> consumedCommandUuids;

    Terminal terminal =
        co_yield_from queryLoop(params, consumedCommandUuids);

    for (const std::string& uuid : consumedCommandUuids) {
        notifyCommandLifecycle(uuid, "completed");
    }

    co_return terminal;
}

一句话:

text 复制代码
query() 是外层代理生成器。
真正的 agent loop 在 queryLoop()。

6. queryLoop() 总览

位置:src/query.ts:247-1741

queryLoop() 是 Agent Loop 的心脏。它太长,所以先用主线地图看:

text 复制代码
1. 初始化 state
2. while(true) 进入 agent loop
3. 从 state 取出本轮状态
4. 准备 messagesForQuery
5. 做上下文处理:tool_result budget / snip / microcompact / collapse / autocompact
6. 初始化本轮临时容器:assistantMessages / toolResults / toolUseBlocks
7. 调用模型并流式接收 assistant message
8. 从 assistant message 中收集 tool_use
9. 如果没有 tool_use,走 completed 路径
10. 如果有 tool_use,执行工具
11. 收集工具产生的 tool_result
12. 注入附件类上下文
13. 构造下一轮 State
14. continue 回到 while 顶部

极简 C++ 伪代码:

cpp 复制代码
Terminal queryLoop(QueryParams params) {
    State state = initState(params);

    while (true) {
        auto messagesForQuery = prepareMessages(state.messages);

        std::vector<AssistantMessage> assistantMessages;
        std::vector<ToolUseBlock> toolUseBlocks;
        std::vector<Message> toolResults;

        callModelAndCollect(
            messagesForQuery,
            assistantMessages,
            toolUseBlocks
        );

        if (toolUseBlocks.empty()) {
            return Completed;
        }

        runToolsAndCollectResults(toolUseBlocks, toolResults);

        state.messages = concat(
            messagesForQuery,
            assistantMessages,
            toolResults
        );

        continue;
    }
}

7. queryLoop() 入口与 State 初始化

7.1 函数头和固定参数

位置:src/query.ts:247-269

ts 复制代码
async function* queryLoop(
  params: QueryParams,
  consumedCommandUuids: string[],
): AsyncGenerator<
  | StreamEvent
  | RequestStartEvent
  | Message
  | TombstoneMessage
  | ToolUseSummaryMessage,
  Terminal
> {
  const {
    systemPrompt,
    userContext,
    systemContext,
    canUseTool,
    fallbackModel,
    querySource,
    maxTurns,
    skipCacheWrite,
  } = params
  const deps = params.deps ?? productionDeps()

queryLoop() 也是异步生成器。它会边执行边 yield 事件,最后返回 Terminal

consumedCommandUuids 是外层 query() 传入的数组。TypeScript 数组是引用对象,所以 queryLoop() 内部 push 进去的 uuid,外层 query() 结束时还能看到。

C++ 类比时要用引用:

cpp 复制代码
AsyncGenerator<QueryEvent, Terminal> queryLoop(
    QueryParams params,
    std::vector<std::string>& consumedCommandUuids
);

deps 是依赖注入:

ts 复制代码
const deps = params.deps ?? productionDeps()

意思是:

text 复制代码
测试时可以传假的 deps;
生产环境默认用 productionDeps()。

7.2 初始化 State

位置:src/query.ts:271-286

ts 复制代码
let state: State = {
  messages: params.messages,
  toolUseContext: params.toolUseContext,
  maxOutputTokensOverride: params.maxOutputTokensOverride,
  autoCompactTracking: undefined,
  stopHookActive: undefined,
  maxOutputTokensRecoveryCount: 0,
  hasAttemptedReactiveCompact: false,
  turnCount: 1,
  pendingToolUseSummary: undefined,
  transition: undefined,
}

这一步把外部参数里的 messagestoolUseContext 放入内部状态。

源码注释说:

text 复制代码
Continue sites write `state = { ... }` instead of 9 separate assignments.

意思是:后面进入下一轮时,不是单独改字段,而是整体构造新 state:

ts 复制代码
state = {
  messages: newMessages,
  toolUseContext: newContext,
  ...
}
continue

这个风格让每个 continue 分支都明确表达:"下一轮到底带着什么状态继续"。


8. while(true):每一轮从 State 取当前状态

位置:src/query.ts:312-327

ts 复制代码
while (true) {
  let { toolUseContext } = state
  const {
    messages,
    autoCompactTracking,
    maxOutputTokensRecoveryCount,
    hasAttemptedReactiveCompact,
    maxOutputTokensOverride,
    pendingToolUseSummary,
    stopHookActive,
    turnCount,
  } = state

while(true) 是 agent loop 主体。

它不是死循环 bug,而是:

text 复制代码
只要模型还在请求 tool_use,就继续循环;
直到模型没有 tool_use,才 completed。

toolUseContextlet,因为本轮内部可能重新赋值。

其他字段用 const,本轮内通常不直接改;如果需要进入下一轮,会整体重建 state


9. 准备 messagesForQuery

位置:src/query.ts:371-373

ts 复制代码
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]

let tracking = autoCompactTracking

这里要分清:

text 复制代码
messages
= state.messages,完整历史 / 当前长期状态

messagesForQuery
= 本轮模型实际输入的工作副本

messagesForQuery 后面会继续被裁剪、压缩、预算处理。

9.1 getMessagesAfterCompactBoundary()

定义位置:src/utils/messages.ts:4643-4656

ts 复制代码
export function getMessagesAfterCompactBoundary<
  T extends Message | NormalizedMessage,
>(messages: T[], options?: { includeSnipped?: boolean }): T[] {
  const boundaryIndex = findLastCompactBoundaryIndex(messages)
  const sliced = boundaryIndex === -1 ? messages : messages.slice(boundaryIndex)
  if (!options?.includeSnipped && feature('HISTORY_SNIP')) {
    const { projectSnippedView } =
      require('../services/compact/snipProjection.js') as typeof import('../services/compact/snipProjection.js')
    return projectSnippedView(sliced as Message[]) as T[]
  }
  return sliced
}

它的作用:

text 复制代码
如果没有 compact boundary,返回全部 messages。
如果有 compact boundary,只返回最后一个 boundary 之后的消息。

查找 boundary 的函数在 src/utils/messages.ts:4618-4629

ts 复制代码
export function findLastCompactBoundaryIndex<
  T extends Message | NormalizedMessage,
>(messages: T[]): number {
  for (let i = messages.length - 1; i >= 0; i--) {
    const message = messages[i]
    if (message && isCompactBoundaryMessage(message)) {
      return i
    }
  }
  return -1
}

boundary 判断在 src/utils/messages.ts:4608-4612

ts 复制代码
export function isCompactBoundaryMessage(
  message: Message | NormalizedMessage,
): message is SystemCompactBoundaryMessage {
  return message?.type === 'system' && message.subtype === 'compact_boundary'
}

一句话:

text 复制代码
Claude Code 不一定把完整历史都发给模型。
它先从最近一次 compact 边界之后取消息,形成本轮模型输入基础。

10. 上下文处理:继续加工 messagesForQuery

src/query.ts:375-549,代码连续做了几类上下文处理。

它们都在处理同一个对象:

text 复制代码
messagesForQuery

可以理解成流水线:

text 复制代码
state.messages
-> getMessagesAfterCompactBoundary
-> messagesForQuery
-> applyToolResultBudget
-> HISTORY_SNIP
-> microcompact
-> contextCollapse
-> autocompact
-> 最终本轮发给模型的 messagesForQuery

10.1 applyToolResultBudget:限制工具结果大小

位置:src/query.ts:375-400

ts 复制代码
messagesForQuery = await applyToolResultBudget(
  messagesForQuery,
  toolUseContext.contentReplacementState,
  persistReplacements
    ? records =>
        void recordContentReplacement(
          records,
          toolUseContext.agentId,
        ).catch(logError)
    : undefined,
  new Set(
    toolUseContext.options.tools
      .filter(t => !Number.isFinite(t.maxResultSizeChars))
      .map(t => t.name),
  ),
)

解决的问题:

text 复制代码
工具结果可能很大。
比如 Read 读了一个 5MB 文件,或者 Grep 返回几万行。
如果完整放入下一轮 messages,模型上下文会爆。

所以它专门处理 tool_result 的体积。

10.2 HISTORY_SNIP:裁剪历史视图

位置:src/query.ts:402-416

ts 复制代码
let snipTokensFreed = 0
if (feature('HISTORY_SNIP')) {
  queryCheckpoint('query_snip_start')
  const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
  messagesForQuery = snipResult.messages
  snipTokensFreed = snipResult.tokensFreed
  if (snipResult.boundaryMessage) {
    yield snipResult.boundaryMessage
  }
  queryCheckpoint('query_snip_end')
}

作用:

text 复制代码
模型调用前进一步减少 messagesForQuery。
它偏向"裁剪模型看到的历史视图"。

snipTokensFreed 会被后面的 autocompact 使用,用来判断前面已经释放了多少 token。

10.3 microcompact:小粒度压缩

位置:src/query.ts:418-432

ts 复制代码
const microcompactResult = await deps.microcompact(
  messagesForQuery,
  toolUseContext,
  querySource,
)
messagesForQuery = microcompactResult.messages
const pendingCacheEdits = feature('CACHED_MICROCOMPACT')
  ? microcompactResult.compactionInfo?.pendingCacheEdits
  : undefined

它通过 deps.microcompact 注入。

先记住:

text 复制代码
microcompact 是 autocompact 之前的小粒度上下文压缩/整理。

10.4 contextCollapse:折叠上下文视图

位置:src/query.ts:434-453

ts 复制代码
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
  const collapseResult = await contextCollapse.applyCollapsesIfNeeded(
    messagesForQuery,
    toolUseContext,
    querySource,
  )
  messagesForQuery = collapseResult.messages
}

注释里说它是:

text 复制代码
collapsed view
read-time projection

它不是简单删除,也不一定是生成一个大 summary,而是在读取时把上下文投影成折叠后的视图。

它放在 autocompact 前面。如果折叠后上下文已经足够小,后面的 autocompact 可能就不需要跑。

10.5 autocompact:兜底的大型自动压缩

位置:src/query.ts:455-549

先构造完整 system prompt:

ts 复制代码
const fullSystemPrompt = asSystemPrompt(
  appendSystemContext(systemPrompt, systemContext),
)

再调用 autocompact:

ts 复制代码
const { compactionResult, consecutiveFailures } = await deps.autocompact(
  messagesForQuery,
  toolUseContext,
  {
    systemPrompt,
    userContext,
    systemContext,
    toolUseContext,
    forkContextMessages: messagesForQuery,
  },
  querySource,
  tracking,
  snipTokensFreed,
)

如果成功:

位置:src/query.ts:534-542

ts 复制代码
const postCompactMessages = buildPostCompactMessages(compactionResult)

for (const message of postCompactMessages) {
  yield message
}

messagesForQuery = postCompactMessages

也就是:

text 复制代码
把压缩结果变成 postCompactMessages;
yield 给外部;
用 postCompactMessages 替换 messagesForQuery。

如果失败:

位置:src/query.ts:542-549

ts 复制代码
} else if (consecutiveFailures !== undefined) {
  tracking = {
    ...(tracking ?? { compacted: false, turnId: '', turnCounter: 0 }),
    consecutiveFailures,
  }
}

失败时不改 messagesForQuery,只更新失败计数,避免后续无限重试。

10.6 这些上下文处理的区别

机制 处理对象 主要目的 粒度 你可以怎么记
applyToolResultBudget tool_result 防止工具输出撑爆上下文 工具结果级 管工具输出
HISTORY_SNIP 历史视图 裁剪模型看到的旧历史 历史片段级 管历史裁剪
microcompact 局部 messages 小规模压缩/缓存友好处理 小粒度 小压缩
contextCollapse 上下文视图 折叠旧上下文,保留可投影结构 视图级 折叠视图
autocompact 整体上下文 上下文太长时总结压缩 大粒度 兜底压缩

11. 同步 messagesForQuerytoolUseContext

位置:src/query.ts:551-555

ts 复制代码
toolUseContext = {
  ...toolUseContext,
  messages: messagesForQuery,
}

这一步不是更新 state.messages

它只是把本轮处理后的模型输入上下文同步到当前 toolUseContext,让后面的工具执行、权限判断、hook、subagent 等逻辑能看到本轮实际使用的 messages。


12. 初始化本轮临时容器

位置:src/query.ts:557-564

ts 复制代码
const assistantMessages: AssistantMessage[] = []
const toolResults: (UserMessage | AttachmentMessage)[] = []
const toolUseBlocks: ToolUseBlock[] = []
let needsFollowUp = false

这四个变量是 Agent Loop 主线的关键。

变量 作用
assistantMessages 收集本轮模型返回的 assistant message
toolUseBlocks 收集 assistant message 里的 tool_use
toolResults 收集工具执行后产生的 tool_result 和后续附件
needsFollowUp 标记本轮是否需要继续执行工具 / 进入下一轮

重要协议:

text 复制代码
assistant 产生 tool_use。
本地 runtime 执行工具。
工具结果作为 user message 中的 tool_result 回填给模型。

13. 调模型前准备

位置:src/query.ts:566-586

ts 复制代码
const useStreamingToolExecution = config.gates.streamingToolExecution
let streamingToolExecutor = useStreamingToolExecution
  ? new StreamingToolExecutor(
      toolUseContext.options.tools,
      canUseTool,
      toolUseContext,
    )
  : null

const appState = toolUseContext.getAppState()
const permissionMode = appState.toolPermissionContext.mode
let currentModel = getRuntimeMainLoopModel({
  permissionMode,
  mainLoopModel: toolUseContext.options.mainLoopModel,
  exceeds200kTokens:
    permissionMode === 'plan' &&
    doesMostRecentAssistantMessageExceed200k(messagesForQuery),
})

这里还没有真正调用模型,只是在准备:

  • 是否启用 StreamingToolExecutor
  • 本轮实际使用哪个模型 currentModel
  • 当前权限模式 permissionMode

14. 调用模型:deps.callModel

位置:src/query.ts:661-719

ts 复制代码
let attemptWithFallback = true

while (attemptWithFallback) {
  attemptWithFallback = false
  try {
    let streamingFallbackOccured = false
    for await (const message of deps.callModel({
      messages: prependUserContext(messagesForQuery, userContext),
      systemPrompt: fullSystemPrompt,
      thinkingConfig: toolUseContext.options.thinkingConfig,
      tools: toolUseContext.options.tools,
      signal: toolUseContext.abortController.signal,
      options: {
        async getToolPermissionContext() {
          const appState = toolUseContext.getAppState()
          return appState.toolPermissionContext
        },
        model: currentModel,
        fallbackModel,
        querySource,
        ...
      },
    })) {
      ...
    }
  } catch (innerError) {
    ...
  }
}

这里有两个循环,不要混:

text 复制代码
外层 while(true):agent loop。
内层 while(attemptWithFallback):当前这一轮模型调用的 fallback 重试。

真正的模型调用是:

ts 复制代码
for await (const message of deps.callModel(...)) {

这说明 callModel() 也是异步流,不是一次性返回完整答案。

核心参数:

  • messages: prependUserContext(messagesForQuery, userContext)
  • systemPrompt: 完整 system prompt
  • tools: 当前可用工具列表
  • model: 当前模型
  • signal: abort 信号

关键理解:

text 复制代码
模型每一轮都会收到 messages 和 tools。
模型必须知道 tools,才可能返回 tool_use。

15. 从 assistant 输出中收集 tool_use

位置:src/query.ts:834-856

ts 复制代码
if (!withheld) {
  yield yieldMessage
}

if (message.type === 'assistant') {
  assistantMessages.push(message)

  const msgToolUseBlocks = message.message.content.filter(
    content => content.type === 'tool_use',
  ) as ToolUseBlock[]
  if (msgToolUseBlocks.length > 0) {
    toolUseBlocks.push(...msgToolUseBlocks)
    needsFollowUp = true
  }

  if (
    streamingToolExecutor &&
    !toolUseContext.abortController.signal.aborted
  ) {
    for (const toolBlock of msgToolUseBlocks) {
      streamingToolExecutor.addTool(toolBlock, message)
    }
  }
}

这段做了四件事:

  1. 正常情况下,把模型消息 yield 给外部/UI。
  2. 如果是 assistant message,保存到 assistantMessages
  3. 从 assistant content 中筛出 type === 'tool_use' 的 block。
  4. 如果有 tool_use,加入 toolUseBlocks,并把 needsFollowUp 置为 true

这就是模型输出到本地 runtime 的关键转折点:

text 复制代码
模型说:我想调用工具。
本地 runtime 记录下来:待会儿我来执行。

16. Streaming 模式下提前收集工具结果

位置:src/query.ts:858-873

ts 复制代码
if (
  streamingToolExecutor &&
  !toolUseContext.abortController.signal.aborted
) {
  for (const result of streamingToolExecutor.getCompletedResults()) {
    if (result.message) {
      yield result.message
      toolResults.push(
        ...normalizeMessagesForAPI(
          [result.message],
          toolUseContext.options.tools,
        ).filter(_ => _.type === 'user'),
      )
    }
  }
}

如果启用了 streaming tool execution,那么工具可能在模型还没完全输出结束时就已经开始执行,甚至已经完成。

结果会:

text 复制代码
yield 给外部/UI
normalize 成 user/tool_result message
push 到 toolResults

17. 没有 tool_use:Agent Loop 结束

位置:src/query.ts:1073-1368

主线入口:

ts 复制代码
if (!needsFollowUp) {

needsFollowUp 是前面发现 tool_use 时才置为 true 的。

所以:

text 复制代码
!needsFollowUp
= 模型这一轮没有请求工具
= 不需要 runTools
= agent loop 可以结束

这个分支里有很多工程增强逻辑:

  • prompt too long recovery
  • media recovery
  • max output tokens recovery
  • stop hooks
  • token budget continuation

但主线最终是:

位置:src/query.ts:1368

ts 复制代码
return { reason: 'completed' }

18. 有 tool_use:执行工具

如果 needsFollowUp === true,说明本轮 assistant 输出了工具调用请求,于是跳过 completed 分支,继续往下执行工具。

位置:src/query.ts:1371-1394

ts 复制代码
let shouldPreventContinuation = false
let updatedToolUseContext = toolUseContext

const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)

两条路径:

text 复制代码
streamingToolExecutor 存在:
  工具可能已经执行了一部分,这里取剩余结果。

streamingToolExecutor 不存在:
  调用 runTools(...) 执行 toolUseBlocks。

runTools 定义在 src/services/tools/toolOrchestration.ts:19-24

ts 复制代码
export async function* runTools(
  toolUseMessages: ToolUseBlock[],
  assistantMessages: AssistantMessage[],
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {

MessageUpdate 类型在 src/services/tools/toolOrchestration.ts:14-17

ts 复制代码
export type MessageUpdate = {
  message?: Message
  newContext: ToolUseContext
}

19. 收集工具执行产生的 tool_result

位置:src/query.ts:1396-1420

ts 复制代码
for await (const update of toolUpdates) {
  if (update.message) {
    yield update.message

    if (
      update.message.type === 'attachment' &&
      update.message.attachment.type === 'hook_stopped_continuation'
    ) {
      shouldPreventContinuation = true
    }

    toolResults.push(
      ...normalizeMessagesForAPI(
        [update.message],
        toolUseContext.options.tools,
      ).filter(_ => _.type === 'user'),
    )
  }
  if (update.newContext) {
    updatedToolUseContext = {
      ...update.newContext,
      queryTracking,
    }
  }
}

这段是工具执行结果进入下一轮模型上下文的关键。

19.1 工具结果先 yield 给外部

ts 复制代码
yield update.message

这个 yield 是给 UI / transcript / 上层调用方看的,不是直接发给模型。

19.2 工具结果规范化成 user message

ts 复制代码
toolResults.push(
  ...normalizeMessagesForAPI(
    [update.message],
    toolUseContext.options.tools,
  ).filter(_ => _.type === 'user'),
)

为什么只保留 type === 'user'

因为工具协议里:

text 复制代码
assistant 发出 tool_use。
user 回填 tool_result。

所以工具结果要作为 user message 进入下一轮模型输入。

19.3 工具执行可能更新上下文

ts 复制代码
if (update.newContext) {
  updatedToolUseContext = {
    ...update.newContext,
    queryTracking,
  }
}

工具执行不仅会产生消息,也可能更新工具上下文。下一轮要用更新后的 updatedToolUseContext


20. 工具执行后的收尾

位置:src/query.ts:1421-1532

这段还没有进入下一轮,只是在工具执行后做收尾。

20.1 可选生成 tool use summary

位置:src/query.ts:1423-1494

条件:

ts 复制代码
if (
  config.gates.emitToolUseSummaries &&
  toolUseBlocks.length > 0 &&
  !toolUseContext.abortController.signal.aborted &&
  !toolUseContext.agentId
) {

它会把每个 tool_use 和对应 tool_result 配对:

位置:src/query.ts:1450-1477

ts 复制代码
const toolInfoForSummary = toolUseBlocks.map(block => {
  const toolResult = toolResults.find(
    result =>
      result.type === 'user' &&
      Array.isArray(result.message.content) &&
      result.message.content.some(
        content =>
          content.type === 'tool_result' &&
          content.tool_use_id === block.id,
      ),
  )
  ...
  return {
    name: block.name,
    input: block.input,
    output:
      resultContent && 'content' in resultContent
        ? resultContent.content
        : null,
  }
})

核心匹配关系:

text 复制代码
tool_use.id === tool_result.tool_use_id

这也是工具协议中最重要的关联方式。

20.2 工具阶段被中断

位置:src/query.ts:1496-1528

ts 复制代码
if (toolUseContext.abortController.signal.aborted) {
  if (toolUseContext.abortController.signal.reason !== 'interrupt') {
    yield createUserInterruptionMessage({
      toolUse: true,
    })
  }
  return { reason: 'aborted_tools' }
}

如果工具执行时用户中断,agent loop 直接结束。

20.3 hook 阻止继续

位置:src/query.ts:1530-1532

ts 复制代码
if (shouldPreventContinuation) {
  return { reason: 'hook_stopped' }
}

如果工具执行过程中某个 hook 要求停止,就不再进入下一轮。


21. 工具结果后的附件注入

位置:src/query.ts:1535-1688

这一段容易误解:它不是普通工具执行,而是把额外上下文附件也放进 toolResults

关键注释在 src/query.ts:1547-1548

ts 复制代码
// Be careful to do this after tool calls are done, because the API
// will error if we interleave tool_result messages with regular user messages.

意思是:

text 复制代码
必须先收完 tool_result,再加入普通附件/用户上下文消息。
API 不允许 tool_result 和普通 user message 乱序交错。

21.1 queued command attachments

位置:src/query.ts:1592-1602

ts 复制代码
for await (const attachment of getAttachmentMessages(
  null,
  updatedToolUseContext,
  null,
  queuedCommandsSnapshot,
  [...messagesForQuery, ...assistantMessages, ...toolResults],
  querySource,
)) {
  yield attachment
  toolResults.push(attachment)
}

这一步会生成附件消息:

text 复制代码
yield 给外部/UI;
push 到 toolResults;
因此下一轮模型也能看到这些附件。

21.2 memory attachments

位置:src/query.ts:1611-1626

ts 复制代码
const memoryAttachments = filterDuplicateMemoryAttachments(
  await pendingMemoryPrefetch.promise,
  toolUseContext.readFileState,
)
for (const memAttachment of memoryAttachments) {
  const msg = createAttachmentMessage(memAttachment)
  yield msg
  toolResults.push(msg)
}

memory prefetch 完成后,也会变成 attachment message,加入 toolResults

21.3 skill discovery attachments

位置:src/query.ts:1632-1640

ts 复制代码
const skillAttachments =
  await skillPrefetch.collectSkillDiscoveryPrefetch(pendingSkillPrefetch)
for (const att of skillAttachments) {
  const msg = createAttachmentMessage(att)
  yield msg
  toolResults.push(msg)
}

skill discovery 结果也会作为附件进入下一轮上下文。

所以这时 toolResults 的含义变宽了:

text 复制代码
toolResults =
  工具执行结果 tool_result
  + queued command attachments
  + memory attachments
  + skill attachments
  + 其他 attachment messages

21.4 刷新工具列表

位置:src/query.ts:1671-1688

ts 复制代码
if (updatedToolUseContext.options.refreshTools) {
  const refreshedTools = updatedToolUseContext.options.refreshTools()
  if (refreshedTools !== updatedToolUseContext.options.tools) {
    updatedToolUseContext = {
      ...updatedToolUseContext,
      options: {
        ...updatedToolUseContext.options,
        tools: refreshedTools,
      },
    }
  }
}

const toolUseContextWithQueryTracking = {
  ...updatedToolUseContext,
  queryTracking,
}

进入下一轮前刷新工具列表。比如新的 MCP server 连接后,工具列表可能变化。


22. 闭环完成:构造下一轮 State

位置:src/query.ts:1690-1739

这就是 Phase 1 最小闭环最关键的闭合点。

22.1 turnCount + 1

位置:src/query.ts:1690-1691

ts 复制代码
// Each time we have tool results and are about to recurse, that's a turn
const nextTurnCount = turnCount + 1

只要有工具结果并准备进入下一轮,就算一个新 turn。

22.2 maxTurns 限制

位置:src/query.ts:1716-1724

ts 复制代码
if (maxTurns && nextTurnCount > maxTurns) {
  yield createAttachmentMessage({
    type: 'max_turns_reached',
    maxTurns,
    turnCount: nextTurnCount,
  })
  return { reason: 'max_turns', turnCount: nextTurnCount }
}

如果超过最大轮数,就不再继续。

22.3 构造下一轮 State

位置:src/query.ts:1727-1739

ts 复制代码
const next: State = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  toolUseContext: toolUseContextWithQueryTracking,
  autoCompactTracking: tracking,
  turnCount: nextTurnCount,
  maxOutputTokensRecoveryCount: 0,
  hasAttemptedReactiveCompact: false,
  pendingToolUseSummary: nextPendingToolUseSummary,
  maxOutputTokensOverride: undefined,
  stopHookActive,
  transition: { reason: 'next_turn' },
}
state = next

这行是整个 agent loop 的闭环:

ts 复制代码
messages: [...messagesForQuery, ...assistantMessages, ...toolResults]

它把三段拼起来:

text 复制代码
messagesForQuery
本轮模型看到的上下文

assistantMessages
本轮模型输出,包括 text / tool_use

toolResults
本地工具执行结果,包括 user/tool_result 和附件

拼完后写回:

ts 复制代码
state = next

然后 while(true) 自然进入下一次循环。

C++ 伪代码:

cpp 复制代码
State next{
    .messages = concat(
        messagesForQuery,
        assistantMessages,
        toolResults
    ),
    .toolUseContext = toolUseContextWithQueryTracking,
    .turnCount = turnCount + 1,
    .transition = Continue{ .reason = "next_turn" },
};

state = next;
// while(true) 下一轮开始

这就是最小闭环:

text 复制代码
模型 tool_use
-> 本地 runTools
-> tool_result
-> 写回 state.messages
-> 下一轮 callModel

22. Phase1总结

最后以一张完整流程图结束Phase1阶段Claude源码的阅读

text 复制代码
query(params)
  |
  v
queryLoop(params)
  |
  v
State 初始化
  |
  v
while (true)
  |
  v
messagesForQuery = getMessagesAfterCompactBoundary(state.messages)
  |
  v
上下文处理:budget / snip / microcompact / collapse / autocompact
  |
  v
deps.callModel(messagesForQuery, tools, systemPrompt, ...)
  |
  v
assistantMessages.push(message)
  |
  v
从 assistant content 中提取 tool_use -> toolUseBlocks
  |
  +-- 没有 tool_use --> return completed
  |
  +-- 有 tool_use
        |
        v
      runTools(toolUseBlocks, ...)
        |
        v
      toolResults.push(user/tool_result)
        |
        v
      state.messages =
        messagesForQuery + assistantMessages + toolResults
        |
        v
      continue 下一轮
相关推荐
m0_463672201 小时前
如何优雅处理SQL存储过程异常_使用TRY-CATCH块机制
jvm·数据库·python
li星野1 小时前
动态规划十题通关:从爬楼梯到编辑距离(Python + C++)
c++·python·学习·算法·动态规划
OpenCSG1 小时前
MiniCPM-V 4.6:口袋里的多模态AI,在手机上实现GPT-4V级视觉理解
人工智能·智能手机
ㄟ留恋さ寂寞1 小时前
HTML5中SharedWorker生命周期与浏览器进程关闭的关系
jvm·数据库·python
wzl202612131 小时前
企业微信SCRM系统多账号管理与客户智能分层技术实现
人工智能·自动化·企业微信·ai-native
彳亍1011 小时前
MongoDB备节点无法读取数据怎么解决_rs.slaveOk()与Secondary读取权限
jvm·数据库·python
m0_690825821 小时前
CSS如何实现圆形头像裁剪_使用border-radius50属性
jvm·数据库·python
拾薪1 小时前
CodeGraph安装使用
人工智能·ai·codegraph
栈溢出了1 小时前
GAT(Graph Attention Network)学习笔记
人工智能·深度学习·算法·机器学习