说明:本文讨论的是把图片与文档图像交给多模态模型时的输入路径选择,属于工程架构话题,不涉及具体模型版本与价格。AI 领域版本迭代极快,凡涉及版本号、价格、可用性,请以你阅读时的官方页面为准。文中代码为结构示意,请按自己的技术栈调整后再上生产。
一、视觉通道不是「更高级的 OCR」
不少团队接多模态的第一反应,是把原来的 OCR 管道换成「直接让模型看图」,理由大致是模型能看懂版面,比 OCR 强。这个判断在少数场景里成立,但它把两条路径的差别理解错了——差别不在精度高低,而在中间产物存不存在。
结构化管道是这样的:图像先过 OCR 与版面解析,被拆成带类型的字段,再把这些字段拼成文本交给模型。视觉直读则是把图(或它的切图)直接送进模型,由模型在同一次前向里同时处理像素与指令。前者在「图」和「模型」之间插了一层可以读、可以存的中间表示,后者没有。
这层中间表示看着多余,它实际买的是四样东西:可复现(同一张图重跑解析,字段应当一致)、可定位(字段错了,是 OCR 错还是解析错,能分开看)、可校验(合计对不对、编号校验位对不对,都用确定性代码判)、可审计(事后能回答「当时系统读到的原始值是什么」)。视觉直读把这四样换成了「模型说它看到了什么」。
结构化管道:图像 ──▶ OCR / 版面解析 ──▶ 中间字段(带类型)──▶ 文本提示 ──▶ 模型 └─ 可 diff、可校验、可留档 视觉直读 :图像 ──▶ 缩放 / 切图 ──▶ 视觉编码 ─────────────────────▶ 模型 └─ 无中间字段,只有模型输出反过来,视觉直读买到的是另一类东西:版面关系、图内元素的位置与依赖,以及不必为每种版式写规则。像「这一栏是不是空的」「签章压在哪个字段上」「折线是升还是降」,这些信息在转成纯文本字段的过程里会被丢掉,想保留就得再写一堆几何规则。两条路径的取舍摆在一起是这样:
| 维度 | 结构化管道 | 视觉直读 |
|---|---|---|
| 中间产物 | 有,带类型的字段 | 无,只有模型输出 |
| 版面语义 | 需额外规则才保留 | 天然保留 |
| 出错可定位性 | 可分到 OCR / 解析 / 模型三段 | 只能定位到「这一处看错了」 |
| 确定性校验 | 字段级可校验 | 只能事后抽样比对 |
| 新增版式的边际成本 | 每类版式都要补规则 | 通常没有额外开发 |
| 输入形态 | 文本,长度可控 | 图像块,随分辨率增长 |
| 审计 | 中间结果可留档 | 需另行记录原图与请求参数 |
所以第一章要建立的判断不是「哪个更准」,而是:这条链路的输出,是给人看的结论,还是要进系统、要担责、要复算的字段?偏前者就倾向视觉直读,偏后者就倾向结构化管道。这并不是二选一,第六章会讲两条路径并存时怎么分工;但在分工之前,先得看清每条路径各自擅长什么、各自把什么代价留给了你。
二、适合视觉直读的四类场景
判断某类文档该不该走视觉,不要从「模型能不能读」出发,那个问题答案往往是能。要从答案长什么样出发。下面四类,答案本身依赖像素,结构化管道要么做不到,要么做到的成本不划算。
其一,版面语义本身就是答案的一部分。比如这份合同有没有骑缝章、金额栏有没有被涂改、这张表里哪一列是手写的。这些问题的答案不在文字内容里,而在位置、覆盖、笔迹这些像素级特征上。先把图转成字段,等于先把答案丢掉,再想办法把它造回来。
其二,图内元素之间的关系构成答案。折线的趋势、流程图的走向、电路图的连接、公式里上下标与分式的归属,都依赖元素之间的相对位置和连线。纯文本序列会把这些关系压平——「3」和「2」谁在谁右上角,压成一行字之后就分不出来了。
其三,版式多变,规则写不完。一家供应商一种模板,一个季度改一次版。每接一种版式就要补一次解析规则、回归一次、维护一套异常分支,边际成本不收敛。这类场景里视觉直读「零规则」的优势,会随着版式数量增加而越来越明显。
其四,样本量小,摊不回建管道的成本。某类单据一天几张、一年几次。建一套 OCR 加版面解析加校验加监控,前期的开发与后续的维护摊到这些样本上,单页成本会高到离谱,而视觉直读的接入成本几乎就是一次接口调用。
这四类有一个共同前提:你愿意接受「结果由模型给出、且难以逐字段复算」。如果业务上不接受这个前提,那它就不属于这四类,哪怕它长得再像。
| 场景 | 判据 | 典型例子 |
|---|---|---|
| 答案在像素上 | 问题依赖位置、覆盖、笔迹 | 印章有无、涂改痕迹、手填栏 |
| 元素关系即答案 | 依赖相对位置与连线 | 图表趋势、流程图、公式结构 |
| 版式持续变化 | 每类版式写规则的成本不收敛 | 多来源票据、外部来件 |
| 样本量小 | 管道建设与维护摊不回 | 低频专用单据 |
三、适合走结构化管道的四类场景
反过来看,有四类场景,视觉直读不是不能用,而是用了之后你要额外承担它带不来的几样东西:精确、审计、成本可控、类型约束。
其一,字段固定且必须精确。账号、编号、金额、税号、日期。这类值错一位就是错,而视觉模型在形近字、手写体、印章压字下的稳定性,通常不如「OCR 加校验位加金额合计」这套确定性组合。结构化管道能把错误拦在系统内部,而不是拦在人工复核环节——后者要花人的时间,还有漏检。
其二,需要可审计的中间结果。金融、医疗、法务这类场景,事后要能回答「当时系统读到的是什么、依据在哪」。有中间字段就有一份可留档的读取记录;没有中间字段,只能把原图和请求参数存下来,审计时要人对着图再读一遍,成本高得多。
其三,批量且对单次成本敏感。每天数万页时,单页可变成本会被页数成倍放大。视觉直读按图像块消耗,分辨率越高越贵;结构化管道的那一次 OCR 结果可以缓存,同一页重跑几乎不再产生模型调用。这条在第五章讲重试时会再出现一次。
其四,下游强依赖字段类型。字段要进数据库、要参与计算与关联、要有类型。这时候「模型给的是一句话」和「模型给的是一个带类型的值」之间的差别,会一路传导到下游每一处。结构化输出能缓解一部分,但类型与取值范围这类约束,仍然要在管道里显式定义,不能指望模型每次都自觉。
这四类里最容易算错的是第三条。很多人只算了单次调用的费用,没算重试和复核。视觉路径失败重试要重新付一次图像输入;复核要占用人的工时。把这两样摊进去,批量的成本账通常比第一眼看到的更偏向管道一侧。
| 场景 | 判据 | 反例(其实更适合视觉) |
|---|---|---|
| 字段精确 | 错一位即为错,可用校验位与合计复核 | 只用于展示、不参与计算的描述性文字 |
| 要能审计 | 需要留档「系统当时读到什么」 | 一次性分析、不留痕 |
| 批量成本 | 页数大,单页成本被成倍放大 | 每天几页且单页价值高 |
| 下游依赖类型 | 字段进库、参与计算与关联 | 结果的最终消费方是人 |
把字段定义成带校验的结构,是这条路径能立住的地基。一个最小的字段清单示意:
⚠️ 代码待验证
fromdataclassesimportdataclass@dataclassclassField:name:strtype:strrequired:boolpattern:str=None# 确定性校验,命中即拦下,不依赖模型DOC_FIELDS=[Field("doc_no","str",True,r"^[0-9A-Z]{8,20}$"),Field("amount_total","decimal",True),# 还需与明细合计对账Field("issue_date","date",True,r"^\d{4}-\d{2}-\d{2}$"),Field("issuer_id","str",True,r"^[0-9A-Z]{15,20}$"),]四、喂图的真实成本结构
把图交给模型,成本不只是「这张图值多少」。它由四个变量共同决定:分辨率、切图、图的数量、以及失败之后的重试。这几项叠起来,同一页的成本可能差出一个数量级。
分辨率与切图。模型接收图像时通常按块处理,块数与像素量近似线性。图太大时,要么接口自己缩放,要么需要你先切图。缩放会吃掉小字号与细线;切图会引入新问题——跨块的元素被切断,比如表格的一行落在两个块里,模型在单块中看到的是一段没有表头的数字。这两件事都不是「把分辨率调大」就能解决的:分辨率上升,块数上升,成本上升,而模型对超长边还有上限,超过之后照样被缩放。可行的做法是先测出能读清最小字符的像素密度,按这个下限设参数,而不是无脑用最大分辨率。
单图与多图。多图不是简单相加。每张图有固定开销,模型还要在多张图之间分配注意力。一次发一页、分多次处理,和一次塞进十页,后者的单页信息密度通常更高。页数多时,先分页再逐页处理,往往比一次性丢一张长图更稳,成本也更好核算。
重试的重复计费。视觉请求里图像输入是计费的一部分。一次因为超时或格式问题失败,重试时整张图的输入要重新付一遍。结构化管道的重试发生在 OCR 阶段,成本低,而且 OCR 结果可以本地缓存——同一页重跑不再产生模型调用。这就是批量场景里管道更容易算得过来的原因,也是很多团队在成本压力下把路径改回管道的主要动因。
先压缩再识别。降分辨率、降质量、转灰度都能降成本,代价是吞掉细节。临界点仍然是那个「最小字符的像素密度」。压缩到临界点以上,通常只是省钱;压到以下,错误率会非线性上升,而且上升得很安静——直到某天抽样复核,才发现小字号的金额开始读错。这类错误不会报警,只会悄悄进库,所以要靠第五章的验证机制兜住。
| 成本因素 | 结构化管道 | 视觉直读 |
|---|---|---|
| 计费对象 | 文本字段,长度可控 | 图像块,随分辨率增长 |
| 超大图的处理 | 分页与裁剪由自己控制 | 可能被静默缩放 |
| 重试成本 | 低,解析结果可缓存 | 高,图像输入要重付 |
| 多页处理 | 逐页解析,页间互不干扰 | 注意力跨页分配,单页密度下降 |
| 压缩空间 | 压缩图像不影响字段 | 压到阈值以下会非线性掉精度 |
| 人工复核 | 可按字段抽核 | 通常要整页回看 |
把这些因素收敛成显式参数,是避免「成本悄悄涨上去」的第一步。一个请求预算的示意:
⚠️ 代码待验证
{"page_image":{"max_long_side_px":1600,"tile_grid":[2,2],"overlap_ratio":0.1,"decode_format":"png"},"request_budget":{"max_images_per_call":4,"max_pages_per_doc":12,"retry_on_input_error":false}}五、失败模式与验证
视觉直读最危险的地方,不是它答不出来,而是它答得很像对的。下面几类失败模式,线下演示时基本看不到,线上抽样才会冒头。
模型看到的和你以为它看到的不是一回事。图被缩放了、被裁了、旋转信息没处理、长图被压扁。你以为发的是清晰原图,实际进模型的是缩到某个长边之后的版本,小字已经糊了。页面复杂时,切图还会让同一段内容被拆到两个块里。
静默降级。有的接口在图像超限时自动缩小,但不在响应里报错,返回值看起来一切正常。你发现不了,除非在请求记录里把实际生效的分辨率也存下来,而不是只存你传进去的那张图。
补全式幻觉。某一栏其实是空的,模型凭「这类表格通常有金额」的先验,填了一个看起来合理的值。数字字段上这种错误最难发现,因为它长得就像真的,格式对、位数对、位置也对。
形近字与遮挡。金额、编号里的形近字符、手写体、印章压字,是视觉路径错误率最高的一类。它们往往集中在少数几张图上,随机抽样很难抽到,所以抽样方式要专门设计。
对这四类失败,「让模型再读一遍」基本没用——它大概率会给出同样的答案,甚至连错法都一样。可行的做法有三种,通常要一起上。
第一是交叉验证:同一页同时走两条路径,逐字段比对。一致的部分可以直接采信,不一致的部分进复核队列。这条的价值不在「多数表决」,而在于它把不确定的页挑了出来——差异本身就是信号,一致率高不是目标,能把分歧暴露出来才是。
第二是落地抽样人工核:抽样不能只按随机,要按版式、来源、字段风险分层。新出现的版式、金额最大的那一批、被拒识过的页,都要加权抽。随机抽会让罕见但致命的错误永远抽不到,等它自己冒出来时往往已经积了一批。
第三是确定性校验兜底:能算的就算——合计、日期合法性、编号校验位。这一层不依赖任何模型,成本几乎为零,能把一部分错误挡在入库之前。它和交叉验证的分工是:交叉验证负责发现「两条路径不一致」,确定性校验负责发现「两条路径一致地错」。
⚠️ 代码待验证
fromenumimportEnumclassSource(str,Enum):VISION="vision"PIPELINE="pipeline"defcross_check(page,fields,call_vision,call_pipeline):v=call_vision(page,fields)# 视觉直读p=call_pipeline(page,fields)# OCR + 版面解析diffs=[]forfinfields:ifv.get(f)!=p.get(f):diffs.append({"field":f,"vision":v.get(f),"pipeline":p.get(f)})ifdiffs:return{"value":None,"needs_review":True,"diffs":diffs}return{"value":p,"needs_review":False,"source":Source.PIPELINE.value}| 失败模式 | 表象 | 检出手段 |
|---|---|---|
| 隐性缩放 / 裁切 | 小字读错,但接口无异常 | 记录实际生效分辨率,抽样回看原图 |
| 静默降级 | 响应正常,精度局部下降 | 比对请求参数与实际生效参数 |
| 补全式幻觉 | 空格被填上看似合理的值 | 与管道结果逐字段交叉验证 |
| 形近字与遮挡 | 数字或编号个别字符错 | 校验位、合计对账、按风险分层抽核 |
验证机制跑顺之后,两条路径就不再是「用哪条」的问题,而是怎么让它们互相看着对方。这正是下一章要处理的事。
完整版资料清单:本文用到的多模态输入判据与成本对照表都整理在里面了,扫码即可获取:
六、两条路径并存时的架构
一旦两条路径都用上,问题就从「选哪条」变成「怎么让它们共存」。这里有三件事必须提前定,而且都要写进架构,不能留给口头约定。
分流判据怎么定。分流不该按「哪条更先进」来定,而该按文档类型、字段风险和单页价值来定。一个可用的起点是两条规则:字段要精确入库、且有确定性校验手段的,走管道;答案依赖版面语义或元素关系的,走视觉。两条都不满足时,默认走可审计的那一侧——出问题时能定位,比快一点更重要。
结果不一致以谁为准。这里最常见的错误,是写一条全局规则「以某条路径为准」就完事。正确的做法是按字段定权威源,而不是按路径定。金额、编号这类有校验位的字段,以管道为准,并且必须过校验;「有没有骑缝章」「这一栏是不是空的」这类只有视觉能给的,以视觉为准,但默认要求人工核。同一个页面上,不同字段的权威源可以不一样,这一点要在数据模型里就分清楚。
来源可回溯。每条最终字段都要记下:来源路径、当时用的参数版本、时间、是否被人工改过、改之前是什么。下游永远只消费「最终值」,但排查时能一路回到「这个值是哪条路径在哪次调用里给的」。没有这一层,两条路径并存只会让排障更难——你分不清自己面对的是模型的错误、管道的错误,还是两者打架。
还有一个容易被忽略的原则:冲突不自动消解。不要写一条「不一致时以某侧为准」的规则,把分歧悄悄吃掉。不一致要先入复核队列,因为持续出现的不一致往往指向一个真实问题——某类版式管道解析不出、或者某个字段模型系统性读偏。自动消解会让这些信号消失。
| 字段类别 | 权威源 | 校验方式 | 不一致时处置 |
|---|---|---|---|
| 金额、编号、日期 | 管道 | 校验位、合计、格式正则 | 拦住不下发,进复核队列 |
| 版面语义(印章、涂改、手填) | 视觉 | 人工核 | 视觉结果加强制复核 |
| 描述性文本 | 两者可比对 | 抽样核 | 一致即采信,否则复核 |
| 任一字段拒识 | 不下发 | — | 直接进人工队列 |
分流逻辑落到代码上,大致是「先按文档性质选路,再按字段选权威源」两段:
⚠️ 代码待验证
FIELD_AUTHORITY={"amount_total":"pipeline","doc_no":"pipeline","stamp_present":"vision","layout_note":"vision",}defroute(doc):ifdoc.fields_fixedanddoc.has_checksum:return"pipeline"# 可确定性复核的一侧ifdoc.needs_layout_semanticsordoc.templates_per_month<3:return"vision"# 版式多变或样本量小return"pipeline"defresolve(field,vision_val,pipeline_val):authority=FIELD_AUTHORITY.get(field,"pipeline")ifvision_val==pipeline_val:return{"value":vision_val,"source":"both","needs_review":False}return{"value":None,"source":authority,"needs_review":True,"candidates":{"vision":vision_val,"pipeline":pipeline_val}}七、推进顺序与观测
两条路径并存之后,能不能持续推进,取决于你有没有把「好」变成可观测的数字。这一章给一套顺序和四个指标。
先挑一类文档试点。选的标准是:字段少、量大、有唯一校验。例如某类票据上的三五个关键字段。用交叉验证把两条路径同时跑一段时间,先建立基线,而不是一上来就决定只留一条。试点的目的不是省钱,是拿到「两条路径在这类文档上各自错在哪」的真实画像。
推进顺序大致四步。一期,单一文档类型,交叉验证加全量人工核,目的只是建立基线;二期,定下字段级权威源,人工核从全量降为分层抽样;三期,扩到第二类版式,验证「新排版式的接入速度」是不是真如预期;四期,才考虑继续下调人工比例。跳过前三期直接砍人工,等于把还没被量化的风险直接放给业务。
四个指标要一起看。单看任何一个都会被误导。
- 字段级准确率:以人工复核结果为真值,按文档类型和字段名分别统计。整页级准确率会把「一页十个字段只错一个」藏起来,而那个字段可能恰好是金额。
- 复核率:进入人工复核的页数占比。它的下降应当是结果,而不是目标;主动压它通常只是把错误藏起来。
- 单页成本:要算全,含图像输入、重试、以及复核工时的折算,不含一次性的管道建设。
- 拒识率:模型明确表示「看不清」的比例。它要和准确率一起看——拒识高、准确率也高,说明它在诚实地暴露边界;拒识低、准确率却在掉,通常意味着它在猜。
最后一条特别值得写进团队共识:要允许模型说不认识。拒识看起来像是「没干活」,但它把不确定的样本推给了人工,比编一个格式正确、看起来合理的值安全得多。用「识别率」去逼模型少拒识,短期指标会好看,长期会把错误埋进数据里。
⚠️ 代码待验证
metrics:-name:field_accuracyunit:ratioslice:[doc_kind,field_name]note:以人工复核结果为真值,字段级而非整页级-name:review_rateunit:rationote:进入人工复核的页数占比,随版式稳定逐级下调-name:cost_per_pageunit:relativenote:含图像输入、重试与复核工时折算,不含一次性建设-name:abstain_rateunit:rationote:模型明确拒识的比例,与 field_accuracy 一起解读把试点的四期顺序和这四个指标固定下来,多模态输入这件事就从「接一个能看图的接口」变成了可管理的工程问题。至于模型换代时这些判据要不要调,那是另一个话题,判据本身是相对稳定的。
完整版资料清单:本文用到的多模态输入判据与成本对照表都整理在里面了,扫码即可获取:
附表 A:关键取舍一览
本文涉及的所有工程判断集中在这里,方便按需回看。
| 取舍 | 本文结论 | 判断依据 | 位置 |
|---|---|---|---|
| 两条路径的差别 | 在有没有中间产物,不在精度 | 中间产物决定可复现与可审计 | 第一章 |
| 该走哪条 | 看答案长什么样,不看模型能力 | 答案依赖像素时管道做不到 | 第二章 |
| 版式持续变化 | 倾向视觉直读 | 每类版式写规则的边际成本不收敛 | 第二章 |
| 字段精确入库 | 倾向结构化管道 | 有校验位与合计可做确定性复核 | 第三章 |
| 图像分辨率 | 按最小字符像素密度设下限 | 一味调高既费钱又照样被缩放 | 第四章 |
| 多页处理 | 先分页再逐页 | 一次多图会摊薄单页信息密度 | 第四章 |
| 重试成本 | 优先选结果可缓存的一侧 | 视觉重试要重付图像输入 | 第四章 |
| 降质压缩 | 压到阈值为止 | 阈值以下错误率非线性上升 | 第四章 |
| 让模型复读 | 不足以判定 | 模型再读一遍往往给同样答案 | 第五章 |
| 交叉验证 | 用于挑出不一致,不做多数表决 | 差异本身才是信号 | 第五章 |
| 抽样方式 | 按版式与字段风险分层 | 随机抽抽不到罕见致命错误 | 第五章 |
| 不一致取谁 | 按字段定权威源 | 同一页不同字段权威源可不同 | 第六章 |
| 结论冲突 | 不自动消解,进复核队列 | 全局规则会悄悄吃掉分歧 | 第六章 |
| 来源记录 | 记来源、参数版本与人工改动 | 不然排障分不清是谁的错 | 第六章 |
| 人工比例 | 随版式稳定逐级下调 | 先建基线再砍人工 | 第七章 |
| 拒识率 | 与准确率一起看 | 拒识低而准确率掉意味着在猜 | 第七章 |
附表 B:术语速查表
| 术语 | 含义 |
|---|---|
| 多模态输入 | 图像与文本一并送入模型,由模型直接处理像素 |
| 视觉直读 | 不经过 OCR 与字段抽取,整图或切图直接交给模型 |
| 结构化管道 | 先用 OCR 与版面解析把图转成带类型的字段,再走后续流程 |
| 版面解析 | 从图像中恢复区域、行列、阅读顺序等结构信息 |
| 切图 | 把大图切成若干有重叠的小块分别送入模型 |
| 像素密度 | 单位物理尺寸上的像素数,决定最小可读字号 |
| 交叉验证 | 同一输入走两条路径并逐字段比对 |
| 字段级准确率 | 以字段为单位统计的正确比例,区别于整页级 |
| 复核率 | 进入人工复核的页数在总页数中的占比 |
| 拒识率 | 模型明确表示无法识别、而非给出猜测的比例 |
| 静默降级 | 接口在超限时自动缩放或裁剪,但不向前端报错 |
| 补全式幻觉 | 模型用先验补出输入中并不存在的值 |
| 权威源 | 某字段在两条路径结果不一致时被采信的那一侧 |
写在最后:这篇用到的资料
写这篇文章时,我把几个多模态项目的输入实测记录和参数表都对了一遍,顺手整理成几份配套的东西:
- 大模型学习路线图:从 LLM 基础到 Agent 开发,各阶段该学什么、用什么资料
- 大模型全套教程:按主题分好的视频与文档清单
- 大模型实战好书:24 本,附每本适合的阶段
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「大模型」,优先通过。
拿到之后建议先看学习路线图那一份,先定位自己在哪个阶段,再决定学什么,比一上来就啃框架效率高得多。