🤖

Amazon BedrockのClaudeモデルをPydanticを使って構造化出力させてみる

2024/12/08に公開

構造化出力について

LLM(大規模言語モデル)の出力を構造化することは、データの一貫性を保ち、後処理を容易にするために重要です。OpenAIの「Structured Outputs」やClaudeのJSONモード機能は、事前に定義したJSONスキーマに従ってモデルの応答を整形し、信頼性の高いデータ処理を可能にします。

BedrockのConverse APIで構造化出力を行う

BedrockのConverse APIでClaudeのJSONモードを使った構造化出力をやってみます。

使用例

今回は、会議の文字起こしをLLMに渡し、その議事録を生成してもらうことを考えます。
出力としては以下を考えます。

  • meeting_data : string
  • participants : string
  • decisions_made: List(string)
  • action_items : object
    • task : string
    • assignee : string
    • due_date : string
  • next_meeting_date : string

JSON modeを使うには、tool_difinitionというものを定義し、inputschemaに出力させたいスキーマを記述する必要があります。今回の例では以下のようになるかと思います。

tool_definition = {
   "toolSpec": {
        "name": "meeting_minute_extract",
        "description": "会議の文字起こしから議事録を作成します。",
        "inputSchema": {
            "json": {
                "type": "object",
                "properties": {
                    "meeting_date": {
                        "description": "会議が行われた日付",
                        "title": "Meeting Date",
                        "type": "string"
                    },
                    "participants": {
                        "description": "会議に参加した人のリスト",
                        "title": "Participants",
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "decisions_made": {
                        "description": "会議で決定された内容のリスト",
                        "title": "Decisions Made",
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "action_items": {
                        "description": "アクション項目のリスト(担当者、期限を含む)",
                        "title": "Action Items",
                        "type": "array",
                        "items": {
                            "$ref": "#/$defs/ActionItem"
                        }
                    },
                    "next_meeting": {
                        "description": "次回の会議予定日",
                        "title": "Next Meeting",
                        "type": "string"
                    }
                },
                "$defs": {
                    "ActionItem": {
                        "properties": {
                            "task": {
                                "description": "アクション項目の内容",
                                "title": "Task",
                                "type": "string"
                            },
                            "assignee": {
                                "description": "担当者",
                                "title": "Assignee",
                                "type": "string"
                            },
                            "due_date": {
                                "description": "期限",
                                "title": "Due Date",
                                "type": "string"
                            }
                        },
                        "required": ["task", "assignee", "due_date"],
                        "title": "ActionItem",
                        "type": "object"
                    }
                },
                "required": [
                    "meeting_date",
                    "participants",
                    "decisions_made",
                    "action_items",
                    "next_meeting"
                ],
            }
        }
    }
}

ですが、スキーマをそのまま書くのはとても冗長で可読性の低い物になってしまいます。

Pydanticを使う

Pydanticはデータモデルを扱うことができるライブラリです。
先程のスキーマはPydanticを使うと以下のように書くことができます。

from pydantic import BaseModel, Field
from typing import List

class ActionItem(BaseModel):
    task: str = Field(description="アクション項目の内容")
    assignee: str = Field(description="担当者")
    due_date: str = Field(description="期限。yyyymmmddの形式で出力する。")

class MeetingMinutesExtraction(BaseModel):
    meeting_date: str = Field(description="会議が行われた日付。yyyymmmddの形式で出力する。")
    participants: List[str] = Field(description="会議に参加した人のリスト")
    decisions_made: List[str] = Field(description="会議で決定された内容のリスト")
    action_items: List[ActionItem] = Field(description="アクション項目のリスト(担当者、期限を含む)")
    next_meeting: str = Field(description="次回の会議予定日。yyyymmmddの形式で出力する。")

JSONで書くよりも可読性がとても上がりました。
これをスキーマとして渡す場合は、model_json_schema() 関数を呼び出してあげるだけです。

tool_definition = {
    "toolSpec": {
        "name": "meeting_minute_extract",
        "description": "会議の文字起こしから議事録を作成します。",
        "inputSchema": {
            "json": MeetingMinutesExtraction.model_json_schema()
        }
    }
}

実行例

今回はChatGPTで適当に生成した架空の会議の文字起こしを渡してみます。

マーケティング定例 2024年10月31日
田中太郎:皆さん、本日はお集まりいただきありがとうございます。まず最初の議題は、新製品のマーケティング戦略についてです。発売日についての議論を進めたいと思いますが、皆さんのご意見をお聞かせください。
鈴木花子:私としては、年末商戦を狙って2024年12月1日に発売するのが最適だと思います。競合製品も多いですが、それだけ消費者の関心が高まる時期です。
山田一郎:確かに、12月の売上は重要ですし、その時期ならば、年内に大きなプロモーションを展開できるメリットがありますね。スケジュール上も問題ないと思います。
田中太郎:それでは、発売日は2024年12月1日に決定としましょう。次に、プロジェクトの進捗についてですが、ここは月次で進捗を確認していく方向で進めていくのが良いでしょうか?
鈴木花子:そうですね。プロジェクトの内容が多岐にわたるので、定期的な進捗確認は必須だと思います。
山田一郎:私がスケジュール調整をして、次回会議で詳細を報告するようにします。
田中太郎:ありがとうございます。最後に、来年度の予算計画ですが、現在の状況を考慮して10%増額を提案します。皆さん、どうでしょうか?
鈴木花子:賛成です。新プロジェクトも控えているので、必要な増額だと思います。
山田一郎:私も賛成です。新しいチームの拡充も考えると、それくらいの増額は必要です。
田中太郎:それでは、来年度の予算を10%増額することで決定します。以上で本日の議題は終了です。アクションアイテムの確認ですが、私は11月5日までにマーケティングチームと宣伝資料を作成します。
山田一郎:私は明日までにスケジュール調整を行います。
田中太郎:では、次回の会議は2024年11月5日としましょう。本日はありがとうございました。

実行すると想定通りのスキーマで出力されていることが確認できました。

{'meeting_date': '20241031',
 'participants': ['田中太郎', '鈴木花子', '山田一郎'],
 'decisions_made': ['新製品の発売日を2024年12月1日に決定した',
                    'プロジェクトの進捗を月次で確認していくことにした',
                    '来年度の予算を10%増額することにした'],
 'action_items': [{'task': 'マーケティングチームと宣伝資料を作成する',
                   'assignee': '田中太郎',
                   'due_date': '20241105'},
                  {'task': 'スケジュール調整を行う',
                   'assignee': '山田一郎',
                   'due_date': '20241101'}],
 'next_meeting': '20241105'}

まとめ

Pydanticを使うことで構造化出力を簡単に扱うことができました。
構造化出力はLLMをアプリケーションに取り込む際には必須の機能だと思うので、ぜひお試しください。

コード全文

今回使用したコードです。
from pprint import pprint

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

client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
modelId = 'anthropic.claude-3-haiku-20240307-v1:0'

class ActionItem(BaseModel):
    task: str = Field(description="アクション項目の内容")
    assignee: str = Field(description="担当者")
    due_date: str = Field(description="期限。yyyymmmddの形式で出力する。")

class MeetingMinutesExtraction(BaseModel):
    meeting_date: str = Field(description="会議が行われた日付。yyyymmmddの形式で出力する。")
    participants: List[str] = Field(description="会議に参加した人のリスト")
    decisions_made: List[str] = Field(description="会議で決定された内容のリスト")
    action_items: List[ActionItem] = Field(description="アクション項目のリスト(担当者、期限を含む)")
    next_meeting: str = Field(description="次回の会議予定日。yyyymmmddの形式で出力する。")

tool_name = "meeting_minute_extract"

tool_definition = {
    "toolSpec": {
        "name": tool_name,
        "description": "会議の文字起こしから議事録を作成します。",
        "inputSchema": {
            "json": MeetingMinutesExtraction.model_json_schema()
        }
    }
}

meeting_script = """
マーケティング定例 2024年10月31日
田中太郎:皆さん、本日はお集まりいただきありがとうございます。まず最初の議題は、新製品のマーケティング戦略についてです。発売日についての議論を進めたいと思いますが、皆さんのご意見をお聞かせください。
鈴木花子:私としては、年末商戦を狙って2024年12月1日に発売するのが最適だと思います。競合製品も多いですが、それだけ消費者の関心が高まる時期です。
山田一郎:確かに、12月の売上は重要ですし、その時期ならば、年内に大きなプロモーションを展開できるメリットがありますね。スケジュール上も問題ないと思います。
田中太郎:それでは、発売日は2024年12月1日に決定としましょう。次に、プロジェクトの進捗についてですが、ここは月次で進捗を確認していく方向で進めていくのが良いでしょうか?
鈴木花子:そうですね。プロジェクトの内容が多岐にわたるので、定期的な進捗確認は必須だと思います。
山田一郎:私がスケジュール調整をして、次回会議で詳細を報告するようにします。
田中太郎:ありがとうございます。最後に、来年度の予算計画ですが、現在の状況を考慮して10%増額を提案します。皆さん、どうでしょうか?
鈴木花子:賛成です。新プロジェクトも控えているので、必要な増額だと思います。
山田一郎:私も賛成です。新しいチームの拡充も考えると、それくらいの増額は必要です。
田中太郎:それでは、来年度の予算を10%増額することで決定します。以上で本日の議題は終了です。アクションアイテムの確認ですが、私は11月5日までにマーケティングチームと宣伝資料を作成します。
山田一郎:私は明日までにスケジュール調整を行います。
田中太郎:では、次回の会議は2024年11月5日としましょう。本日はありがとうございました。
"""

response = client.converse(
    modelId=modelId,
    system=[{"text": "与えられた会議の文字起こしから議事録を生成してください。"}],
    messages=[
            {
                "role": "user",
                "content": [{"text": meeting_script}],
            }
        ],
    inferenceConfig={"temperature": 0},
    toolConfig={
        "tools": [tool_definition],
        "toolChoice": {
            "tool": {
                "name": tool_name,
            },
        },
    },
)

pprint(response["output"]["message"]["content"][0]["toolUse"]["input"], sort_dicts=False)

Discussion