昨天我說,先讓你看它壞掉。
今天直接跑這個測試。
規則很簡單:不搭架構。一頁圖丟進去,叫它把字讀出來。
沒有版面偵測、區塊分流、前後處理,也沒有驗證。
這是刻意保留的最低限度做法。
我要記錄的,是它會在哪裡失手。
有個觀念常被跳過,偏偏很容易讓後面的優化走歪。
你如果直接上一套很完整的架構,它跑出 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 |
這是整個實驗最重要的設計決定。
我用 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")
同一頁,一邊抽文字層當答案,另一邊渲染成圖丟給模型。
好處是不用人工標註;缺點是文字層本身也不完美。它的閱讀順序常跟視覺版面不同,表格還會被拉成一長串。這會直接影響後面怎麼判定模型是否真的出錯。
我刻意把 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 條 (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_reason 是 stop,不是被 max_tokens 截斷。這三張表單獨跑時,完全沒有這些問題。
輸出 token 數也是線索:合併跑只吐了 1464 個 token,明顯低於三頁分開跑的推算量
模型沒有拒絕,也沒有報錯,只是「大概講一下」,而且犧牲的是排在後面的內容。
這是單一樣本,統計把握度有限,不能宣稱「一次丟多頁一定會偷工」。不過在這個樣本中,後段內容確實被省略;我把它視為需要防的風險,不當作已證實的通則。
把 26 條錯誤放在一起看,可以整理成五條系統性弱點:
第 9 頁覆蓋率只有 76.2%,單頁就漏掉 9 個條目。
有框線的財務表反而幾乎不會漏(98%+)。框線提供明確結構線索,鬆散排列的圖例清單沒有。
上一節的三頁測試就是例子。
「飽和聚酯樹脂」→「酚醛樹脂」就是典型。
它不會變成亂碼,反而給你一個看似合理的錯誤答案。這比亂碼難防得多。
這是我最意外的發現。
我原本以為數字最危險,畢竟 4 和 9 長得像。
實際上數字的可靠度相當高,錯的大多是標籤:科目名稱、項目名稱和欄位抬頭。
麻煩的是,數字可能都對,卻被掛在錯的科目底下。
欄位對應整個失真,但數字沒有丟。
最後會得到一張每個數字都存在、但擺錯格子的表。
先說好的部分:baseline 沒有想像中差。
純文字頁 99.4%、有框線的財務表 98%+。單看認字,這顆 26B 模型處理繁中財報的表現相當可以。
問題也正出在這裡。
今天的錯誤可整理如下:
finish_reason: stop,沒有任何錯誤訊號沒有一條會讓程式噴 exception。
這就是 Day 1 所說「OCR 正確不等於可信任」的實際樣貌。98% 的覆蓋率聽起來很高,但那 2% 裡可能藏著把支出變收入的錯。
這顆模型不會告訴你自己不確定,仍會用同樣的語氣說出錯的東西。後面的架構設計要處理的,就是怎麼把這類錯誤找出來。
那要怎麼讓錯誤發出聲音?
最直覺的做法是換更大的模型,或多跑幾次取共識。
明天會看到,這兩條路在我這台機器上都走不通。卡住它們的不是模型能力,而是 273 GB/s。