← 返回文章列表

把 OCR 管线整个删掉:一次架构回退的决策记录

我做的一个小工具需要「拍商品包装,读出生产日期和保质期」。 这个需求听起来像 OCR 的经典场景,我一开始也确实按 OCR 的思路做了。 但最后落地的版本里,OCR 一行代码都没留下。

这篇不是「OCR 不行,视觉模型行」这种结论文。我想记录的是:一个为了绕开 具体缺陷而设计的架构,会在缺陷被修复的那一刻失去存在理由, 以及回退时应该保住什么。

一、第一版为什么绕开视觉模型

最直接的做法是:把照片丢给视觉大模型,让它直接输出结构化结果。我最初就是这么做的, 但很快遇到三个问题:

  • :模型在给答案前会先生成一大段推理过程,单张图要三四秒;
  • 不稳:同一张图两次调用,推理长度不一样,输出也不完全一样;
  • 偶发幻觉:非商品图(比如随手拍的一堵墙)也会硬凑出一个日期。

当时的判断是「不能让模型自由推理」。于是我改成两步走: 客户端用 OCR 把图上的文字全部抽出来变成纯文本,服务端再把文本交给模型做结构化。 多了一步,但模型面对的是确定性的文本,推理量小、结果可控。

照片 → 客户端 OCR 抽文字 → 上传文本 → 服务端汇总端点 → 结构化 JSON

这条管线跑通了,也确实是稳的。为了处理 OCR 分块输出的碎片, 我还写了一套文本汇总与字段合并的逻辑。整体上,它是一个「绕开模型推理」的架构。

二、OCR 的三个硬短板

稳定是稳定了,但准确率上不去。问题不在我的管线,而在 OCR 本身:

  • 艺术字与异形字体:食品包装上的日期经常被设计成描边字、 点阵字、或者带底纹的印章样式,通用 OCR 模型识别率很低。
  • 纯钢印 / 压印日期:很多瓶罐的日期是压出来的,没有颜色对比。 OCR 拿到的是一张几乎没有文字纹理的图。
  • 外文包装:进口商品的日期格式和语种都不固定, OCR 抽出来的文本还要再做一轮语言判断和格式解析。

这三个短板不是调参能解决的。文字是「看图看出来的」,不是「扫描扫出来的」—— 而这恰好是视觉模型比 OCR 强的地方。

三、转折点:一个参数

后来我在模型的接口文档里注意到一个控制思考过程的参数,可以直接关闭推理。 抱着试试看的心态加上去之后,同一张图的实测结果是:

开启推理关闭推理
耗时约 3.4 秒约 0.8 秒
推理 token2820
结果正确正确

我又跑了一轮更宽的验证:混合了不同品类的包装图、以及几张故意不是商品的图。 结果是推理量稳定为 0、耗时稳定在 3 秒上下、非商品图也不再硬凑日期。

这一步很关键:我当初绕开视觉模型的唯一理由是「它推理失控」。 当这个理由不成立时,整套 OCR 管线的存在意义就一起消失了。

四、决定回退,以及回退时保住什么

回退不是把代码删干净重写。OCR 那条管线虽然白做了,但过程中沉淀了两样东西, 在新架构里价值反而更大:

1. 统一的 JSON 状态结构

为了让 OCR 的分块结果能拼起来,我定义了一套统一的结果 schema: 品牌、品名、类别、生产日期、到期日、保质期文本……每个字段都是 { value, confidence } 的形式,而不是裸字符串。

这个结构在视觉直识里同样成立——因为「带置信度」让后续的合并与裁决有了依据。

2. 字段级合并与日期推算(纯函数)

多张照片、多次调用的结果必须能合并。我把它写成了不依赖任何外部服务的纯函数: 按字段比置信度决定取值,日期推算(生产日期 + 保质期 → 到期日)也放在代码里, 不让模型算。

纯函数的好处是可测试——这部分逻辑至今没改过,换架构时原封不动搬过去了。

五、新架构长什么样

砍掉 OCR 和独立的汇总端点之后,整条链路收缩成:

每张照片:压缩 → 视觉模型(关闭推理)
           ↑ 请求携带「当前已识别的状态 JSON」
           ↓ 返回合并后的状态 JSON(含逐字段置信度)
客户端更新状态 → 下一张照片带着新状态再识别

单端点、单模型、单提示词。类别判断也交回给模型了——看图判断品类, 本来比看文字更可靠(艺术字包装、没有文字特征的品类,OCR 根本帮不上忙)。

六、我从中得到的两条经验

第一,要分清「绕过缺陷」和「解决问题」。 我当初的 OCR 管线是一个绕路方案,它有效,但它把架构绑死在「模型会失控」 这个前提上。前提一变,绕路就成了负担。如果当时就意识到这点, 或许会先花时间确认那个参数到底能不能用。

第二,抽象出来的部分比管线本身更长寿。 这次真正活下来的,是统一 JSON 状态和纯函数合并逻辑——因为它们描述的是 「问题本身」,而不是「某一版实现」。管线换了两次,这两样东西一直在用。

顺带一提:删代码比写代码需要更多勇气。但删掉之后,可维护的东西明显变多了。