space ocr
指南文章價格文件
developer

「自我校驗」這句話,關掉再測一次

對 OCR 校驗流程做的對照實驗:把校驗與修復功能逐項關掉來測效果。在 333 個人工標註儲存格上,全部關掉後值準確率從 93.7% 掉到 91.3%,10 個儲存格失去座標。修好 22 個,弄壞 0 個——外加一套可以搬到你自己流程上的測量方法。

9 分鐘閱讀· 2026-07-29

「會自我校驗的 OCR」是一句主張,而無法關掉驗證的主張不算測量。所以我們把它關掉了。

這篇文章就是結果:把 space-ocr 引擎裡的校驗與修復功能一項一項關掉,用兩個與引擎無關的正確答案來源打分。數字、打分方法、以及每個訊號究竟保證什麼,都寫在這裡。如果你正在 LLM 之上搭建抽取流程,方法比我們的數字更有用——最後一節講的就是方法。

「和昨天一樣就算對」的陷阱

多數 OCR 回歸測試把「以前跑得好的那一次」保存下來,拿今天的結果和它對比打分。抓變化很好用,但當不了正確性的證據:如果昨天的輸出就是正確的定義,今天的輸出必然和它一致。我們自己的測試也會打出 bbox 318/318 = 1.000,這個數字在測試之外沒有任何意義。我們在內部文件裡把它標為循環指標,不對外引用。

要測校驗是否真的起作用,正確答案必須來自流程影響不到的地方。我們用了兩個:

  • —— ground_truth.json,由人對著原圖手工謄寫的文字,它的產生與引擎輸出無關。
  • 座標 —— 頁面自己的 Vision OCR 結果。把落在回傳框內的單詞連起來讀,看是否和回傳的值一字不差。全程不看任何歷史結果。
座標的裁判 —— 讓頁面來給流程打分
1
2
3
4
5
6
7
8
9
10
11
12
# 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/333292/317 = 92.1%
全部關閉304/333 = 91.3%307/333278/307 = 90.6%

總量會掩蓋方向——「+2.4 個百分點」既可能是修好二十個、悄悄弄壞十個,也可能一個都沒弄壞。所以逐儲存格對照:

關掉的環節救回的值弄壞的值救回的座標弄壞的座標
單詞比對00100
座標提示0020
寫法還原8020
三項全關80140

修好 22 個儲存格,弄壞 0 個。 整個文件集裡沒有任何一個儲存格因為開啟校驗而變差。這才是我們真正想確認的結果,也比開頭的百分比更有分量:一個修好八個、又悄悄弄壞三個的修復功能,就算均值上升也不該開。

✓ Verified

修好的八個值是怎麼回事。 八件全是寫法(分隔符)問題——交貨日期 delivery_date 三件、小計 subtotal 兩件、合計 total 兩件、明細列單價 unit_price 一件。頁面上印的是 2025年09月05日¥1,451,模型卻寫成了 2025-09-051451。數字本身從頭到尾是對的,變的只是寫法。把頁面原文還原回去之所以安全,是因為條件卡得很窄:數字序列必須完全一致、兩側的非數字字元都要在白名單裡、值中間夾著貨幣符號一律拒絕、逗號分組要合理。最初上線時最後這道關卡擋掉了一個候選——修復功能本就該這樣。

標記指向的是真問題嗎

每個帶框的儲存格都會帶回兩個標記: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)。

把這套測法搬到你的流程上

值得帶走的不是我們的百分比,而是測量的搭法:

  1. 別讓昨天的輸出定義什麼是對的。 與歷史執行比對的回歸測試只能告訴你什麼變了,無法告訴你是否正確。
  2. 去找流程影響不到的正確答案。 值可以人工謄寫。座標——你的 OCR 層已經把整頁文字和位置都給你了,那是給你輸出的每一個框的免費獨立裁判,但幾乎沒人這麼用。
  3. 按儲存格打分,不要只看平均。 平均會把得失抹平。「修好 22 個、弄壞 0 個」比「+2.4 個百分點」有力得多,能告訴你該不該發布的也是它。
  4. 固定輸入再重播。 把 Vision 輸出和模型回應保存下來,只切換被測的那一項。對含 LLM 的環節做線上 A/B,測到的多半是 LLM 的隨機波動而不是你的改動。
  5. 和 schema 驗證搭配使用。 字元級交叉核對保證出處——每個值確實來自座標指向的位置;語意約束(合計對得上、日期在範圍內、必填欄位齊全)保證紀錄本身。經得起生產考驗的流程兩樣都跑。

想親眼看到本文說的這些訊號:POST /ocr/fields 會在每個值上回傳 bboxverticesbbox_sourcetext_verifiedneeds_review,並附上列出被標記欄位的 review_summary。在應用裡怎麼用它們分流自動接受與人工複核,見用邊界框校驗 OCR 結果

為什麼不拿保存的歷史結果來給座標打分?
歷史執行的快照把「和昨天一致」定義成正確。它能抓回歸,但回答不了座標對不對。用頁面自己的 OCR 文字當裁判就沒有這個循環——如果回傳框裡的文字連起來讀和回傳的值對不上,那麼不管歷史結果怎麼說,這個框都是錯的。
「修好 22 個、弄壞 0 個」具體是什麼意思?
把完整流程和關掉校驗修復功能的流程逐儲存格對比,有 8 個值和 14 個座標在開啟時正確、關閉時錯誤。沒有任何一個走向相反——不存在關掉反而正確、開著反而錯誤的儲存格。測量於 2026 年 7 月,語料為自建 333 儲存格。
帶上 text_verified: false 標記就代表值錯了嗎?
不是。它表示框內的 OCR 文字與模型給出的值不一致。在我們的語料裡這類儲存格有 75% 確實值或座標出了問題,所以它是很強的複核訊號,但它是一份「兩個引擎意見分歧」的報告,不是判決。
它能取代 schema 驗證或人工複核嗎?
不能——它決定人力該花在哪裡。交叉核對保證每個值確實讀自座標指向的位置,標記把人的注意力集中到訊號不一致的地方。合計對帳、日期範圍、必填欄位這類語意規則,屬於結構化輸出之上的 schema 驗證。生產流程兩層都跑。

以上所有數字的來源與時點:自建回歸語料 11 個案例、333 個人工標註儲存格、快取重播、2026 年 7 月。線上數字來自生產日誌,2026 年 7 月 15–27 日。不是公開基準,也無法與廠商的基準表相比。

相關文章