PydanticAI ビジュアルガイド

Lesson CH04-L02

Streamed Output

ストリーミングで部分構造を逐次受け取り、UIに流す。

読了目安
10 min
Colab目安
16 min
合計
26 min
前提
output_type

関連: 公式ドキュメント

一行サマリ

agent.run_stream(...) は応答全体を待たず、部分的なテキストや構造化データを逐次受け取る ストリーミング API。Web UI に「タイピング中」を見せたり、長い応答の体感速度を上げるのに使う。

ヒーロー: 一括 vs ストリーミング

通常の run / run_sync応答全体が確定してから返りますrun_stream生成の途中から逐次 受け取れるので、ユーザー体感の "待ち時間" を実質ゼロに近づけられます。

図を読み込み中…
図1. run と run_stream の体感時間比較

概念: 何がストリーミングできるのか

ストリーミングの中身は 2 種類あります。

種別API中身
テキストストリーミングresponse.stream_text()部分的に伸びていく文字列
構造化ストリーミングresponse.stream(output_type)部分的に埋まっていく Pydantic Model (一部フィールドが None だったり値が暫定)

どちらも async with agent.run_stream(...) as response:response を開き、async for chunk in ... で受け取ります。

コード: 3 つのパターン

パターン 1: テキスト逐次表示 (タイピング風)

import asyncio
from pydantic_ai import Agent
 
agent = Agent('google-gla:gemini-3-flash-preview', instructions='短い日本語で。')
 
async def main():
    async with agent.run_stream('PydanticAI の特徴を 3 つ教えて') as response:
        async for text in response.stream_text(delta=True):
            print(text, end='', flush=True)
        print()  # 末尾改行
 
asyncio.run(main())

ポイント:

  • stream_text(delta=True)新しく届いた差分だけ を返す (Ch0-L02 で覚えた書き方)
  • delta=False (デフォルト) は累積テキストを返す
  • ターミナルでは \r でカーソル戻しもできるが、Colab/Jupyter では改行で見える

パターン 2: 構造化応答のストリーミング

output_type を指定した Agent を run_stream で開くと、部分的に埋まった Pydantic Model を逐次受け取れます。

import asyncio
from pydantic import BaseModel
from pydantic_ai import Agent
 
class Recipe(BaseModel):
    title: str
    ingredients: list[str]
    steps: list[str]
 
agent_r = Agent(
    'google-gla:gemini-3-flash-preview',
    output_type=Recipe,
    instructions='レシピを構造化して返してください。',
)
 
async def main():
    async with agent_r.run_stream('簡単なオムレツのレシピを教えて') as response:
        async for partial in response.stream():
            # partial は Recipe (の部分的な状態)
            print(f'\\rtitle={partial.title or "(未確定)"} / ingredients={len(partial.ingredients or [])} 件', end='')
        print()
        # ループを抜けたら完成形が確定
        final = await response.get_output()
        print(f'\\nFINAL: {final}')
 
asyncio.run(main())

ポイント:

  • response.stream()output_type部分的に埋まったインスタンス を yield する
  • 早い段階では title だけ埋まり ingredients=[] のような状態がありうる
  • 最終結果は await response.get_output() で取れる (完全に検証済み)

パターン 3: 部分結果を Web UI に流す (FastAPI 想定)

実プロジェクトでは、ストリームをそのまま HTTP SSE (Server-Sent Events) に流すのが王道。

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic_ai import Agent
 
app = FastAPI()
agent = Agent('google-gla:gemini-3-flash-preview', instructions='短く日本語で。')
 
@app.get('/chat')
async def chat(q: str):
    async def gen():
        async with agent.run_stream(q) as response:
            async for text in response.stream_text(delta=True):
                yield f'data: {text}\\n\\n'
        yield 'data: [DONE]\\n\\n'
    return StreamingResponse(gen(), media_type='text/event-stream')

ブラウザ側は EventSource で受けて、文字を逐次描画します。これで「ChatGPT 風のタイピング表示」が完成。

図を読み込み中…
図2. ストリーミングのライフサイクル

いつストリーミングを使うか

状況選ぶ
Colab / CLI で結果だけ欲しいrun_sync
バッチ処理run
Web UI に「タイピング中」を見せたいrun_stream + stream_text(delta=True)`
構造化応答の各フィールドが順に埋まる様を UI で見せたいrun_stream + stream()
1 文字ずつ TTS / 音声合成にかけるrun_stream + delta

ストリーミングは ユーザー体感 が改善する一方、コードはやや複雑になります。「待ち時間が UX を決める」場面でだけ 採用するのが無難です。

まとめ

  • agent.run_stream(...)async with で開くと、応答を逐次受け取れる
  • stream_text(delta=True) で差分テキスト、stream(output_type) で部分的な構造化モデル
  • 最終結果は await response.get_output()
  • UI / TTS など「待ち時間が UX を決める」場面で使う

次レッスンでは、複数の output_type 候補を Union 型 で表現し、LLM に「どの型を返すか」を選ばせるパターンを扱います。

Colab で実際に動かす

本レッスンの内容を Google Colab 上で実行できるノートブックを用意しています。下のボタンから自分のColab環境に開けます (要 Google アカウント / GOOGLE_API_KEY)。

Open in Colab

notebooks/ch04/02-streamed-output.ipynb