space-ocr 與 LLM OCR 有何不同:可驗證、結構化的擷取
space-ocr 與直接請 LLM 做 OCR 的差別:欄位會連同頁面上的來源座標與 verified 判定一起回傳,需要複核的項目則列在 review 裡。
你可以把一張收據或發票丟給 GPT-4o、Gemini 或 Claude,請它回報總金額、廠商與明細項目。多數情況下,你會拿到一份看起來合理的 JSON。麻煩是在你要大量信任這些結果時才開始:模型回傳的是一串字串,而字串沒有位址。如果總金額回來是 48,200,它讀的是頁面上哪些像素?那個數字究竟是印在文件上,還是模型自己補了一個看似合理的值?單靠一次原始的 LLM 呼叫,你沒辦法回答這些問題,除非自己再把整頁重讀一遍。
這道落差,就是「把通用 LLM 當 OCR 工具」和「用 space-ocr」之間的全部差別。space-ocr 並不反對 LLM。它目前在底層把一個 OCR 引擎(Google Cloud Vision)搭配 Gemini 來做結構化——這是實作細節,而不是 API 契約的一部分。它多出來的,是圍繞模型建立的那一層:回傳的值都會拿去和 OCR 實際在頁面上看到的內容核對、給出判定,上傳到試算表之後再存成一列你可以查詢的資料。這正是原始 LLM 呼叫留給你自己處理的兩件事:可以驗證的逐值來源,以及不必自建資料庫就能查詢的結構化輸出。
原始 LLM OCR 與 space-ocr 的對照
| 原始 LLM OCR (GPT-4o / Gemini / Claude) | space-ocr | |
|---|---|---|
| 逐值位置 | 一次 JSON 擷取呼叫回傳的是文字,與獨立 OCR 判讀交叉核對過的來源座標並不在契約裡 | 凡是能定位的值都附一個位置框(xmin、ymin、xmax、ymax,建立在 0–1000 的格線上)與一個四點方向四邊形;定位不到的值會以 nobox 出現在 review.flagged 裡 |
| 逐值驗證 | 沒有,你只能信任那串字串 | 判定在 cells[path].verified,原因在 review.reasons;核對本身放在 evidence 裡——text_match、source,以及 match_ratio,也就是該值有多少字元能在頁面偵測到的符號中被找到 |
| 宣告的規則 | 驗證邏輯要自己寫 | required、pattern、enum、min/max、near 與 not_near 隨請求一起宣告,由伺服器端判定,違規會落到 review.flagged |
| 在情境中核對某個值 | 自己把文件重讀一遍 | 在應用程式裡點一格,它在原圖上的精確區域就會亮起 |
| 輸出形狀 | 依提示而定、每次執行都不一樣的 JSON | 固定的結構:定義一次 fields,或讓 autoFields 提議一份,data.values 就照這個形狀回傳 |
| 儲存與查詢 | 要自己建 | POST /ocr/fields 只在回應裡回傳,不保存影像;上傳到試算表後,每一頁落成一列,用 GET /view 查詢(where、sort、select、limit、offset),不重跑 OCR,也不收費 |
| 文字語系 | 看模型與提示而定 | 日文、韓文、中文、英文等自動偵測,沒有語言參數 |
| 建置 | 自己的一整條提示、重試、解析與驗證流程 | 一次 HTTPS 呼叫,帶一把 Bearer 金鑰 |
「已驗證」到底是什麼意思,值得說清楚,因為這件事很容易被誇大。語言模型並不會產生座標。它回傳的是每個值的文字,加上一些字詞 token 的提示;引擎接著把這段文字逐字元和 Google Cloud Vision 在頁面上偵測到的符號比對。它把位置框落在那些真實的符號上,而核對的結果會寫進 cells[path].evidence:逐字元核對本身是 text_match,該值的字元有多少比例被找到是 match_ratio,位置框怎麼找到的是 source。verified 是疊在上面的判定:只要這一格帶上任何一條原因就是 false,核對跑過而沒有任何原因就是 true,根本沒有可核對的對象則是 null。比對偏弱會立起 low_ratio 之類的原因,那條 path 隨即出現在 review.flagged 裡,這就是你轉給人工處理的清單。values 是模型讀出來的字串,不是逐位元組的副本,所以當你需要拿去和主檔做精確比對時,請用 evidence.printed_text——OCR 在那組座標上讀到的字形。這並不是在保證模型永遠不會出錯:token 提示仍可能偏移,兩個引擎也可能在同一處誤讀上取得一致。它的意思是:每個值都會拿去和頁面核對並給出結論,而不是照單全收。
{
"status": "success",
"data": {
"values": {
"vendor": "ACME Trading Co."
},
"cells": {
"vendor": {
"box": { "xmin": 120, "ymin": 84, "xmax": 512, "ymax": 118 },
"quad": [
{ "x": 120, "y": 84 }, { "x": 512, "y": 84 },
{ "x": 512, "y": 118 }, { "x": 120, "y": 118 }
],
"verified": true,
"review": null,
"evidence": {
"text_match": true,
"source": "vision_symbol_match",
"match_ratio": 1.0,
"ocr_confidence": 0.97
}
}
},
"review": {
"unit": "field",
"declared": 1,
"returned": 1,
"boxed": 1,
"verified": 1,
"flagged": []
},
"image": { "width": 1654, "height": 2339 }
}
}位置框是建立在與影像尺寸無關的 0–1000 格線上,所以繪製時你要把它換算回像素:pixel_x = xmin / 1000 * image_width。寬與高請取自同一份回應裡的 data.image——那是實際被判讀的那一頁(EXIF 旋轉已經固化,大張照片也已經縮小過),而不是你送出的那個檔案。那個四點四邊形會跟著傾斜或旋轉的掃描一起走,順序是左上、右上、右下、左下。一旦有對不上的地方,同一條 path 就會以 { "path": "total", "reasons": ["text_mismatch"] } 的形式出現在 data.review.flagged 裡,交給複核的人是一份可以逐條走完的清單,而不是一個還要自己決定門檻的分數。對通用模型發出的 JSON 擷取呼叫不會附帶這些,所以你想要的任何稽核軌跡,都得自己手動拼出來。
從一個個值,到一張可查詢的表
原始的 LLM 呼叫到 JSON 就結束了。你還得自己把它存下來,而當你想問「這一季有哪些發票超過 40,000?」的那一刻,你得先蓋一座資料庫和一層查詢介面。POST /ocr/fields 同樣到 JSON 為止——它只在回應裡回傳,不保留影像。不同的是,同一次擷取可以走儲存層:先照你的欄位建一張試算表(POST /create),再把頁面傳進去(POST /upload),每一頁就成為一列,帶著同樣的 values、cells 與 review。之後查詢就是一次 API 呼叫:GET /view 搭配 where=total>=40000、sort=-invoice_date、select=vendor,total,再加上 limit 與 offset 做分頁。它在伺服器端執行,不會重跑 OCR,也不收費。你可以把試算表匯出成 CSV(帶 BOM 的 UTF-8,這樣日文、韓文、中文的文字與貨幣在 Excel 裡才能正確開啟,明細項目的陣列也會各自展開成獨立的資料列)。
什麼時候原始 LLM 才是更好的工具
當你只是要讀一次、要一份鬆散的摘要,或是要推理一份文件的含意時,通用 LLM 才是對的選擇。「這份合約在講什麼?」是個模型問題,不是「附帶座標的 OCR」問題。當你要大量處理文件,而且需要每個值都可驗證、結構一致、可查詢時,就該找 space-ocr:應付帳款自動化、費用核銷、把名片匯入 CRM,或是把積壓的一大批收據數位化。老實說,space-ocr 是一套有驗證與儲存層的 LLM 驅動 OCR,而不是要跟模型本身較勁的對手。
計費是按掃描張數的用多少付多少,另有可選的月方案與每月一定額度的免費掃描,而且失敗的掃描不收費。目前的實際金額請見定價頁。