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,每次运行都不一样 | 固定的 schema:定义一次 fields,或让 autoFields 提议一份,data.values 就按这个形状返回 |
| 存储与查询 | 得你自己搭 | POST /ocr/fields 只在响应里返回,不保存图像;上传到表格后,每一页落成一行,用 GET /view 查询(where、sort、select、limit、offset),不重新 OCR,也不计费 |
| 文种 | 取决于模型和提示词 | 日语、韩语、中文、英语等,自动识别,无需任何语言参数 |
| 接入 | 自己的提示词、重试、解析、校验一整套流水线 | 一次带 Bearer 密钥的 HTTPS 调用 |
这里把“已验证”到底是什么意思说清楚,因为这一点很容易被夸大。语言模型并不输出坐标。它返回的是每个值的文本,外加词元(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 在那组坐标上读到的字形。这并不是承诺模型永远不会出错:词元提示本身仍可能偏移,两个引擎也可能在同一处误读上达成一致。它的意思是:每个值都会与页面核对并给出结论,而不是凭信任照单全收。
{
"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,而不是要跟这些模型本身较劲。
计费按扫描量随用随付,可选月度套餐,每月还有一定额度的免费扫描,失败的扫描不计费。具体数字见价格页面。