インボイス番号を取りこぼしていたので、レシート読み取りのモデルを乗り換えた
印字されているのに、抽出されない
レシートを撮ると項目を読み取って経費データにしてくれる、という処理を作っています。店名・日付・金額・品目、そして 登録番号(インボイス番号) を取り出すやつです。
この登録番号が曲者でした。適格請求書の要件で必要になるので、レシートに印字されていれば拾わないといけません。ところが、印字されているのに抽出結果に入っていないことがある。毎回ではなく、たまに落ちる。
読み取りには Claude Haiku 4.5 を使っていました。安くて速いので画像1枚を読ませるには手頃です。ただ、この取りこぼしが気になってきたので、他のモデルと比べてみることにしました。
大規模なベンチマークはしていない
以前、Vision LLM 9種を日本語OCRで比較した記事を書きました。あれは合成データを用意して9モデルを横並びにする、それなりに大掛かりな検証でした。
今回はそこまでやっていません。手元にあった実際のレシート5枚と、品目を多く並べた合成レシート3枚だけです。枚数としては少ないのですが、目的が「今使っているものを乗り換えるかどうか」なので、実際に処理している画像で差が出るかを見れば足ります。
比較相手には Gemini 3.1 Flash-Lite を選びました。Haiku と同じく「安くて速い」層のモデルなので、置き換えるなら条件が近いところから、という理由です。
結果
同じプロンプト、同じ画像で比べた結果です。
| 項目 | Claude Haiku 4.5 | Gemini 3.1 Flash-Lite |
|---|---|---|
| 登録番号の抽出(実レシート5枚) | 3/5 | 5/5 |
| 店名の誤読 | 1件 | 0件 |
| 品目名の一致(合成・10品目) | 1〜2/10 | 7〜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つ削れた。処理を移したら、前のために付けた権限が残っていないか見る
- 画像の送信先が変わったら、プライバシーポリシーの記載も直す
大掛かりなベンチマークをしなくても、自分が実際に処理しているデータを数枚流すだけで、乗り換えるかどうかの判断はつきます。気になっているなら試してみるのが早いと思います。
参照リンク
- Gemini API — 構造化出力 —
responseMimeTypeで JSON を要求する - Amazon Bedrock の料金
- Gemini API の料金
- 適格請求書等保存方式(インボイス制度)|国税庁 — 登録番号の記載要件