[LangChain] with_structured_output を使用して、Pydanticのクラスをレスポンスとして受け取る
はじめに
こんにちは。PharmaXでエンジニアをしている諸岡(@hakoten)です。
LangChainには、各LLMモデルのレスポンスをあらかじめ決まった構造のクラスで受け取ることができるwith_structured_output
というメソッドがあります。
この記事では、with_structured_output
を使用してPydanticのクラスをレスポンスとして受け取る方法についてをご紹介します。変換の仕組みについても簡単に解説していますので、よければぜひ一読ください。
環境
この記事執筆時点では、以下のバージョンで実施しています。
LangChain周りは非常に開発速度が早いため、現在の最新バージョンを合わせてご確認ください
- langchain: 0.3.1
- langchain-openai: 0.2.1
- Python: 3.12.4
with_structured_output メソッドとは
with_structured_output
メソッドは、指定したスキーマに基づいて、モデルが生成する出力を構造化するためのメソッドです。
このメソッドを使うと、モデルからの応答がPydanticクラスやTypedDict、JSONスキーマなどの特定の形式に沿って返されるようになります。これにより、出力データが事前に定義された構造に一致していることを保証し、信頼性の高いデータ処理が可能になります。
with_structured_outputの使い方
まずは、with_structured_output
の簡単な使い方を説明します。
基本的な使い方としては、langchain_openai
パッケージの ChatOpenAI
インスタンスを初期化する際に、with_structured_output
と共に出力用のモデルクラスを指定するだけです。
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
# 出力の構造をモデルクラスとして定義する
class OutputModel(BaseModel):
"""
食材の構成要素
"""
name: str = Field(..., description='食材の名前')
weight: str = Field(..., description='食材の重さ')
protein: str = Field(..., description='食材の蛋白質')
fat: str = Field(..., description='食材の脂肪')
carbohydrate: str = Field(..., description='食材の炭水化物')
prompt = ChatPromptTemplate.from_messages(
[
(
'system',
"""
与えられた質問に対して、構成要素を返してください
""",
),
('placeholder', '{messages}'),
]
)
# with_structured_outputの引数にモデルクラスを指定する
chain = prompt | ChatOpenAI(model='gpt-4o', temperature=0).with_structured_output(OutputModel)
print(chain.invoke({'messages': ['かぼちゃの構成要素を教えて?']}))
実行結果は以下の通りです。
name='かぼちゃ' weight='100g' protein='1.5g' fat='0.1g' carbohydrate='20.1g'
with_structured_output
を使うことで、モデルクラスのインスタンスを直接レスポンスとして受け取ることが可能になります。LLMの結果が各プロパティに値としてセットされ、返される形になります。
with_structured_outputのパラメタ
with_structured_outputメソッドのパラメタは、次のようなものになります。
パラメータ | 型 | 説明 |
---|---|---|
schema | Pydanticクラス、TypedDict、JSONスキーマなど | モデル出力をフォーマットするためのスキーマ。出力がこのスキーマに従って構造化される |
method | str | モデル生成の制御方法。次のいずれかを指定可能: "function_calling"、"json_schema"、"json_mode"。(デフォルト: function_calling) |
include_raw | bool | Trueの場合、元のモデル応答(rawデータ)と解析された出力の両方が返される。 |
strict | bool または None | Trueの場合、スキーマと出力の検証が厳密に行われる。 |
モデルクラスへの変換方式(method)とその仕組み
with_structured_output
は、次の3つの変換方式に対応しています。
- function_calling
- json_schema
- json_mode
デフォルトの指定はfunction_calling
です。
ここでは、LangChainがどのようにして各方式を使い、モデルクラスへの変換を行っているかを簡単に解説します。
function_calling
function callingとは、OpenAIが提供するAPIの機能で、あらかじめ関数名とパラメータを指定することで、LLMの実行時にその関数名が呼び出される仕組みです。
function calling
自体の詳しい説明は割愛しますが、function calling
の指定パラメータには次のようなものがあります。
- name: 関数名
- description: 関数の説明
- parameters: 関数の引数とその説明
function calling
は、OpenAIの呼び出しを行う外部システム(アプリケーション)が、意図する関数呼び出しを正確に実行するための機能ですが、LangChainではこの機能を使って特定の構造モデルへの変換処理を行っています。
実際の流れとしては、以下のコードに示す変換処理(convert_pydantic_to_openai_function
)を通じて、Pydanticのデータクラスをfunction calling
の呼び出し方式(name、description、parameters)に変換(エンコード)しAPIに渡します。
そして、OpenAI側で実行された関数の結果をPydanticのデータクラスとして逆変換(デコード)するという仕組みになっています。
具体的には、
- クラス名 -> name
- クラスのdescription -> description
- 各プロパティとdescription -> parameters
のように変換が行われます。以下の箇所がコードの変換部分です。
(パラメタ変換部分)
...
return {
"name": name or title,
"description": description or default_description,
"parameters": _rm_titles(schema) if rm_titles else schema,
}
前述の例に挙げたデータクラスでは、
class OutputModel(BaseModel):
"""
食材の構成要素
"""
name: str = Field(..., description='食材の名前')
weight: str = Field(..., description='食材の重さ')
protein: str = Field(..., description='食材の蛋白質')
fat: str = Field(..., description='食材の脂肪')
carbohydrate: str = Field(..., description='食材の炭水化物')
以下のようにfunction callingのパラメータに変換されます。
{
"name": "OutputModel",
"description": "食材の構成要素",
"parameters": {
"properties": {
"name": {
"description": "食材の名前",
"type": "string"
},
"weight": {
"description": "食材の重さ",
"type": "string"
},
"protein": {
"description": "食材の蛋白質",
"type": "string"
},
"fat": {
"description": "食材の脂肪",
"type": "string"
},
"carbohydrate": {
"description": "食材の炭水化物",
"type": "string"
}
},
"required": [
"name",
"weight",
"protein",
"fat",
"carbohydrate"
],
"type": "object"
}
}
APIの結果としては次のようなレスポンスを受け取ります。この結果を基にOutputModelクラスにデコードする流れになります。
{
"tool_calls": [
{
"id": "call_5bIz4yMrzCZjaZlzu3lUXsNV",
"type": "function",
"function": {
"arguments": "{'name': 'かぼちゃ', 'weight': '100g', 'protein': '1.5g', 'fat': '0.1g', 'carbohydrate': '20.1g'}",
"name": "OutputModel"
}
}
]
}
json_mode
json_modeは、2023年から提供されているOpenAIの機能で、レスポンスをJSON形式で返してくれるものです。
内部的には、OpenAIへのリクエストに "type": "json_object"
を追加し、レスポンスをJSON形式で受け取ります。その後、PydanticOutputParser
というパーサーを使ってデコードします。このPydanticOutputParser
の中身は、PydanticのBaseModelがもともと持っているJSONからオブジェクトへのデシリアライズ処理を利用しており、それを使って変換を行います。
(PydanticOutputParserのコード)
json_schema
json_schemaは、2024年8月に発表された比較的新しいOpenAIの機能で、APIにおいて構造化されたデータ型を指定することで、レスポンスとしてそのデータ型を返してくれるものです。
要するに、この記事で紹介している with_structured_output
の機能と同様のものです。
Pydanticモデルの場合、内部的にはOpenAIとのパラメータ変換だけが行われ、呼び出し自体はそのまま処理されているようです。
感想
function calling
の変換は非常に興味深く、LangChainはtool呼び出しを含め、このfunction calling
をうまく活用しており、とても勉強になります。
デフォルト設定がfunction calling
になっているのも、json_mode
よりもdescriptionを含められることで、精度が高くなるのかなと思っています。(実際どうなんでしょう?)
一方、2024年8月に発表されたOpenAIのjson_schema
機能については、ほぼ同じ機能ということもあり、新しいモデルを使う場合はjson_schema
で十分なのではないかとも感じています。
ちなみにPharmaXでは、諸事情により現時点ではstructured_outputを使用せず、独自のパース処理を行っていますが、REST APIのライブラリなどと同様に、いずれはレスポンスをモデル変換するのが主流になると考えており、どこかで置き換えも検討したいと思っています。
終わりに
以上、with_structured_output
によるPydanticのクラスをレスポンスとして受け取る方法をご紹介しました。
PharmaXでは、AIやLLMに関連する技術の活用を積極的に進めています。もし、この記事が興味を引いた方や、LangGraphの活用に関心がある方は、ぜひ私のXアカウント(@hakoten)やコメントで気軽にお声がけください。PharmaXのエンジニアチームで一緒に働けることを楽しみにしています。
まずはカジュアルにお話できることを楽しみにしています!
PharmaXエンジニアチームのテックブログです。エンジニアメンバーが、PharmaXの事業を通じて得た技術的な知見や、チームマネジメントについての知見を共有します。 PharmaXエンジニアチームやメンバーの雰囲気が分かるような記事は、note(note.com/pharmax)もご覧ください。
Discussion