【連載】pydantic-ai徹底解説 (4) テスト・Evalsと開発運用支援（GoogleColab付）



ここまでで pydantic-ai を使ったLLMエージェント構築の基本、ツール呼び出し、依存性注入、構造化レスポンス、ストリーミングなどを学びました。  
最終回となる今回は、テストやエバリュエーション(Evals)といった開発・運用面のサポート機能、そしてログ出力による可視化について紹介します。


## インストール方法

pydantic-aiとその例を実行するには、以下の手順でインストールを行います：

### 基本的なインストール

```python
# pipを使用する場合
!pip install 'pydantic-ai[examples]' loguru
```

```python
import os
from google.colab import userdata

os.environ['GEMINI_API_KEY'] = userdata.get('GEMINI_API_KEY')
os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')
```

```python
import nest_asyncio
nest_asyncio.apply()
```

---

## テスト容易性

LLMを用いたアプリケーションのテストは難しいものですが、pydantic-aiは以下の機能でサポートします。

- **`TestModel`**：LLM APIを実際には呼ばずに、ツールスキーマに基づくダミーデータを自動生成。
- **`FunctionModel`**：任意のPython関数を使ってモックレスポンスを柔軟に定義。
- **`ALLOW_MODEL_REQUESTS`**：Falseにすることで実APIコールを抑制し、テストを安定化。

### 簡易例：TestModelを使ったモック応答

```python
import pytest
from pydantic_ai import Agent, models
from pydantic_ai.models.test import TestModel
from loguru import logger

models.ALLOW_MODEL_REQUESTS = False  # LLM API呼び出しを禁止
agent = Agent('openai:gpt-4o')

with agent.override(model=TestModel()):
    result = agent.run_sync('面白いジョークを教えてください。')
    logger.info("Mocked Result: {}", result.data)
```

上記では`TestModel`により、ネットワークやAPIキーへの依存を排除し、純粋なロジックテストが実現可能です。  
`loguru`で結果をログ出力することで、テスト結果の可視性が向上します。

---

## エバリュエーション (Evals)

Evalsはモデル応答品質を評価するプロセスです。pydantic-ai専用のEvalフレームワークはありませんが、構造化レスポンスとモックモデルでEvalフローを自動化できます。

- 構造化レスポンスを解析し、出力の質を自動スコアリング。
- `TestModel`や`FunctionModel`で安定した再現性のある評価シナリオを作成。

---

## Logfireとの統合

pydantic-aiは[Pydantic Logfire](https://pydantic.dev/logfire)との連携も容易で、ログやトレース、メトリクスを一元管理できます。  
本番環境でのトラブルシューティングやパフォーマンス改善が容易になります。

---

## 高度な例：猫猫カンパニーエージェント

以下では、猫用商品のECサイト「猫猫カンパニー」を想定し、エージェントが在庫問い合わせや出荷情報、FAQに回答するシナリオを示します。

### 5.1 シナリオ概要

- 製品情報問い合わせ  
- 注文状況確認  
- 猫関連FAQ回答

本番ではOpenAIやGeminiを利用しますが、テストでは`TestModel`や`FunctionModel`でレスポンスをモックします。

### 5.2 TestModelを用いたCI/CDテスト

```python
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
from pydantic_ai import models
from loguru import logger

models.ALLOW_MODEL_REQUESTS = False

inventory_agent = Agent('openai:gpt-4o', tools=[])  # 在庫チェックツールを仮定
with inventory_agent.override(model=TestModel()):
    prompt = "「キャットニップデラックス」の在庫はありますか？"
    result = inventory_agent.run_sync(prompt)
    logger.info("在庫テスト結果: {}", result.data)
    # ここではresult.dataに"十分"という文字があるかは不定
    # TestModelは自動生成したデータを返します

```

ここでは、LLMレスポンスを`TestModel`でモックし、「在庫は十分にあります」という決まった回答を返すテストです。  
`loguru`でログ出力することで、CI上でもテキスト出力がわかりやすくなります。

### 5.3 FunctionModelによる高度なモックレスポンス

```python
from pydantic_ai import Agent, models
from pydantic_ai.models.function import FunctionModel
from pydantic_ai.messages import (
    Message,
    ModelAnyResponse,
    ModelTextResponse
)
from pydantic_ai.models.function import AgentInfo
from loguru import logger

models.ALLOW_MODEL_REQUESTS = False

def conditional_response(messages: list[Message], info: AgentInfo) -> ModelAnyResponse:
    # ユーザーの最後のメッセージ内容を取得
    user_prompt = messages[-1].content if messages else ""

    if "キャットニップデラックス" in user_prompt:
        return ModelTextResponse("「キャットニップデラックス」は在庫が少なくなっています。")
    else:
        return ModelTextResponse("他の製品は在庫潤沢です。")

conditional_agent = Agent('openai:gpt-4o', tools=[])

with conditional_agent.override(model=FunctionModel(conditional_response)):
    result_deluxe = conditional_agent.run_sync("「キャットニップデラックス」在庫状況は？")
    logger.info("条件付きレスポンス(デラックス): {}", result_deluxe.data)
    assert "少なく" in result_deluxe.data

    result_other = conditional_agent.run_sync("通常版猫用おもちゃの在庫状況は？")
    logger.info("条件付きレスポンス(通常版): {}", result_other.data)
    assert "潤沢" in result_other.data

```

`FunctionModel`により入力プロンプト内容に応じた動的なモックレスポンスを実装し、より複雑なテストが可能になります。

### 5.4 Evalsシナリオ：プロンプト改善の効果測定

```python
from pydantic_ai import Agent, models
from pydantic_ai.models.function import FunctionModel
from pydantic_ai.messages import Message, ModelAnyResponse, ModelTextResponse
from pydantic_ai.models.function import AgentInfo
from loguru import logger

baseline_prompt = "「キャットニップデラックス」について教えてください。"
improved_prompt = "「キャットニップデラックス」について、出荷予定日も含めて教えてください。"

def eval_function(messages: list[Message], info: AgentInfo) -> ModelAnyResponse:
    # メッセージリストからユーザーの最後のプロンプトを取得
    user_prompt = messages[-1].content if messages else ""
    if "出荷予定" in user_prompt:
        return ModelTextResponse("「キャットニップデラックス」は人気商品です。出荷予定日は2日後です。")
    else:
        return ModelTextResponse("「キャットニップデラックス」は人気商品です。")

eval_agent = Agent('openai:gpt-4o')
models.ALLOW_MODEL_REQUESTS = False

with eval_agent.override(model=FunctionModel(eval_function)):
    baseline_result = eval_agent.run_sync(baseline_prompt)
    improved_result = eval_agent.run_sync(improved_prompt)

    baseline_score = 0
    improved_score = 0
    if "出荷予定日" in baseline_result.data:
        baseline_score += 1
    if "出荷予定日" in improved_result.data:
        improved_score += 1

    logger.info("ベースラインスコア: {}", baseline_score)
    logger.info("改善後スコア: {}", improved_score)
    # 改善後プロンプトでより詳細情報(出荷予定日)が加わったことを確認

```

`loguru`でログに出力することで、Evals結果を明確に可視化し、どのプロンプトがより良い応答を引き出したかを簡単に確認できます。

### 5.5 loguruを用いたログ出力例

`loguru`は、シンプルかつ強力なロギングツールで、色分け・フォーマット・フィルタリングなどを簡潔に行えます。  
上記例のように`logger.info`や`logger.debug`を使うことで、テストやEvalsのプロセスを人間が読みやすい形で残せます。

```python
from loguru import logger

logger.add("debug_log.log", format="{time} {level} {message}", level="DEBUG")

# テストコード中
logger.debug("Evals用ベースラインプロンプト: {}", baseline_prompt)
logger.debug("Evals用改善後プロンプト: {}", improved_prompt)
logger.info("Evals結果 - ベースライン: {}, 改善後: {}", baseline_result.data, improved_result.data)
```

これで、実行時にはコンソール出力と`debug_log.log`ファイル両方でプロセスを追跡できます。

### 5.6 Logfireでの観測とデバッグ

本番では、Logfireを導入することで運用状況をより深く観測できます。  
`loguru`でローカルなテキストログを扱いつつ、Logfireでは構造化ログやトレースを管理し、問題発生時の根本原因分析が容易になります。


<blockquote class="twitter-tweet" data-media-max-width="800"><p lang="ja" dir="ltr">AI エージェントフレームワーク「pydantic-ai」を触ってみる⑩<br>《テスト・Evals編》<br>logfire で記録を保存してみた！<br>これはエージェントの処理内容を追うのに使えそう！ <a href="https://t.co/Vw7cORdab5">https://t.co/Vw7cORdab5</a> <a href="https://t.co/orUQstZHTO">pic.twitter.com/orUQstZHTO</a></p>&mdash; Maki@Sunwood AI Labs. (@hAru_mAki_ch) <a href="https://twitter.com/hAru_mAki_ch/status/1865777170683167229?ref_src=twsrc%5Etfw">December 8, 2024</a></blockquote> 


```python
import os
from google.colab import userdata

os.environ['LOGFIRE_TOKEN'] = userdata.get('LOGFIRE_TOKEN')
```

```python
import logfire

logfire.configure()

production_agent = Agent('openai:gpt-4o', tools=[])
# 実運用中に Logfire へ送信されるログをもとに、問題発生時のデバッグが可能。
```

---

## まとめ

- `TestModel`・`FunctionModel`などのモックモデルでLLMレスポンスを安定化し、CI/CDでのテストを容易化。
- `ALLOW_MODEL_REQUESTS`で実APIコールを遮断し、外部依存性のないテストを構築。
- Evalsでプロンプト改善効果を定量化し、開発サイクルを改善。
- `loguru`でログ出力フォーマットを改善し、テスト結果・Evals結果を可読性高く可視化。
- Logfireなどの観測ツールと組み合わせることで、本番環境での運用・デバッグが強化。

この一連の例を通して、pydantic-aiによるLLMエージェント開発が、より型安全・テスト可能・拡張性に優れ、開発から運用、改善に至るまでのプロセスを包括的にサポートできることが示されました。


## 📒ノートブック

https://colab.research.google.com/drive/1HRa-Dfcn2u-XnKhm7XAv8kMWiGlDGkgj?usp=sharing


## 参考サイト

https://github.com/pydantic/pydantic-ai

https://github.com/pydantic/pydantic-ai/issues/144

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>