はじめに
最近、評価駆動開発が注目を集めています。これは、AIシステムの振る舞いを保証し、改善の方向性を明確にする上で欠かせない手法です。今回は、エージェントフレームワークPydanticAIが提供する評価ツール「Pydantic Evals」を活用し、LLMの推論能力を検証する方法を使ってみました。
今回のタスク:コインパーキング料金計算
今回は、コインパーキングの看板画像から料金体系を正確に読み取り、ユーザーの駐車時間に対する料金を算出する処理を題材とします。例えば、「金曜21時から翌日の7時までとめた時の料金はいくらか」といった質問に正しく答えることができるか調べます。
Pydantic Evalsのデータモデル
https://ai.pydantic.dev/evals/#pydantic-evals-data-model
Pydantc Evalsのデータモデルは以下のようになります。
Dataset (1) ──────────── (Many) Case
│ │
│ │
└─── (Many) Experiment ──┴─── (Many) Case results
│
└─── (1) Task
│
└─── (Many) Evaluator
- ケース(Case):入力と期待出力(正解)のセット
- ただし、LLMアプリでは出力の文章が適切であるか正解データを利用せず、LLMに評価させること(LLM as a Judge)もあるため、正解データの設定は必須でありません
- 評価器(Evaluator):ケースの出力が期待通りか否かを判定する処理
- 例えば、出力が正解と一致しているか、長さが適切か、処理時間は許容範囲内か、出力テキストはわかりやすいか、といった処理を記述します。
- データセット(Dataset):複数のケースと評価器をいれたもの
- 実験(Experiment):データセットに対して処理を実行する一連の工程。
使ってみる
評価対象の処理の作成
まず評価対象の処理を実装します。評価対象として、gemini-2.5-flashモデルを使用したエージェントを実装しました。ついでに、入力と出力のデータクラスも実装します。
from pydantic import BaseModel
from pydantic_ai import Agent, BinaryContent, ModelSettings, NativeOutput
from pydantic_ai.models.google import GoogleModel
from pydantic_ai.providers.google import GoogleProvider
class AgentInput(BaseModel):
image_path: Path
query: str
class AgentResponse(BaseModel):
thinking_process: str = ""
fee: int
def run_agent(agent_input: AgentInput) -> AgentResponse:
provider = GoogleProvider(api_key=settings.GEMINI_API_KEY)
model = GoogleModel(
"gemini-2.5-flash", settings=ModelSettings(temperature=0.0), provider=provider
)
agent = Agent(
model,
output_type=NativeOutput(AgentResponse),
system_prompt=(
"あなたは、コインパーキングの看板画像を読み取り、ユーザーの質問に答える有能なアシスタントです。\n"
"看板に記載されている情報を読み取り、ユーザーの金額に関する質問に答えてください。"
),
)
image_path = agent_input.image_path
query = agent_input.query
with image_path.open("rb") as f:
image_data = f.read()
image_input = BinaryContent(data=image_data, media_type="image/jpeg")
result = agent.run_sync([image_input, query])
return result.output
評価ケースの作成
評価ケースを3つ作成します。簡単なケースから、日をまたぐ少し複雑なケースを作成します。
from pydantic_evals import Case
case1 = Case(
name="weekday_2hours",
inputs=AgentInput(
image_path=Path("./data/sample.jpg"),
query="平日の12時から14時までとめた時の料金はいくらですか?",
),
expected_output=AgentResponse(fee=1980)
)
case2 = Case(
name="saturday_6hours",
inputs=AgentInput(
image_path=Path("./data/sample.jpg"),
query="土曜の18時から24時までとめた時の料金はいくらですか?",
),
expected_output=AgentResponse(fee=1390),
)
case3 = Case(
name="weekday_to_holiday_overnight",
inputs=AgentInput(
image_path=Path("./data/sample.jpg"),
query="木曜の21時から7時までとめた時の料金はいくらですか?なお翌日は祝日です。",
),
expected_output=AgentResponse(fee=400)
)
評価器を作る
料金が適切であるか評価するためのFeeMatchクラスを実装します。
from pydantic_evals.evaluators import Evaluator, EvaluatorContext
@dataclass
class FeeMatch(Evaluator):
def evaluate(self, ctx: EvaluatorContext) -> bool:
output: AgentResponse = ctx.output
expected: AgentResponse = ctx.expected_output
return output.fee == expected.fee
データセットを作る
ケースと評価器をまとめて、評価データセットを作成します。
from pydantic_evals.evaluators.common import IsInstance
from pydantic_evals import Dataset
dataset = Dataset(
name="ParkingFeeDataset",
cases=[case1, case2, case3],
evaluators=[
IsInstance(type_name="AgentResponse"),
FeeMatch(),
],
)
評価の実行
評価を実行します。
report = dataset.evaluate_sync(run_agent)
report.print(include_input=True, include_output=True)
評価結果(長いため省略)
Evaluation Summary: run_agent
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Case ID ┃ Inputs ┃ Outputs ┃ Assertions ┃ Duration ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━┩
│ weekday_2hours │ image_path=PosixPath('data/ │ thinking_process='ユーザー │ ✔✔ │ 6.1s │
│ │ sample.jpg') │ は平日の12時から14時までの │ │ │
│ │ query='平日の12時から14時ま │ 駐車料金を尋ねています。看 │ │ │
│ │ でとめた時の料金はいくらで │ 板の料金体系を確認します。 │ │ │
│ │ すか?' │ 平日は「月〜土」に該当し、1 │ │ │
│ │ │ って、330円 × 6単位 = │ │ │
│ │ │ 1980円となります。' │ │ │
│ │ │ fee=1980 │ │ │
├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┼────────────┼──────────┤
│ saturday_6hours │ image_path=PosixPath('data/ │ thinking_process='ユーザー │ ✔✔ │ 9.0s │
│ │ sample.jpg') │ は土曜日の18時から24時まで │ │ │
│ │ query='土曜の18時から24時ま │ の駐車料金を尋ねています。 │ │ │
│ │ でとめた時の料金はいくらで │ 看板の「月〜土曜」の料金体 │ │ │
│ │ すか?' │ 系を確認します。駐車時間帯 │ │ │
│ │ │ は18:00から24:00です。この │ │ │
│ │ │ 時間帯は、基本料金と最大料 │ │ │
│ │ │ 金の適用時間帯にまたがって │ │ │
│ │ │ います。まず、18:00から19:0 │ │ │
│ │ │ 0までの1時間(60分)の料金 │ │ │
│ │ │ を計算します。基本料金は20 │ │ │
│ │ │ 分330円なので、60分では3単 │ │ │
│ │ │ 位となり、330円 × 3 = │ │ │
│ │ │ 990円です。次に、19:00から2 │ │ │
│ │ │ 4:00までの5時間の料金を計算 │ │ │
│ │ │ します。この時間帯は「19:00 │ │ │
│ │ │ 〜8:00」の最大料金400円が適 │ │ │
│ │ │ 用されます。したがって、19: │ │ │
│ │ │ 00から24:00までの料金は400 │ │ │
│ │ │ 円となります。合計料金は、1 │ │ │
│ │ │ 8:00〜19:00の料金と19:00〜2 │ │ │
│ │ │ 4:00の料金を合算し、990円 + │ │ │
│ │ │ 400円 = 1390円です。' │ │ │
│ │ │ fee=1390 │ │ │
├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┼────────────┼──────────┤
│ weekday_to_holiday_overnig… │ image_path=PosixPath('data/ │ thinking_process='ユーザー │ ✔✗ │ 19.5s │
│ │ sample.jpg') │ は木曜の21時から翌日7時まで │ │ │
│ │ query='木曜の21時から7時ま │ の駐車料金を質問しています │ │ │
│ │ でとめた時の料金はいくらで │ 。翌日は祝日であるという情 │ │ │
│ │ すか?なお翌日は祝日です。' │ 報が与えられています。看板 │ │ │
│ │ │ の料金体系を確認します。\n\ │ │ │
│ │ │ n1. │ │ │
│ │ │ **駐車開始日(木曜)の料金 │ │ │
│ │ │ 計算**: │ │ │
│ │ │ 木曜の21時から24時までの駐 │ │ │
│ │ │ 車です。これは「月〜土」の │ │ │
│ │ │ 「19:00〜8:00」の時間帯に該 │ │ │
│ │ │ 当し、この時間帯の最大料金 │ │ │
│ │ │ は400円です。この3時間(21: │ │ │
│ │ │ 00〜24:00)の基本料金は20分 │ │ │
│ │ │ 330円で計算すると2970円にな │ │ │
│ │ │ りますが、最大料金400円が適 │ │ │
│ │ │ 用されます。\n2. │ │ │
│ │ │ **翌日(祝日)の料金計算**: │ │ │
│ │ │ 翌日(祝日)の0時から7時ま │ │ │
│ │ │ での駐車です。これは「日・ │ │ │
│ │ │ 休日」の「19:00〜8:00」の時 │ │ │
│ │ │ 間帯に該当し、この時間帯の │ │ │
│ │ │ 最大料金は400円です。この7 │ │ │
│ │ │ 時間(0:00〜7:00)の基本料 │ │ │
│ │ │ 金は20分330円で計算すると69 │ │ │
│ │ │ 30円になりますが、最大料金4 │ │ │
│ │ │ 00円が適用されます。\n\n看 │ │ │
│ │ │ 板には「タイムズパーキング │ │ │
│ │ │ の最大料金は繰り返し適用さ │ │ │
│ │ │ れます」と記載されています │ │ │
│ │ │ 。これは、日をまたぐ場合や │ │ │
│ │ │ 、最大料金の適用期間を超え │ │ │
│ │ │ て駐車した場合に、再度最大 │ │ │
│ │ │ 料金が適用されることを意味 │ │ │
│ │ │ します。今回のケースでは、 │ │ │
│ │ │ 木曜の夜間料金と祝日の夜間 │ │ │
│ │ │ 料金がそれぞれ独立して適用 │ │ │
│ │ │ されると解釈するのが妥当で │ │ │
│ │ │ す。\n\nしたがって、木曜の │ │ │
│ │ │ 夜間料金として400円、祝日の │ │ │
│ │ │ 夜間料金として400円が適用さ │ │ │
│ │ │ れ、合計料金は400円 + 400円 │ │ │
│ │ │ = 800円となります。' │ │ │
│ │ │ fee=800 │ │ │
├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┼────────────┼──────────┤
│ Averages │ │ │ 83.3% ✔ │ 11.5s │
└─────────────────────────────┴─────────────────────────────┴─────────────────────────────┴────────────┴──────────┘
ケース1,2は正しく評価できていますが、ケース3は適切でないようです。期待する正解は400円でしたが、エージェントは800円と回答しました。 これは、エージェントが「24時(日付変更)」を境に料金計算をリセットし、2日分の夜間最大料金を足し合わせてしまったためです。改善するにはモデルをflashからproにすることや、プロンプトを修正する必要がありそうです。
まとめ
今回は Pydantic Evalsを活用して、コインパーキング料金計算エージェントの評価駆動開発を実践してみました。実際にツールを使ってみて、以下のような点に気づきました。
評価のデータモデルが良い
- DatasetやCaseなどの評価データモデルがよくできており、テストケースの管理や評価ロジックをシンプルかつ抽象度高く記述できます
- そのおかげで、実装がすっきりまとまりました
インフラ実装の手間が減る
- 複数テストケースを並列で評価する処理などを自前で実装する必要がなく、手間が大幅に削減されました