DeepSeek Harness · 會話與迴圈

Model-visible ⟺ logged:一條會崩給你看的不變數

模型看到的一切都要能從日誌重建,發請求前還要現場驗一遍,驗不過直接崩。

課程目標讀完你能說清三件事:為什麼 DSH 規定對話只能有一份真相,模型看到的一切都必須能從會話日誌重建;發請求之前它怎麼現場重建一遍、逐位元組比對一遍;比對不過時它為什麼選擇當場崩潰,連一行告警都懶得打。
互動演示 · 日誌篡改實驗室

先玩再講。左邊是一條只能往後追加的事件日誌,右邊是從日誌重建出來的訊息陣列,和即將發給模型的請求。點播放看事件流入。播完你可以當一次壞人:刪一條日誌,或者繞過日誌直接改請求,再點「發起下一次請求」,看比對怎麼逐條打鉤、在分歧處打紅叉、然後當場崩給你看。

只追加的事件日誌session-1
(空日誌)
從日誌重建的訊息陣列deriveMessages()
(還沒有要給模型看的內容)
即將發給模型的請求
(請求尚未構建)
點「播放」,看事件逐條流入日誌。

實驗臺的比對順序與原始碼一致,紅條裡的報錯保留原始碼原文:比對邏輯在 packages/core/agent-loop/src/invariant.ts 第 31 至 42 行,報錯前綴拼接在 packages/runtime-diagnostics/invariants/src/index.ts 第 62 行。核對日期 2026-08-13。

思路一 · 只留一份真相

它解決什麼問題。大多數聊天程式都有兩份對話:記憶體裡一份陣列,磁碟上一份存檔,各寫各的。某天行程崩了,你從存檔恢復會話,恢復出來的歷史比模型當時實際看到的少了一條工具結果。模型接下來的回答全對不上號,你還查不出原因,因為兩份狀態誰也證明不了誰。只要真相有兩份,它們遲早漂移。

思路是什麼。DSH 把真相壓縮到一份。規矩寫在倉庫根部的 AGENTS.md 第 107 行:

Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event. 出處:deepseek-harness-master 倉庫 AGENTS.md 第 107 行,核對日期 2026-08-13

拆開說。會話日誌是一份只追加的事件流,任何想讓模型看到的內容,必須先變成一條事件寫進日誌。訊息歷史從日誌派生,官方文件的說法是「從不單獨儲存」。所以在 DSH 裡,日誌就是對話本身,系統裡沒有第二份對話狀態。

出處:docs/subsystems/session.zh.md 第 5 行,核對日期 2026-08-13。

然後是標題裡那個雙向箭頭 ⟺。日誌推得出請求,請求也必須能被日誌解釋。光寫日誌做不到這一點,還得有人在發請求的路口站崗。每次請求出站前,DSH 從日誌現場重新派生一份應有的訊息陣列,和請求裡實際帶的那份做全量字串比對;系統提示詞、模型名、取樣參數、工具清單也要和日誌裡的請求頭快照逐欄位對上。全對上才放行。

只追加的事件日誌 請求頭快照 使用者訊息 助手訊息 工具呼叫 · 工具結果 唯一真相,別處沒有副本 現場重建 從日誌派生應有的請求 訊息陣列 + 請求頭快照 迴圈實際構建的請求 深度凍結,過檢後改不了 出站口逐位元組比對 一致,放行發出 不一致,當場崩
教學化結構圖:節點與連線用於解釋原始碼關係,內容經過課程化整理。

站崗的核心就四行,短到可以當金句貼牆上。它在證明一件事:每次派發前,DSH 真的會從日誌重新派生一份訊息,和實際請求做全量字串比對,對不上就地失敗:

packages/core/agent-loop/src/invariant.ts第 39 至 42 行
    const expected = session.deriveMessages()
    if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
      fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
    }
原始碼快照說明:依據本地倉庫 deepseek-harness-master,核對檔案 packages/core/agent-loop/src/invariant.ts,核對日期 2026-08-13。程式碼塊保留原始碼原文。

還有一個細節。檢查用的派生函式,和恢復、回放用的是同一套公開函式,檢查者和被檢查者共享同一套重建規則,誰也沒有私貨。請求頭的重建也只是一個七行的純函式,掃一遍事件、取最後一個快照。

出處:完整檢查順序在 packages/core/agent-loop/src/invariant.ts 第 22 至 52 行;請求頭重建在 packages/core/session/src/request-header.ts 第 65 至 71 行。核對日期 2026-08-13。

為什麼長期成立。single source of truth(單一事實來源)是資料庫領域用了五十年的老道理:帳本只能有一本,其餘全是帳本的視圖,視圖可以隨便丟、隨便重建。DSH 只是把這條紀律搬進了 agent 的對話管理。哪天原始碼重寫成 Rust、事件型別翻三倍,帳本只有一本這件事照樣成立。

日誌就是對話本身,其餘都是視圖。
思路二 · 驗不過就崩,不要帶病執行

它解決什麼問題。設想站崗的發現對不上,只打一行告警會怎樣。告警意味著違規請求已經發給了模型:某個外掛繞過日誌偷偷改了訊息,模型看到的和日誌記的從這一刻起是兩回事。日誌接著記,記的全是錯帳。三天後有人拿這份日誌回放排查,怎麼都復現不出線上的怪行為。靜默偏差比崩潰可怕,它把排查成本悄悄轉嫁給了未來。

思路是什麼。所以 DSH 選了最硬的那條路:比對不過,拋異常,這次請求當場作廢,根本發不出去。設計筆記裡明確否決過溫和方案,對「比較連續請求,發散時告警」這條備選的判詞是「因違規必須在介面層面不可表達而否決」。

出處:.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md「曾考慮的替代方案」一節,核對日期 2026-08-13。

配套還有兩個小機關。檢查器插在事件監聽佇列的隊頭,防止別的監聽器提前短路、把檢查靜默跳過,站崗的人要第一個看到請求。然後請求物件和訊息陣列必須深度凍結,堵住先過檢、再改內容的後門。

出處:隊頭註冊見 invariant.ts 第 20 至 21 行與第 54 行,凍結與會話檢查見第 22 至 29 行。核對日期 2026-08-13。

告警方案(被設計筆記否決) 發現請求和日誌有分歧 打一行告警 違規請求照樣發出 日誌從此記的是錯帳 回放、審計全部失真 DSH 的選擇 發現請求和日誌有分歧 拋異常,當場崩 請求作廢,發不出去 日誌保持乾淨 修好 bug 重跑即可
同一個分歧,兩種處理方式的後果對比。教學化示意圖。

為什麼長期成立。這就是 fail-fast(快速失敗)。崩潰把損失鎖在零:日誌裡沒有被汙染的輪次,修好 bug 重跑就行。帶病執行的成本是複利,跑得越久壞資料越多,最後連哪天開始壞的都查不出來。在邊界上崩一次,比在三天後的詭異現象裡考古便宜得多。這條判斷和語言、框架都沒有關係。

請求發出去的那一刻,日誌就再也解釋不了模型。
思路三 · 一份日誌買到全家桶

前兩個思路各花一份力氣,回報在這裡一起結算。日誌既然是唯一真相,圍繞它能白拿一整套能力:

連 Compaction(壓縮,把過長的歷史換成摘要)都被這條不變數罩著:壓縮產生的摘要同樣以事件形式寫進日誌,壓縮之後的請求照樣要過出站比對,壓縮實現寫出 bug 會當場崩,逃不掉。

為什麼長期成立。這套玩法有個學名,event sourcing(事件溯源),銀行和會計系統用了很多年:不存餘額,只存流水,餘額永遠從流水算出來。DSH 只是把交易流水換成了對話事件。只要事件流還是唯一真相,這些能力就一直是免費的副產品。

橫向對比 · 都寫日誌,誰在反向校驗

會話持久化幾乎是編碼 agent 的標配,差別在方向。Grok Build(xAI 開源的 Rust 編碼 agent)的持久化是一條單行道:記憶體裡的對話狀態是主,落盤是從。每來一條訊息,把記憶體條目拷一份丟進落盤通道,傳送結果直接丟棄,落盤失敗也不打斷對話;壓縮時還允許把整份落盤歷史一次性替換掉。這是合理的產品取捨,只是持久層不承擔正確性職責:沒有任何路徑把日誌反向派生回來、和出站請求比對。

出處:grok-build-main 倉庫 crates/codegen/xai-grok-shell/src/session/chat_persistence.rs 第 30 至 38 行(persist_message 與 replace_history),核對日期 2026-08-13。

Claude Code 閉源,公開可見的是 ~/.claude 下的 .jsonl 會話日誌和恢復功能。基於已公開證據,它屬於事後記錄型持久化;沒有公開材料表明存在派發時刻從日誌重建請求並比對的執行時斷言,內部有沒有等價機制屬於未知。

所以這裡只比機制方向:Grok Build 和 Claude Code 把持久化當恢復手段,DSH 把日誌升格為需要執行時證明的第一性原理。Codex 在同一道題上把閘門開在了更早的地方:每一種注入先得是一個登記過的型別,裝配時按欄位分揀,站內 Codex 上下文碎片 一課有原始碼拆解。

課堂練習
01

推演一次觸發路徑

某外掛監聽請求出站事件,想在發出前往訊息陣列裡塞一條系統提示。按本課的檢查順序推演兩種情況:它直接改那個被凍結的陣列會發生什麼?它先克隆整個請求、改克隆體再轉發,又會在哪一步被攔下?提示:凍結檢查和逐位元組比對各自守住什麼。

Takeaway:DSH 的會話日誌是對話的唯一真相,恢復、分叉、回放、審計共用同一份事件流。每次請求出站前從日誌現場重建並逐位元組比對,對不上當場拋異常,請求發不出去。崩潰優於告警,因為請求發出去的那一刻,日誌就再也解釋不了模型的行為。