🛠️

LangChainのStructured Outputが便利だった

に公開

注意書き

  • 個人的なメモとして簡略化して説明を書いているため、もし参考にされる際は合わせて公式ドキュメントを参照してください。
  • 誤っている内容がありましたら、ご指摘して頂けたらと思います。

対象読者

  • 最近LangChainなどを触り始めた方

概要

  • RAGの機能を開発していたが、ユーザーのプロンプトからベクトル検索時のフィルタリング条件を抽出する機能の要望が上がった
  • 最初はシステムプロンプトのみでフィルタリング条件の抽出を指示していた
  • Structured Outputを使用することで確実に期待する形(JSON)でフィルタリング条件を抽出することが可能となった

Structured Outputとは

  • チャットボットなどの場合、モデルの最終的な回答は自然言語で返す必要があるが、それまでの過程では自然言語ではなく構造化された形式で出力する必要があるシナリオも出てくる
  • 上記のニーズからモデルに特定の出力構造で応答するように指示できる概念が出てきた
  • 事前に自分が望むスキーマを定義し、with_structured_output()を使用してLLMに結び付けることで、LLMに「このスキーマの形で回答を返す必要がある」と認識させることができる

システムプロンプトのみの場合

  • 前提
    • 商品検索システムに関するRAGでベクトル検索を行うと想定
  • 問題点
    • LLMが指示を無視してこちらが想定していない形で値を返してくる
    • システムプロンプトが長くなるので保守性が悪い
# システムプロンプト
SYSTEM_PROMPT = """
  あなたはECサイトのユーザーの検索クエリから、商品検索のフィルタリング条件を抽出するアシスタントです。

  以下のJSON形式で条件を抽出してください:
  {
    "category": "商品カテゴリ(electronics/fashion/sports/home など)",
    "brand": "ブランド名",
    "price_range": {
      "min": 最小価格(数値),
      "max": 最大価格(数値)
    },
    "tags": ["タグ1", "タグ2"],
    "colors": ["色1", "色2"],
    "in_stock_only": 在庫ありのみ(true/false)
  }
price_rangeのminやmaxは'〜円から〜円'、'〜円以下'、'〜円台'などの表現から抽出してください。
"""
model = ChatOpenAI(model="gpt-4o", temperature=0)
# 以降モデルを実行してjsonでparseなどしていく

Structured Outputを使用した場合

  • まずはPydanticでスキーマを定義
# Pydanticモデルでスキーマを定義
  class PriceRange(BaseModel):
      """商品価格の範囲フィルター"""
      min: Optional[float] = Field(
          None,
          description="最小価格。この金額以上の商品を検索。日本円での金額"
      )
      max: Optional[float] = Field(
          None,
          description="最大価格。この金額以下の商品を検索。日本円での金額"
      )

  class ProductSearchFilters(BaseModel):
      """
      AWS OpenSearchで使用する商品検索フィルター条件。
      ECサイトでユーザーのクエリから抽出可能な情報のみを設定する。
      確実にこの形でLLMに返してほしいというものを設定する。
      """
      category: Optional[str] = Field(
          None,
          description="商品カテゴリ。例: electronics(家電)、fashion(ファッション)、sports(スポーツ)、home(ホーム・インテリア)、beauty(美容)など"
      )
      brand: Optional[str] = Field(
          None,
          description="商品のブランド名。例: Nike、Adidas、Sony、Apple、無印良品など"
      )
      price_range: Optional[PriceRange] = Field(
          None,
          description="商品の価格範囲。'〜円から〜円'、'〜円以下'、'〜円台'などの表現から抽出"
      )
      tags: Optional[List[str]] = Field(
          None,
          description="商品に関連するタグのリスト。例: ['セール', 'アウトレット', '限定', '新作', '人気']"
      )
      colors: Optional[List[str]] = Field(
          None,
          description="商品の色。複数指定可能。例: ['黒', '白', '赤']"
      )
      in_stock_only: Optional[bool] = Field(
          None,
          description="在庫ありの商品のみを検索するかどうか。'在庫あり'、'すぐに買える'などの表現から抽出"
      )
  • 定義したスキーマをwith_structured_output()で紐づけて設定する
  • LLMはレスポンスはProductSearchFiltersの形で返す必要があると認識してくれる。
  • ProductSearchFiltersで定義した各Fieldのdescriptionを見てユーザーのプロンプトから該当する内容をそれぞれのFieldの値として設定していく。
  • もしユーザーが言及していないFieldがあればその値はNoneになる。
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o", temperature=0)
model_with_structure = model.with_structured_output(ProductSearchFilters)
structured_output = model_with_structure.invoke("1万円台のNikeのスニーカーで黒または白のものを紹介してください。")
  • 以下のようなレスポンスが返ってくる
ProductSearchFilters(
      category='sports',
      brand='Nike',
      price_range=PriceRange(min=10000.0, max=19999.0),
      tags=None,
      colors=['黒', '白'],
      in_stock_only=None
  )

まとめ

  • LLMの動きは時に予想もつかない動きになることがあるので、より高確率で自分の望むように動きを制御できる機能があれば、それを使用していく

補足

  • ちなみに、LangChainのtoolデコレータ使用時にargs_schemaにProductSearchFiltersを設定して「このtoolを使うにはこのinputが必要ですよ」とLLMに教えて、LLMがツールを使う必要があると判断したタイミングでProductSearchFiltersの各Fieldの値を抽出させる方法もある
  • 忘れないようにメモとして上記も記事にしていく

Discussion