「自我校驗」這句話,關掉再測一次
對 OCR 校驗流程做的對照實驗:把校驗與修復功能逐項關掉來測效果。在 333 個人工標註儲存格上,全部關掉後值準確率從 93.7% 掉到 91.3%,10 個儲存格失去座標。修好 22 個,弄壞 0 個——外加一套可以搬到你自己流程上的測量方法。
「會自我校驗的 OCR」是一句主張,而無法關掉驗證的主張不算測量。所以我們把它關掉了。
這篇文章就是結果:把 space-ocr 引擎裡的校驗與修復功能一項一項關掉,用兩個與引擎無關的正確答案來源打分。數字、打分方法、以及每個訊號究竟保證什麼,都寫在這裡。如果你正在 LLM 之上搭建抽取流程,方法比我們的數字更有用——最後一節講的就是方法。
「和昨天一樣就算對」的陷阱
多數 OCR 回歸測試把「以前跑得好的那一次」保存下來,拿今天的結果和它對比打分。抓變化很好用,但當不了正確性的證據:如果昨天的輸出就是正確的定義,今天的輸出必然和它一致。我們自己的測試也會打出 bbox 318/318 = 1.000,這個數字在測試之外沒有任何意義。我們在內部文件裡把它標為循環指標,不對外引用。
要測校驗是否真的起作用,正確答案必須來自流程影響不到的地方。我們用了兩個:
- 值 ——
ground_truth.json,由人對著原圖手工謄寫的文字,它的產生與引擎輸出無關。 - 座標 —— 頁面自己的 Vision OCR 結果。把落在回傳框內的單詞連起來讀,看是否和回傳的值一字不差。全程不看任何歷史結果。
# The page's own OCR is the judge — no snapshot, no circularity.
def spells(box, vision_words, value):
inside = [
w["text"] for w in vision_words
if box["xmin"] <= (w["bbox"]["xmin"] + w["bbox"]["xmax"]) / 2 <= box["xmax"]
and box["ymin"] <= (w["bbox"]["ymin"] + w["bbox"]["ymax"]) / 2 <= box["ymax"]
]
return norm(value) in norm("".join(inside))
# A returned coordinate is wrong when the words under it do not spell
# the value that coordinate was attached to.
bad = [c for c in boxed_cells if not spells(c["bbox"], words, c["value"])]有意思的是第二個。回傳一個座標,不是說「上個月也回傳過這個框」,而是一句主張:這個值是從這個位置讀到的。這句主張頁面自己就能檢驗,因為整頁都覆蓋著位置已知的 OCR 文字。如果框裡的文字連起來讀和值對不上,這個座標就是錯的,不需要任何歷史結果。
測量用的文件集:11 個真實出過問題的回歸案例,人工標註了 333 個儲存格,其中 317 個帶回座標。測量方式是重播保存好的 Vision 輸出和模型回應,所以結果是確定的——跑十次是同一組數字。這是我們自己刻意收集難件的內部案例集,不是公開基準。下面每個數字都帶著這個前提。
把校驗逐項關掉
引擎裡模型回應之後的校驗、修復環節可以單獨開關,也就是能在模型輸出逐位元組不變的前提下只拿掉某一項。這裡關掉的是三項:
- 單詞比對 —— 模型指出「這個值是從頁面上這些單詞讀來的」,引擎把這些單詞和真實 OCR 單詞核對、確認確實拼得出這個值,才採用其位置
- 座標提示 —— 由另一次模型呼叫給出的「值大概在這附近」的粗略位置,用來在同一個值出現多次時挑出正確的那一個
- 寫法還原 —— 頁面上印的是
2025年09月05日,模型卻改寫成2025-09-05時,把 Vision 讀到的原文原樣還原
| 設定 | 值準確率(對照人工答案) | 帶座標的儲存格 | 框內文字與值一致 |
|---|---|---|---|
| 完整流程 | 312/333 = 93.7% | 317/333 | 292/317 = 92.1% |
| 全部關閉 | 304/333 = 91.3% | 307/333 | 278/307 = 90.6% |
總量會掩蓋方向——「+2.4 個百分點」既可能是修好二十個、悄悄弄壞十個,也可能一個都沒弄壞。所以逐儲存格對照:
| 關掉的環節 | 救回的值 | 弄壞的值 | 救回的座標 | 弄壞的座標 |
|---|---|---|---|---|
| 單詞比對 | 0 | 0 | 10 | 0 |
| 座標提示 | 0 | 0 | 2 | 0 |
| 寫法還原 | 8 | 0 | 2 | 0 |
| 三項全關 | 8 | 0 | 14 | 0 |
修好 22 個儲存格,弄壞 0 個。 整個文件集裡沒有任何一個儲存格因為開啟校驗而變差。這才是我們真正想確認的結果,也比開頭的百分比更有分量:一個修好八個、又悄悄弄壞三個的修復功能,就算均值上升也不該開。
修好的八個值是怎麼回事。 八件全是寫法(分隔符)問題——交貨日期 delivery_date 三件、小計 subtotal 兩件、合計 total 兩件、明細列單價 unit_price 一件。頁面上印的是 2025年09月05日、¥1,451,模型卻寫成了 2025-09-05、1451。數字本身從頭到尾是對的,變的只是寫法。把頁面原文還原回去之所以安全,是因為條件卡得很窄:數字序列必須完全一致、兩側的非數字字元都要在白名單裡、值中間夾著貨幣符號一律拒絕、逗號分組要合理。最初上線時最後這道關卡擋掉了一個候選——修復功能本就該這樣。
標記指向的是真問題嗎
每個帶框的儲存格都會帶回兩個標記:text_verified(框內的 OCR 文字和模型給的值是否一致)與 needs_review(是否需要人看一眼)。自然要問:這些標記會不會真的落在出錯的儲存格上。
先定義問題儲存格:值與人工答案不符,或框內文字與值對不上。333 個裡有 39 個,佔 11.7%。
| 訊號 | 測量結果 |
|---|---|
text_verified: false | 帶此標記的 8 個儲存格裡 6 個(75%)確實是問題儲存格——是隨機抽取(11.7%)的 6.4 倍。兩個引擎對框裡的內容各執一詞時,四次裡有三次那裡真的有問題 |
needs_review | 撈起了 40% 的座標錯誤(10/25)——這是靠歷史結果打分根本無法測量的數字 |
text_verified: false 是精準的那一個:落上的儲存格少,一旦落上就值得看。needs_review 則有意撒得更寬——寧可多裝也不沉默,餵給人工審核佇列的訊號本該如此。
這套校驗保證什麼、不保證什麼
要正確讀這些數字,得先說清它保證什麼。交叉核對保證的是框和文字對得上——你拿到的值確實是從座標指向的那個位置讀出來的。至於讀的是不是正確的欄位——單價有沒有拿成隔壁欄、敬稱算不算名字的一部分——那是語意問題,應該交給結構化輸出之上的 schema 驗證和業務規則。兩層是互補的:座標讓每個值都能被親眼核對,schema 讓整筆紀錄自洽。生產流程兩層都跑。
用線上數字看規模
上面的文件集又小又刻意偏難——這套訊號的價值要在真實流量裡才看得出來。2,165 次生產請求共回傳 9,685 個欄位,97.2% 帶座標。標記確實起到了分流作用:大約五個欄位裡有四個不帶任何複核訊號,可以直接自動接受,needs_review 把人的注意力集中到剩下的那一個上。延遲在具代表性的一天(262 次請求)為 p50 7.2 秒、p90 10.5 秒——這是我們日誌裡觀測到的分布,不是服務承諾(SLA)。
把這套測法搬到你的流程上
值得帶走的不是我們的百分比,而是測量的搭法:
- 別讓昨天的輸出定義什麼是對的。 與歷史執行比對的回歸測試只能告訴你什麼變了,無法告訴你是否正確。
- 去找流程影響不到的正確答案。 值可以人工謄寫。座標——你的 OCR 層已經把整頁文字和位置都給你了,那是給你輸出的每一個框的免費獨立裁判,但幾乎沒人這麼用。
- 按儲存格打分,不要只看平均。 平均會把得失抹平。「修好 22 個、弄壞 0 個」比「+2.4 個百分點」有力得多,能告訴你該不該發布的也是它。
- 固定輸入再重播。 把 Vision 輸出和模型回應保存下來,只切換被測的那一項。對含 LLM 的環節做線上 A/B,測到的多半是 LLM 的隨機波動而不是你的改動。
- 和 schema 驗證搭配使用。 字元級交叉核對保證出處——每個值確實來自座標指向的位置;語意約束(合計對得上、日期在範圍內、必填欄位齊全)保證紀錄本身。經得起生產考驗的流程兩樣都跑。
想親眼看到本文說的這些訊號:POST /ocr/fields 會在每個值上回傳 bbox、vertices、bbox_source、text_verified 與 needs_review,並附上列出被標記欄位的 review_summary。在應用裡怎麼用它們分流自動接受與人工複核,見用邊界框校驗 OCR 結果。
為什麼不拿保存的歷史結果來給座標打分?
「修好 22 個、弄壞 0 個」具體是什麼意思?
帶上 text_verified: false 標記就代表值錯了嗎?
它能取代 schema 驗證或人工複核嗎?
以上所有數字的來源與時點:自建回歸語料 11 個案例、333 個人工標註儲存格、快取重播、2026 年 7 月。線上數字來自生產日誌,2026 年 7 月 15–27 日。不是公開基準,也無法與廠商的基準表相比。