多张照片认一个东西:状态增量识别与逐字段置信度合并
用户拍一个商品,往往不会只拍一张:正面拍一张看品名,背面拍一张看配料, 再翻过来拍一张瓶盖上的日期。信息天然是分散的。
每张照片单独识别、最后再把结果拼起来——这是我最初的写法,也是问题最多的地方。 这篇讲我换成「状态增量」之后,合并逻辑是怎么简化的。
一、独立识别再对齐,为什么会崩
最初的流程是:
照片 A → 识别结果 A ┐
照片 B → 识别结果 B ├─→ 事后合并 → 最终结果
照片 C → 识别结果 C ┘
跑起来之后暴露了几个绕不过去的问题:
- 同一字段多个候选,谁对? 三张图可能给出三个不同的品名写法, 事后合并没有判断依据,只能按顺序取最后一个或者第一个。
-
跨照片的语义断了。 背面印着「生产日期见瓶盖」,
而瓶盖照片上只有一串裸码
20260701。 单独看这两张图,谁都不知道这串数字是什么——只有把「已知信息」 和「新照片」放在一起,才能判断出来。 - 重复信息互相干扰。 三张图都拍到了品名,但清晰度不同, 合并时反而容易挑到最模糊那次的结果。
二、换成「带着已知状态去识别」
核心改动只有一句话:每次请求都携带「目前已知的状态」, 模型返回的是更新后的完整状态,而不是这张图独立的识别结果。
空状态 + 照片 A → 状态 1
状态 1 + 照片 B → 状态 2 ← 模型能看到 A 已经识别出什么
状态 2 + 照片 C → 状态 3 ← 也知道「生产日期见瓶盖」这条线索
这样一来,跨照片的语义判断从「事后补救」变成了「识别时天然具备」。 上面那个瓶盖裸码的例子,在状态 2 里已经记录了「生产日期见瓶盖」这个提示, 所以处理瓶盖照片时会把这串数字解读成生产日期,而不是当成一个无意义的数字丢掉。
三、每个字段都带置信度
状态里每个字段不是裸值,而是 { value, confidence }:
{
"brand": { "value": "", "confidence": 0 },
"name": { "value": "纯牛奶", "confidence": 0.9 },
"productionDate": { "value": "2026-07-01", "confidence": 0.75 },
"expiryDate": { "value": "", "confidence": 0 },
"shelfLifeText": { "value": "常温 6 个月", "confidence": 0.85 },
"candidateDates": { "value": ["20260701"], "confidence": 0.6 }
}
置信度是这个设计里最关键的一环。它让「合并」从一个模糊的判断题变成了可执行的规则:
- 新结果某字段为空 → 保留旧值;
- 新结果非空且置信度更高 → 覆盖;
- 新结果非空但置信度更低 → 保留旧值,但把新候选记下来备查。
注意这是字段级合并,不是整条记录覆盖。旧状态里品名很确定、 新照片只把日期认得更好,这种情况合并后应该是「旧品名 + 新日期」, 而不是两份结果二选一。
四、日期推算不交给模型
识别出「生产日期」和「保质期文本」之后,到期日是需要计算的。 我一开始让模型直接算,结果它时不时算错——尤其是遇到「保质期 6 个月」 这种需要跨月推的情况。
后来我把这一步拿回代码:模型只负责读数(把包装上的字变成结构化字段), 推算交给一个纯函数。
productionDate + shelfLifeText → 到期日 // 纯函数,可单测
好处有两个:一是算得准,二是可以写测试。日期边界(月末、闰年、 「保质期 18 个月」)这些 case 全都用单元测试覆盖掉了, 而模型的行为没法这样验证。
分工原则:模型负责「看图看出来的东西」,代码负责「能算出来的东西」。 让模型做算术,既浪费它的能力,又引入不确定性。
五、一次降级重试
有一个场景经常出错:所有照片都识别完了,但日期字段依然是空的。 这时候我会把原图缩小到约 240px 再识别一次,而且只补空字段, 不覆盖已有结果。
至于为什么缩小反而更容易读出来,我没有做严格的归因。 一个观察是:整图缩小后,模型似乎更倾向于先抓版式上最显眼的那个大字码, 而不是被包装上的细碎文字带偏。这属于经验性做法, 所以我把它限定成「只在前一次失败时触发」的兜底,而不是主路径。
六、客户端侧的几个工程细节
请求要有硬超时
网络异常时连接可能长时间不返回,界面就一直转圈。
客户端用 AbortController 给每个请求封了一个 150 秒的上限,
到点直接中断并报错,用户至少知道发生了什么。
多张照片要错峰发送
一次选多张图时,如果同时发出去,在 Hermes 引擎上会撞到一个已知的并发问题。 处理办法很简单:每张之间错开约 100ms 发出。加上一个引用标记防止重入, 避免用户连点两次导致同一批照片被识别两遍。
结果落地要合并渲染
多张照片的识别结果陆续返回,如果每回来一张就更新一次界面状态, 会触发大量重复渲染。做法是先把结果攒起来,一次性合并进界面状态。
七、小结
这版设计里,我觉得最值钱的不是「状态增量」这个技巧本身,而是 把不确定性显式地表达出来: 置信度写进数据结构,合并就有规则可循;推算写成纯函数,就能被测试覆盖。
反过来说,如果一个系统里到处是「差不多取一个吧」的隐式判断, 那它出问题时是没法定位的。