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