量測工具自己量錯了七次,每一次的方向都對自己不利
互動式 TUI 的 A/B harness 有七個量測錯誤,外加兩條只在程式碼註解裡的。每一個都在真 pi 上抓到,而且每一個的方向都是讓數字長得比實際好看——8 個捏造的結果、5 格提示從未送出。
這一篇記的是量測工具自己的錯。
commit b0eb0680 的第一句寫得很清楚:
這不是排版修飾。每一條都在真 pi 0.87.1 的 TUI 上抓到,而且每一條的方向都是讓 harness 對自己不利或對 agent 有利,也就是說這些數字原本會長得比實際好看。
「方向一致」是關鍵。如果七個錯誤的方向是隨機的,那是雜訊;如果全部同向,那是系統性的——表示這個 harness 有結構性的問題。
#為什麼互動式 TUI 特別難量
互動式介面沒有 API 要打。要量的東西(閘門橫幅出現在哪裡、模型跑到第幾回合、settle 了沒)全部要從畫面上刮出來。
量法是 tmux 偽終端機:開一個 pane 跑 agent,等它停下來,capture-pane 把畫面抓下來,用正則從裡面找裁決。
這就是七個錯的來源——每一個都在「怎麼從畫面讀出真相」這一層。
#錯誤 1:arm A 跑到別的模型去了
要量「沒裝 Vacant 的 agent 表現如何」,就要有一個 arm 完全沒裝。問題是:模型連線是從設定來的,不是從啟動參數來的。
做法是把 models.json 複製一份到測試環境。但這不會讓 pi 選那個 provider。 沒帶啟動釘選時,pi 用自己的預設。
實測落到 openrouter/moonshotai/kimi-k2.6——不同模型、不同主機,而且沒有一個 byte 經過這一跑的 proxy。
後果是:arm A 把「模型失敗」記成「agent 失敗」。 整個 A 臂的每一格都在量錯的東西。
程式碼註解寫:
⚠ THIS IS THE WHOLE A/B, AND I GOT IT WRONG THE FIRST TIME.
修法:每格記錄 seen_model,報告可以拒絕比較不同模型的格(INVALID 欄位)。
有意思的是,repo 自己在 2026-09-22 就踩過同一個洞,寫在 gateshim.py:578-582。不是新問題,是沒人把兩邊對起來。
#錯誤 2:settle 認錯信號
原規則:B/C 臂一看到閘門橫幅就回報 settled。
問題是 arm C 是唯一跑多輪的臂。第一輪橫幅一出現就回報,然後立刻殺 session。快照抓到的是第二輪還在想——token 凍結在 ↑5.9k ↓79,而第一輪的裁決已經被擠出可見區。
那一格讀出 settled: true 配 gate_accepted: null。
程式碼註解:
兩半都是 harness 在說謊。 不是那一臂在說謊。
修法:抓完整 scrollback,輪詢計數,只在 TUI 回到閒置後才 settle。
#錯誤 3:只讀可見高度
capture-pane 預設只給可見的約 50 行。改用 -S -4000 拿完整 scrollback。
為什麼這條會讓數字變好看:多輪 session 早先的裁決會捲出畫面,而那正是要量的變數。 只讀可見高度等於系統性地只看到最後一個裁決——而最後一個裁決通常比較有利。
#錯誤 4:把前言當成裁決
閘門先印這一行:
Vacant 閘門:驗收來源 dir:tests_visible(…);快照 sha256 …
真裁決長這樣:
✓ 可見驗收全過(2/2)
✗ 拒交裁決(visible_fail,可見驗收 0/2)——互動模式不擋檔案:…
兩個都叫「Vacant 閘門:」,開頭一模一樣,但前者沒有裁決。
原來的正則只認開頭,於是 saw_gate 為真,arm C 在前言後就宣告 settled。每一格回來都是 gate_accepted: null。
這條是整批裡最嚴重的,因為它是全有全無:不是把某一格讀錯,是所有格都讀不出來。
修法:兩處正則都要求標記(✓ / ✗ / ⚠)。
順帶一個括號形狀的坑:拒交橫幅寫的是 可見驗收 0/2)——逗號+空格,沒有左括號(左括號花在 stop reason 上了),通過橫幅才是 可見驗收全過(2/2)。只照通過那個形狀寫正則,所有拒交都會 parse 成 None。
#錯誤 5:infra_void 被記成拒交
bool(None) 在 Python 是 False。所以:
gate_accepted = None (閘門沒有裁決)
↓
False (記成「裁決拒絕」)
這是 repo 自己 infra_void 規則明文禁止的事:把 void 變成一個被計數的失敗。 一格什麼都沒發生,跟一格被拒交,意義完全不同。
程式碼註解:
Noneis a real value here, not a missing one.
#錯誤 6:同一筆記錄裡兩個欄位互相矛盾
先用「最後一次通過」設 visible,再被「最後一次拒絕」覆蓋。
後果:最後一輪是 2/2 全過的格,報 1/2。
修法:由最後一個橫幅一次決定。
這條比較小,但方向一樣——把比較悲觀的結果報出來。
#錯誤 7:停滯被算成 agent 失敗
實測簽章顯示:模型呼叫不存在的工具 ls_r、退回 shell、然後不再產出。而 backend 本身是健康的(緊接著 2 秒回 say ok)。
這是 void 不是失敗:模型呼叫失敗既不是對答案也不是錯答案。
程式碼註解:
A failed model call is neither a right answer nor a wrong one; it is a void, and a void is re-run, not counted.
修法:重跑乾淨 workspace,永不計分,並記錄 attempts/voids。MAX_ATTEMPTS = 3。
#兩條只有程式碼註解裡的
這兩條 commit message 沒列,我是在讀 ab_harness/tui_ab.py 的註解時發現的。
#arm A 的 8 格全部是捏造的結果
pi 的狀態列在沒送出時是 0.0%/262k,開始後才有 ↑N ↓M。
在 24 格批次裡 arm A 的 8 格全部整輪停在 0.0%:提示從未被送出,TUI 就坐著不動。而舊的 arm-A settle 規則(「idle 4 polls」——立刻成立)把它們全部記成 settled + hidden_pass: false。
程式碼註解:
那是 8 個捏造的結果。 The only thing that would have caught it is this check.
arm A 的每一格都是錯的——不是數字偏差,是格子本身不存在。
#Enter 掉了 6 格,而且我抓錯了兩次
pi 開機會印:
Warning: tmux extended-keys is off. Modified Enter keys may not work.
第一個 24 格批次裡 Enter 掉了 6 格,形狀都一樣:提示留在輸入框、狀態列 0.0%/262k,沒有回合、沒有閘門、沒有裁決。那些格被記成 timeout。
這就是 arm B 看起來 void 掉 8 格裡的 5 格的原因,而真相是 8 格裡的 5 格提示從未被送出。
作者記下自己抓錯法兩次:
- 用
grep -F "$prompt"確認——pi 會把提示回顯到對話轉錄裡,所以文字永遠在畫面上,這個檢查永遠過不了。於是他重送 Enter 十次並在每一格都exit 2——包括那些畫面明明顯示↑2.8k ↓34與正確 provider 的格。那會把 arm A 報成 8/8submit_failed,而 arm A 其實每次都送出了。 tmux set -g extended-keys on——量到關 3/3 送出、開 3/3 送出,從來不是問題,而且它會改變 Enter 的編碼。
程式碼註解:
both recorded because the second one is worse than having no check at all
第二種錯法比沒有檢查更糟:它會把對的答案報成錯的。
#修完之後
read_verdict 對五種真實 pane 形狀有測試:
- 多輪最後通過
- 單輪拒絕
- 多輪最後拒絕
infra_void- 無閘門(arm A)
ruff 乾淨。
但那個批次的數字不能用——A 臂 8 格是捏造的,B 臂 5 格提示從未送出。修好之後要重跑。
遠端 vacant-dev 在 3 個 arm-A 格之後整台不可達(port 22、ICMP、tailscale ping 全無回應,同網段其他機器正常)。「那些格沒有備份,恢復後要先拉回來。」
#我從這七條學到什麼
這一段是我的判斷。
1. 同向的錯誤才是重點。
七條裡如果方向是隨機的,那是雜訊。但「每一條都讓數字長得比實際好看」表示結構性問題——具體說,是這個 harness 系統性地偏好「看起來有東西發生了」而不是「看起來沒東西發生」。
這在爬蟲上是個通病。抓不到東西的時候,人(和程式)傾向於記成「失敗」而不是「沒發生」,因為前者感覺起來像有資料。
2. 「靜默無效」比「效果較小」更該被當成錯誤回報。
如果 W2 那條(opencode 沒有 stop 事件)被讀成「追緝在 opencode 上效果較小」,那是一個可以接受的結論,沒有人會去修。
但實際上是 37 個 step 的完整簽章病歷、零個裁決、沒有 delivery.md。看起來完全正常。
37 個 step 的完整簽章病歷、零個裁決。這比不做還危險,因為 receipt 會看起來是對的。
3. 量測工具的 bug 應該跟產品的 bug 同一個標準管。
這七條在 commit message 裡是「eval(tui)」,跟「feat(eval)」看起來是同一類。但在方法上,它們跟產品缺陷沒有分別——都是「會讓人以為成立、其實不成立」。

