VacantAI 的可究責層GitHub

 · 紀錄與多方執行 · 未複驗

量測工具自己量錯了七次,每一次的方向都對自己不利

互動式 TUI 的 A/B harness 有七個量測錯誤,外加兩條只在程式碼註解裡的。每一個都在真 pi 上抓到,而且每一個的方向都是讓數字長得比實際好看——8 個捏造的結果、5 格提示從未送出。

TUIA/B量測缺陷infra_void自認缺陷

這一篇記的是量測工具自己的錯。

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 變成一個被計數的失敗。 一格什麼都沒發生,跟一格被拒交,意義完全不同。

程式碼註解:

None is 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 格提示從未被送出。

作者記下自己抓錯法兩次:

  1. 用 grep -F "$prompt" 確認——pi 會把提示回顯到對話轉錄裡,所以文字永遠在畫面上,這個檢查永遠過不了。於是他重送 Enter 十次並在每一格都 exit 2——包括那些畫面明明顯示 ↑2.8k ↓34 與正確 provider 的格。那會把 arm A 報成 8/8 submit_failed,而 arm A 其實每次都送出了。
  2. 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 形狀有測試:

  1. 多輪最後通過
  2. 單輪拒絕
  3. 多輪最後拒絕
  4. infra_void
  5. 無閘門(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)」看起來是同一類。但在方法上,它們跟產品缺陷沒有分別——都是「會讓人以為成立、其實不成立」。

命名異議:為什麼把分歧寫進紀錄|R455 的結構性上限|原生平台與靜默無效

相關

閱讀約 10 分鐘

世新大學世新大學資訊傳播學系Vacant

世新大學資訊傳播學系 2026 畢業專題・實體展覽