iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI Engineering

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

Day 2 - 單一模型 baseline:Gemma 4 直接跑整頁會怎樣

  • 分享至 

  • xImage
  •  

昨天我說,先讓你看它壞掉。

今天直接跑這個測試。

規則很簡單:不搭架構。一頁圖丟進去,叫它把字讀出來。

沒有版面偵測、區塊分流、前後處理,也沒有驗證。

這是刻意保留的最低限度做法。

我要記錄的,是它會在哪裡失手。


為什麼要做 baseline?

有個觀念常被跳過,偏偏很容易讓後面的優化走歪。

你如果直接上一套很完整的架構,它跑出 92% 的準確率

那 92% 到底是誰的功勞?

是模型本身就有 90%,你的架構只加了 2%?
還是模型只有 60%,你的架構硬拉了 32%?

沒有基線,就沒辦法歸因。

歸因一錯,後面 30 天可能都在優化錯的地方。

所以今天的產出不是「一個很爛的結果」,而是一把尺。之後每次調整架構,我都會回頭用今天的數字比對。

這條原則在 Day 1 的量化(先跑 BF16 再量化)和 Day 25 的 flywheel 還會出現;整個系列都會用同一條基線做比較。


實驗設計

測試文件

我用真實的 MOPS 文件,不是自己造的假資料:

項目 內容
來源 公開資訊觀測站(MOPS)
網址 https://mopsov.twse.com.tw/nas/STR/171720260319M001.pdf
內容 永光化學(1717)2025 Q4 法說會簡報
頁數 40 頁
取得方式 直接下載,免登入、免爬蟲

Day 1 已經說過為什麼選 MOPS。補一個很實際的理由:它能直接用 curl 下載。我原本以為會碰到驗證碼或 session 檢查,實測沒有,這段流程因此能自動化。

頁面選擇

40 頁裡我挑了 5 頁,刻意選到三種版面:

類型 文字層字數
1 純文字 179
9 圖文混排 409
11 表格 523
12 表格 324
14 圖文混排 256

Ground truth 怎麼來?

這是整個實驗最重要的設計決定。

我用 PyMuPDF 抽出的 PDF 原生文字層當對照答案:

import pymupdf

doc = pymupdf.open("test1.pdf")
page = doc[11]
gt_text = page.get_text()          # 原生文字層
pix = page.get_pixmap(dpi=200)     # 同一頁渲染成圖
pix.save("page_11.png")

同一頁,一邊抽文字層當答案,另一邊渲染成圖丟給模型。

好處是不用人工標註;缺點是文字層本身也不完美。它的閱讀順序常跟視覺版面不同,表格還會被拉成一長串。這會直接影響後面怎麼判定模型是否真的出錯。

Baseline 的 prompt

我刻意把 prompt 寫得很直白,沒有做 prompt engineering:

你可以這麼寫:

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

就這樣。沒有 few-shot、輸出格式約束,也沒有「你是一個專業的 OCR 專家」。

今天要測的是模型的裸能力,不是我的 prompt 功力。

請求長這樣

標準的 OpenAI 相容格式,圖片走 base64 data URI:

payload = {
    "model": "gemma-4-26b",
    "messages": [{
        "role": "user",
        "content": [
            {"type": "text", "text": PROMPT},
            {"type": "image_url",
             "image_url": {"url": f"data:image/png;base64,{b64}"}}
        ]
    }],
    "max_tokens": 4096,
    "temperature": 0
}

temperature=0,因為 OCR 不需要創意。


先看效能數據

錯誤案例先放一邊,先看速度:

類型 prompt tok completion tok 耗時 tok/s finish_reason
1 純文字 298 141 3.40s 41.5 stop
9 圖文混排 298 387 8.11s 47.7 stop
11 表格 298 737 15.18s 48.5 stop
12 表格 298 459 9.52s 48.2 stop
14 圖文混排 298 358 7.78s 46.0 stop

幾個觀察:

prompt_tokens 全部都是 298

一整頁 200 DPI 的 PNG,編碼後只佔約 280 個 token(298 減掉 prompt 文字的部分)。

這比我預期少很多。圖片不貴,輸出才貴。

② 表格頁的輸出 token 明顯爆炸

第 11 頁文字層只有 523 字,模型卻吐了 737 個 token。

Markdown 表格的 |--- 和換行都要算 token,格式成本甚至比內容高。

③ 全部 finish_reason: stop

沒有任何一頁被 max_tokens 截斷。這點要記下來:後面看到的錯誤不是因為輸出講到一半被切掉。

④ 每頁要 3-15 秒

一份 40 頁的年報,照這個速度單線跑完要 5-10 分鐘。

這還只是讀出文字,還沒做語意理解、驗證或結構化。

Day 5 會再用到這個數字。5-10 分鐘一份文件不算嚇人;換成「一季 100 家公司的年報」,就是 10 小時起跳。單看一份文件,很難察覺吞吐量問題。


那,它讀得對嗎?

先講怎麼判定「錯」

這件事比想像中麻煩,因為 ground truth 本身不完美。

我把差異分成三類,只有第一類算模型的錯:

類別 內容 算不算錯
(a) 模型真的認錯、漏掉、或多生出來
(b) 純格式差異(Markdown 表格 vs 拉平文字、全半形、空白) 不算
(c) ground truth 自己有問題(文字層順序亂、缺字) 不算

正規化規則(做完才比對):NFKC 全半形統一、去掉 Markdown 表格符號、去掉 PDF 的 PUA 項目符號、去空白

省掉這步,CER 會高得嚇人,但那幾乎都是格式差異的假象。我最後沒有採用 CER 當主要指標:模型輸出 Markdown 表格,文字層卻是拉平的,光是重排就會把 CER 灌爆。用「字元覆蓋率」比較誠實。

覆蓋率結果

類型 覆蓋率
1 純文字 99.4%
11 表格 98.8%
12 表格 98.1%
9 圖文混排(圖例清單) 76.2%

前三頁看起來很不錯。

第 9 頁是圖文混排的圖例清單,覆蓋率為 76.2%。同一顆模型、同一個 prompt、同一份文件,版面不同就出現明顯落差。


26 條錯誤

總共挖出 26 條 (a) 類真實錯誤:

錯誤類型 條數
錯字 8
漏字 7
漏行 5
科目名稱錯認 2
幻覺補字 1
表格欄位錯位 1
數字錯誤 1
金額單位錯(負號遺失) 1

挑四條來看,都是逐字對照:

① 化學品名被換成「像但錯」的詞

原文:飽和聚酯樹脂
輸出:酚醛樹脂

它不是亂碼,而是一個真實存在、同樣屬於樹脂類的化學名詞。

模型很篤定地給了一個錯的專有名詞。

② 混合型崩壞

原文:淨利歸屬予母公司業主
輸出:純利歸屬於母公司able

一行裡有三種錯:「淨利」→「純利」(錯字)、「予」→「於」(錯字)、「業主」→「able」(幻覺補字)。

able 的來源無法從原文判斷。

③ 最危險的一條:負號不見了

原文:其他 (1,678)
輸出:其他 1,678

看起來只少了一對括號。

但在會計表達裡,括號就是負號。

(1,678)-1,678

這條錯誤不會被拼字檢查抓到,數字本身正確,格式也看似正常。它可能一路進到資料庫,把一筆支出變成收入。

④ 科目名稱 + 數字同時錯

原文:一年內到期長期負債 4,455
輸出:短期負債 4,555

科目被簡化成另一個科目(「一年內到期長期負債」和「短期負債」在會計上不同),數字也從 4,455 變成 4,555。

③ 和 ④ 只出現在「三頁一起丟」的測試;同樣兩張表單獨跑時完全沒有這些錯。


三頁一起丟的結果

我另外做了一個壓力測試:把第 11、12、13 頁一次丟進去,要求全部辨識。

結果:

  • 三頁的內容都有出現,沒有整頁被吃掉。
  • finish_reasonstop,不是被 max_tokens 截斷。
  • 排在輸出最後段的資產負債表,漏掉了「股東權益總計」和「流動比率」兩整列。
  • 綜合損益表漏掉「營業外收入及支出」一列。
  • 前面兩條最危險的錯誤(負號遺失、數字錯誤)也出現在這次。

這三張表單獨跑時,完全沒有這些問題。

輸出 token 數也是線索:合併跑只吐了 1464 個 token,明顯低於三頁分開跑的推算量

模型沒有拒絕,也沒有報錯,只是「大概講一下」,而且犧牲的是排在後面的內容。

這是單一樣本,統計把握度有限,不能宣稱「一次丟多頁一定會偷工」。不過在這個樣本中,後段內容確實被省略;我把它視為需要防的風險,不當作已證實的通則。


失敗模式分類:五條系統性弱點

把 26 條錯誤放在一起看,可以整理成五條系統性弱點:

1. 無框線的清單/圖例頁最危險

第 9 頁覆蓋率只有 76.2%,單頁就漏掉 9 個條目。

有框線的財務表反而幾乎不會漏(98%+)。框線提供明確結構線索,鬆散排列的圖例清單沒有。

2. 多頁合併會犧牲後段完整度

上一節的三頁測試就是例子。

3. 專有名詞會被替換成「像但錯」的詞

「飽和聚酯樹脂」→「酚醛樹脂」就是典型。

它不會變成亂碼,反而給你一個看似合理的錯誤答案。這比亂碼難防得多。

4. 科目「標籤」比「數字」更容易錯

這是我最意外的發現。

我原本以為數字最危險,畢竟 4 和 9 長得像。

實際上數字的可靠度相當高,錯的大多是標籤:科目名稱、項目名稱和欄位抬頭。

麻煩的是,數字可能都對,卻被掛在錯的科目底下。

5. 視覺上並排的雙表格會被揉成一張

欄位對應整個失真,但數字沒有丟。

最後會得到一張每個數字都存在、但擺錯格子的表。


今天的結論

先說好的部分:baseline 沒有想像中差。

純文字頁 99.4%、有框線的財務表 98%+。單看認字,這顆 26B 模型處理繁中財報的表現相當可以。

問題也正出在這裡。

今天的錯誤可整理如下:

  • 負號不見了 → 數字完全正確,只是正負顛倒
  • 科目名稱錯認 → 格式完全正常,只是掛錯科目
  • 化學品名幻覺 → 是真實的專有名詞,只是不是這一個
  • 多頁偷工 → finish_reason: stop,沒有任何錯誤訊號

沒有一條會讓程式噴 exception。

這就是 Day 1 所說「OCR 正確不等於可信任」的實際樣貌。98% 的覆蓋率聽起來很高,但那 2% 裡可能藏著把支出變收入的錯。

這顆模型不會告訴你自己不確定,仍會用同樣的語氣說出錯的東西。後面的架構設計要處理的,就是怎麼把這類錯誤找出來。

那要怎麼讓錯誤發出聲音?

最直覺的做法是換更大的模型,或多跑幾次取共識。

明天會看到,這兩條路在我這台機器上都走不通。卡住它們的不是模型能力,而是 273 GB/s。


上一篇
Day 1 - 從 LLM 到 Harness:打造隱私與可信任的繁中進階 OCR Agent 開賽宣言
系列文
從 LLM 到 Harness: 打造隱私與可信任的繁中進階 OCR Agent2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言