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