我把整段驗收交給 Opus,它四次停在『看起來對』


analyzer 是我的交易紀錄分析專案。trader 把實盤紀錄換成新的匯出格式 v2,連歷史資料一起 全量回填;schema 審查和整段驗收,我交給 Opus 自己跑。事後讀它留下的過程日誌,讀到 最後,我對自己說的是這句:

整段都是 Opus 自己處理的。不過整段跑下來後,會讓我有些懷疑 Opus 在處理事情上的正確性。

先把話講清楚:我不在場,也沒有獨立核過。這篇文章能站的證據,是 Opus 自己寫下的日誌, 和 trader 回覆的內容。但正因為它是自述,那四個瞬間才更值得看——日誌裡每一個都是同一個 形狀:證據看起來支持自己,就停了。

一、grep 命中,以為驗證了

Opus 審 schema 提了 10 項意見,其中 A-3 後來證明是錯的。它在 trader 的文件裡 grep partial,找到 partial_1325,對照新 schema 的扁平列表寫的是 partial,就下結論:

匯出層是原樣搬 DB 值,所以 DB 才是真值——schema 這份列表至少有一項是錯的。

trader 回覆指出:partial(pipeline 派生)與 partial_1325(來源 DB)是兩個 adapter 的兩個相異值,不是筆誤;schema 真正的問題是把兩個來源的 enum 混成一張表,來源側還漏列 8 個值。Opus 回頭核策略模組裡 # skip_reason closed enum (must match trade_store / plan D2) 這個區塊,確認 trader 是對的,並在結案文件明確認錯。

它給自己的錯誤歸因:grep 到一個支持自己假設的字串就停了,沒問「這個 enum 會不會是 per-source 的」。命中只證明了字串存在於某處,沒證明它與另一個字串的關係。

二、設計好的驗收,做不到

驗收條件之一是「pipeline 不回歸」。Opus 原本的計畫是最直接的那種:把同一天的 v1 與 v2 直接 diff。實際去拿 baseline:

$ git ls-files data/…/live/ | wc -l
       0
$ cd cronjobs && git ls-files data/…/live/ | wc -l
       0

data/ 是 symlink,兩邊 repo 都沒追蹤 live/,而 trader 的全量回填是原地覆蓋——要比對 的當下,v1 的原始內容已經不存在於任何地方。設計好的驗收,執行時才發現沒有東西可以比。

它只好改用三項間接證據判 PASS:v1 頂層 key 保留、records 與 summary 數值 8/8 相等、 _records_from_pipeline 吃的就是 v1 的 summary 所以構造上同源。判之前它停了一下:要不要 把這條判「無法驗證」而不是 PASS?最後判 PASS,因為第三項是構造性同源,不是統計巧合。但它 把「這個 PASS 背後沒有 diff」明白寫進驗收文件,而不是靜靜放行: 下一個人如果不知道 baseline 已經沒了,會以為那個 PASS 背後有一份比對。

三、差點冤枉一個誠實的揭露

trader 主動揭露:四個交易日的 realized_pnl 因結算腿 fill_price 預設 0.0 而偏低。 Opus 去驗,第一次掃的是 records[].legs:

=== fill_price == 0.0 的腿 ===
  無

零筆。它當下的念頭是「他多列了日期」,準備當成一項發現寫進驗收。錯在哪:records[].legs 只帶進場腿,0.0 的是出場/結算腿,在 v1 頂層的 exit.fills。第二次掃對欄位才對上 ——四個日期的腿數完全一致,無遺漏無誤報。trader 的揭露是對的,而且列得剛剛好。

更前面還有一個更隱蔽的:第一次印 exit block 時,它只截了前 400 字元,看到真實價格,就 先入為主覺得「這幾天看起來正常」。截斷的輸出讀出的結論是假的,而截斷是它自己加的。

四、24 檔自洽,缺 14 天

最後一個,是整份日誌裡最有價值的一段。trader 的完工報告寫的是「24 檔全量回填」,磁碟上 確實有 24 檔——數字自洽。Opus dump sessions 表本來只是要拿兩個指定日期的真值來比對 匯出檔,輸出裡多出一列:

id  trade_date  day_type  entered  observed_cost  trade_seq
1   <日期>      normal    0        <遮蔽>

這是警戒序列的種子列——schema 文件裡自己都提過它。去磁碟看 data/live/<日期>.json:沒有這個檔。這才觸發完整的 DB 覆蓋交叉比對(來源表 distinct 日期 vs 實檔):來源有 31 天紀錄、只匯出 24 檔、缺 14 天。

如果沒有那次為了別的目的 dump 整張表,這 14 天不會被發現——報告說 24,磁碟有 24,每一個 數字都對得上,缺的是「24 這個數字對不對」,而那要拿來源去對才知道。Opus 的教訓原文:

報告裡的數字自洽不代表數字正確。要抓「該有而沒有」的東西,必須從來源列舉,不能從產物清點。

同一個形狀,同一個解藥

四個瞬間,四次都是「證據看起來支持自己,就停了」。而四個解藥,其實是同一件事——回到 第一性:

  • grep 命中的解藥,是問結構不問表面字串:「這個 enum 是 per-source 的嗎?」
  • 截斷輸出的解藥,是看全集,不看樣本;
  • 24 檔自洽的解藥,是完整性從來源推導,不從產物清點;
  • baseline 蒸發的解藥,是在計劃階段就把「事後怎麼證明」當成第一約束。

這不是「agent 不能用」。這四個瞬間我看得到,正是因為 Opus 把每一個都寫進了日誌;A-3 的 認錯、14 天的交叉比對,最後也都是它自己完成並如實記錄的。但這份誠實同時告訴我:沒人盯的 時候,它有四次停在「看起來對」。驗收的責任,不會因為交給 agent 就消失。

我打算試的方向:讓它做計劃時用第一性原理

這個方向,不是我讀日誌當下就有的。讀完日誌,我有的只是開頭那句懷疑——沒有頭緒。是後來 把四個瞬間整理歸納成「同一個形狀」,解藥才跟著形狀浮出來:

還好有今天的整理歸納,我才能想出可能的修正方向。不然,我原本是沒有頭緒的。

方向本身很樸素,還沒實行、也未驗證:

  1. 計劃時從真值出發定義完整性——全集從來源列舉,不接受「產物內部自洽」當驗收證據。
  2. 驗收條件先問「事後怎麼從真值證明」,把可驗證性當計劃的第一約束——該保存的 baseline, 在覆蓋前就保存。
  3. 查證時問結構(per-source?哪一層?哪個欄位?);grep 命中與截斷輸出只當線索,不當結論。

日誌裡的兩個小註腳

兩件事跟主線無關,但都讓我對這份日誌多一分信任。Opus 在文件裡承諾「修好後重跑同一份 驗收腳本確認」,寫完才發現腳本在 session 的 scratchpad 裡,session 結束就沒了——承諾 當場是空的。它把腳本搬進 tools/verify_live_export_v2.py,並把理由寫了出來。另一件: pnl_summary(exclude_suspect=True) 這個預設排除可疑日的設計,它自認擦邊(混進平均會 無聲拉偏統計),補了三個出口,並聲明「這個判斷我到現在仍不確定是對的」。

把不確定寫成不確定,把空的承諾補成真的——這種段落,就是我把它的日誌當證據的理由。

結論

自洽不代表正確。要抓「該有而沒有」的東西,必須從來源列舉,不能從產物清點——這是 Opus 自己的結論,也是我讀完整份日誌之後,最想留下的一句。

再多一層是我自己的:交辦不等於免責,而且疑慮本身不是方向。我讀完日誌時只有懷疑, 是整理歸納讓懷疑變成了可以試的東西。「讀過日誌」不等於「做完功課」——交辦者的事後功課 是整理,不只是讀。