iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

從 LLM 到 Harness: 打造隱私與可信任的繁中進階 OCR Agent系列 第 8

Day 8 - 繁中挑戰總結:這些問題怎麼倒推出系統設計

  • 分享至 

  • xImage
  •  

兩天前我說要把繁中的難點攤開,現在攤完了

桌上有一堆壞掉的東西:位置跑掉的貨幣符號、被吃掉的總數、讀成「線」的「綠」、重複一百次的材料名、以及三個誠實的 0

今天要做的事只有一件:把這堆東西,一條一條翻譯成設計決策。

而且我要先回答那個從 Day 1 就懸著的問題——這些,換一顆更大的模型不就好了嗎?


先把兩天的難點收成一張清單

Day 6 和 Day 7 一共列出九條。先編號,今天整篇都會用這些編號:

編號 難點 實測證據 哪天
N1 模型自加結構描述,輸出膨脹 2–3 倍 page_02 的四個方位標籤;九頁全部有敘述性開場白 Day 6
N2 中英數交界處內容搬移 page_05 的 NT$ 從數字右邊搬到左邊 Day 6
N3 關鍵數字被吃掉 page_05 的「生產基地24個」整個不見 Day 6
N4 形近字誤判被語言先驗修飾成通順文字 page_24:綠→線、阻燃→防燃、載具→製具、補強→補償 Day 6
N5 表格欄位對應垮掉,但字元指標好看 page_24 三欄變三批,覆蓋率仍有 0.9437 Day 6
N6 低資訊量區域誘發重複退化 page_19 finish_reason=length,一串重複上百次 Day 6
N7 GT 文字層本身就漏抽 page_02 底紋、page_19 圖表標籤 Day 6
N8 繁簡與異體字讓字串比對失去意義 實測 0 次,但 Day 25 飛輪與 Day 17 XBRL 校驗必炸 Day 7
N9 規則式分類器的未知率高 87 個 diff 塊有 37 個落進 other,約 43% Day 7

九條裡有一條特別怪:N8 的實測計數是 0

我還是把它放進來了,理由昨天講過——它在 OCR 階段不炸,在比對階段炸。而我的整個可信任機制建立在比對上


關鍵問題:換一顆更大的模型能解嗎?

這是 Phase 2 收斂到 Harness 的核心論證,所以我要慢慢講

把九條難點按「性質」重新分三堆:

第一堆:規模真的會改善的

難點 為什麼規模有效
N4 形近字誤判 更大的模型看過更多字形、更多語境,判別力真的會提升
N6 重複退化 更強的模型比較不容易陷入自迴圈(但不保證,這也跟解碼參數有關)

這兩條我承認:換模型會變好。

但注意「變好」不等於「解決」。N4 的性質是「錯了你看不出來」——一顆準確率從 95% 提到 98% 的模型,只是把「每頁 5 個你看不出來的錯」變成「每頁 2 個你看不出來的錯」

2 個看不出來的錯,跟 5 個看不出來的錯,在財報場景裡是同一件事:不能用。

而且 Day 3 已經把這條路堵了一半。GB10 的記憶體頻寬是硬上限,模型越大、每個 token 要搬的權重越多、吞吐越低。我實測 48.5 tok/s 的那顆模型,換成兩倍大的,速度會掉到不能接受的區間——而且塞不塞得進記憶體都是問題

所以第一堆的正確講法是:規模有效,但天花板不夠高,而且我付不起。

第二堆:規模沒用,因為問題不在模型裡

難點 為什麼規模無效
N7 GT 文字層漏抽 錯的是答案不是模型,模型再強也不會讓 PyMuPDF 抽到底紋
N8 繁簡異體字比對 這是「相等定義」的問題,發生在模型輸出之後
N9 分類器未知率 43% 這是我的規則寫得不夠,跟模型無關

這三條最容易被忽略,因為它們長得不像模型問題

N7 尤其致命。Day 6 的 page_02,模型把背景底紋的化學元素表讀出來了,而 GT 沒有——在我的 CER 表上,這被算成模型的錯

你換一顆更強的模型,它會把底紋讀得更清楚,然後 CER 更高

模型越強,指標越難看。 這種評測系統已經壞了,而且壞在你看不到的地方

第三堆:規模沒用,因為問題在「單次前向傳播」這個形式裡

這是最重要的一堆:

難點 為什麼規模無效
N1 自加結構描述 模型不知道你要的是純文字還是結構描述,它在猜你的意圖
N2 內容搬移 模型按視覺順序讀,GT 按 PDF 物件順序存,兩邊都對,沒有共同標準
N3 關鍵數字被吃掉 模型輸出時沒有任何「必須包含總數」的約束
N5 欄位對應垮掉 模型必須同時做版面理解與文字辨識,兩件事在一次生成裡搶容量

這四條共同的性質是:問題不是「模型不知道正確答案」,是「模型不知道自己輸出對不對」

N3 就是最乾淨的例子。模型把生產基地列成 bullet list,七個國家的數字 5+13+1+1+2+1+1 加起來剛好是 24——答案就在它自己的輸出裡,但它沒有任何機制去檢查這件事

一顆大十倍的模型會不會剛好記得輸出「24個」?

可能會。但它還是不會檢查。它只是碰巧對了

碰巧對的系統,跟會檢查的系統,是兩種東西。

這就是 Day 5 結尾那句話的完整版:

你要的東西不是一個更準的模型,是一個會發現自己錯了的系統

Day 5 是從硬體限制推到這句話,今天是從繁中難點推到同一句話。兩條路,同一個終點。

💡Tip: 我覺得判斷「該換模型還是該改架構」有一個很實用的準則:看錯誤是不是安靜的。會噴錯、會超時、會輸出亂碼的問題,通常換模型或調參數有救;而「輸出看起來完全正常但內容是錯的」這類問題,幾乎都要靠架構——因為你要加的是一個獨立的檢查環節,而不是一個更好的猜測者。


難點 × 設計決策對照表

好,正題。把九條難點對應到後面真的會出現的設計:

編號 難點 對應設計 出現在
N1 模型自加結構描述 結構化輸出約束 + Checksum 驗證 Day 22
N2 中英數交界內容搬移 Region Routing:位置先鎖住再送模型 Day 18
N3 關鍵數字被吃掉 跨欄位一致性檢查(總數 = 明細加總) Day 16
N4 形近字誤判被修飾成通順文字 Tesseract 交叉驗證 + Logprob 門檻 + MOPS XBRL 對帳 Day 22、Day 17
N5 表格欄位對應垮掉 Docling 版面偵測 + 純文字區/表格區分流 Day 18、Day 10
N6 重複退化 獨立 verifier 檢查 finish_reason 與重複率,觸發重試 Day 11、Day 23
N7 GT 文字層漏抽 不依賴 GT 的驗證:MOPS XBRL 當外部 ground truth Day 17
N8 繁簡異體字比對 比較鍵(fold)進入 diff 對齊層 Day 25
N9 分類器未知率 43% JSONL 記錄 + 2–5% 抽樣人工稽核補規則 Day 26、Day 27

九條難點,對應到七個不同的設計環節

注意沒有任何一條的解法是「換更大的模型」。

下面逐條走一遍,講清楚為什麼是那個設計。

① N2 / N5 → Day 18 的 Region Routing

N2 和 N5 的共同根因是閱讀順序

page_05 的 NT$ 搬家、page_24 的三欄變三批——兩個都是「模型自己決定該從哪讀到哪」造成的

而現在主流的做法(整頁圖丟進 VLM)根本沒有給模型任何位置約束。你把一張二維的版面壓成一維的 token 序列,順序由模型猜

Day 18 要做的事很直接:在送進模型之前,先用 PyMuPDF 抽文字層、用 Docling 做版面偵測,把頁面切成有座標的區塊,再逐區塊送模型

這樣模型每次只看一個區塊,它不需要猜順序——順序是我給的

代價是什麼?更多次推論請求。 一頁從 1 次變成 5 到 10 次,延遲直接乘上去。Day 10 會實測純文字區與表格區的速度差(50 tok/s 對 7 tok/s 那個級距),這個代價是真的,而且很痛

但這個代價換到的是一個結構性的好處:N2 那種「兩邊都對但順序不同」的問題會直接消失,因為區塊的座標就是共同標準

② N3 → Day 16 的跨欄位一致性檢查

「生產基地 24 個」被吃掉,而 5+13+1+1+2+1+1 = 24

這是我最喜歡的一個案例,因為它證明了一件事:有些錯誤不需要正確答案就能抓出來

你不需要 GT、不需要 XBRL、不需要另一顆模型。你只需要一條規則:如果輸出裡同時有總數和明細,兩者必須相等

Day 16 要建的就是這類規則庫:

規則類型 例子
加總一致 各期明細加總 = 合計欄
跨頁一致 同一個日期在封面、內頁、附註要一致
名稱一致 簽約對象全名在全文出現多次時必須相同(這裡要用 Day 7 的 fold)
格式合理 百分比在 0–100、金額非負(除非明確標示括號負數)

這層的性價比高到不合理——幾乎沒有推論成本,卻能抓到 N3 這種字元級指標完全抓不到的錯

而且它抓到的是「語意層」的錯,不是「字元層」的錯。 這條線後面 Phase 4 會整個展開

③ N4 → Day 22 的四重驗證

形近字誤判是最難的一條,因為它沒有任何表面徵兆

「阻燃」變「防燃」,兩個都是合法中文詞、都放在合理位置、模型的 finish_reasonstop、格式完全正常

要抓它,我目前想到的只有一條路:用一個不共享同樣先驗的東西去對照

Day 22 的四重驗證就是在做這件事,四層各自對付不同的東西:

驗證層 抓什麼 對 N4 有效嗎
Checksum(結構化輸出驗證) 欄位缺漏、型別錯誤、格式不合 無效
Logprob 門檻 模型自己也不確定的位置 部分有效——形近字誤判時,兩個候選的機率常常很接近
Tesseract 交叉驗證 傳統 OCR 沒有語言模型先驗,不會把「阻」修飾成「防」 有效
MOPS 領域語意 Verifier 用官方 XBRL 對帳公司名、科目名、金額 對關鍵欄位有效

第三層特別有意思。傳統 OCR(Tesseract)在純辨識準確率上比不過 VLM,但它有一個 VLM 沒有的優點:它不會用語言模型去「潤飾」結果

它看到什麼字就輸出什麼字,看不清楚就輸出亂碼或空白。它的錯誤是吵的,不是安靜的

所以拿一個比較弱但誠實的模型,去對照一個比較強但會修飾的模型——兩者不一致的地方,就是要人看的地方

這是 Day 11 那個原則的具體應用:驗證者不能跟被驗證者共享同一套偏誤

④ N6 → Day 11 的獨立 verifier 與 Day 23 的重試

page_19 的重複退化有個好消息:它抓起來很簡單

finish_reason == "length",或是「輸出裡最長重複子串的出現次數超過門檻」——兩個都是幾行程式碼的檢查

問題在於誰來檢查

如果你讓生成的模型自己檢查自己的輸出,它會說沒問題。這不是模型笨,這是機制問題:產生那段重複文字的機率分布,跟評估那段文字的機率分布,是同一個分布

Day 11 要做的就是把 verifier 拆成獨立角色(用 gpt-oss:20b),而 Day 23 要定義的是「檢查不過之後怎麼辦」——重試?換參數重試?降級?還是標記為需人工處理?

page_19 這頁的正確處置其實很明確:max_tokens 拉高再重試一次,如果還是撞牆,就標記人工

但這個處置邏輯必須寫在流程裡,不能靠我盯著看。這就是 Pipeline 跟 Harness 的差別,Day 19 會專門講

⑤ N7 → Day 17 的 MOPS XBRL 當外部 ground truth

N7 是整個系列裡我覺得最有價值的一個發現:我的答案本身是錯的

page_02 的底紋、page_19 的圖表標籤——PyMuPDF 抽不到,但模型看得到。在我的 CER 表上,模型被冤枉了

這個問題沒有辦法靠改進 OCR 解決,因為錯的不是 OCR

Day 17 的解法是換一個完全不同的 ground truth 來源:MOPS 本身就提供 XBRL 格式的官方結構化財報

這意味著對於財報數字這類關鍵欄位,我有一份跟 PDF 同源、但完全獨立於 PDF 抽取流程的答案。它不會因為底紋是圖片就漏抽,因為它根本不是從 PDF 抽出來的

當然 XBRL 只涵蓋結構化財務數字,不涵蓋敘述性文字與圖表。所以它不是萬用解,它是關鍵欄位的高信度校驗源

而這正好對應到 Day 16 那個「關鍵欄位」的概念:不是所有欄位都值得付出同樣的驗證成本

💡Tip: 「找一個獨立的 ground truth 來源」這件事,值得你在任何評測專案的一開始就花時間想。我一開始用 PyMuPDF 文字層,是因為它免費、自動、零標註成本——這些優點都是真的,但我花了到 Day 6 才看清楚它的代價。如果你的 ground truth 跟你的待測系統共享同一個上游,你量到的東西會比你以為的少很多。

⑥ N1 → Day 22 的結構化輸出約束

模型自己加「這張圖片中包含的文字內容如下:」這種開場白,九頁全中

解法其實很無聊:不要讓它輸出自由文字,讓它輸出 JSON

Day 22 會用 Pydantic 定義 schema,模型輸出必須能被 parse 成那個 schema,parse 不過就是失敗。這樣「敘述性開場白」這種東西在結構上就沒有位置可以放

但這裡有個取捨要講清楚:結構化輸出會讓模型變笨一點

當你要求模型同時做辨識、又要維持 JSON 格式、又要填對欄位,它的容量被分走了。這在 Day 10 講速度取捨的時候會有具體數字

所以 N1 的解法不是純粹的勝利,是一個交易:用一點辨識品質,換一個可機器驗證的輸出形態

而我選擇做這個交易的理由很簡單:不可驗證的高品質輸出,對一個要求可信任的系統來說是零分。

⑦ N8 / N9 → Day 25 的飛輪與 Day 26/27 的記錄與稽核

最後兩條是關於這套系統怎麼變好

N8 是折疊層要進 diff 對齊。Day 25 的 flywheel 核心是「把模型輸出跟參考答案 diff,把 diff 變成校準燃料」——如果台/臺這種差異會產生假陽性,那飛輪會被假訊號餵成一個奇怪的形狀

所以 Day 7 那個 fold() 函式,它真正的部署位置不是 OCR 後處理,是 diff 對齊層內部

N9 則是承認一件事:我的規則不夠,而且我知道它不夠

87 個 diff 塊有 37 個落進 other。這 43% 就是我今天所有結論的不確定區間

要把這 43% 降下來,靠的不是想得更周全,是看更多真實案例。Day 26 要設計 JSONL/SQLite 的記錄 schema 把每一筆 diff 存下來,Day 27 要設計 2–5% 抽樣人工稽核把 other 裡的東西撈出來分類

規則不是設計出來的,是從 other 桶裡長出來的。

💡Tip: 我建議任何規則式分類器都要把 other 的比例當成一個一級指標,跟主要指標並排放在儀表板上。它就是你的「已知的未知」。而當它開始下降,代表你的規則在追上現實;當它開始上升,代表現實變了(換文件類型、換模型、換 prompt),該回頭看了。


那「調 prompt」呢?

講完「換更大的模型」,還有一個更常見的反駁我得處理:這些難道不是 prompt 沒寫好嗎?

平心而論,這個反駁對其中幾條是成立的

N1(模型自加結構描述)就是最明顯的一條。我的 baseline prompt 逐字是:

請完整辨識這張圖片中的所有文字,保留表格結構。

沒有輸出格式約束、沒有「不要加任何說明文字」、沒有 few-shot 範例

我如果加一句「只輸出辨識到的文字,不要加任何說明、標題或註解」,九頁裡那九段開場白大概率會少掉一大半

同樣地,N6 的重複退化,我如果把 max_tokens 從 1500 調到 4096,page_19 那 15.4 的 CER 馬上會變成一個正常得多的數字

所以我要老實說:這兩條有很大一部分是我的 baseline 設定造成的,不是模型的錯。

那為什麼我不改?

因為 Day 2 的那把尺要能一直用下去。 Day 6 新測四頁的時候我刻意沿用同一組參數——同一個 endpoint、同一顆模型、同一段 prompt、同樣的 max_tokens=1500、同樣的 temperature=0——為的是讓九頁的數字可以放在同一張表裡比

一旦我在 Day 6 改了 prompt,前五頁跟後四頁就不可比了。而 Day 2 那五頁的數字,是後面 22 天每一次架構改動的對照組

變數控制的價值,高於單次結果好看。

但更重要的是第二個理由:prompt 能解決的那幾條,恰好是最不嚴重的那幾條

回頭看那三堆分類:

難點 prompt 能解嗎 嚴重性
N1 自加結構描述 大部分能 低——輸出膨脹很吵,但很好發現
N6 重複退化 (調 max_tokens 低——finish_reason 直接標出來了
N2 內容搬移 部分能(要求保留原始順序)
N5 欄位對應垮掉 部分能(要求輸出 markdown 表格)
N3 關鍵數字被吃掉 不能
N4 形近字誤判 不能 最高
N7 GT 漏抽 不能(問題不在模型端)
N8 繁簡異體字比對 不能(發生在模型之後)

prompt 能解的,全部集中在「吵的錯誤」;prompt 解不了的,全部是「安靜的錯誤」。

這個分布不是巧合。prompt 是在調整模型的行為傾向,而安靜的錯誤之所以安靜,正是因為模型的行為完全正常——它沒有偏離你的指示,它只是讀錯了一個字然後很自信地寫下來

你沒辦法用 prompt 叫一個模型「不要犯你自己不知道自己在犯的錯」

💡Tip: 我會這樣排優先順序:先把 prompt 跟參數這種便宜的手段用到盡,因為它們幾乎零成本;但同時要很清楚它們的天花板在哪裡。判準還是同一條——如果一個錯誤會自己喊出來(報錯、超時、截斷、格式壞掉),先試便宜的手段;如果它安靜地混在正確答案裡,那就準備動架構。


三件我現在還不知道怎麼解的

倒推設計講到這裡都很漂亮,但有幾條我手上還沒有答案,先標記起來,免得後面自己騙自己

第一,N4 的形近字誤判,我沒有把握四重驗證真的夠。

Tesseract 交叉驗證的想法是對的——它不共享語言模型先驗。但 Tesseract 在中文小字級上的表現本來就不好,它很可能在 page_24 那種密度下噴出一堆自己的錯誤,反而製造大量假陽性

真正的答案可能是「兩者不一致的地方一律標記人工」,但那樣的人工量是多少?我現在不知道,要等 Day 22 實測。

第二,Region Routing 的代價我還沒量。

一頁切成 5–10 個區塊逐塊送模型,聽起來很美好,但延遲會直接乘上去。Day 3 那條記憶體頻寬的天花板還在那裡

如果一頁從 4 秒變成 30 秒,那一份 200 頁的年報要跑一個半小時——這個數字能不能接受,取決於使用場景,而我還沒定義清楚那個場景。 Day 10 會先量純文字區跟表格區的速度差,那是這個問題的第一塊拼圖

第三,N7 那個「GT 本身是錯的」問題,XBRL 只解了一部分。

XBRL 涵蓋結構化財務數字,不涵蓋敘述性文字、不涵蓋圖表標籤、不涵蓋重大契約摘要裡的合約條款

而 page_19 那張散點圖上的材料標籤,XBRL 幫不上任何忙。那類內容的 ground truth 要怎麼來,我目前的答案是「人工標,而且只標抽樣到的那 2–5%」——這是 Day 27 的範圍,但我要先承認這是一個妥協,不是一個解法

💡Tip: 我覺得技術文章最容易失真的地方,就是「倒推設計」這種回頭看的敘事——因為你已經知道答案了,所以每一步都顯得必然。實際上我寫這三天的時候,有至少三個地方是「我知道有問題但還不知道怎麼解」。把那些地方標出來,讀者才知道哪些是結論、哪些是待辦。 而且三個月後的我會感謝我自己。


這張表其實只在講一件事

九條難點、七個設計環節,如果要收成一句話:

每一條難點,對應的都是「加一個能產生回饋訊號的環節」。

  • Region Routing 產生的回饋是位置
  • 一致性檢查產生的回饋是內部矛盾
  • Tesseract 交叉驗證產生的回饋是兩個系統的分歧
  • XBRL 校驗產生的回饋是外部事實
  • finish_reason 檢查產生的回饋是生成過程的異常
  • 抽樣稽核產生的回饋是人的判斷

而 Day 1 引用過的 Harness 定義是:

讓 agent 根據目標,持續、正確地動作的工程。核心材料是「回饋訊號」。

所以 Phase 2 這三天,表面上在講繁體中文的字型跟版面,實際上在做一件事:把「這個系統需要哪些回饋訊號」這個問題,用第一手實測數據回答出來。

如果我 Day 6 就直接開始畫架構圖,我畫出來的會是一張抄來的架構圖。而現在這七個環節,每一個都指得出它是為了哪一條實測難點而存在的

這就是「倒推」的意思


Phase 2 收斂:這三天的產出

產出 內容 後面誰會用
四維難度評分函式 跨欄表格/罕用字/中英混排/小字註腳,全 40 頁分數落檔 Day 27 的抽樣設計
新測 4 頁的 OCR 結果 CER 1.4815 / 0.6885 / 15.4000 / 1.0085 所有後續架構改動的對照組
9 頁錯誤分類統計 8 類別逐頁次數,other 未知率 43% Day 26 的 schema 設計
9 條難點清單 N1–N9,每條都有實測證據 今天這張對照表
比較鍵 fold() 保守的異體字折疊,只影響相等判定 Day 25 的 diff 對齊層
三份誠實的限制說明 樣本乾淨、對照表不完備、GT 抽取瑕疵 每一次引用這些數字的人

最後一列我想多說一句

這三天我寫進文件的「限制」比「結論」還多:dense_table 最高分只有 0.1429 所以沒測到真表格、字型變體與直橫混排沒有樣本、page_19 的 CER 被 finish_reason=length 污染、三個類別的 0 有一個確定是假的

這些不是免責聲明,是適用範圍。

一個沒有標明適用範圍的結論,跟沒有結論是一樣的——因為下一個人不知道什麼時候該停止相信它


今天的結論

  • Day 6 與 Day 7 攤出九條難點(N1–N9),每一條都有落檔的實測證據,沒有一條是我想像出來的
  • 這九條裡,只有兩條(N4 形近字、N6 重複退化)換更大的模型會改善,而且只是改善,不是解決——而 Day 3 的記憶體頻寬上限已經讓「換更大的模型」這條路變得很貴
  • 另外七條,問題根本不在模型裡:有的在 ground truth(N7)、有的在比對定義(N8)、有的在我的規則(N9)、有的在「單次前向傳播沒有檢查環節」這個形式本身(N1、N2、N3、N5)
  • 九條難點對應到七個設計環節:Day 10 區塊分流、Day 11 獨立 verifier、Day 16 欄位一致性、Day 17 MOPS XBRL 校驗、Day 18 Region Routing、Day 22 四重驗證、Day 25 diff 飛輪
  • 這七個環節的共同性質是:每一個都在產生一種回饋訊號。而回饋訊號正是 Harness 的核心材料
  • Phase 2 的真正產出不是「繁中很難」這個結論,是一份「這個系統需要哪些回饋訊號」的需求清單,而且每一項需求都指得回一個具體的實測案例

架構不是設計出來的,是被錯誤逼出來的。

你的架構有多好,取決於你有多認真地看過自己壞掉的樣子。

Phase 2 到這裡收掉

明天進 Phase 3,第一個問題是選模型:為什麼是 Gemma 4?

而且我不會從「它很強」開始講。我會從 26B MoE 跟 31B Dense 這兩種架構的差別開始——因為今天這九條難點裡,有幾條是吃記憶體頻寬的、有幾條是吃推理深度的,而這兩種架構在 GB10 這台機器上的表現,差得比你想的多

明天見 👋


上一篇
Day 7 - 繁簡混排與異體字:正規化前先搞懂差異
系列文
從 LLM 到 Harness: 打造隱私與可信任的繁中進階 OCR Agent8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言