2026-01-11,某些文章具有時(shí)效性,若有錯(cuò)誤或已失效,請(qǐng)?jiān)谙路?a href="#comment">留言或聯(lián)系老夜。Claude Code 源碼揭秘:消息結(jié)構(gòu)里藏著的秘密
↑閱讀之前記得關(guān)注+星標(biāo)??,??,每天才能第一時(shí)間接收到更新
「Claude Code 源碼揭秘」系列的第二篇,上一篇聊了 Agent 循環(huán)的細(xì)節(jié),但那個(gè)循環(huán)操作的核心數(shù)據(jù)結(jié)構(gòu)是什么?是消息數(shù)組。每一條用戶輸入、模型回復(fù)、工具請(qǐng)求、工具結(jié)果,全部通過這個(gè)數(shù)組流轉(zhuǎn)。
今天拆解一下這個(gè)消息結(jié)構(gòu)。有個(gè)設(shè)計(jì)讓我第一次看到的時(shí)候愣了一下——工具結(jié)果居然是偽裝成用戶消息發(fā)的。

消息數(shù)組的基本結(jié)構(gòu)
Claude Code 的對(duì)話狀態(tài)就是一個(gè)消息對(duì)象數(shù)組,遵循 Anthropic Messages API 的格式。每條消息有兩個(gè)關(guān)鍵字段:role(角色,只有 user 和 assistant 兩種)和?content(內(nèi)容,可以是字符串或內(nèi)容塊數(shù)組)。
messages = [? {?role:?"user",?content:?"..."?},? {?role:?"assistant",?content: [...] },? {?role:?"user",?content: [...] },? {?role:?"assistant",?content: [...] }]
有一條死規(guī)矩:角色必須交替。用戶消息后面必須是助手消息,助手消息后面必須是用戶消息。這個(gè)約束影響了整個(gè)工具結(jié)果的處理方式。
內(nèi)容塊的幾種類型
content 字段可以包含不同類型的內(nèi)容塊。
文本塊——最簡(jiǎn)單的,兩種角色都能用:
{? "type":?"text",? "text":?"src 目錄里有什么文件?"}
工具調(diào)用塊——只有 assistant 角色能發(fā)。模型想用工具的時(shí)候,就發(fā)這個(gè):
{? "type":?"tool_use",? "id":?"toolu_01ABC123",? "name":?"Read",? "input":?{? ? "file_path":?"/project/package.json"? }}
三個(gè)關(guān)鍵字段:id?是這次工具調(diào)用的唯一標(biāo)識(shí),name?是工具名稱,input?是工具參數(shù)。
工具結(jié)果塊——這里有意思了。工具結(jié)果是作為 user 消息發(fā)的,不是什么特殊角色:
{? "type":?"tool_result",? "tool_use_id":?"toolu_01ABC123",? "content":?" 1\t{\n 2\t \"name\": \"my-project\",\n 3\t \"version\": \"2.0.0\"\n 4\t}",? "is_error":false}
tool_use_id?必須跟對(duì)應(yīng)的 tool_use 塊的 id 匹配。is_error?標(biāo)記工具是否執(zhí)行失敗——這個(gè)字段很關(guān)鍵,后面會(huì)說。
圖片塊——只有 user 角色能發(fā),用來附加圖片:
{? "type":?"image",? "source":?{? ? "type":?"base64",? ? "media_type":?"image/png",? ? "data":?"iVBORw0KGgoAAAANSUhEUg..."? }}
工具結(jié)果偽裝成用戶消息——這個(gè)設(shè)計(jì)挺巧妙
這是最容易讓人困惑的地方:沒有 tool 角色。工具輸出被格式化成包含 tool_result 塊的用戶消息。
為什么這么設(shè)計(jì)?
因?yàn)榻巧仨殗?yán)格交替。模型發(fā)了 tool_use 請(qǐng)求,按規(guī)矩下一條必須是 user 消息。工具結(jié)果正好可以”借用”這個(gè)位置。
更深層的原因是,模型需要把工具輸出當(dāng)成普通輸入來處理。用用戶消息的形式發(fā)送,對(duì)話就能保持嚴(yán)格的交替模式,模型把工具輸出當(dāng)成新信息來推理。
user: "package.json 里版本號(hào)是多少?"assistant: [tool_use: Read package.json] ?← 模型請(qǐng)求工具user: [tool_result: 文件內(nèi)容] ? ? ? ? ? ? ?← CLI 把結(jié)果當(dāng)用戶消息發(fā)回去assistant: "版本號(hào)是 2.1.0" ? ? ? ? ? ? ? ?← 模型回復(fù)
我第一次看到這個(gè)設(shè)計(jì)的時(shí)候覺得有點(diǎn)”取巧”,但仔細(xì)想想,確實(shí)是最簡(jiǎn)潔的方案。不用引入新角色,不用改 API 規(guī)范,利用現(xiàn)有的交替規(guī)則就把問題解決了。
一個(gè)完整的對(duì)話示例
來看一個(gè)真實(shí)的多輪對(duì)話長(zhǎng)什么樣。
第一輪:用戶提問
{? "role":?"user",? "content":?"package.json 里的版本號(hào)是多少?"}
第二輪:助手調(diào)用 Read 工具
{? "role":?"assistant",? "content":?[? ? {? ? ? "type":?"text",? ? ? "text":?"我來讀一下 package.json 文件。"? ? },? ? {? ? ? "type":?"tool_use",? ? ? "id":?"toolu_01Read",? ? ? "name":?"Read",? ? ? "input":?{?"file_path":?"package.json"?}? ? }? ],? "stop_reason":?"tool_use"}
第三輪:工具結(jié)果(作為用戶消息)
注意 Read 工具的輸出帶行號(hào):
{? "role":?"user",? "content":?[? ? {? ? ? "type":?"tool_result",? ? ? "tool_use_id":?"toolu_01Read",? ? ? "content":?" 1\t{\n 2\t \"name\": \"my-project\",\n 3\t \"version\": \"2.1.0\",\n 4\t \"description\": \"A sample project\"\n 5\t}"? ? }? ]}
第四輪:助手最終回復(fù)
{? "role":?"assistant",? "content":?[? ? {? ? ? "type":?"text",? ? ? "text":?"package.json 里的版本號(hào)是 2.1.0。"? ? }? ],? "stop_reason":?"end_turn"}
一個(gè)簡(jiǎn)單的問題,四條消息。這也是為什么工具調(diào)用多的任務(wù)會(huì)快速消耗上下文窗口。
一次調(diào)用多個(gè)工具
助手可以在一條消息里請(qǐng)求多個(gè)工具:
{? "role":?"assistant",? "content":?[? ? {? ? ? "type":?"text",? ? ? "text":?"我來搜索 TypeScript 文件并查找 TODO 注釋。"? ? },? ? {? ? ? "type":?"tool_use",? ? ? "id":?"toolu_01Glob",? ? ? "name":?"Glob",? ? ? "input":?{?"pattern":?"**/*.ts"?}? ? },? ? {? ? ? "type":?"tool_use",? ? ? "id":?"toolu_02Grep",? ? ? "name":?"Grep",? ? ? "input":?{?"pattern":?"TODO",?"path":?"src/"?}? ? }? ],? "stop_reason":?"tool_use"}
對(duì)應(yīng)的工具結(jié)果也打包在一條用戶消息里:
{? "role":?"user",? "content":?[? ? {? ? ? "type":?"tool_result",? ? ? "tool_use_id":?"toolu_01Glob",? ? ? "content":?"src/index.ts\nsrc/utils.ts\nsrc/types.ts"? ? },? ? {? ? ? "type":?"tool_result",? ? ? "tool_use_id":?"toolu_02Grep",? ? ? "content":?"src/index.ts:45: // TODO: refactor this"? ? }? ]}
多個(gè) tool_use 對(duì)應(yīng)多個(gè) tool_result,通過 id 匹配。清晰明了。
工具失敗的處理
工具執(zhí)行失敗怎么辦?用?is_error?標(biāo)記:
{? "role":?"user",? "content":?[? ? {? ? ? "type":?"tool_result",? ? ? "tool_use_id":?"toolu_01Edit",? ? ? "content":?"Error: old_string not found in file. The content may have changed.",? ? ? "is_error":true? ? }? ]}
模型看到這個(gè)錯(cuò)誤,可以決定重新讀文件、換個(gè)方法試、或者把失敗情況告訴用戶。
這個(gè)設(shè)計(jì)比硬編碼異常處理靈活多了。不是代碼來決定”文件讀取失敗后該干嘛”,而是讓模型自己判斷。上一篇也提到過這個(gè)思路。
System Prompt 不在消息數(shù)組里
還有個(gè)容易搞混的點(diǎn):系統(tǒng)提示詞不是消息數(shù)組的一部分,而是單獨(dú)的參數(shù):
await?anthropic.messages.create({? model:?"claude-sonnet-4-20250514",? system: systemPrompt,? ? // 單獨(dú)的參數(shù)? messages: messages,? ? ? // 對(duì)話歷史? tools: toolDefinitions,? stream:?true});
系統(tǒng)提示詞包含身份指令、環(huán)境信息(工作目錄、平臺(tái)、日期)、CLAUDE.md 內(nèi)容、工具使用指南、行為約束等。它保持不變,而消息數(shù)組隨著交互不斷增長(zhǎng)。
消息驗(yàn)證規(guī)則
API 有嚴(yán)格的校驗(yàn):
-
??角色必須交替:user 和 assistant 必須輪流 -
??第一條必須是 user:對(duì)話必須由用戶發(fā)起 -
??tool_use_id 必須匹配:每個(gè) tool_result 的 tool_use_id 必須對(duì)應(yīng)前面某個(gè) tool_use 的 id -
??內(nèi)容不能為空:消息不能有空的 content 數(shù)組 -
??JSON 必須合法:工具輸入必須是有效的 JSON
違反這些規(guī)則會(huì)直接報(bào) API 錯(cuò)誤。
Token 消耗
不同內(nèi)容類型消耗 token 不一樣:
-
? 文本塊:按普通文本分詞 -
? 工具調(diào)用塊:輸入 JSON 分詞,加大約 20 token 開銷 -
? 工具結(jié)果塊:內(nèi)容按文本分詞,加大約 10 token 開銷 -
? 圖片塊:根據(jù)圖片尺寸計(jì)算
一次工具調(diào)用循環(huán)就增加兩條消息(助手請(qǐng)求 + 用戶結(jié)果),這就是為什么工具調(diào)用多的對(duì)話會(huì)快速吃掉上下文。后面專門聊上下文管理的時(shí)候會(huì)深入講這個(gè)問題。
理解了消息結(jié)構(gòu),很多之前覺得奇怪的行為就能解釋了。比如為什么有時(shí)候模型會(huì)”忘記”之前讀過的文件——可能是老消息被壓縮掉了。比如為什么多輪對(duì)話越來越慢——消息數(shù)組越來越大,每次 API 調(diào)用都要發(fā)完整歷史。
下一篇聊工具執(zhí)行流程——從權(quán)限檢查到結(jié)果格式化,工具調(diào)用經(jīng)過了哪些環(huán)節(jié)。
本文基于 Claude Code 2.0.76 版本源碼分析。
最后記得??我,每天都在更新:歡迎點(diǎn)贊轉(zhuǎn)發(fā)推薦評(píng)論,別忘了關(guān)注我

夜雨聆風(fēng)
