30 · 驗收探針與實驗預先登記:開跑前就把判準釘死
一輪要跑兩小時。跑完之後才決定「這樣算不算成功」,你會挑一個讓結果好看的定義——不是因為不誠實,是因為跑完之後,每一個定義看起來都同樣合理,而你已經知道結果了。
這一篇講兩件互補的事:驗收探針(把成敗變成機器可判的訊號)與預先登記(在結果出來之前把判準、樣本數、停止規則寫死)。附一套實驗紀錄的範本。
驗證狀態:內容來自一段現地場域的 Isaac Sim 6.0 調校(2026-08)的實際做法。統計計算與門檻是那個場景的;⚠ 探針的具體門檻值有幾個是 n=1 校準,文中逐處標明。
1. 為什麼「一個階段驗一次」不夠
舊的驗收是每個階段結束後看一次。三個具體後果:
| 症狀 | 實例 |
|---|---|
| 取貨失敗後在殘局上繼續跑放貨 | 一路跑到底,回頭分不清哪一步先壞 |
| 顆數對但位置錯看不出來 | 兩顆都進去了,但整串偏深 0.24 m,舊驗收全綠 |
| 「這一格到底有沒有被推進去」沒有獨立證據 | 只看被搬物的 x,而那個 x 會被叉齒帶著走而失真 |
第一條是最貴的:失敗之後繼續跑,會把單一根因變成一串無法歸因的現象。閘門要插在動作完成的當下,不是階段結束才驗。
2. 探針量什麼,由機構原理決定
這是設計探針最關鍵的一步,而且它不是「憑經驗設一個容差」。
後推式通道的落點不是自由的:每放一顆都會把整串往內推一格。所以通道裡有 N 顆時,它們必然佔走道側往內數的前 N 個深度槽。
這個推論直接把判準升級了:
舊: --expect 某通道:3 → 「數到三顆就過」
新: --expect 某通道:3 → 「數量 = 3 且槽位集合 = {0, 1, 2}」
預期落點是算出來的,不是量出來的。 從機構原理推得的判準,不需要先跑一堆樣本去統計「正常大概落在哪」,而且它在樣本外一樣成立。
2.1 第二條獨立證據鏈
被搬物的位置會被叉齒帶著走而失真——它「在正確的地方」可能只是因為它還在叉齒上。
所以加了第二個觀測對象:通道裡的板車。板車受軌道約束、不會被叉齒帶走,所以它回答的是另一個問題:「這一格真的被推進去了嗎?」
兩條證據鏈互相獨立,才驗得出「東西到了正確位置」與「東西真的交出去了」的差別。
⚠ 單一判準會被巧合矇混過去。 有一輪的三條判準裡,只有「被叉齒載著」那一條真正抓到問題(被叉齒往內推 0.595 m、翹起 14.5°),另外兩條都因為數值巧合而顯示通過。閘門擋下來了,但擋下的方式很脆弱。
3. 探針設計的四個坑
3.1 查詢範圍 ≠ 場景事實
探針寫死只查兩個群組,於是它報出來的台數是查詢範圍,不是場景裡真的有幾台。
這個錯誤特別危險,因為輸出看起來完全正常——有數字、有單位、沒有錯誤。要避開它,探針要能回報「我查了哪些範圍」,而不只是「我找到幾個」。
3.2 偏離要計入失敗,不能只印註記
原本偏離槽位中心只印一行註記。結果:沒有被 --expect 點名的那條通道,板車跑掉 3 m 也照樣過關。
只印警告不算失敗的閘門,等於沒有閘門——因為沒有人會在幾十輪的 log 裡逐行看註記。
3.3 不要用名稱推對應關係
mainD/t11 與 mainD/t12 不是同一條通道的兩台。名稱看起來有規律,實際上沒有。
歸屬一律用幾何判:通道用 y、層數用 z,不看名字。 這與22 篇的「prim 原點不是功能面」、21 篇的「哪一份能用不寫在檔名上」是同一類錯誤——用命名推語意,而命名不受任何機制保證。
3.4 查不到就明說,不要猜一個值
清單裡沒有來源通道時,探針明說「本趟沒有取貨閘門」,而不是填一個預設值進去。
猜一個值會讓「沒有資料」偽裝成「有資料而且通過」,那是最難查的一種假綠燈。
4. ⚠ 只跑正對照,不算驗過探針
這一條值得單獨拉出來:
一個永遠回 ✅ 的閘門,與一個正確的閘門,在正對照下的輸出完全相同。
驗探針一定要有反對照:餵它一個你知道應該失敗的情境,確認它真的擋下來。沒有這一步,你手上那個「全部通過」的紀錄,同時相容於「系統很好」與「閘門根本沒在判」兩個世界。
4.1 表頭有欄位 ≠ 欄位裡有值
一個真實的例子:記錄腳本的正則式只收兩種標籤,而第三種標籤每一行都在正則這一關被濾掉。
- 產生標籤的函式寫好了 ✅
- CSV 表頭有那 64 欄 ✅
- 報表看起來完整 ✅
- 看門狗不報錯 ✅
- 實測 13632 格 100% 是 nan 🔴
教訓:加欄位之後要驗的是「欄位裡有沒有值」,不是「欄位在不在表頭」。
4.2 門檻是 n=1 校準時,要說出來
有個門檻定在 0.60,校準依據是一次成功 0.417 vs 一次失敗 0.660。
處置不是不用它,而是:先用著,同時每一趟都把峰值寫進紀錄檔,累積出分布之後再定門檻。兩個點可以拿來開始,不能拿來定案。
5. 預先登記:在結果出來之前寫死
預先登記是一份在開跑前寫、寫完就不改的文件。它至少要有五樣東西。
5.1 樣本數 —— 它決定你能問什麼問題
一輪三進三出約 2 小時。這決定了能問什麼、不能問什麼:
| 統計單位 | 一輪貢獻 | 6 輪的樣本 | 6 輪全過時的 95% 信賴下界 |
|---|---|---|---|
| 每次交接 | 6 次 | 36 次 | ≈ 92% |
| 整輪三進三出 | 1 次 | 6 次 | ≈ 61% |
下界的算法是單側二項:0.05^(1/6) ≈ 0.61。
🔴 6 輪買到的統計力幾乎全部在「每次交接」這一層。 「整輪成功率 6/6」聽起來很強,但它只能說「≳61%」——要把整輪那一層推到 ≳90%,需要 約 29 輪 ≈ 58 小時。
結論:報告要分兩層寫,而且整輪那一層要附上這個下界,不要只寫 6/6。
這件事必須在開跑前算,因為它會改變你要跑幾輪、以及你打算宣稱什麼。跑完才算,只能發現「原來我證明不了想證明的事」。
5.2 組態 —— 連「不改變的東西」也要寫死
登記要寫死完整的啟動指令與參數,以及與上一批的唯一差別是什麼。
⚠ 更容易漏的是觀測條件。那次登記明文寫了:「逐幀拍攝維持開著,即使這一批不是為了出片。」
理由是:探針數量會改變 RTF,而 RTF 會改變物理(見 29 篇 §2.4)。關掉拍攝會讓這批的結果不能與上一批相比,而比較正是這批的目的。
觀測手段是實驗條件的一部分。 為了「跑快一點」而關掉某個探針,等於換了一個實驗。
5.3 預期結果 —— 三個互斥分支,寫在結果之前
| 若觀察到 | 則 |
|---|---|
| 失敗集中在長程趟次 | 瓶頸確認是長程的沿齒滑移 |
| 失敗平均散在長短程 | 行程關聯不成立,要重新找變因 |
| 幾乎不失敗(≥34/36) | 先前那 3 筆長程失敗可能是別的共變因,要回頭查那三輪 |
三個分支互斥且窮盡,結果出來只需對號入座。沒有這張表,任何結果都可以被解釋成「支持我的假說」。
實際跑完:三筆失敗全在 ≥6 m,短程 6/6 乾淨——正好是分支一。因為預期登記在前,這個吻合才有證明力;事後才說「你看,果然是長程問題」是沒有力量的。
5.4 判讀規則 —— 包含「數字變差時怎麼讀」
登記裡有這一條:
🔴 不要把「隨機組的成功率低於前一批」讀成退步。 前一批刻意避開長程,兩者測的不是同一件事——這一批的數字才是「任意目的地」的真實值。
在還不知道結果的時候寫下「數字變差不代表退步」,和拿到難看的數字之後才這樣說,可信度完全不同。
5.5 停止規則 —— 事先寫死
- 連續 2 輪在同一階段失敗 → 停下來查,不要把剩下的機時燒完
- 模擬容器重啟 → 該輪作廢
- 磁碟低於 50 GB → 停
- 控制端出現 ABORT → 先查是不是那個已知的非確定性卡死
每一條對應一種故障模式。寫死的目的是避免臨場決定要不要繼續——那個決定會被「已經投入這麼多機時了」汙染。
5.6 判讀由固定工具做,不手算
判定綁在一支腳本上,而且接在「事件發生」上,讓判讀與進度同步發生。手算的判讀會在第 20 輪的時候變得寬鬆,而且沒有人會察覺。
6. 預先登記實際擋掉了什麼
系列進行到一半,發現了一個看起來很有鑑別力的新訊號。登記的處置是:
🔴 這一條不在系列進行中加進判準。 加一條新判準會改變什麼算失敗,那正是事前登記要防的事。
新訊號被列為「系列結束後」的候選,不准立刻套用。
6.1 但修「壞掉的尺」是允許的
同一個系列裡確實改過兩處判讀工具——因為那是同一份資料被印出兩個相反結論的缺陷:一個視圖說「在齒上」,另一個說「乾淨」。不修的話整批都用壞掉的尺去量。
界線寫成一句話:
修壞掉的尺 ≠ 換一把尺。
前者是修復,後者是改變什麼算成功。判斷方法:修完之後,已經跑過的那幾輪要不要重判? 要重判就是換尺,那就該停下來重新登記。
7. 登記本身也會有洞
一個誠實的反例:第一批系列在第 1 輪第 2 趟就結束,36 次交接只拿到 2 次。
原因不是實驗,是計畫漏了一件事:批次腳本原本任何一趟失敗就整批中止。那個行為對「找 bug」是對的——快速失敗、不浪費機時。但對「量成功率」是致命的:分母沒了。
停止規則要跟統計目的對齊。 「快速失敗」與「累積樣本」是兩個相反的需求,而同一支腳本不會知道你這次要哪一個。
8. 實驗紀錄的三層範本
紀錄要分三種,因為它們回答的問題不同。範本在 examples/templates/。
| 檔案 | 記什麼 | 回答的問題 |
|---|---|---|
ROUNDS.md |
每一輪改了什麼、量到什麼 | 「這個參數試過沒有?結果是什麼?」 |
rounds/NN-日期-主題.md |
一段期間的敘事 | 「那幾天到底在幹嘛?當時怎麼想的?」 |
manifests/日期-主題.md |
場景檔本身被改了什麼 | 「這個檔案為什麼跟上週不一樣?怎麼回退?」 |
8.1 ROUNDS.md —— 一輪一列
規則只有兩條,但這兩條決定了它有沒有用:
- 改動欄只寫「與上一輪的差異」,不要重複全部設定
- 結果欄寫可驗證的量測值,不寫感想
| 輪 | 改動(與上一輪的差異) | 結果 |
|---|---|---|
| X0 | 基線,承載高度 1.170 | 承重比 **0.021**;被推走 1.148 m。幾何沒進去 + 被托住 |
| X6 | 承載高度 **1.084** | ✅ 插入時位移 **0.000**、抬升 +77 mm、承重比 ≈1.0、roll 全程 −2.62 |
| r3 | 放貨高度 **1.007**(= 1.084 − 0.077) | ✅ 第一顆推深 1.051 m、第二顆落在 **5.091**(中心 5.100,差 9 mm) |
「承重比 0.021」是可驗證的;「感覺比較穩」不是。差別在於三個月後有沒有辦法拿它跟新的一輪比較。
🔴 這是編年流水帳,不是現況。 每一列寫的是那一天當下的理解,後面的輪次會推翻前面的結論,而且推翻得很頻繁。所以它一定要配一份獨立的「現況」文件,以及一張「已被推翻的斷言」表——否則讀者會拿中段的某一列當成現在的答案。
8.2 rounds/ 分檔 —— 固定四個開頭欄位
一段期間一個檔,開頭固定:
# 0814-0815-質心診斷與首次六趟全過
> `ROUNDS.md` 的一段。**這是編年紀錄,寫的是當時的理解**
> —— 現況見〈一句話現狀〉,已被推翻的斷言見同檔那張表。
**期間**:2026-08-14 ~ 15 **主題**:質心診斷(陰性)、首次六趟全過並錄影
**這一段最重要的一件事**:🎉 六趟全過 —— 第一次完整三進三出,
全程逐幀錄影;而質心診斷是**陰性**。
那個警語每一份都要有。單獨讀到某一份的人只會看到那一份,沒有警語他就會把當時的理解當成結論。
「這一段最重要的一件事」是索引用的——十幾份分檔之後,靠它才找得回東西。
8.3 manifests/ —— 場景檔的變更軌跡
跟 rounds 的分工很清楚:rounds 記「跑了哪一輪、量到什麼」,manifest 記「場景資產本身被改了什麼、怎麼回退」。
固定含六樣:
# manifest — 20260807 地面碰撞 + 輪胎材質 + drive 解開 + 質量
> 工具:`scripts/convert_chassis_to_wheels.py --stage ground,c,mass`
> 計畫:docs/171 | 交接:docs/172
| | sha256 前 24 |
|--------|--------------|
| 改動前 | `a75eec7e8be798c8cff98fde` |
| 改動後 | **`9a7bfedac4b5a4cf9cea4b66`** |
備份:`archive-20260807-ground-mass/WAREHOUSE.usd.bak-before-ground-mass`(127 MB)
回退:`--stage ground,c,mass --revert`,或直接複製備份回原位。
| prim | 舊 | 新 |
|---|---|---|
| `Joint/drive` 型別 | `PhysicsFixedJoint` | **`PhysicsRevoluteJoint`** |
sha256 + 備份路徑 + 回退指令這三樣是重點。二進位的 USD 沒有 diff 可看,沒有這三樣就無法回答「這個檔案為什麼跟上週不一樣」。
9. 檢查清單
探針
- [ ] 閘門插在動作完成的當下,不是階段結束才驗
- [ ] 預期值從機構原理推導,不是從樣本統計
- [ ] 至少兩條互相獨立的證據鏈
- [ ] 偏離計入失敗,不是只印註記
- [ ] 對應關係用幾何判,不用命名推
- [ ] 查不到就明說,不填預設值
- [ ] 跑過反對照:餵一個該失敗的情境,確認它擋得下來
- [ ] 加了欄位之後,驗的是「欄位裡有沒有值」
- [ ] n=1 校準的門檻有標出來,而且在累積分布
預先登記
- [ ] 在結果出來之前寫,寫完不改
- [ ] 算過樣本數對應的信賴下界,知道這批能宣稱什麼
- [ ] 組態寫死,包含不改變的觀測條件
- [ ] 預期結果寫成互斥分支
- [ ] 寫下「數字變差時怎麼讀」
- [ ] 停止規則事先寫死,而且與統計目的對齊
- [ ] 判讀綁在固定工具上,不手算
- [ ] 中途發現的新判準列為「系列結束後」候選,不套用到進行中的系列
延伸閱讀:19 調參實驗的方法論(檢定力、二元判準的統計陷阱)、27 失效模式分類學(分類是判準的下游)、29 長跑才會浮現的兩件事(RTF 是未受控的變因)、20 用 Claude Code 跑調參。