Pydantic AIを使ってみる(英単語帳作成エージェントを作る)

2025/02/10に公開

はじめに

普段、LLMエージェントの実装にはLangGraphを使用していますが、以前から気になっていたPydantic AIを試してみることにしました。本記事では、Pydantic AIの基本的な使い方を学びつつ、LangGraphとの違いについても考えます。

https://ai.pydantic.dev/

本記事で触れる範囲

本記事では、以下の内容に焦点を当てます。

  • エージェント出力型の定義(Results)
  • ツールの使用(Function Tools)

PydanticAIはエージェントを評価するときにありがたみが感じられそうな気がしますが、本記事ではそこまで触れません。

ライブラリのバージョン

pydantic-ai>=0.0.23
youtube-transcript-api>=0.6.3

本記事で作るもの(英単語帳作成エージェント)

本記事で実装するエージェントは、英語のYouTube動画をもとに、動画内で使用された英単語の単語帳を作成するものです。
YouTubeで英語のリスニングを学習していますが、難しい単語に詰まることがあります。なるべくストレスなく、たくさんの動画を視聴するため、事前に難しい英単語を把握したいと考えています。

具体的には以下のような英語の動画(データサイエンティストに年収を聞く動画)を入力して、
https://www.youtube.com/watch?v=OR34My84ZXE

以下のような単語帳を作成するエージェントです。

No.1
 - compensated,  (ˈkɑːmpenseɪtɪd)
     - 意味: 報酬を受け取る、補償された
     - 用例: do you feel fairly compensated yeah

No.2
 - degree,  (dɪˈɡriː)
     - 意味: 学位
     - 用例: what kind of degree did you have or certification

実装

Dependency(依存関係)

最初に依存関係を定義します。

  • 今回は、youtubeのvideo_idだけを持ちます。
  • 依存関係はPydanticAIの一番の特徴らしいのですが、今回はあまり活かせてません。
@dataclass
class VocabularyBookDeps:
    video_id: str

Result(エージェント出力の型)

エージェントの出力を定義します。

  • 単語帳の要素を以下のVocabularyBookUnitの様に定義します。
  • 実際に出力したいものは単語帳なので、list[VocabularyBookUnit]を出力型として定義します。
  • LangChainのPydanticOutputParserと同じような感じです。
class VocabularyBookUnit(BaseModel):
    vocabulary: str = Field(description='英単語、英熟語')
    pronunciation: str = Field(description='発音記号')
    meaning: str = Field(description='意味(日本語で記述すること)')
    example_in_transcript: str = Field(description='動画での用例')

Agent(エージェント)

エージェントを定義します。

  • エージェントという言葉の定義はビジネス界でも技術界でも色々ですが、Pydantic AIでのエージェントは、LLMとのインターフェース程度のかなり小さい範囲を指すようです。
  • 依存関係と、出力の型とシステムプロンプトを設定します。
  • Pydantic AIのエージェントの粒度は、LangChainで頻出のchain = prompt | llm | parserと同じような粒度です。
  • LangGraphがグラフ構造全体でエージェントといったりする事と比較すると、かなり小さい粒度をエージェントと呼ぶようです。
generate_book_agent = Agent(
    'openai:gpt-4o',
    deps_type=VocabularyBookDeps,
    result_type=list[VocabularyBookUnit],
    system_prompt=(
        "あなたはYoutubeの字幕内容から単語帳を作成するエージェントです。"
        "あなたはYoutubeから動画の字幕を取得する必要があります。"
        "また単語帳のレベルを定義する必要があります。"
        "動画で使われた単語や熟語を抽出し、発音と意味と動画内での使用例を記述してください。"
    ),
)

Function Tool

ツールを定義します。

  • ここでのツールは、モデルがユーザーの応答に必要な追加情報を得る手段です。RAGのRの部分とドキュメントには書かれています。
  • docstringに説明を記載すると、自動的に使用判断されるようです。この辺はLangGraphのツールノードと似ていると感じます。
  • 便利ですが、どのようにツールを選択しているのか?ReActっぽくしているのか?イマイチわからなかったです。
  • 本エージェントでは、単語帳のレベルを定義するツールと、youtubeから英語字幕を取得するツールを定義しました。
@generate_book_agent.tool_plain
def define_difficulty_level() -> Literal[
        'elementary school', 'high school', 'doctoral course'
    ]:
    """英単語帳の難易度を定義する。"""
    return random.choice(['elementary school', 'high school', 'doctoral course'])

@generate_book_agent.tool
def fetch_transcript(ctx: RunContext[VocabularyBookDeps]) -> str:
    """動画の字幕を取得する。"""
    transcript_list = YouTubeTranscriptApi.list_transcripts(ctx.deps.video_id)
    transcript = transcript_list.find_transcript(['en'])
    transcript_data = transcript.fetch()
    transcript = ' '.join([line['text'] for line in transcript_data])
    return f"The transcript of the video is: {transcript}"

動作結果

エージェントの挙動を確認するためresult.all_messages()の中身を確認して見ます。
最終的な出力はAppendixに記載しています。

  • 最初にLLMへの入力として、システムプロンプトとユーザープロンプトが表示されています。
  • 次にLLMからの応答として、上記プロンプトの結果2つのツールを呼ぶことが決まったようです。
    • ドキュメントの例だと順番に呼ばれていたのですが、並列に呼ばれることもあるということでしょうか?
    • この辺のプランニングなのかReAct的な挙動なのかはよくわかりません。
  • 3番目は両ツールの応答が返ってきています。
  • 4番目にLLMからの応答として、final_resultというツールが呼ばれています。
    • これは何か、よくわかりませんが結果のマージでもやってるんですかね?
    • final_resultツールの入力値(context)には、すでに単語帳が生成されているようです。
  • 5番目にfinal_resultの応答が帰ってきています。

all_messages

まとめと雑感

まとめ

  • Pydantic AIの基本的な機能を使用し、英単語帳作成エージェントを実装しました。

雑感

  • Pydantic AIのエージェントと呼ばれる粒度は、LangChainのprompt|llm|parserと同じような感じ。
  • サンプルコードは非同期関数が多くてなれない...
  • より複雑な挙動を実装するには、エージェントをツールとして使用するAgent delegationや、LangGraphのようにグラフ構造で処理を定義するGraphの章を読むと良さそう。
  • Pydantic AIは趣味で触るにとどまりそうだが、ドキュメントを読むと納得感のある概念や整理は多いため、触っておくと勉強になりそう。

Appendix,ソースコードと出力

ソースコード
import random
from typing import Literal
from pydantic import BaseModel, Field
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from youtube_transcript_api import YouTubeTranscriptApi

@dataclass
class VocabularyBookDeps:
    video_id: str

class VocabularyBookUnit(BaseModel):
    vocabulary: str = Field(description='英単語、英熟語')
    pronunciation: str = Field(description='発音記号')
    meaning: str = Field(description='意味(日本語で記述すること)')
    example_in_transcript: str = Field(description='動画での用例')

generate_book_agent = Agent(
    'openai:gpt-4o',
    deps_type=VocabularyBookDeps,
    result_type=list[VocabularyBookUnit],
    system_prompt=(
        "あなたはYoutubeの字幕内容から単語帳を作成するエージェントです。"
        "あなたはYoutubeから動画の字幕を取得する必要があります。"
        "また単語帳のレベルを定義する必要があります。"
        "動画で使われた単語や熟語を抽出し、発音と意味と動画内での使用例を記述してください。"
    ),
)

@generate_book_agent.tool_plain
def define_difficulty_level() -> Literal[
        'elementary school', 'high school', 'doctoral course'
    ]:
    """英単語帳の難易度を定義する。"""
    return random.choice(['elementary school', 'high school', 'doctoral course'])

@generate_book_agent.tool
def fetch_transcript(ctx: RunContext[VocabularyBookDeps]) -> str:
    """動画の字幕を取得する。"""
    transcript_list = YouTubeTranscriptApi.list_transcripts(ctx.deps.video_id)
    transcript = transcript_list.find_transcript(['en'])
    transcript_data = transcript.fetch()
    transcript = ' '.join([line['text'] for line in transcript_data])
    return f"The transcript of the video is: {transcript}"

async def main():
    deps = VocabularyBookDeps(video_id='OR34My84ZXE')
    result = await generate_book_agent.run(
        'youtubeスクリプトを読み込んで、単語帳を作成してください',
        deps=deps
    )

    for i, unit_vocabulary in enumerate(result.data):
        print(f"No.{i+1}")
        print(f" - {unit_vocabulary.vocabulary},  ({unit_vocabulary.pronunciation})")
        print(f"     - 意味: {unit_vocabulary.meaning}")
        print(f"     - 用例: {unit_vocabulary.example_in_transcript}")
        print()

    print(result.all_messages())

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())
出力(単語帳)
No.1
 - compensated,  (ˈkɑːmpenseɪtɪd)
     - 意味: 報酬を受け取る、補償された
     - 用例: do you feel fairly compensated yeah

No.2
 - degree,  (dɪˈɡriː)
     - 意味: 学位
     - 用例: what kind of degree did you have or certification

No.3
 - certification,  (ˌsɜːrtɪfɪˈkeɪʃən)
     - 意味: 認証、資格
     - 用例: what kind of degree did you have or certification

No.4
 - boot camp,  (buːt kæmp)
     - 意味: ブートキャンプ、集中的な訓練プログラム
     - 用例: I took a data science boot camp last year

No.5
 - renegotiate,  (ˌriːnɪˈɡoʊʃieɪt)
     - 意味: 再交渉する
     - 用例: but then give us like six-ish months and we can renegotiate

No.6
 - work-life balance,  (wɜːrk laɪf ˈbæləns)
     - 意味: ワークライフバランス
     - 用例: I have really great work-life balance

No.7
 - self-taught,  (sɛlf tɔːt)
     - 意味: 独学の
     - 用例: I kind of like self-taught myself into this role

No.8
 - portfolio,  (ˌpɔːrtˈfoʊli.oʊ)
     - 意味: ポートフォリオ、作品集
     - 用例: yeah like a portfolio

No.9
 - networking,  (ˈnɛtˌwɜːrkɪŋ)
     - 意味: ネットワーキング
     - 用例: oh it's networking a networking Network or networking

No.10
 - mentor,  (ˈmɛntɔːr)
     - 意味: 指導者、助言者
     - 用例: find a mentor who can help you navigate different sectors in the industry

Discussion