开发日志/2026-11-06·3 分钟

AI 识别三页单据总失败,我学到:空结果也是失败

一叠三页的单据经过识别,最后只进了一个空文件夹

这周我在一个 bug 上花了不少时间。修完之后,我觉得最值得记下来的不是代码本身,而是一条和 AI 协作的心法:AI 安静地返回一个空结果,比直接报错更危险。

事情是这样的

我在做一个「单据对比器」,用来帮餐饮门店核对供应商的单据:月结单、invoice、送货单(DO),还有供应商价格总单,这几份上的信息要一致。这周刚给它加了 DO 核对,所有单据都能传 PDF 或照片。

上线测试时,一张 3 页、111 行的月结单怎么都识别不出来。

我让 Claude 先 /plan,把原因查清楚再动手。查出来是这样:

  • 3 页一起交给 Qwen3.8 Flash,推理过程把 max_tokens 占满了,跑了 100 秒,最后只回了一个 [];
  • 换成 Gemini,正文被截断;
  • 每次都要 100–200 秒,超过系统设的 120 秒超时,也超过 Cloudflare 的 100 秒限制。

最要命的是,系统把这个 [] 当成「识别成功」,存成了一份空单据。表面上一切正常,谁也不会发现。

第一条:空结果按失败处理

空白的识别结果被判为失败,降级链自动换到下一个模型

以前的降级链只在模型报错时才换下一档。但大模型很多时候不会报错,它会给你一个看起来合法、其实什么都没有的答案。

现在的规则是:

  • 模型只回 [] 或 null,算识别失败;
  • finish_reason=length(输出被截断),算失败;
  • 某一页读出 0 行,先换下一档模型再说。

命中任何一条,降级链就自动换下一个模型,不会再存一份空单据。

第二条:大任务拆小,再并发

三页月结单拆成单页,每页单独识别、并发处理

一次塞 3 页,模型既要看又要推理又要输出 110 多行,token 和时间都不够用。拆开反而简单了:

  • 每页单独识别,最多 6 页并发;
  • 每页各走自己的降级链,一页失败不拖累其他页;
  • 识别月结单这种「照着抄」的任务,把 Qwen 的推理关掉、Gemini 调成低推理,省下的 token 都用来输出;
  • max_tokens 从 16000 提到 32000。

改完再测同一张月结单:27–50 秒识别完,110 行,余额链和期末余额都对得上。

中间也不是一次就好,有一版上线后点「识别」立刻就失败,又回头查了一轮。这就是真实的开发节奏。

我带走的工作流

  1. 先让 AI 查原因再改,别一上来就让它「修好」;
  2. 定义清楚什么算失败,空的、截断的、0 行的都算;
  3. 任务太大就拆小,每个小块各自兜底;
  4. 不需要推理的任务就别让模型推理。

这几条不只适用于单据识别,任何把大模型接进生产流程的场景都用得上。

你在用 AI 的时候,有没有遇到过这种「看起来成功、其实是空的」情况?欢迎留言聊聊。如果你的门店或公司也有类似的对账、单据核对需求,也可以私信我。