Strands Agents SDK 実践 — Structured Output で LLM の出力を型安全にする
目次
はじめに
入門シリーズでは、エージェントの基本からマルチエージェントまでを学んだ。しかし、これまでのエージェントの出力はすべてテキストだった。「250 USD は 37,375 JPY です」という文字列を受け取っても、プログラムで金額を取り出すにはテキストをパースする必要がある。
Structured Output を使えば、Pydantic モデルを渡すだけで LLM の出力が型付きオブジェクトになる。テキストパースは不要だ。
この記事では以下を試す。
- 基本 — Pydantic モデルで LLM の出力を構造化する
- ツールとの併用 — ツール実行結果を構造化する
- バリデーション自動リトライ — バリデーション失敗時に LLM が自動で修正する
- 会話履歴からの抽出 — マルチターン会話の文脈から情報を構造化する
公式ドキュメントは Structured Output を参照。
セットアップ
入門シリーズの環境をそのまま使う。新規の場合は以下を実行する。
mkdir my_agent && cd my_agent
python -m venv .venv
source .venv/bin/activate
pip install strands-agents strands-agents-tools以降の例ではすべて同じモデル設定を使う。各例は独立した .py ファイルとして実行できる。共通設定を先頭に書き、その下に各例のコードを追加する形だ。
from strands import Agent
from strands.models import BedrockModel
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1",
)基本 — Pydantic モデルで出力を構造化する
まずは最もシンプルな例から。テキストから人物情報を抽出し、型付きオブジェクトとして受け取る。
from pydantic import BaseModel, Field
class PersonInfo(BaseModel):
"""Information about a person."""
name: str = Field(description="Full name of the person")
age: int = Field(description="Age in years")
occupation: str = Field(description="Current occupation")
agent = Agent(model=bedrock_model, callback_handler=None)
result = agent(
"John Smith is a 30 year-old software engineer",
structured_output_model=PersonInfo,
)
person = result.structured_output
print(f"Name: {person.name}")
print(f"Age: {person.age}")
print(f"Occupation: {person.occupation}")
print(f"Type: {type(person).__name__}")callback_handler=None はコンソールへのストリーミング出力を無効にする設定だ。結果は result から取得する。以降の例でもすべて同じ設定を使う。
01_basic.py 全体コード(コピペ用)
from strands import Agent
from strands.models import BedrockModel
from pydantic import BaseModel, Field
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1",
)
class PersonInfo(BaseModel):
"""Information about a person."""
name: str = Field(description="Full name of the person")
age: int = Field(description="Age in years")
occupation: str = Field(description="Current occupation")
agent = Agent(model=bedrock_model, callback_handler=None)
result = agent(
"John Smith is a 30 year-old software engineer",
structured_output_model=PersonInfo,
)
person = result.structured_output
print(f"Name: {person.name}")
print(f"Age: {person.age}")
print(f"Occupation: {person.occupation}")
print(f"Type: {type(person).__name__}")python -u 01_basic.py実行結果
Name: John Smith
Age: 30
Occupation: software engineer
Type: PersonInforesult.structured_output が PersonInfo 型のオブジェクトになっている。person.name で名前、person.age で年齢にアクセスできる。テキストをパースする必要はない。
ポイントは 3 つだ。
- Pydantic モデルを定義する —
BaseModelを継承し、フィールドにField(description=...)で説明を付ける。この説明が LLM への指示になる structured_output_modelに渡す —agent()の呼び出し時にモデルクラスを指定するresult.structured_outputで取得する — 型付きオブジェクトとして結果を受け取る
ツールとの併用 — ツール実行結果を構造化する
Structured Output はツールと組み合わせることができる。入門第 2 回で作った為替レート取得ツールの結果を、構造化されたオブジェクトとして受け取ってみる。
出力モデルを定義し、ツールと一緒にエージェントに渡す。
class ConversionResult(BaseModel):
"""Currency conversion result."""
base_currency: str = Field(description="Source currency code")
target_currency: str = Field(description="Target currency code")
exchange_rate: float = Field(description="Exchange rate used")
original_amount: float = Field(description="Original amount")
converted_amount: float = Field(description="Converted amount")
agent = Agent(model=bedrock_model, tools=[get_exchange_rate], callback_handler=None)
result = agent(
"Convert 250 USD to JPY",
structured_output_model=ConversionResult,
)
conv = result.structured_output
print(f"Result: {conv.original_amount} {conv.base_currency} = {conv.converted_amount} {conv.target_currency}")
print(f"Rate: {conv.exchange_rate}")get_exchange_rate ツールは入門第 2 回と同じものを使う。
02_tools.py 全体コード(コピペ用)
from strands import Agent, tool
from strands.models import BedrockModel
from pydantic import BaseModel, Field
import json
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1",
)
@tool
def get_exchange_rate(base: str, target: str) -> dict:
"""Get the current exchange rate between two currencies.
Args:
base: The base currency code (e.g. USD, EUR, JPY)
target: The target currency code (e.g. USD, EUR, JPY)
Returns:
dict: Exchange rate information
"""
rates = {
("USD", "JPY"): 149.50,
("EUR", "USD"): 1.08,
("EUR", "JPY"): 161.46,
}
rate = rates.get((base.upper(), target.upper()))
if rate is None:
return {"error": f"Rate not found for {base}/{target}"}
return {"base": base.upper(), "target": target.upper(), "rate": rate}
class ConversionResult(BaseModel):
"""Currency conversion result."""
base_currency: str = Field(description="Source currency code")
target_currency: str = Field(description="Target currency code")
exchange_rate: float = Field(description="Exchange rate used")
original_amount: float = Field(description="Original amount")
converted_amount: float = Field(description="Converted amount")
agent = Agent(model=bedrock_model, tools=[get_exchange_rate], callback_handler=None)
result = agent("Convert 250 USD to JPY", structured_output_model=ConversionResult)
conv = result.structured_output
print(f"Result: {conv.original_amount} {conv.base_currency} = {conv.converted_amount} {conv.target_currency}")
print(f"Rate: {conv.exchange_rate}")
print("\n--- Metrics ---")
print(json.dumps(result.metrics.get_summary(), indent=2, default=str))python -u 02_tools.py実行結果
Result: 250.0 USD = 37375.0 JPY
Rate: 149.5エージェントは get_exchange_rate ツールでレートを取得し、その結果を ConversionResult モデルに構造化して返した。テキストではなく、conv.converted_amount で数値として直接アクセスできる。
Structured Output の内部動作
メトリクスを見ると、興味深い仕組みが分かる。
Cycles: 2
tool_usage:
get_exchange_rate: calls=1, success=1
ConversionResult: calls=1, success=1ConversionResult がツールとして表示されている。Structured Output は内部的に Pydantic モデルを「ツール」として LLM に登録する仕組みだ。LLM は通常のツール呼び出しと同じ方法で構造化データを出力し、SDK がそれを Pydantic モデルとしてバリデーションする。
つまり、エージェントループの流れは以下のようになる。
- Cycle 1: LLM が
get_exchange_rateツールを呼び出し、レートを取得する - Cycle 2: LLM がレートを使って計算し、
ConversionResultツール(= Pydantic モデル)を呼び出して構造化データを出力する
バリデーション自動リトライ — LLM が自動で修正する
Pydantic の field_validator でカスタムバリデーションを追加すると、バリデーション失敗時に LLM が自動でリトライする。
from pydantic import BaseModel, Field, field_validator
class UserName(BaseModel):
"""A user's name with a required suffix."""
first_name: str = Field(description="First name of the person")
@field_validator("first_name")
@classmethod
def validate_first_name(cls, value: str) -> str:
if not value.endswith("_verified"):
raise ValueError("first_name must end with '_verified' suffix")
return value
agent = Agent(model=bedrock_model, callback_handler=None)
result = agent(
"What is Aaron's first name?",
structured_output_model=UserName,
)
print(f"Result: {result.structured_output}")03_validation.py 全体コード(コピペ用)
from strands import Agent
from strands.models import BedrockModel
from pydantic import BaseModel, Field, field_validator
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1",
)
class UserName(BaseModel):
"""A user's name with a required suffix."""
first_name: str = Field(description="First name of the person")
@field_validator("first_name")
@classmethod
def validate_first_name(cls, value: str) -> str:
if not value.endswith("_verified"):
raise ValueError("first_name must end with '_verified' suffix")
return value
agent = Agent(model=bedrock_model, callback_handler=None)
result = agent("What is Aaron's first name?", structured_output_model=UserName)
print(f"Result: {result.structured_output}")python -u 03_validation.py実行結果
tool_name=<UserName> | structured output validation failed |
error_message=<Validation failed for UserName. Please fix the following errors:
- Field 'first_name': Value error, first_name must end with '_verified' suffix>
Result: first_name='Aaron_verified'Cycles: 2
UserName: calls=2, success=1, errors=1, success_rate=0.51 回目の呼び出しで LLM は first_name="Aaron" を返したが、バリデーションが失敗した。SDK はエラーメッセージを LLM に返し、LLM が 2 回目の呼び出しで first_name="Aaron_verified" に修正した。
この仕組みは以下の場面で有用だ。
- フォーマット制約 — メールアドレスの形式、日付のフォーマットなど
- 値の範囲制約 — 年齢が 0〜150 の範囲内、価格が正の数など
- ビジネスルール — 特定のプレフィックスやサフィックスの付与
開発者はバリデーションルールを Pydantic モデルに書くだけでよい。リトライのロジックは SDK が自動で処理する。
会話履歴からの構造化抽出
入門第 4 回で学んだマルチターン会話と Structured Output を組み合わせると、会話の文脈から情報を構造化できる。
from typing import Optional
agent = Agent(model=bedrock_model, callback_handler=None)
# 会話で情報を蓄積する
agent("My name is Taro and I work as a data scientist at a startup in Tokyo.")
agent("I've been using Python for 5 years and recently started learning Rust.")
class ProfileSummary(BaseModel):
"""Summary of a person's profile from conversation."""
name: str = Field(description="Person's name")
occupation: str = Field(description="Current job title")
location: str = Field(description="Where they work")
primary_language: str = Field(description="Main programming language")
learning: Optional[str] = Field(description="Language currently learning", default=None)
# 会話履歴から構造化データを抽出する
result = agent(
"Based on our conversation, extract my profile information.",
structured_output_model=ProfileSummary,
)
profile = result.structured_output
print(f"Name: {profile.name}")
print(f"Occupation: {profile.occupation}")
print(f"Location: {profile.location}")
print(f"Primary: {profile.primary_language}")
print(f"Learning: {profile.learning}")
print(f"\nMessages in history: {len(agent.messages)}")04_conversation.py 全体コード(コピペ用)
from strands import Agent
from strands.models import BedrockModel
from pydantic import BaseModel, Field
from typing import Optional
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1",
)
agent = Agent(model=bedrock_model, callback_handler=None)
agent("My name is Taro and I work as a data scientist at a startup in Tokyo.")
agent("I've been using Python for 5 years and recently started learning Rust.")
class ProfileSummary(BaseModel):
"""Summary of a person's profile from conversation."""
name: str = Field(description="Person's name")
occupation: str = Field(description="Current job title")
location: str = Field(description="Where they work")
primary_language: str = Field(description="Main programming language")
learning: Optional[str] = Field(description="Language currently learning", default=None)
result = agent(
"Based on our conversation, extract my profile information.",
structured_output_model=ProfileSummary,
)
profile = result.structured_output
print(f"Name: {profile.name}")
print(f"Occupation: {profile.occupation}")
print(f"Location: {profile.location}")
print(f"Primary: {profile.primary_language}")
print(f"Learning: {profile.learning}")
print(f"\nMessages in history: {len(agent.messages)}")python -u 04_conversation.py実行結果
Name: Taro
Occupation: data scientist
Location: Tokyo
Primary: Python
Learning: Rust
Messages in history: 72 回の会話で蓄積された情報が、3 回目の呼び出しで ProfileSummary として構造化された。Optional フィールド(learning)も正しく抽出されている。
この手法は以下の場面で有用だ。
- チャットボットのプロファイル構築 — 会話を通じてユーザー情報を段階的に収集し、最後に構造化する
- 会議メモの構造化 — 自由形式の会話から決定事項やアクションアイテムを抽出する
- フォーム入力の代替 — 会話形式で情報を集め、最終的にフォームデータとして出力する
まとめ
- Pydantic モデルを渡すだけで型安全な出力が得られる —
structured_output_modelにモデルクラスを指定するだけ。テキストパースは不要で、IDE の型補完も効く。 - 内部的にはツールとして動作する — Structured Output は Pydantic モデルをツールとして LLM に登録する。メトリクスにもツールとして表示されるため、通常のツールと同じ方法でパフォーマンスを確認できる。
- バリデーション失敗時は LLM が自動リトライする —
field_validatorでカスタムバリデーションを追加すると、失敗時にエラーメッセージが LLM に返され、LLM が修正して再試行する。開発者がリトライロジックを書く必要はない。 - 会話履歴と組み合わせて情報を抽出できる — マルチターン会話で蓄積された文脈から、任意のタイミングで構造化データを取り出せる。
