Claude Code源码剖析 - Message 与上下文结构

Claude Code源码剖析 - Message 与上下文结构 Phase2Message 与上下文结构src/utils/messages.ts消息创建入口createAssistantMessage()exportfunctioncreateAssistantMessage({content,usage,isVirtual,}:{content:string|BetaContentBlock[]usage?:Usage isVirtual?:true}):AssistantMessage{returnbaseCreateAssistantMessage({content:typeofcontentstring?[{type:textasconst,text:content?NO_CONTENT_MESSAGE:content,}asBetaContentBlock,// NOTE: citations field is not supported in Bedrock API]:content,usage,isVirtual,})}关键点assistant 的 content 不一定是字符串也可以是 content block 数组。如果传入字符串源码会把它包装成{ type: text, text: ... }。这说明 Claude Code 内部更倾向统一处理 content block。assistant message 可以携带tool_useblock这是模型请求工具调用的结构。createUserMessage()exportfunctioncreateUserMessage({content,isMeta,isVisibleInTranscriptOnly,isVirtual,isCompactSummary,summarizeMetadata,toolUseResult,mcpMeta,uuid,timestamp,imagePasteIds,sourceToolAssistantUUID,permissionMode,origin,}:{content:string|ContentBlockParam[]...}):UserMessage作用创建 Claude Code 内部的 user message。关键点user message 不只表示真实用户输入。工具执行结果tool_result也会作为 user message 回填给模型。内部结构里同时有type: user和message.role: user。type更偏 Claude Code 内部分类message.role更接近 API 消息角色。一句话总结Message 不是普通聊天字符串而是 agent loop 中传递状态的结构化对象。normalizeMessages()exportfunctionnormalizeMessages(messages:Message[]):NormalizedMessage[]{letisNewChainfalsereturnmessages.flatMap(message{switch(message.type){...}})}作用把一条可能包含多个 content block 的 message拆成多条更标准的 normalized message。解决的问题assistant 一次响应里可能同时包含 text 和 tool_use。user message 里也可能包含字符串、图片、tool_result 等不同 block。多 block message 不方便 UI 展示、查找、工具配对。拆开后每条 normalized message 通常只包含一个 content block。关键源码使用flatMap表示一条 message 可以拆成多条 message。assistant 分支会遍历message.message.content。user 字符串会被转成{ type: text, text: ... }。拆分后用deriveUUID(message.uuid, index)生成稳定的新 uuid。一句话总结normalizeMessages()是把“API 友好的多 block 消息”转换成“系统内部更容易处理的一 block 消息”。工具请求与工具结果判断位置src/utils/messages.ts:829-851isToolUseRequestMessage()exportfunctionisToolUseRequestMessage(message:Message,):messageisToolUseRequestMessage{return(message.typeassistant// Note: stop_reason tool_use is unreliable -- its not always set correctlymessage.message.content.some(__.typetool_use))}作用判断一条 message 是否是 assistant 发出的工具调用请求。关键逻辑message.type 必须是assistant。message.message.content 里必须存在type tool_use的 block。源码没有依赖stop_reason tool_use因为注释说明它不总是可靠。结论工具请求来自 assistant message真正可靠的判断依据是 content block 里的tool_use。isToolUseResultMessage()exportfunctionisToolUseResultMessage(message:Message,):messageisToolUseResultMessage{return(message.typeuser((Array.isArray(message.message.content)message.message.content[0]?.typetool_result)||Boolean(message.toolUseResult)))}作用判断一条 message 是否是工具执行结果。关键逻辑message.type 必须是user。content 第一个 block 是tool_result或者内部字段toolUseResult存在。结论工具结果会作为 user message 回填给模型。agent loop 的关键配对关系是assistant/tool_use- user/tool_resultreorderMessagesInUI()exportfunctionreorderMessagesInUI(messages:(|NormalizedUserMessage|NormalizedAssistantMessage|AttachmentMessage|SystemMessage)[],syntheticStreamingToolUseMessages:NormalizedAssistantMessage[],)consttoolUseGroupsnewMapstring,{toolUse:ToolUseRequestMessage|nullpreHooks:AttachmentMessage[]toolResult:NormalizedUserMessage|nullpostHooks:AttachmentMessage[]}()作用为 UI 展示重新排列消息把同一次工具调用的相关消息聚合在一起。前置条件通常处理的是 normalize 之后的 message因为函数参数是 NormalizedUserMessage / NormalizedAssistantMessage。核心结构使用Mapstring, ToolUseGroup按 tool_use_id 分组。ToolUseGroup 大致包含toolUseassistant 发出的工具请求preHooks工具执行前相关附件/钩子消息toolResultuser message 形式的工具结果postHooks工具执行后相关附件/钩子消息关键理解这段主要服务 UI 展示不是 agent loop 的主闭环逻辑。一句话总结reorderMessagesInUI()把零散的 tool_use、tool_result、hook 附件按 tool_use_id 组织成一组让用户看到完整的工具调用过程。src/services/api/claude.ts内部消息到 API 消息的转换userMessageToMessageParam()exportfunctionuserMessageToMessageParam(message:UserMessage,addCachefalse,enablePromptCaching:boolean,querySource?:QuerySource,):MessageParam{if(addCache){if(typeofmessage.message.contentstring){return{role:user,content:[{type:text,text:message.message.content,...(enablePromptCaching{cache_control:getCacheControl({querySource}),}),},],}}else{return{role:user,content:message.message.content.map((_,i)({..._,...(imessage.message.content.length-1?enablePromptCaching?{cache_control:getCacheControl({querySource})}:{}:{}),})),}}}// Clone array content to prevent in-place mutations (e.g., insertCacheEditsBlocks// splice) from contaminating the original message. Without cloning, multiple calls// to addCacheBreakpoints share the same array and each splices in duplicate cache_edits.return{role:user,content:Array.isArray(message.message.content)?[...message.message.content]:message.message.content,}}作用把 Claude Code 内部的UserMessage转换成 Anthropic API 的MessageParam。核心转换内部 UserMessage{type: “user”,message: {role: “user”,content: …},uuid,timestamp,…}转换成 API message{role: “user”,content: …}关键点API 不需要uuid、timestamp、toolUseResult等内部运行时字段。如果 content 是数组源码会用[...content]浅拷贝避免后续逻辑原地修改污染原 message。addCache分支是 prompt caching 优化路径暂时不是 agent loop 主线。一句话总结userMessageToMessageParam()是“内部 user message”到“模型 API user message”的出口。assistantMessageToMessageParam()exportfunctionassistantMessageToMessageParam(message:AssistantMessage,addCachefalse,enablePromptCaching:boolean,querySource?:QuerySource,):MessageParam{if(addCache){if(typeofmessage.message.contentstring){return{role:assistant,content:[{type:text,text:message.message.content,...(enablePromptCaching{cache_control:getCacheControl({querySource}),}),},],}}else{return{role:assistant,content:message.message.content.map((_,i)({..._,...(imessage.message.content.length-1_.type!thinking_.type!redacted_thinking(feature(CONNECTOR_TEXT)?!isConnectorTextBlock(_):true)?enablePromptCaching?{cache_control:getCacheControl({querySource})}:{}:{}),})),}}}return{role:assistant,content:message.message.content,}}作用把 Claude Code 内部的AssistantMessage转换成 Anthropic API 的 assistant message。核心转换内部 AssistantMessage{type: “assistant”,message: {role: “assistant”,content: […]},uuid,timestamp,…}转换成 API message{role: “assistant”,content: message.message.content}关键点assistant message 的 content 可能包含 text block。assistant message 的 content 也可能包含 tool_use block。转换到 API message 时tool_use block 会被保留下来。addCache分支属于 prompt caching 优化路径不是当前主线。一句话总结assistantMessageToMessageParam()是“内部 assistant message”到“模型 API assistant message”的出口并保留 assistant 发出的tool_use。src/query/tokenBudget.tstoken budget 与上下文继续策略BudgetTracker / createBudgetTracker()constCOMPLETION_THRESHOLD0.9constDIMINISHING_THRESHOLD500exporttypeBudgetTracker{continuationCount:numberlastDeltaTokens:numberlastGlobalTurnTokens:numberstartedAt:number}exportfunctioncreateBudgetTracker():BudgetTracker{return{continuationCount:0,lastDeltaTokens:0,lastGlobalTurnTokens:0,startedAt:Date.now(),}}作用记录 token budget 自动继续过程中的状态。关键字段continuationCount已经自动继续了几次。lastDeltaTokens上一次检查时相比更早一次新增了多少 token。lastGlobalTurnTokens上一次检查时本 turn 已经消耗的 token 数。startedAt开始追踪的时间用于后续统计耗时。关键阈值COMPLETION_THRESHOLD 0.9达到预算 90% 附近就倾向停止。DIMINISHING_THRESHOLD 500连续新增 token 很少时认为继续收益变小。一句话总结BudgetTracker是 token budget 自动继续机制的“记账本”真正的继续/停止判断在后面的checkTokenBudget()。TokenBudgetDecisiontypeContinueDecision{action:continuenudgeMessage:stringcontinuationCount:numberpct:numberturnTokens:numberbudget:number}typeStopDecision{action:stopcompletionEvent:{continuationCount:numberpct:numberturnTokens:numberbudget:numberdiminishingReturns:booleandurationMs:number}|null}exporttypeTokenBudgetDecisionContinueDecision|StopDecision作用定义checkTokenBudget()的返回结果类型。为什么不用 boolean因为继续和停止都需要携带不同的信息。continue需要 nudgeMessage、当前百分比、token 数、继续次数。stop可能需要 completionEvent记录停止时的统计信息。核心结构ContinueDecision用action: continue表示应该继续。StopDecision用action: stop表示应该停止。TokenBudgetDecision ContinueDecision | StopDecision是联合类型。C 类比可以理解为std::variantContinueDecision, StopDecision。其中action字段是判断当前 variant 类型的标签。一句话总结TokenBudgetDecision把“是否继续”和“继续/停止所需信息”一起表达出来比 true/false 更适合 agent loop。checkTokenBudget()exportfunctioncheckTokenBudget(tracker:BudgetTracker,agentId:string|undefined,budget:number|null,globalTurnTokens:number,):TokenBudgetDecision{if(agentId||budgetnull||budget0){return{action:stop,completionEvent:null}}constturnTokensglobalTurnTokensconstpctMath.round((turnTokens/budget)*100)constdeltaSinceLastCheckglobalTurnTokens-tracker.lastGlobalTurnTokensconstisDiminishingtracker.continuationCount3deltaSinceLastCheckDIMINISHING_THRESHOLDtracker.lastDeltaTokensDIMINISHING_THRESHOLDif(!isDiminishingturnTokensbudget*COMPLETION_THRESHOLD){tracker.continuationCounttracker.lastDeltaTokensdeltaSinceLastCheck tracker.lastGlobalTurnTokensglobalTurnTokensreturn{action:continue,nudgeMessage:getBudgetContinuationMessage(pct,turnTokens,budget),continuationCount:tracker.continuationCount,pct,turnTokens,budget,}}if(isDiminishing||tracker.continuationCount0){return{action:stop,completionEvent:{continuationCount:tracker.continuationCount,pct,turnTokens,budget,diminishingReturns:isDiminishing,durationMs:Date.now()-tracker.startedAt,},}}return{action:stop,completionEvent:null}}作用当用户指定 token budget 时判断当前 agent turn 是否应该继续工作或者停止。参数trackerBudgetTracker 记账本。agentId当前是否是 subagent。budget用户指定的 token budget可能为 null。globalTurnTokens当前 turn 已经消耗的 token 数。第一步排除不适用场景如果满足以下任一条件直接返回 stop当前是 subagentagentId存在。没有 token budgetbudget null。budget 无效budget 0。第二步计算当前进度turnTokens globalTurnTokenspct 当前 token / budget * 100deltaSinceLastCheck 当前 token - 上次检查时 token第三步判断收益递减isDiminishing同时满足已经自动继续过至少 3 次。本次新增 token 小于DIMINISHING_THRESHOLD。上次新增 token 也小于DIMINISHING_THRESHOLD。其中DIMINISHING_THRESHOLD 500第四步continue 条件如果没有收益递减。当前 token 还没达到预算 90%。则更新 tracker。返回action: continue。生成nudgeMessage提醒模型继续工作不要总结。其中COMPLETION_THRESHOLD 0.9第五步stop 条件如果已经收益递减或者此前发生过自动继续则返回action: stopcompletionEvent记录继续次数、token 数、预算、是否收益递减、耗时等。兜底如果没有发生过自动继续也不满足继续条件则普通 stopcompletionEvent为 null。一句话总结checkTokenBudget()是 token budget 自动继续机制的决策函数没到预算且仍有产出就继续接近预算或收益递减就停止。token budget 在 query loop 中的接入if(decision.actioncontinue){incrementBudgetContinuationCount()logForDebugging(Token budget continuation #${decision.continuationCount}:${decision.pct}% (${decision.turnTokens.toLocaleString()}/${decision.budget.toLocaleString()}),)// 判断符合继续执行条件重写statestate{messages:[...messagesForQuery,...assistantMessages,createUserMessage({content:decision.nudgeMessage,isMeta:true,}),],toolUseContext,autoCompactTracking:tracking,maxOutputTokensRecoveryCount:0,hasAttemptedReactiveCompact:false,maxOutputTokensOverride:undefined,pendingToolUseSummary:undefined,stopHookActive:undefined,turnCount,transition:{reason:token_budget_continuation},}continue}作用把checkTokenBudget()的 continue 决策接回 agent loop。调用参数budgetTracker!token budget 记账本。toolUseContext.agentId判断当前是否是 subagent。getCurrentTurnTokenBudget()当前 turn 的 token budget。getTurnOutputTokens()当前 turn 已输出 token。continue 时的核心动作重新构造state.messagesmessages messagesForQueryassistantMessagescreateUserMessage({content: decision.nudgeMessage,isMeta: true})关键点decision.nudgeMessage是“继续工作不要总结”的提示。这条消息用createUserMessage()创建。isMeta: true表示它是系统插入的元消息不是用户真实输入。然后 agent loop 会进入下一轮模型调用。一句话总结token budget 的“继续”不是直接让程序生成而是往 messages 里追加一条 meta user message让模型在下一轮继续完成任务。src/utils/tokenBudget.ts解析用户输入里的 token budgetparseTokenBudget()exportfunctionparseTokenBudget(text:string):number|null{conststartMatchtext.match(SHORTHAND_START_RE)if(startMatch)returnparseBudgetMatch(startMatch[1]!,startMatch[2]!)constendMatchtext.match(SHORTHAND_END_RE)if(endMatch)returnparseBudgetMatch(endMatch[1]!,endMatch[2]!)constverboseMatchtext.match(VERBOSE_RE)if(verboseMatch)returnparseBudgetMatch(verboseMatch[1]!,verboseMatch[2]!)returnnull}作用从用户输入文本里识别 token budget并转换成具体数字。支持的写法开头简写500k ...结尾简写... 500k自然语言use 2m tokens/spend 500k tokens单位换算k 1,000m 1,000,000b 1,000,000,000核心流程先匹配开头简写SHORTHAND_START_RE。再匹配结尾简写SHORTHAND_END_RE。再匹配自然语言形式VERBOSE_RE。如果都没有匹配返回null。关键理解parseTokenBudget()只负责把用户文本里的 budget 提取成数字不负责决定是否继续继续/停止判断在src/query/tokenBudget.ts的checkTokenBudget()。一句话总结parseTokenBudget()是 token budget 机制的输入解析器把500k/use 2m tokens这类文本变成后续 agent loop 能使用的数字预算。token budget 的 REPL 输入入口if(feature(TOKEN_BUDGET)){constparsedBudgetinput?parseTokenBudget(input):null;snapshotOutputTokensForTurn(parsedBudget??getCurrentTurnTokenBudget());}if(feature(TOKEN_BUDGET)){constdecisioncheckTokenBudget(budgetTracker!,toolUseContext.agentId,getCurrentTurnTokenBudget(),getTurnOutputTokens(),)...}作用在用户输入进入 agent loop 之前从输入文本中解析 token budget并记录到当前 turn 状态里。关键流程REPL 拿到用户输入input。调用parseTokenBudget(input)。如果解析到 budget就使用解析结果。如果没有解析到就沿用当前已有的getCurrentTurnTokenBudget()。调用snapshotOutputTokensForTurn(...)保存当前 turn 的 token budget。后续 query loop 通过getCurrentTurnTokenBudget()读取并传给checkTokenBudget()。关键源码constparsedBudgetinput?parseTokenBudget(input):null;snapshotOutputTokensForTurn(parsedBudget??getCurrentTurnTokenBudget());