はじめに
GENDAのインターンで、メーカーごとにフォーマットが異なる注文書・請書から表データを抽出するAIエージェントを開発しました。様々なOCRを比較検討し、最終的に「段階的フォールバック」の仕組みで精度とコストを両立させた話を書きます。
この記事で書くこと
- 複数のOCRツール(Mistral OCR、PyMuPDF、GPT-4o Visionなど)の比較
- コストを抑えつつ精度を確保する段階的フォールバックの設計
- GENDAでのインターンについて
自己紹介
株式会社GENDAのFE/BE開発部でインターンをしている入江です。東京大学工学部機械工学科の4年生で、来年GENDAに入社予定となっています。
入社のきっかけ
3年生の夏に東京大学発のエンジニアサークル UTTC(UTokyo Tech Club)に6期生として入り、Webアプリケーション開発について学びました。
その後UTTCのカリキュラムを終えて、UTTC経由でGENDAのインターンをさせていただくことになりました。
インターン先の候補はいくつかあったのですが、裁量が大きくエンジニアとして成長できそうなこと、エンタメ企業なのでユーザー目線でサービスを考えられること、そしてエンタメ業界に興味があったことからGENDAを選びました。
今回の記事では3月から6月まで関わっていた、オンクレ(オンラインクレーンゲーム)のAI Agent開発についてお話していきます。
プロジェクトの背景
GENDAのオンクレ事業では、商品の注文情報を管理する業務があります。
具体的には、商品を注文する際に注文書を送付し、メーカーからは請書が返送されます。
これらの注文書・請書の情報を一つのエクセルフォーマットに集約するという業務です。
しかし、この業務にはいくつかの問題がありました:
| 課題 | 詳細 |
|---|---|
| フォーマットがバラバラ | メーカーごとに表の配置・文言・拡張子が異なる |
| 文言の揺れ | 同じ意味でも「商品名」「景品名」「摘要」など表記が違う |
| 属人化 | 特定の担当者しか対応できない |
| 手入力の負荷 | 手作業で転記するため時間がかかる |
そこで、様々なフォーマットから表を抽出するAIエージェントを一から開発することになりました。
開発体制
メンターの方にサポートいただきながら、基本的には一人で要件定義から実装まで担当しました。担当者へのヒアリング → 要件定義 → 実装 → リリースという流れで進め、現在は実際の業務で導入されています。
アプリケーションの仕様
技術スタック
- 言語: Python
- フロントエンド: Streamlit
- LLM: GPT-4o
処理フロー
1. ユーザーがファイルをアップロード
2. 拡張子を判別(Excel / PDF / PNG)
3. 拡張子ごとに適切な方法で表を抽出
4. 指定のフォーマットに変換して出力
文言の揺れへの対応
抽出した表データを指定フォーマットに変換する際、「商品名」「景品名」「摘要」といった文言の揺れに対応する必要があります。
LLMに対応関係を判断させることも検討しましたが、以下の理由からconfigファイルで定義する方針にしました:
- 同じ言葉でもメーカーによって別の項目を指すケースがある
- 項目がずれると業務に支障が出る
- メーカーが新規追加されることは稀
OCRの技術選定
現在はExcel・PDF・PNGの3つの拡張子に対応していますが、ここに至るまでに様々な試行錯誤がありました。
Excelの場合(簡単)
Excelファイルは比較的簡単です。行ごとにテキストを取り出してLLMに渡し、ヘッダー行の位置を特定させることで表を抽出しています。
PDF・PNGの場合(難しい)
問題はPDFとPNGでした。正確なテキストデータを構造を保ったまま抽出する必要があります。
試したこと①:Mistral OCR
当時リリースされたばかりで高性能と話題だったMistral OCRを試しました。
Mistral OCRはマルチモーダルな文書からテキストを抽出しマークダウンとして出力可能なAPIです。
結果: うまくいかず
- 欲しいデータが画像として出力されてしまう
- 日本語の文字認識精度が低い
試したこと②:各種OCRライブラリ
PyOCR(Tesseract)など、他のOCRも試しました。
結果: うまくいかず
注文書には半角カタカナが多用されていますが、日本語対応のOCRでも半角カタカナには弱いものが多いです。また、表の枠線が薄いと構造が崩れる問題もありました。
試したこと③:PyMuPDF
PDFのテキストデータを構造化して取り出せるOSSライブラリ「PyMuPDF」を試しました。
RAGの実装でPDFからテキストを抽出する際によく使われるライブラリで、LangChainやLlamaIndexのドキュメントローダーでも採用されています。PDFに埋め込まれたテキストレイヤーを直接読み取るためOCR不要で、page.find_tables()でテーブルを自動検出し構造化データとして取得できます。
結果: めちゃくちゃ使える
メリット
- テーブルを自動検出して取り出せる
- PDFのテキストデータを使うため文字化けしない(半角カタカナも問題なし)
デメリット
- 枠線が細い・点線などの場合、テーブルを検出できない
- スキャンPDFなどテキストデータがないPDFには対応できない
試したこと④:GPT-4o Vision
そこでGPT-4oに画像を直接渡してOCRさせました。
結果: 非常に精度が高い
表形式を崩さず抽出でき、半角カタカナも問題がなかったです。
ただし、処理速度とコストの点では上記の方法よりは劣っています。
比較まとめ
| 手法 | 料金 | 日本語精度 | 表構造の維持 | 備考 |
|---|---|---|---|---|
| Mistral OCR | $1/1000ページ | △ | ○ | マルチモーダルな文書も読み取れるが文字認識精度が低い |
| PyOCR (Tesseract) | 無料 | △ | × | OSSだが精度は低い |
| PyMuPDF | 無料 | ◎ | △ | テキストデータを使用するので文字は正確に抽出できるがテーブル検出できない場合あり |
| GPT-4o Vision | 約2円/回 | ◎ | ◎ | 精度は高いがコスト・速度が課題 |
結論: どの手法も一長一短があり、単体では完璧に対応できない
段階的フォールバックの設計
そこで最終的に採用したのが、段階的フォールバックという仕組みです。
コストの低い方法から順に試し、失敗したら次の方法にフォールバックすることで、精度とコストを両立させています。
LLMに投げる前にフォールバックするので、APIを呼び出すのは一回で済みます。
Excelの場合:ヘッダー行をLLMが検出
Excelに関しては、ヘッダー行をLLMが検出することで、表データを抽出しています。
prompt_template = """
以下のテキストは商品の注文情報です。このテキストは次の三つの部分から構成されています。
a. タイトル、挨拶、宛先、住所、日付など注文内容に関係のない情報(ない場合もある)
b. テーブルのヘッダー行(カラム名(商品名、商品コード、数量、入数、金額、発売月、出荷日、注文日、など複数の項目)が含まれる行(ただし言い回しが違う場合もあります)、必ず存在、1行目にくることもある)
c. テーブルのデータ(商品名、数量、金額などのデータが含まれる行、ヘッダー行の次に必ず存在)
bのヘッダー行(列名が含まれている行)を特定してください。
ヘッダー行は、通常、列名が含まれており、データ行とは異なる特徴を持っています。
ヘッダー行の行番号の整数値のみを返してください。
テキスト:
{prompt_input_text}
ヘッダー行の行番号(整数値のみ):
"""
PDF・PNGの場合:段階的フォールバック
Step 1: PyMuPDFでテーブル抽出 → LLMで整形
まずPyMuPDFでテーブルを抽出します。抽出した全テーブルをLLMに渡し、「商品の詳細情報を含む注文情報テーブル」を判別・整形してもらいます。
テーブル以外もテーブルとして認識してしまうことや、複数テーブルが存在すること、検出できてもテーブルの形式が崩れることがあるため、このようにLLMに整形させるようにしています。
prompt = f"""
以下に、PDFから抽出された複数のテーブルが「抽出された全テーブル」として提示されます。
このテーブルは正確に抽出されているものもあれば、複数行や列が結合されているものもあります。
あなたのタスクは以下の通りです。
1. 最初に、提示された複数のテーブルの中から、「商品の詳細情報(品番、品名、数量、金額など)を含んでいる主要な注文情報テーブル」を一つだけ見つけ出してください。担当者名や合計金額のみが記載された、無関係なメタデータテーブルは無視してください。ただし商品の詳細情報が書かれているテーブルが見つからない場合はタスクを中断し、何も出力しないでください。
2. 次に、見つけ出した主要なテーブルだけを使い、複数行や列が結合されている場合は、それを展開して、最終的にクリーンなCSVを出力してください。
【ルール】
- 重要: 出力は必ず「JSON形式の配列の配列(List of Lists)」としてください。
- 1行目(最初の配列)がヘッダー、2行目以降(2番目以降の配列)がデータとなるように構成してください。
- 説明や言い訳は絶対に含めず、JSONオブジェクトのテキストのみを出力してください。
- 金額や数量が空の場合は、空文字 "" もしくは null を使用してください。
【抽出された全テーブル】
{all_tables_text}
【クリーンな出力CSV】
"""
Step 2: テーブル検出失敗 → 全テキストから構造化
PyMuPDFでテーブルが検出できなかった場合(枠線が細いなど)は、抽出した全テキストをLLMに渡してテーブルを構造化してもらいます。
prompt = f"""
以下に、PDFから抽出された生のテキストが「ドキュメント全文」として提示されます。
あなたのタスクは、このテキスト全体を注意深く読み、「商品の詳細情報(品番、品名、数量、金額など)を含んでいる主要な注文情報テーブル」を見つけ出し、その内容をクリーンなCSV形式で出力することです。
【ルール】
- 重要: 出力は必ず「JSON形式の配列の配列(List of Lists)」としてください。
- 1行目(最初の配列)がヘッダー、2行目以降(2番目以降の配列)がデータとなるように構成してください。
- 説明や言い訳は絶対に含めず、JSONオブジェクトのテキストのみを出力してください。
- 金額や数量が空の場合は、空文字 "" もしくは null を使用してください。
【ドキュメント全文】
{full_text}
【クリーンな出力CSV】
"""
Step 3: テキストなし → GPT-4o Vision OCR
テキストデータが含まれないPDFや画像ファイルの場合、最終手段としてGPT-4o Visionで画像を直接OCRします。
messages = [{
"role": "user",
"content": [
{"type": "text", "text": f"""
以下に提示される画像の中から、商品の注文情報が記載された主要なテーブルを見つけ出し、その内容をクリーンなCSVで出力してください。
【ルール】
- 重要: 出力は必ず「JSON形式の配列の配列(List of Lists)」としてください。
- 1行目(最初の配列)がヘッダー、2行目以降(2番目以降の配列)がデータとなるように構成してください。
- 説明や言い訳は絶対に含めず、JSONオブジェクトのテキストのみを出力してください。
- 金額や数量が空の場合は、空文字 "" もしくは null を使用してください。
【クリーンな出力CSV】
"""}]
}]
for b64_img in base64_images:
messages[0]["content"].append({
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64_img}"}
})
精度の担保について
最後に残る課題は「本当に正確に読み取れているのか」という点です。
現時点で100%正確なOCRは不可能だと考えています。そこでこのアプリでは、変換元のファイルと変換結果を並べて表示し、人の目で最終チェックしてもらうUIにしました。
この方法は結局工数がかかってしまうため、より良い方法がないか引き続き検討していきたいです。
振り返り
学んだこと
- OCRツールの特性: 各ツールの得意・不得意を実際に試して把握できた
- 一気通貫の開発経験: 要件定義からリリースまでを一人で担当
特にドキュメントを構造化データとして抽出するというのは、RAGの実装など様々な事業領域で発生しうる課題だと思うので、その知見が溜まったのは大きかったです。
反省点
担当者とのコミュニケーション不足が最大の反省点です。
- 要件を十分に擦り合わせずに開発着手 → 手戻りが発生
- メーカーごとの複雑な処理ロジックの理解に時間がかかった
- 結果としてスケジュールが遅延
もっと早い段階で担当者と擦り合わせておくべきでした、、、ただインターンでこの経験ができたことは、今後に活かせる貴重な学びだと思っています。
成果発表
GENDAでは定期的にインターン生の成果発表会があり、この内容を発表しました。
自分の成果をアピールしつつ、他のインターン生の取り組みも知れる刺激的な場です。
おわりに
今はこのオンクレの経験を活かして、別のプロジェクトを任せていただいています。
GENDAでは要件定義から任せていただけることが多く、自走力や課題解決力が特に身についたと感じています。今後はより大きなプロジェクトに携わり、さらに成長していきたいです。
ここまで読んでいただきありがとうございました!



