【入門編】Langfuseで画像OCRの精度検証をシンプルに始める方法
- Hiromi Kuwa
- 1月26日
- 読了時間: 6分
Geminiの性能向上によりOCRは実用的になりましたが、高精度を目指すならプロンプト調整は必須です。しかし、調整のたびに画像と結果を目視で見比べるのは、手間がかかりミスも誘発します。
そこで本記事では、Langfuseを使ってこの作業を自動化します。「評価」と聞くと難しそうですが、今回は複雑な指標や設定を使わず、かつ、チーム運用は一旦忘れ、まずは「自分のPC上で、正解データと一致するか」だけをチェックする気軽な構成を目指します。
なお、プロンプト改善のサイクルを回すことが目的なので、今回は繰り返しテストに適した Dataset Run 機能を利用します。
Datasetの準備
Dataset Runを実行するには、データセットの作成が必要です。
※ 基本的な作成方法については、以前の記事(Langfuseデータセット構築ガイド:UI・CSV・SDKの徹底比較)にて紹介しています。
しかし、ここで一つ問題が発生します。
期待する結果の値(Expected Output)はテキストなので問題なく登録できますが、こちらの記事でも言及している通り、 2026年1月現在、LangfuseのDataset Itemsはマルチモーダルコンテンツ(画像やPDFの直接保持)をサポートしていません。
また、Web UIから実行する場合のDataset Run機能も、同様にマルチモーダルには未対応です。
そこで今回は、SDK を使ってDataset Runを実行するアプローチをとります。
「スクリプトを書く必要がある」と聞くと手間に感じるかもしれませんが、Web上で完結させないことによるメリットもあります。
スクリプトが動く環境に画像ファイルを置いておけば、Langfuseに画像実体をアップロードする必要や、Amazon S3 や Google Cloud Storageなどのストレージサービスを準備する必要も無く、LLMに投げる直前にローカルファイルを読みこむだけで済むため、構成がシンプルになります。
データセットの構造
ファイルの実体はローカルに置きますが、「どのデータセット項目が、どの画像ファイルに対応するか」を紐付ける必要があります。
今回は手軽さを優先し、ひとまず Datasetの `input` にはファイル名(パス)だけを記述することにします。
例:

この形式なら、実ファイルを含める必要が無いため、CSVを使った一括インポートなど、好きな方法でDataset Itemsを登録できます。
評価データとスクリプトの準備
ディレクトリ構成イメージ
今回は以下の画像ファイルを評価にかけます。
ディレクトリ構成は以下の通りです。
├── ocr.py # 今回作成するスクリプト
└── data/ # 画像置き場
├── image_001.png
├── image_002.png
└── ...評価データ
今回はテスト用に以下を準備しました。
テスト用画像ファイル:Nano Banana Proを用いて、適当なレシート画像を作成
Dataset:テストデータ(csv)をLangfuseのWeb UIから「Import CSV」で一括登録
テスト用画像ファイル



テストデータ(csv)
Expected Output に shop_name、date、price をマッピングしています。
input | shop_name | date | price |
image001.png | DAILY STORE | 2024/01/25 | 780 |
image002.png | スーパーABC | 2024/01/26 | 1098 |
image003.png | COFFEE STAND | 2024/01/27 | 1600 |
実際のデータセット(一部)

スクリプトの準備
今回は手軽にやるため、処理内容・評価関数共に最低限の作りにします。
LLMは Vertex AI(Gemini 2.5 Flash)を利用し、評価関数としては「店舗名の取得が正常に行えているか?」のチェックのみを実施します。
また、プロンプトの改善を容易にするため、プロンプトはコードへの記述ではなく、Langfuseで管理する形にしました。
import os
from dotenv import load_dotenv
from langfuse import Langfuse, Evaluation
from google import genai
import json
# 設定(APIキー等は環境変数から読み込み)
load_dotenv()
langfuse = Langfuse()
client = genai.Client(
project=os.getenv("GOOGLE_CLOUD_PROJECT"),
location=os.getenv("GOOGLE_CLOUD_REGION", "asia-northeast1"),
vertexai=True,
http_options=genai.types.HttpOptions()
)
target_file_path = "data/"
prompt = langfuse.get_prompt("ocr")
dataset = langfuse.get_dataset(name="ocr_test")
# OCRタスクの定義
def ocr_test(item):
# Datasetのinput(ファイル名)から実際のパスを生成
input_filename = item.input
compiled_prompt = prompt.compile(input = item.input)
# ローカルファイルをバイナリで読み込む
file_path = os.path.join(target_file_path, input_filename)
with open(file_path, "rb") as f:
file_data = f.read()
# Geminiへ送信(画像データ + プロンプト)
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=[
genai.types.Part.from_bytes(data=file_data, mime_type='image/png'),
compiled_prompt,
],
config=genai.types.GenerateContentConfig(
response_mime_type="application/json",
temperature=0.0,
response_schema={
"type": 'ARRAY',
"items": {
"type": "OBJECT",
"properties": {
"shop_name": {
"type": 'STRING',
"description": '店舗名',
},
"date": {
"type": 'STRING',
"description": '日付',
},
"price": {
"type": 'NUMBER',
"description": '金額',
}
},
"required": [
"shop_name",
"date",
"price"
],
},
}
),
)
return response.text
# 評価関数(単純一致)
def simple_evaluator(*, input, output, expected_output=None, **kwargs):
# 実務では output のチェックや、改行コードの削除や正規化(strip等)を推奨
response_data = json.loads(output)
check = response_data[0].get("shop_name") == expected_output.get("shop_name")
if check:
comment = "Success"
else:
comment = "False"
return Evaluation(
name='output_check',
value=check,
metadata={
"expected": expected_output,
"actual": output
},
comment=comment,
data_type="BOOLEAN"
)
# status: ACTIVEのみを取得
active_items = [item for item in dataset.items if item.status == "ACTIVE"]
langfuse.run_experiment(
name=f"{prompt.name}_{prompt.version}",
description=f"Dataset Run from SDK: {prompt.name}_{prompt.version}",
task=ocr_test,
data=active_items,
evaluators=[simple_evaluator],
)※ dataset.run_experiment でも Dataset Run は実行できますが、データのフィルタリング(data 引数の指定)が出来ません。そのため、ARCHIVE済みアイテムも含む全件が対象となってしまいます。
評価について
LLMによる採点(LLM-as-a-judge)を入れた方がそれらしく見えますが、LLMを使う以上コストが発生します。OCRのように正解が明確なタスク(完全一致または簡単なルールベースで比較ができる場合)であれば、LLMより単純な比較の方が精度が見込める場合も多いです。
今回はスクリプトで評価まで完結させていますが、「あいまいな表現に対しても評価したい」など、別軸でLLM-as-a-judgeも適応させたい場合は、Langfuseの設定で後から追加することも可能です。
実行結果
スクリプトを実行すると、Langfuseの画面に結果が反映されます。

今回は結果がBOOLEANとなる評価を設定しましたが、このようにEvaluatorで設定した項目に対して、True・Falseの件数が表示されます。

詳細画面を確認すると、このように実際の値の比較や、どのデータが OK/NG だったかの確認も一目で出来ます。
今回は項目が少なく、評価軸もシンプルなので、Output と Expected Output を見比べれば何が問題かを容易に読み取れます。
実際の運用では、Metadataなどを活用するとよさそうです。
上記画面では、結果に対してマウスオーバーで Evaluation にて設定したコメントやMetadataが表示されます。


さらに、過去の実行結果の比較も出来るため、「さっきのプロンプトの方が精度が良かったな…」といった振り返りも容易です。

差分の比較方法については、LangfuseのExperiments Compare ViewのBaseline機能を解説もあわせて確認頂くと、より詳細な比較が行えるかと思います。
まとめ
今回はとにかく「マルチモーダルな評価」を「とりあえず簡単にやってみる」ことに着目して実験しました。
この構成なら、ひとまずLLMが使える状態さえ準備出来れば、スクリプトを実行するだけで、OCRの精度評価をLangfuseに任せてしまえます。
チーム開発への展望
今回は個人のPCで完結させましたが、もしチームで実行したい場合は Amazon S3 や
Google Cloud Storage などのストレージサービスを利用するのがおすすめです。
スクリプト上のローカルファイルからファイルを取得する処理を、各々のストレージサービスに対応した形に修正すれば、同じように実行できます。
一例として、Vertex AI(Gemini) + Google Cloud Storage (GCS) の構成であれば、スクリプト上でファイルを読み込む必要もありません。権限の設定は必要ですが、GCS上のファイル(`gs://...`)は直接Geminiに読み込ませることが可能となっています。
(スクリプトの「ローカルファイルをバイナリで読み込む」ブロックの処理も不要となります。)



コメント