DeepSeek Harness · 模型與外部接入

測試一個非確定性系統

確定性回放、性質測試、專門騙 LLM 客戶端的故障伺服器。核心材料:docs/testing.zh.md 與 packages/test-support/。

課程目標Agent 的行為依賴模型輸出,模型輸出每次都不一樣,呢套系統點測?讀完你能説清 DSH 的三件武器:把真實會話日誌直接變成回放腳本的確定性回放,讓不確定性只留在錄製那一次;專門給 LLM 客戶端製造斷流、半包、限流的腳本化故障伺服器;以及用隨機交錯序列掃蕩協議程式碼的性質測試,它首次運行就抓到過一個真 bug。
互動演示 · 故障注入與回放實驗室

先玩再講。情景 A 是一台真的 HTTP 故障伺服器:行為排成隊,每接一個請求消耗一個,看客戶端怎麼接招。情景 B 是確定性回放:拿一份真實會話日誌,一鍵推導回放腳本重跑,再動手篡改一行,看 diff 怎麼當場作證。

故障伺服器 · 行為腳本隊列seed 0x5A3C
(空)
客戶端這頭 · 分類 / 重試 / 落日誌
(空)
選擇情景後點「播放」,或滾動到此處自動播放情景 A。
邏輯拆解 · 先把地基打確定

測非確定性系統的思路只有一條:把不確定的部分圈起來,讓其餘一切確定。DSH 的測試分層(docs/testing.zh.md)就是圍着這條思路搭的。單元測試盯邊界情況、錯誤路徑、事件順序和併發競態;CI 的覆蓋率門禁對 packages/*/*/src 按文件要求 100%(AGENTS.md 第 65 行 Commands 一節),文檔同時把話説死:行覆蓋率是必要條件,永遠不是充分條件,沒跑過的行往往是該刪的死程式碼,而非該補的測試。

帶密鑰的真實 API 測試是另一層。這裏有句很有身份特色的話:

「我們是 DeepSeek,不要吝惜真實 API 測試。無密鑰測試只能證明底層通路;只有帶密鑰運行才能證明 agent 能對接真實模型正常工作。」 出處:docs/testing.zh.md「帶密鑰策略」一節,核對日期 2026-08-13

推理對自家便宜,冒煙測試就往真裏做:啓動真實示例、發一條提示詞、檢查外部世界。斷言也有講究:e2e 要重新讀文件、重跑命令來驗證結果,對 agent 自身輸出做關鍵詞探測會讓作弊的 agent 通過。缺密鑰的環境自動跳過,不阻塞任何人。

邏輯拆解 · 日誌即測試資產

重頭戲是回放。上一課講過(見 LLM 適配層),每個流式分片都以 assistant/chunk 事件原樣落進會話日誌。dsh-llm-replay 插件把這件事反過來用:拿一份錄好的 session.jsonl,把 chunk 事件按 (turn, step) 分組,每組就是當年一次模型調用的完整分片序列。測試時真實 agent 照常跑,只是模型那頭換成回放適配器,逐幀吐回錄製的分片。不確定性只存在於錄製那一次,之後每次重跑都逐字節一致,不需要 API Key。

這就是日誌即測試資產的意思:fixture 不用手寫 mock 數據,直接就是生產格式的會話日誌本身。快照測試拿它固定整個組裝後的行為,改一行程式碼導致行為分叉,diff 當場標紅。還有個精巧的細節在 fork(分叉會話):子會話的日誌開頭繼承了父會話的種子事件,回放推導腳本時必須從 seedLength 邊界之後開始切,否則父會話的分片會被誤當成子會話的調用重放:

packages/test-support/llm-replay/src/index.ts第 532 至 542 行
    const text = readFileSync(childFile, 'utf8')
    const header = parseSessionHeader(text)
    // Derive the child's script from its own events only — events AT OR after the seed
    // boundary.
    const ownEvents = parseSessionLog(text).slice(header.seedLength)
    children.push({
      recordedId: header.id,
      createdAt: header.createdAt,
      entries: deriveReplayScript(ownEvents),
      primary: false,
    })
源碼快照説明:依據本地倉庫 deepseek-harness-master,核對文件 packages/test-support/llm-replay/src/index.ts,核對日期 2026-08-13。程式碼塊保留源碼原文。

跨平台紀律也從這裏來:簽入的 fixture 必須在 macOS 和 Linux 上都能回放,錄出來的快照哪個平台掛了就修 fixture 本身。AGENTS.md 第 123 行的原話是「fix fixtures, not normalizers」:修 fixture,別寫歸一化器。歸一化器是在測試和現實之間墊棉花,墊多了就測不到現實了。

邏輯拆解 · 一台專門騙 LLM 客戶端的伺服器

回放測的是行為不變,還差一塊:傳輸層的花式死法。連接被拒、發一半 socket 重置、正常關閉但沒發 [DONE]、限流帶 Retry-After、乾脆停滯不動,每一種在適配器和恢復層眼裏都是不同的東西,用進程內 mock 全測不到,因為 mock 繞過了 fetch、SSE 分幀、socket 終止和空閒看門狗這些真實邊界。所以 DSH 造了 dsh-llm-mock-server:一台真的 Node HTTP 伺服器,説 OpenAI 方言,行為完全由腳本控制,每接一個請求消耗一個行為,腳本耗盡就明確報錯(設計動機見 Agent Note 2026-07-25-scriptable-llm-wire-fault-server.zh.md)。開發者想手動復現故障,改一下 base URL 和 key 就能把任何應用接上來。

它還有個 random 模式,按權重隨機抽故障做壓力測試,seed 公開且可復現。預設權重表本身就是一份清單,列着 LLM 客戶端在野外會遇到什麼:正常成功佔 48、慢速成功 10、半途掐綫(partial_disconnect)10,然後是各佔 5 的連接重置、斷流、空回覆和限流,伺服器錯誤 4,觸頂 max_tokens、停滯掛起和 503 各 2,最刁的兩種各佔 1:流正常收尾卻沒發完的 partial_eof,和吐出壞 JSON 的 malformed_json。源碼註釋還專門提醒:這是可調的測試壓力配置,並非生產事故頻率的估算。

出處:packages/test-support/llm-mock-server/src/index.ts 第 56 至 70 行的 DEFAULT_MOCK_LLM_RANDOM_WEIGHTS,核對日期 2026-08-13。

伺服器的操守很剋制:只報告協議層事實,不判斷可不可以重試,策略歸 harness 自己。真實組合測試讓請求依次穿過 DeepSeek 適配器、agent loop 和重試插件,驗證的東西很具體:請求次數精確、重試步驟帶編號、失敗的半截分片不泄漏進歷史、正常 EOF 的半截輸出歸類為 STREAM_CLOSED 且預設不重試。

邏輯拆解 · 性質測試,第一槍就見血

最後一件武器對付的是沒人想得到的交錯。協議形態的程式碼(分片流、事件日誌、收件箱調度)輸入空間是組合爆炸的,示例測試只能固定想到的用例。DSH 給每個協議形態的包配一個 fast-check 驅動的性質測試:生成器造出逼真但對抗性的輸入(重複索引、滯後分片、缺 block-start 的畸形流),斷言的不是具體輸出,是不變式,比如組裝出的塊數不能超過見過的不同索引數、重複調用的結果必須穩定。失敗自動列印可復現的 seed。

它的戰績寫在 Agent Note 的第一行:

「屬性測試套件首次運行即發現了 BlockAssembler 重複 block-end 的真實 bug。」同一索引處重複的 block-end 會改寫已經完成的塊,而這個 bug 是在 happy path 100% 行覆蓋率之下存活的。 出處:.agents/notes/implemented/testing/2026-06-11-property-based-testing.zh.md,核對日期 2026-08-13。修復後的「首次關閉優先」防禦見 LLM 適配層一課的源碼面板。

這套基礎設施還有個副產品:BENCHMARK.md 給的官方基準測試路徑就是 Python SDK 加 minimal 變體,每個任務獨立 workspace。測試體系搭紮實了,跑評測只是換個輸入。

覆蓋率是必要不充分

按文件 100% 是 CI 門禁,但它只證明行被執行過。真 bug 藏在交錯序列裏,那是性質測試的地盤;藏在傳輸邊界裏,那是故障伺服器的地盤。

fixture 就是會話日誌

回放 fixture 不是手搓的 mock,是生產格式的 session.jsonl 本身。錄一次,處處重放,跨平台必須過,掛了修 fixture 不修歸一化器。

故障伺服器不做策略

它只誠實地按腳本掐綫、限流、停滯,重不重試是 harness 的事。測試基礎設施保持中立,才能同時給適配器、loop 和重試層作證。

橫向對比 · 別家怎麼測

Grok Build:也有腳本化 mock,但停在 HTTP 層

Grok Build 的 xai-grok-test-support 裏有一台 MockInferenceServer(crates/codegen/xai-grok-test-support/src/mock_server.rs):預設 echo 模式回聲最後一條用戶消息,支持按路徑排隊的腳本化響應(精確控制狀態碼、body 和 SSE 事件),一台伺服器同時伺候 chat-completions、responses、messages 三種 API 方言,所有請求帶 header 全量記錄供斷言。思路和 DSH 的故障伺服器同源。差距在覆蓋面:從已核對的源碼看,它做的是 HTTP 響應層面的腳本化;DSH 的故障伺服器往下多打了一層,socket 重置、發一半掐綫、停滯掛起這些傳輸層死法都進了行為詞彙,還配了可復現的加權隨機模式。回放這塊,Grok 用 xai-sqlite-journal 做持久化,但基於已公開證據,未見把生產日誌直接推導成回放腳本的等價機制。

Claude Code:閉源產品的測試黑箱

還原源碼(restored-src)裏可見的測試痕跡有限,這符合還原的性質:從產物反推出來的是產品程式碼,測試程式碼本來就不隨產物發佈。所以這裏能下的結論只有一條:基於已公開證據,外界無法評估 Claude Code 的測試體系長什麼樣。這恰好反襯出開源 harness 的一個價值:DSH 的測試策略、覆蓋門禁、fixture 紀律全部寫在倉庫裏,測試基礎設施本身也是可以被學習和複用的交付物。

課堂練習
01

設計一條屬於你的不變式

假設你要給 BlockAssembler 再補一條性質測試。生成器會隨機吐出合法與畸形交錯的分片流(重複 block-end、缺 block-start、滯後 delta)。參考本課講的組裝塊數不變式,再寫出兩條你認為值得斷言的不變式,並説明每條防的是哪類真實故障。然後推演:錄製的快照 fixture 在 macOS 通過、Linux 上因為路徑分隔符 diff 掛了,按「fix fixtures, not normalizers」的紀律,你改哪裏,為什麼不在比對器裏把路徑統一替換掉?

Takeaway:測非確定性系統的辦法是把不確定性圈死在錄製那一刻:會話日誌直接推導回放腳本,fixture 就是生產格式的日誌本身。傳輸層的死法用一台真 HTTP 故障伺服器逐個演練,交錯空間交給性質測試掃蕩,它首戰就抓到了覆蓋率門禁放過的真 bug。覆蓋率證明程式碼跑過,只有這三件武器證明程式碼是對的。