iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

AI Reliability Lab:30 天用 Python 把「AI 好像很準」變成可以量的工程指標系列 第 11 篇

Day 11|模型沒壞,實驗先斷線了:把 REAL Experiment 做成可以斷點續跑

  • 分享至 

  • xImage
  •  

Day 9 跑第一個正式 REAL benchmark 時,我遇到了一個很現實的問題。

不是模型答錯。

不是 calibration 算壞。

而是:

網路斷了。

當時完整 benchmark 原本需要 48 次 API request,但第一輪跑到第 37 次時網路中斷。

結果是:
https://ithelp.ithome.com.tw/upload/images/20260925/20184158LOoQVf58qm.png

同一批 12 筆 REAL records 使用 2~6 個 bins 計算 ECE,本次資料得到完全相同的結果;這主要與 confidence 高度集中、取值離散有關,不能解讀成 ECE 一般而言不受 binning 影響。

  • 37 次 request attempts
  • 36 次成功
  • 1 次 network failure
  • 沒有輸出不完整的正式 dataset

最後我只能等網路恢復後,把完整的 48 次 request 從頭再跑一次。

也就是說,那一天實際總共送出了 85 次 request。

這件事當時看起來只是「網路不好」。

但回頭看,其實暴露的是 AI Reliability Lab 自己的一個 reliability 問題:

如果未來一個實驗有 100 題、500 題甚至更多,難道只要第 499 次 request 斷線,就要全部重新開始?

所以 Day 11 我沒有再增加新的 calibration metric。

今天反而回頭修實驗基礎設施本身。

目標只有一個:

讓 REAL Experiment 可以 checkpoint,也可以在中斷後安全 resume。


問題不只是「記住跑到第幾題」

一開始我以為 resumable experiment 可能很簡單。

例如跑到第 30 題,就記:

current_question = 30

下一次從第 31 題開始。

但真正開始設計後,發現這樣不夠。

因為目前一個 benchmark case 不只會呼叫模型一次。

Day 9 的每一題包含:

  • 1 次 primary response
  • 多次 independent sampling

所以即使同樣是 D9-009,也可能出現:

  • primary 已完成
  • sample 1 已完成
  • sample 2 還沒完成
  • sample 3 還沒完成

如果只記錄「跑到 D9-009」,根本不知道這題裡哪些 request 已經真的成功過。

所以 Day 11 把一個 case 再拆成更小的 logical request slot。

例如 D9-009 可以變成:

D9-009:primary

D9-009:sample:1

D9-009:sample:2

每一個 slot 都有 deterministic identity。

這樣 checkpoint 不再只是記錄「做到哪一題」,而是可以精確知道:

哪些 logical request 已經成功完成?

Resume 時,只要看到某個 slot 已經存在,就不應該再次送出。


Checkpoint 到底保存什麼?

接下來的問題是:

如果要讓實驗恢復,需要把什麼寫進 checkpoint?

這次保存的資訊包含:

  • Run ID
  • Experiment name
  • Provider
  • Model
  • Benchmark version
  • Benchmark SHA-256
  • Case IDs
  • Prompt version
  • Sample count
  • Reasoning effort
  • Token limit
  • Timeout
  • Retry 設定
  • store 設定
  • Normalization / scoring identity
  • 已完成的 logical request IDs
  • Sanitized derived answers
  • Request accounting
  • Start / update timestamps
  • Completion status

但有一些東西刻意不存:

  • API key
  • Authorization headers
  • Raw provider responses
  • 不必要的 provider metadata

這裡其實延續了前幾天一直在做的原則:

為了 reproducibility 保存需要的資訊,但不要為了方便把敏感資料一起留下來。


Resume 前不能只問「有 checkpoint 嗎?」

假設昨天用:

gpt-5.6-luna

今天卻把模型改成另一個模型。

或者昨天:

sample_count = 3

今天變成:

sample_count = 5

如果程式看到 checkpoint 就直接繼續跑,最後的 dataset 其實是兩種不同設定混在一起。

表面上看起來完整,實際上實驗已經失去一致性。

所以 Day 11 加入了 experiment identity validation。

Resume 之前會檢查:

  • provider 是否相同
  • model 是否相同
  • benchmark 是否相同
  • prompt version 是否相同
  • sample count 是否相同
  • reasoning effort 是否相同
  • token limit 是否相同
  • retry / store 等設定是否相同
  • normalization / scoring identity 是否相同

只要 identity-defining setting 不一致:

拒絕 resume。

不是偷偷繼續。

也不是偷偷建立另一種設定的資料。

這件事比「可以續跑」本身還重要。

因為 reliability 不只是程式不要 crash,也包括:

crash 之後不要悄悄產生一份方法ologically 不一致的資料。


Checkpoint 本身也可能寫到一半

還有另一種滿尷尬的情況。

假設 API request 成功了,程式正在更新 checkpoint,結果這時程式被強制關掉。

如果直接對 checkpoint 原檔寫入,有可能最後留下半個 JSON。

下一次 resume 時,程式看到檔案存在,卻不知道這是一個有效 checkpoint,還是寫到一半的檔案。

所以 Day 11 也補上 atomic write。

概念是:

先寫 temporary file,確認寫入完成後,再 replace 正式檔案。

這不能解決世界上所有 filesystem failure,但至少比直接覆寫正式 checkpoint 安全很多。

而最後公開 REAL output 也採取類似概念:

實驗還沒完整結束時,不會直接把半成品放到正式 public run directory。

只有所有 logical requests 都存在之後,才進行 final publication。


先用 Mock 把 Resume 弄壞一次

在真的花 API 額度之前,我先用 fake provider 測試中斷與恢復。

Day 11 最後完整 test suite:
https://ithelp.ithome.com.tw/upload/images/20260925/20184158pwwdu6qVSK.png
93 passed in 2.13s

其中 resumable runner 至少驗證了這些情況:

  • 正常不中斷可以完成
  • 中途停止會留下可 resume checkpoint
  • Resume 只執行剩餘 request
  • 已完成 request 不會重複
  • Interrupted + resumed 的結果和 deterministic uninterrupted run 一致
  • Experiment configuration 不一致會拒絕 resume
  • Corrupted checkpoint 不會被偷偷忽略
  • 尚未完整時不會發布正式 REAL dataset
  • 跨 session 的 request accounting 正確
  • checkpoint / public output 不含 secret 與 raw response

這些都通過後,才進 REAL smoke test。


接著故意把 REAL Experiment 跑到一半停掉

這次我沒有等待網路真的再斷一次。

而是加入一個 controlled interruption mechanism。

它的用途很單純:

故意在成功完成 N 個新的 logical requests 後停止。

這不是 fake provider failure。

API request 還是真的。

只是 runner 被要求:

做到這裡先停。

REAL smoke experiment 使用四個 Day 9 已經存在的 case:

  • D9-001
  • D9-006
  • D9-009
  • D9-011

每題這次使用:

  • 1 primary
  • 2 independent samples

因此:

4 cases × 3 logical requests = 12 logical requests


第一個 Session:做到第 5 個,故意停

第一次執行:

項目 數量
Planned logical requests 12
Newly completed 5
API attempts 5
Remaining 7

跑完五個之後,runner 按照設定停止。

這時候最重要的事情有兩個。

第一:

Checkpoint 還在。

第二:

正式 public REAL dataset 還沒有被發布。

也就是說,系統知道這是一個 incomplete experiment。

它不會因為已經拿到五個成功回答,就讓外面看到一份看起來像完整 run 的資料。


第二個 Session:不是再跑 12 個,而是只跑剩下 7 個

接著重新啟動同一個 run。

Runner 先讀 checkpoint。

確認 experiment identity 完全一致後,找到五個已經完成的 logical request。

所以這五個:

直接 skip。

第二個 session 最後是:

項目 數量
Previously completed and skipped 5
Newly completed 7
API attempts 7

加起來:

5 + 7 = 12

整個 REAL experiment 的 API attempts 最後就是:

12。

不是:

5 + 12 = 17

而且最後檢查 12 個 logical request ID:

沒有任何 duplicate。

這就是 Day 11 最重要的驗證結果。


最後四筆 REAL records

這次四個 case 最後也正常組裝成完整 record:

Case Primary Correct Self Confidence Samples Agreement
D9-001 Paris Yes 1.0 Paris, Paris 1.0
D9-006 40 Yes 1.0 40, 40 1.0
D9-009 20 No 1.0 20, 20 1.0
D9-011 INSUFFICIENT_INFORMATION Yes 1.0 INSUFFICIENT_INFORMATION ×2 1.0

但這裡要特別說:

這四筆不是拿來做新的 calibration evidence。

今天的目的不是比較模型表現。

就算 D9-009 又答錯,而且這次三個回答全部都是 20,也不應該把四題拿去算新的 ECE 然後下結論。

今天真正被測的是:

實驗 runner 自己。


Checkpoint 完成之後怎麼處理?

目前採用的 policy 是:

如果 run 還沒完成:

checkpoint 保留。

因為下一次需要靠它 resume。

但當實驗已經完整完成,而且 final public output 已經成功發布後:

completed checkpoint 刪除。

這次 REAL smoke test 最後 checkpoint 也確實成功清掉。

正式留下的是:

  • metadata.json
  • records.jsonl
  • resume_summary.json

也就是最終可公開、非敏感,而且能描述這次 experiment 的資料。


這是不是 Exactly-Once Delivery?

不是。

這是今天一定要講清楚的限制。

目前 runner 可以保證:

已知已經成功完成並寫入 checkpoint 的 logical request,在 validated resume 時不會被故意重新送出。

但還存在一個更麻煩的 edge case。

假設發生:

  1. Client 把 request 送給 provider
  2. Provider 其實已經成功處理
  3. Response 回程時網路斷掉
  4. Client 永遠沒收到 response
  5. Checkpoint 也就沒有這一格完成的紀錄

從 client 的角度,它只能看到:

這個 slot 沒完成。

下一次 resume 時,這個 request 仍然可能再次送出。

所以 Day 11 做的不是 distributed system 意義下的「真正 exactly-once delivery」。

比較精確的說法是:

Validated resume prevents known completed logical requests from being intentionally reissued.

這個限制我反而覺得值得保留。

因為如果我把它寫成「現在 API experiment 保證 exactly once」,那其實是在隱藏一個很重要的 distributed systems 問題。


Day 9 的一次斷網,最後變成 Day 11 的功能

回頭看整件事滿有趣。

Day 9 第一次 benchmark 斷網的時候,我只覺得:

好吧,只能再跑一次。

結果最後:

37 次 request 作廢,完整 benchmark 又重新跑了 48 次。

到了 Day 11,我才真正把這件事當成工程問題。

現在 runner 的流程變成:

REAL request → success → sanitized result → atomic checkpoint

如果中斷:

Checkpoint → identity validation → skip completed slots → continue remaining requests

全部完成後:

Private staging → validate completeness → publish final REAL output → remove completed checkpoint

這讓之後 benchmark 規模開始變大時,不需要每次都賭:

希望這幾百次 request 中間都不要出任何事情。


Day 11 小結

今天沒有增加新的 calibration metric。

也沒有增加新的 benchmark。

反而花了一整天改善「實驗怎麼可靠地跑完」。

最後完成:

  • deterministic logical request slots
  • resumable checkpoint
  • atomic checkpoint writes
  • experiment identity validation
  • persistent request accounting
  • controlled interruption
  • incomplete publication gating
  • completed checkpoint cleanup
  • mocked resume tests
  • REAL interruption + resume smoke test

完整測試:

93 passed

REAL smoke:

  • 第一個 session:5 API attempts
  • Resume session:7 API attempts
  • 總共:12 API attempts
  • Planned logical requests:12
  • Duplicated logical requests:0

Day 9 的斷網原本只是一次失敗。

Day 11 之後,它變成 AI Reliability Lab 的一個正式能力。

而且我開始慢慢覺得:

AI Reliability 不應該只是在研究模型會不會出錯。

拿來研究模型的那套系統,本身也必須值得信任。


上一篇
Day 10|只有 12 筆資料,ECE 到底能不能信?我開始檢查自己的實驗結果
下一篇
Day 12|別只相信我的 JSON:讓 REAL Experiment 自己證明結果沒被改過
系列文
AI Reliability Lab:30 天用 Python 把「AI 好像很準」變成可以量的工程指標 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言