AI

インボイス番号を取りこぼしていたので、レシート読み取りのモデルを乗り換えた

レシートから店名・日付・金額・登録番号といった項目が抽出されていく様子を表した図

印字されているのに、抽出されない

レシートを撮ると項目を読み取って経費データにしてくれる、という処理を作っています。店名・日付・金額・品目、そして 登録番号(インボイス番号) を取り出すやつです。

この登録番号が曲者でした。適格請求書の要件で必要になるので、レシートに印字されていれば拾わないといけません。ところが、印字されているのに抽出結果に入っていないことがある。毎回ではなく、たまに落ちる。

読み取りには Claude Haiku 4.5 を使っていました。安くて速いので画像1枚を読ませるには手頃です。ただ、この取りこぼしが気になってきたので、他のモデルと比べてみることにしました。

大規模なベンチマークはしていない

以前、Vision LLM 9種を日本語OCRで比較した記事を書きました。あれは合成データを用意して9モデルを横並びにする、それなりに大掛かりな検証でした。

今回はそこまでやっていません。手元にあった実際のレシート5枚と、品目を多く並べた合成レシート3枚だけです。枚数としては少ないのですが、目的が「今使っているものを乗り換えるかどうか」なので、実際に処理している画像で差が出るかを見れば足ります。

比較相手には Gemini 3.1 Flash-Lite を選びました。Haiku と同じく「安くて速い」層のモデルなので、置き換えるなら条件が近いところから、という理由です。

結果

同じプロンプト、同じ画像で比べた結果です。

項目Claude Haiku 4.5Gemini 3.1 Flash-Lite
登録番号の抽出(実レシート5枚)3/55/5
店名の誤読1件0件
品目名の一致(合成・10品目)1〜2/107〜10/10
1枚あたりのコスト$0.0051$0.0006
レイテンシ6.9秒3.5秒

全部の軸で差がつきました。 ここまで一方的だと逆に疑いたくなりますが、何度か回しても傾向は変わりませんでした。

登録番号の取りこぼし

いちばん効いたのがこれです。Haiku が落としていたレシートも、Gemini は拾いました。

登録番号は T + 13桁の数字という決まった形をしています。レシートの下のほうに小さく印字されていることが多く、他の数字(電話番号やレジ番号)と紛れやすい。この「小さくて他と紛れるもの」を拾えるかどうかが、そのまま差になった形です。

店名の誤読

漢字1文字だけ違う、という誤読が Haiku で1件ありました。読み取れないのではなく、もっともらしい別の字に化けるタイプの間違いです。

これは検出しにくいので厄介です。空欄なら気づけますが、それらしい店名が入っていると、後から見ても間違いだと分からない。

品目の取りこぼし

品目を10個並べた合成レシートでは、差がもっと開きました。Haiku は10個中1〜2個しか一致しないことがあり、Gemini は7〜10個。

こちらは「読めない」というより、途中で力尽きて省略しているような挙動に見えました。

JSON が壊れる回があった

精度の話とは別に、出力の安定性でも差がありました。

読み取り結果は JSON で受け取っています。ところが Haiku は、クォートが欠けた不正な JSON を返す回がありました。パースに失敗するので、その回はまるごと失敗します。

Gemini 側は responseMimeType: "application/json" を指定して JSON で返すよう要求できます。今回の比較の範囲では、パースに失敗する回はありませんでした。

構造化された出力を前提にしている処理だと、中身の精度と同じくらい「毎回パースできること」が効いてきます。落ちる確率が数%でも、利用者から見れば「たまに失敗するツール」になるので。

乗り換えて、権限が減った

移行そのものは、呼び出し先を差し替えるだけの作業です。ただ副産物がありました。

もともとこの処理は Amazon Bedrock 経由で Haiku を呼んでいました。Gemini に移したことで Bedrock を呼ぶ処理がなくなり、Lambda に付けていた IAM 権限を削れました

削除できた権限:
  - bedrock:InvokeModel
  - aws-marketplace:ViewSubscriptions

残った権限:
  - SSM のパラメータ1つを読む権限だけ

API キーは SSM の SecureString に置いて、Lambda にはそのパラメータを読む権限だけを与えています。「この関数が何をできるか」がぐっと狭くなりました。

権限は放っておくと増える一方なので、乗り換えのついでに減らせたのは思わぬ収穫でした。処理を移したら、移す前のために付けた権限が残っていないかを見る。これは今後もやろうと思います。

送信先が変わるので、プライバシーポリシーも直した

忘れやすいところなので書いておきます。

レシートの画像を外部のサービスに送っている以上、どこに送っているかは利用者に開示すべき情報です。送信先を変えたなら、その記載も直す必要があります。

技術的には呼び出し先を差し替えただけですが、利用者から見れば「自分のレシートが渡る相手が変わった」ことになります。実装と同じタイミングで文書も更新しました。

どちらを選ぶかの整理

今回の比較で見た軸をまとめておきます。

精度 — 小さく印字された定型の文字列(登録番号のような)を拾えるか。項目数が多いときに省略しないか。誤読が「空欄」ではなく「それらしい別の値」になっていないか

コスト — 1枚あたりの単価。今回は8分の1になりました。枚数が増えるほど効いてきます

レイテンシ — 待たされる時間。6.9秒と3.5秒は、体感でははっきり違います

運用 — キーの置き場所、必要な権限の広さ、依存するサービスの数。ここは精度の話に隠れがちですが、後から効いてきます

安いモデル同士でも、用途によってはこれだけ差が出ます。「安くて速い層」でひとくくりにせず、自分のデータで比べたほうがいい、というのが今回の実感でした。

まとめ

  • 印字されている登録番号が抽出されないことがあり、モデルを比べ直した
  • 手元のレシート5枚+合成3枚という小さい比較でも、乗り換えの判断には足りた
  • 登録番号 3/5→5/5、店名の誤読 1→0、品目一致 1〜2/10→7〜10/10、コスト 1/8、レイテンシ 半分
  • 精度だけでなく、JSON が毎回パースできるかも実運用では重要
  • 移行の副産物として IAM 権限を2つ削れた。処理を移したら、前のために付けた権限が残っていないか見る
  • 画像の送信先が変わったら、プライバシーポリシーの記載も直す

大掛かりなベンチマークをしなくても、自分が実際に処理しているデータを数枚流すだけで、乗り換えるかどうかの判断はつきます。気になっているなら試してみるのが早いと思います。

参照リンク