話實(shí)現(xiàn)原理:Pi 的 Session 工作流程全解析(二)——從 JSONL 到 AgentLoop 的 TaoToken 配置實(shí)踐)
1. 從 JSONL 到 AgentLoopPi Session 工作流程里最容易踩坑的銜接點(diǎn)如果你正在本地調(diào)試 AI Agent大概率遇到過(guò)這種場(chǎng)景JSONL 文件里明明寫(xiě)滿(mǎn)了 user、assistant、toolResult 三類(lèi)記錄可 AgentLoop 跑起來(lái)之后模型看到的上下文卻和文件里對(duì)不上——要么少了一輪工具結(jié)果要么分支選錯(cuò)了葉子節(jié)點(diǎn)要么配置變更沒(méi)生效。Pi 的 Session 設(shè)計(jì)把「持久化」和「運(yùn)行時(shí)」拆成了兩層JSONL 是賬本AgentLoop 是記賬員中間靠buildSessionContext這個(gè)純函數(shù)做翻譯。理解這三者的銜接機(jī)制比單純記住事件名重要得多。這篇是 Pi Session 工作流程解析的第二篇聚焦 JSONL 會(huì)話記錄與 AgentLoop 的銜接。我會(huì)先講清楚message_end、turn_end、agent_end三層生命周期邊界到底怎么嵌套再給出可復(fù)制的 TaoToken 統(tǒng)一 Key/API 通道配置片段含 endpoint 與auth.json示例最后演示一次完整的會(huì)話回放驗(yàn)證動(dòng)作確認(rèn) AgentLoop 在多輪 Session 中的狀態(tài)流轉(zhuǎn)正確。適合已經(jīng)在本地跑通 Pi、想進(jìn)一步排查「為什么回放結(jié)果和預(yù)期不一致」的開(kāi)發(fā)者。核心檢索詞先擺出來(lái)Pi Session 是什么、能做什么、適合誰(shuí)。Pi Session 是 Pi 這個(gè) Coding Agent 框架里的會(huì)話層負(fù)責(zé)把對(duì)話和它發(fā)生的所有上下文當(dāng)成可追加的事件日志來(lái)存它能做斷電恢復(fù)、分支追溯、壓縮回滾、配置留痕適合本地調(diào)試 AI Agent、需要多輪工具調(diào)用、想自己掌控會(huì)話數(shù)據(jù)的開(kāi)發(fā)者。JSONL 是它的默認(rèn)存儲(chǔ)格式一行一條SessionTreeEntrycat就能讀。我試過(guò)在本地反復(fù)回放同一個(gè) JSONL發(fā)現(xiàn)最容易出問(wèn)題的不是寫(xiě)入而是「讀回來(lái)之后 AgentLoop 拿到的上下文和寫(xiě)入時(shí)的意圖不一致」。下面按銜接順序拆開(kāi)講。2. 三層生命周期邊界message_end、turn_end、agent_end 的嵌套關(guān)系與 JSONL 落盤(pán)時(shí)機(jī)很多人第一次看 Pi 的事件流會(huì)把message_end、turn_end、agent_end當(dāng)成三個(gè)并列事件。實(shí)際上它們是三層嵌套的生命周期邊界一次 Agent 運(yùn)行包含多個(gè) turn一個(gè) turn 包含多條 message。從外到內(nèi)是agent_start/agent_end→turn_start/turn_end→message_start/message_update/message_end。message_end是 Session 落盤(pán)的最小單位。它一觸發(fā)那條消息就立刻appendEntry寫(xiě)進(jìn) JSONL。user prompt 沒(méi)有流式message_start之后馬上message_endassistant 有流式message_update會(huì)觸發(fā) N 次直到message_end才把最終內(nèi)容寫(xiě)盤(pán)。toolResult 同理執(zhí)行完就寫(xiě)。這意味著「AI 一句話講完就落盤(pán)」斷電也不丟用戶(hù)已經(jīng)看到的字。turn_end是真正的「工作單元」邊界。一個(gè) turn 一次 assistant 回復(fù) 它觸發(fā)的所有工具調(diào)用 所有 toolResult。turn 結(jié)束后harness 才會(huì)去檢查「要不要換模型、要不要壓縮、要不要插入 steer 消息」這些批量配置變更model_change、thinking_level_change、active_tools_change在turn_end時(shí)統(tǒng)一落盤(pán)避免每改一個(gè)就寫(xiě)一次磁盤(pán)。turn_end也是prepareNextTurn鉤子的觸發(fā)點(diǎn)。agent_end是「是否還活著」的信號(hào)。它帶messages: AgentMessage[]即這次 run 新產(chǎn)生的所有消息并發(fā)settled事件讓 TUI 知道可以解鎖輸入框。如果長(zhǎng)時(shí)間沒(méi)收到TUI 知道 Agent 還在忙。實(shí)際時(shí)序參考agent-loop.ts的事件發(fā)射順序大致是這樣emit({ type: agent_start }) // 整個(gè) loop 啟動(dòng) 1 次 emit({ type: turn_start }) // 第一個(gè) turn 開(kāi)始 emit({ type: message_start, prompt }) // user prompt emit({ type: message_end, prompt }) // user prompt 立刻結(jié)束無(wú)流式 emit({ type: message_start, assistantPartial }) emit({ type: message_update, ... }) // 流式過(guò)程中觸發(fā) N 次 emit({ type: message_end, assistantFinal }) // 如果有工具調(diào)用 emit({ type: tool_execution_start, ... }) emit({ type: tool_execution_end, ... }) emit({ type: message_start, toolResult }) emit({ type: message_end, toolResult }) emit({ type: turn_end, message, toolResults }) // turn 邊界批量 flush 配置變更 // 如果需要繼續(xù)assistant 還要看 toolResult 再回話 emit({ type: turn_start }) // 開(kāi)新 turn // ... 再次流式 assistant ... emit({ type: turn_end, ... }) // 直到 assistant stopReason stop emit({ type: agent_end, messages }) // 整個(gè) loop 收尾這里有個(gè)常見(jiàn)誤解必須點(diǎn)破agent_start/agent_end≠ 一次 Session。Session 是整本對(duì)話筆記本可能跨多個(gè)工作日、幾百條消息而agent_start/agent_end只是「AI 響應(yīng)一次用戶(hù)輸入」的完整流程。一次 Session 里會(huì)有很多次agent_start/agent_end。用算賬的方式理解一次 Session N 次agent_start/agent_end你每說(shuō)一句話算一次 一次agent_start/agent_end 1~M 次turn_start/turn_endAI 調(diào)一次工具就多一個(gè) turn 一次turn_start/turn_end 2~K 條消息user assistant 0~N 個(gè) toolResult所以三層事件和 Session 的關(guān)系是Session 是賬本三層事件是「這次記賬里具體寫(xiě)哪幾行、什么時(shí)候結(jié)算」。把a(bǔ)gent_start/agent_end誤當(dāng)成 Session是排查回放問(wèn)題時(shí)第一個(gè)要排除的認(rèn)知偏差。批量寫(xiě)的取舍也很明確減少 IO 次數(shù)但若程序在中途崩潰最后一小段配置變更可能丟失——不過(guò)用戶(hù)消息和 AI 回復(fù)都已經(jīng)寫(xiě)盤(pán)了不會(huì)丟對(duì)話內(nèi)容。這個(gè)取舍在本地調(diào)試時(shí)尤其要注意如果你在turn_end之前強(qiáng)殺進(jìn)程model_change這類(lèi)配置可能沒(méi)落盤(pán)回放時(shí)模型 ID 會(huì)對(duì)不上。3. TaoToken 前置統(tǒng)一 Key/API 通道配置片段endpoint auth.json 示例在講 AgentLoop 怎么讀 JSONL 之前先把模型通道配好。Pi 這類(lèi)本地 Agent 框架通常允許你自定義 Base URL 和 Key我用 TaoToken 的統(tǒng)一通道來(lái)演示因?yàn)樗讯嗉夷P偷?endpoint 收斂成一個(gè)回放時(shí)不用來(lái)回改配置。TaoToken 的 API 地址是https://taotoken.net/api官網(wǎng)是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。注意 API 地址不帶 UTM 參數(shù)直接用于請(qǐng)求。先看auth.json示例。Pi 的認(rèn)證配置一般放在項(xiàng)目根目錄或用戶(hù)配置目錄下字段名以你本地版本為準(zhǔn)下面這份是可復(fù)制的結(jié)構(gòu){ providers: { taotoken: { type: openai-compatible, baseURL: https://taotoken.net/api, apiKey: sk-你的TaoTokenKey, models: { default: { id: claude-sonnet-4-20250514, maxTokens: 8192 }, fast: { id: gpt-4o-mini, maxTokens: 4096 } } } }, defaultProvider: taotoken }如果你用的是 TOML 風(fēng)格的配置部分 Pi 版本或周邊工具支持等價(jià)寫(xiě)法[providers.taotoken] type openai-compatible baseURL https://taotoken.net/api apiKey sk-你的TaoTokenKey [providers.taotoken.models.default] id claude-sonnet-4-20250514 maxTokens 8192三件套必須寫(xiě)全Base URL、Key、Model ID。少任何一個(gè)AgentLoop 在prepareNextTurn階段就會(huì)報(bào)錯(cuò)。Model ID 要和你在 TaoToken 控制臺(tái)看到的模型名一致寫(xiě)錯(cuò)了會(huì)在流式階段返回reading choices相關(guān)錯(cuò)誤。如果你用 Claude Code 或類(lèi)似的 CLI 工具環(huán)境變量方式也可以export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的TaoTokenKey export ANTHROPIC_MODELclaude-sonnet-4-20250514配置好之后先別急著跑 AgentLoop用一條最小請(qǐng)求驗(yàn)證通道curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoTokenKey \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: ping}], max_tokens: 16 }返回里有choices數(shù)組就說(shuō)明通道通了。這一步很重要因?yàn)楹竺?AgentLoop 回放失敗時(shí)你要能區(qū)分是「通道問(wèn)題」還是「Session 銜接問(wèn)題」。通道驗(yàn)證通過(guò)后再進(jìn)入 JSONL 回放環(huán)節(jié)。4. 可復(fù)制配置與驗(yàn)證請(qǐng)求一次完整的會(huì)話回放確認(rèn) AgentLoop 狀態(tài)流轉(zhuǎn)現(xiàn)在進(jìn)入正題怎么用一份 JSONL 回放確認(rèn) AgentLoop 在多輪 Session 中的狀態(tài)流轉(zhuǎn)正確。核心思路是——JSONL 是賬本buildSessionContext是翻譯器AgentLoop 是消費(fèi)者?;胤啪褪亲尫g器重新讀一遍賬本看它吐出的SessionContext和當(dāng)初寫(xiě)入時(shí)的意圖是否一致。先看一份簡(jiǎn)化的 JSONL 會(huì)話記錄每行一個(gè)SessionTreeEntry{id:e1,parentId:null,type:message,role:user,content:幫我讀一下 config.json,ts:1710000001} {id:e2,parentId:e1,type:message,role:assistant,content:好的我來(lái)讀取。,ts:1710000002} {id:e3,parentId:e2,type:tool_call,tool:read_file,args:{path:config.json},ts:1710000003} {id:e4,parentId:e3,type:message,role:toolResult,content:{\port\:8080},ts:1710000004} {id:e5,parentId:e4,type:message,role:assistant,content:端口是 8080。,ts:1710000005} {id:e6,parentId:e5,type:config_change,key:model_change,value:gpt-4o-mini,ts:1710000006} {id:e7,parentId:e6,type:message,role:user,content:換成小模型再總結(jié)一遍,ts:1710000007}注意e6這條config_change它是在turn_end時(shí)批量落盤(pán)的?;胤艜r(shí)如果 AgentLoop 沒(méi)讀到它e7之后的請(qǐng)求還會(huì)用舊模型?;胤膨?yàn)證的代碼骨架TypeScript示意import { Session } from ./session; import { buildSessionContext } from ./session; import { runAgentLoop } from ./agent-loop; async function replay(jsonlPath: string) { const session await Session.loadFromJSONL(jsonlPath); const branch session.getBranch(); // 從根到當(dāng)前葉子的全部條目 const context buildSessionContext(branch); // 扁平化成一維消息流 console.log(回放上下文條數(shù):, context.messages.length); console.log(當(dāng)前模型:, context.activeModel); console.log(當(dāng)前工具集:, context.activeTools); const result await runAgentLoop({ context, provider: taotoken, baseURL: https://taotoken.net/api, apiKey: process.env.TAOTOKEN_API_KEY, }); console.log(stopReason:, result.stopReason); console.log(新產(chǎn)生消息數(shù):, result.messages.length); } replay(./sessions/demo.jsonl);跑完之后重點(diǎn)看三個(gè)輸出context.messages.length是否等于 JSONL 里從根到葉子的消息條數(shù)context.activeModel是否等于最后一條config_change的值result.stopReason是否為stop。這三個(gè)對(duì)上了說(shuō)明buildSessionContext的翻譯和 AgentLoop 的消費(fèi)是一致的。這里有個(gè)微妙之處同一回合內(nèi)buildContext會(huì)被調(diào)用兩次——一次在prepareNextTurnAgentHarness 準(zhǔn)備上下文一次在runAgentLoop內(nèi)部真正調(diào) LLM 前。因?yàn)閜repareNextTurn會(huì)先注入 steer 消息再讓 AgentLoop 拿到最新上下文?;胤艜r(shí)如果你只調(diào)了一次buildSessionContext可能漏掉 steer 注入帶來(lái)的差異。驗(yàn)證請(qǐng)求本身可以用 TaoToken 的模型對(duì)話通道快速確認(rèn)模型側(cè)是否正常但真正的狀態(tài)流轉(zhuǎn)確認(rèn)靠的是上面這段回放代碼的輸出對(duì)比。建議把回放前后的context做一次 diff尤其是activeTools和activeModel這兩個(gè)字段。5. 本篇常見(jiàn)錯(cuò)排查401、local proxy failed、reading choices、OAuth 對(duì)照回放過(guò)程中最常見(jiàn)的四類(lèi)報(bào)錯(cuò)我按實(shí)際遇到的頻率排一下。第一類(lèi)401 Unauthorized。這個(gè)基本是 Key 問(wèn)題。檢查auth.json里的apiKey是否和 TaoToken 控制臺(tái)一致注意有沒(méi)有多余空格或換行。如果你用環(huán)境變量確認(rèn)ANTHROPIC_API_KEY或?qū)?yīng)變量在當(dāng)前 shell 里生效。還有一種情況是 Base URL 寫(xiě)成了帶 UTM 的官網(wǎng)地址請(qǐng)求打到了網(wǎng)頁(yè)而不是 API也會(huì) 401。記住 API 地址是https://taotoken.net/api不帶參數(shù)。第二類(lèi)local proxy failed。這個(gè)報(bào)錯(cuò)通常出現(xiàn)在你本地配了某個(gè)轉(zhuǎn)發(fā)層但轉(zhuǎn)發(fā)層沒(méi)起來(lái)或者端口不對(duì)。排查順序先確認(rèn)本地轉(zhuǎn)發(fā)進(jìn)程是否在跑再確認(rèn)baseURL指向的端口和轉(zhuǎn)發(fā)層監(jiān)聽(tīng)端口一致。如果你沒(méi)配轉(zhuǎn)發(fā)層卻報(bào)這個(gè)錯(cuò)檢查是不是某個(gè)環(huán)境變量殘留了舊的代理地址。清掉之后重啟終端再試。第三類(lèi)reading choices相關(guān)錯(cuò)誤。這個(gè)一般出現(xiàn)在流式響應(yīng)解析階段說(shuō)明返回的 JSON 結(jié)構(gòu)和 AgentLoop 預(yù)期的對(duì)不上。常見(jiàn)原因是 Model ID 寫(xiě)錯(cuò)了或者請(qǐng)求里帶了模型不支持的參數(shù)比如某些模型不支持thinking字段。對(duì)照 TaoToken 控制臺(tái)里的模型名把a(bǔ)uth.json里的id改對(duì)。如果還報(bào)把max_tokens調(diào)小到 1024 再試排除是響應(yīng)過(guò)大導(dǎo)致解析中斷。第四類(lèi)OAuth相關(guān)報(bào)錯(cuò)。如果你用的是 Claude Code 這類(lèi)帶 OAuth 流程的工具報(bào) OAuth 錯(cuò)誤通常是因?yàn)樗鼉?yōu)先走了官方登錄態(tài)沒(méi)走你配的 API Key。這時(shí)候要顯式指定用 API Key 模式或者在配置里把 OAuth 相關(guān)字段清掉。CC Switch 這類(lèi)工具切換配置時(shí)也要確認(rèn)切換后 Base URL、Key、Model ID 三件套都更新了別只換了 Key。排查時(shí)有個(gè)通用方法把 AgentLoop 的日志級(jí)別調(diào)到 debug看它實(shí)際發(fā)出的請(qǐng)求 URL 和 headers。URL 不對(duì)就是配置問(wèn)題headers 里 Authorization 不對(duì)就是 Key 問(wèn)題返回體結(jié)構(gòu)不對(duì)就是 Model ID 或參數(shù)問(wèn)題。這三層分清楚大部分報(bào)錯(cuò)都能定位。另外提醒一句JSONL 文件本身的問(wèn)題也會(huì)偽裝成上述報(bào)錯(cuò)。比如某行 JSON 格式壞了Session.loadFromJSONL解析到那一行會(huì)拋異常但錯(cuò)誤信息可能被上層包裝成別的樣子?;胤徘跋扔胘q或python -m json.tool逐行校驗(yàn)一遍 JSONL能省很多時(shí)間。6. 語(yǔ)義一致 CTA把回放跑通之后繼續(xù)往下走回放跑通、stopReason為stop、activeModel和最后一條config_change對(duì)上之后說(shuō)明你的 JSONL 到 AgentLoop 這條鏈路是通的。接下來(lái)如果要做更長(zhǎng)時(shí)間的編碼任務(wù)或者多輪 Agent 調(diào)度可以考慮用 Coding Plan 來(lái)統(tǒng)一管理模型額度和通道避免每次調(diào)試都手動(dòng)換 Key。配置過(guò)程中如果卡在 Key 或通道上直接去 API Keys 頁(yè)面生成和核對(duì)接入細(xì)節(jié)看接入文檔里面有各語(yǔ)言的最小請(qǐng)求示例。想先驗(yàn)證某個(gè)模型在 TaoToken 上的響應(yīng)質(zhì)量用模型對(duì)話頁(yè)面發(fā)幾條消息試試比在 AgentLoop 里反復(fù)回放快得多?;胤膨?yàn)證這件事我的經(jīng)驗(yàn)是把它做成一個(gè)腳本每次改完 Session 相關(guān)代碼就跑一遍輸出context.messages.length、activeModel、activeTools、stopReason四個(gè)值和歷史基線對(duì)比。這樣狀態(tài)流轉(zhuǎn)一旦出問(wèn)題你能立刻知道是哪一層的事件沒(méi)銜接上而不是等到線上跑飛了才回頭翻 JSONL。