⛓️

[LangChain] with_structured_output を使用して、Pydanticのクラスをレスポンスとして受け取る

2024/10/07に公開

はじめに

こんにちは。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 メソッドとは

https://python.langchain.com/docs/how_to/structured_output/#the-with_structured_output-method

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のデータクラスとして逆変換(デコード)するという仕組みになっています。

https://github.com/langchain-ai/langchain/blob/7a07196df683582c783edf164bfb6fe813135169/libs/core/langchain_core/utils/function_calling.py#L78-L120

具体的には、

  • クラス名 -> 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のコード)
https://github.com/langchain-ai/langchain/blob/0da5078cad3a689e51cd451c7cac63ac35b5accd/libs/core/langchain_core/output_parsers/pydantic.py#L23-L47

json_schema

https://openai.com/index/introducing-structured-outputs-in-the-api/

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テックブログ

Discussion