🎉

Pydantic・Instructorで安定したエージェント作成

に公開

基本

Pydanticは、Pythonの型ヒントを使って データのバリデーション(妥当性のチェック) を行うライブラリです。

LLMから返ってくる不安定な文字列を、プログラムで扱いやすい「綺麗なオブジェクト」に変換するための**「型による防波堤(ハーネス)」**のような役割を果たします。

1. 最小構成のモデルを作ってみる

Pydanticを利用するには、BaseModelを継承してクラスを作成します。

from typing import List, Optional
from pydantic import BaseModel, Field

class UserProfile(BaseModel):
    # 基本的な型定義
    name: str
    age: int

    # Fieldを使うと、LLMへの「命令文(description)」を書き込める
    hobbies: List[str] = Field(description="ユーザーの趣味のリスト")

    # 任意のフィールド(値がない場合はNoneを許容)
    bio: Optional[str] = Field(default=None, description="短い自己紹介文")

def main():
    user = UserProfile(name="田中", age=25, hobbies=["読書", "テニス"])
    print(user.name)  # 田中

if __name__ == "__main__":
    main()

Fieldの役割

Instructorを使う際、Pydanticの Field(description="...") に書いた内容は、そのままLLMへのシステムプロンプトとして送信されます。

例えば、age: int = Field(description="年齢。不明な場合は0を返してください") と書くだけで、LLMはそのルールを守ろうとします。これが**「型定義がそのままプロンプトになる」**という強力な概念です。


ハンズオン:Pydanticのモデルを設計してみよう

「ウェブサイトからレストランの情報を抜き出すAI」を作成すると仮定して、以下の条件を満たすモデル Restaurant を作成してみてください。

  1. name: レストラン名(文字列)
  2. rating: 星評価(浮動小数点数)
  3. is_open: 営業中かどうか(真偽値)
  4. top_dishes: おすすめ料理のリスト(文字列のリスト)
  5. price_range: 価格帯。Field を使って「安価、中価格、高級のいずれか」という指示を追加してください。

<details><summary>回答例を表示</summary>

from typing import List, Literal
from pydantic import BaseModel, Field

class Restaurant(BaseModel):
    name: str
    rating: float
    is_open: bool
    top_dishes: List[str]
    price_range: Literal["安価", "中価格", "高級"] = Field(
        description="価格帯を選択してください"
    )

</details>


2. Instructorで「構造化」の魔法を手に入れる

モデルが定義できたら、次はこれを使ってLLMから構造化データを取り出します。
instructor の役割は、OpenAIなどのLLMクライアントに「Pydanticを理解する能力」をパッチすることです。

クライアントの準備

通常、LLMのレスポンスはただの文字列ですが、Instructorを使うと戻り値が最初からPydanticのオブジェクトになります。

import os
import instructor
from openai import OpenAI
from dotenv import load_dotenv
from restaurant_model import Restaurant # 先ほど定義したモデル

load_dotenv()

def main():
    client = instructor.from_openai(OpenAI())

    content = """
豫園 金山店 
金山総合駅南口から徒歩3分。特級調理師の資格を持つシェフが腕を振るう中国上海料理のお店です。
メニューは手頃なセットから高級中華まで170品以上の豊富な品揃え。
特にランチメニューはリーズナブルでボリューム満点と大好評です。
おすすめはXO醤野菜炒め、ふかひれラーメン、牛のハチノス。
★3.4点
"""

    restaurant_data = client.chat.completions.create(
        model="gpt-5-nano-2025-08-07",
        response_model=Restaurant,
        messages=[{"role": "user", "content": content}]
    )

    # restaurant_dataがそのままオブジェクトとして使える!
    print(f"店名: {restaurant_data.name}")
    print(f"評価: {restaurant_data.rating}")
    print(f"おすすめ: {', '.join(restaurant_data.top_dishes or [])}")

if __name__ == "__main__":
    main()

ハルシネーションの罠

ここで、入力テキスト(content)を全く関係のないものに変えるとどうなるでしょうか。

content = "純喫茶 サンジェルマン。店長の笑顔が素敵。お店の前の花壇の花がきれいです。"

出力結果(例):

店名: 純喫茶 サンジェルマン
評価: 4.5
おすすめ: ブレンドコーヒー, 厚切りトースト
価格帯: 中価格

テキストに記載がない「評価」や「おすすめ」まで、LLMが空気を読んで生成(ハルシネーション)してしまいました。


3. ハルシネーションを抑え込む

LLMは「何かを返さなければならない」という性質があるため、情報がない場合でも無理やり値を埋めてしまいます。これを防ぐには、**「該当情報がない場合はNoneを返す」**ようにモデルを改良します。

Optionalと判定フラグの追加

from typing import List, Literal, Optional
from pydantic import BaseModel, Field

class Restaurant(BaseModel):
    # そもそもレストランの情報が含まれているかを判定させる
    is_restaurant_info: bool = Field(
        description="具体的なレストランの紹介であればTrue、無関係な内容ならFalse"
    )

    # すべてを Optional にし、デフォルトを None に設定
    name: Optional[str] = Field(
        default=None,
        description="店名。記載がない場合は必ずNoneにしてください",
    )
    rating: Optional[float] = Field(
        default=None,
        description="評価。記載がない場合は必ずNoneにしてください",
    )
    # ...他項目も同様にOptional化


4. バリデーションによる「再試行」

Pydanticの @field_validator を使うと、さらに厳密なチェックが可能です。
例えば、「おすすめ料理は3つまで」「評価は0〜5の間」といったルールを追加します。

from pydantic import field_validator

class Restaurant(BaseModel):
    # ...フィールド定義...

    @field_validator("top_dishes")
    @classmethod
    def check_dish_count(cls, v: Optional[list[str]]) -> Optional[list[str]]:
        if v and len(v) > 3:
            # このエラーメッセージがLLMへの「修正指示」として送信される
            raise ValueError("おすすめ料理は3つ以内に絞ってください。")
        return v

    @field_validator("rating")
    @classmethod
    def check_rating_range(cls, v: Optional[float]) -> Optional[float]:
        if v is not None and not (0 <= v <= 5):
            raise ValueError("評価は0.0から5.0の間で指定してください。")
        return v

自動リトライ機能

Validationエラーが発生した際、InstructorはLLMに対して「エラーが出たから直して再送して」と自動で再送要求を出します。

restaurant_data = client.chat.completions.create(
    model="gpt-5-nano-2025-08-07",
    response_model=Restaurant,
    messages=[{"role": "user", "content": content}],
    max_retries=2  # 最大2回まで自動で修正依頼を出してくれる
)

リトライ回数を超えてもエラーが解消されない場合は InstructorRetryException が発生するため、プログラム側で安全にエラーハンドリングが可能です。これがAIエージェントを「安定して動かす」ための鍵となります。

Discussion