📖

LangChain Expression Language(LCEL)を使ってOutputParserを動かしてみる

2024/12/25に公開

やること

LCELでOutputParserを動かしてみる

参考書籍

最近出版されたこちらの書籍をベースに進めていきます。
https://gihyo.jp/book/2024/978-4-297-14530-9

OutputParserとは?

テキスト形式のLLMの出力をJSONや辞書などのオブジェクトに変換することができます。出力をそのまま文字列として返すStrOutputParserや、Pydanticモデルを用いて構造化データに変換するPydanticOutputParserなどいくつか種類があります。

LangChain Expression Language(LCEL)とは?

簡単に言うとプロンプト、LLMの呼び出し、出力の変換など一連の処理を「|」でつなげて書く記法です。LangChainでは2023年10月頃からLCELを使う実装が標準的になったそうなので、LangChainを使って開発する場合は今後この書き方に慣れていく必要があります。

従来のコードとLCELによるコードの比較

例としてプロンプトテンプレートやモデルを用いて出力させる場合を考えてみます。Prompt, Model, OuputParserをそれぞれ独立したインスタンスとして作成し、LLMChainオブジェクトでそれらを1つのパイプラインとして順番に実行します。

without_lcel.py
from langchain.prompts.chat import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain.chains import LLMChain
from dotenv import load_dotenv
import os

load_dotenv()

# Azure OpenAI の設定 (環境変数から読み取る)
api_key = os.getenv("AZURE_OPENAI_API_KEY")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment_name = os.getenv("AZURE_DEPLOY_NAME")

# プロンプトテンプレートの作成
messages = [
    ("system", "ユーザーが入力した日付において、日本人がよく食べる料理を教えてください"),
    ("human", "{date}")
]
prompt = ChatPromptTemplate.from_messages(messages)

# AzureChatOpenAI モデルの設定
model = AzureChatOpenAI(
    api_key=api_key,
    api_version=api_version,
    azure_endpoint=azure_endpoint,
    deployment_name=deployment_name,
    temperature=0
)

# StrOutputParserの設定
output_parser = StrOutputParser()

# チェーンを明示的に作成
chain = LLMChain(prompt=prompt, llm=model, output_parser=output_parser)

# チェーンを実行
output = chain.invoke({"date": "12月24日"})
print(output)
LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17 and will be removed in 1.0. Use :meth:`~RunnableSequence, e.g., `prompt | llm`` instead.
  chain = LLMChain(prompt=prompt, llm=model, output_parser=output_parser)
{'date': '12月24日', 'text': '12月24日はクリスマスイブです。この日、日本ではクリスマスを祝うために特別な料理が食べられることが多いです。一般的には、以下のような料理が人気です。\n\n1. **クリスマスケーキ** - スポンジケーキにクリームとフルーツをトッピングしたものが一般的です。イチゴのショートケーキが特に人気です。\n\n2. **ローストチキン** - 家族や友人と一緒に楽しむために、ローストチキンを用意する家庭も多いです。\n\n3. **ピザ** - パーティー感を出すために、ピザを注文することもよくあります。\n\n4. **サラダ** - クリスマスの食卓には、色とりどりのサラダが並ぶことが多いです。\n\n5. **シャンパンやワイン** - 乾杯のために、シャンパンやワインを用意することも一般的です。\n\nこれらの料理は、クリスマスの雰囲気を楽しむために多くの家庭で用意されます。'}

上の実行結果に示されているようにLLMChainはv1.0で削除されるので、LCELを使って記述するよう警告が出ています。これをLCELで書き換えると以下のようになります。

with_lcel.py
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
import os

load_dotenv()

# Azure OpenAI の設定 (環境変数から読み取る)
api_key = os.getenv("AZURE_OPENAI_API_KEY")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment_name = os.getenv("AZURE_DEPLOY_NAME")

# プロンプトテンプレートの作成
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "ユーザーが入力した日付において、日本人がよく食べる料理を教えてください"),
        ("human", "{date}")
    ]
)

# AzureChatOpenAI モデルの設定
model = AzureChatOpenAI(
    api_key=api_key,
    api_version=api_version,
    azure_endpoint=azure_endpoint,
    deployment_name=deployment_name,
    temperature=0
    )

# StrOutputParserを連鎖に追加
chain = prompt | model | StrOutputParser()
output = chain.invoke({"date": "12月24日"})
print(output)
12月24日はクリスマスイブで、日本では特別な料理や食事が楽しまれる日です。一般的に、日本人がこの日に食べる料理としては以下のようなものがあります。
1. **クリスマスケーキ** - 多くの家庭では、イチゴや生クリームを使ったショートケーキが人気です。
2. **ローストチキン** - 鶏肉を焼いた料理で、クリスマスの定番として楽しまれます。
3. **ピザ** - 家族や友人と一緒に楽しむために、ピザを注文することも多いです。
4. **サラダ** - クリスマスディナーの一部として、色とりどりのサラダが用意されることがあります。
これらの料理は、クリスマスを祝うための特別な食事として楽しまれます。

プロンプトテンプレートとモデルを独立したインスタンスとして作成した後、LCELを使えばchain = prompt | model | StrOutputParser()とすっきり表現することができます。

PydanticOutputParserの場合

PydanticOutputParserを使うと、モデルの出力をPydanticモデルに変換することができます。今回は日付(date)と対応する料理(food)のリストを持つJapaneseFoodというPydanticモデルを定義し、LLMが返すデータをJapaneseFood 型に変換してみました。

with_pydantic.py
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

# Azure OpenAI の設定 (環境変数から読み取る)
api_key = os.getenv("AZURE_OPENAI_API_KEY")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment_name = os.getenv("AZURE_DEPLOY_NAME")

class JapaneseFood(BaseModel):
    date: list[str] = Field(description="ユーザーが入力した日付")
    food: list[str] = Field(description="日本人がよく食べる料理")

output_parser = PydanticOutputParser(pydantic_object=JapaneseFood)

# プロンプトテンプレートの作成
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "ユーザーが入力した日付において、日本人がよく食べる料理を教えてください \n\n{format_instructions}"),
        ("human", "{date}")
    ]
)

prompt_with_format_instructions = prompt.partial(
    format_instructions=output_parser.get_format_instructions()
)

model = AzureChatOpenAI(
    api_key=api_key,
    api_version=api_version,
    azure_endpoint=azure_endpoint,
    deployment_name=deployment_name,
    temperature=0
    ).bind(response_format={"type":"json_object"})

chain = prompt_with_format_instructions | model | output_parser

food = chain.invoke({"date": "12月24日"})
print(type(food))
print(food)
<class '__main__.JapaneseFood'>
date=['12月24日'] food=['クリスマスケーキ', 'ローストチキン', '寿司', 'おせち料理']

ただ、まともにPydanticOutputParserを使うと途中get_format_instructions() メソッドでモデルに期待する出力形式のフォーマット指示を生成し、partialメソッドでプロンプトテンプレートの一部のパラメータを事前に固定するといった手続きを実装する必要があります。これを簡潔に表現するため、LLMに構造化データを出力させるときはwith_structured_outputを使うのがいいようです。

with_structured_output.py
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

# Azure OpenAI の設定 (環境変数から読み取る)
api_key = os.getenv("AZURE_OPENAI_API_KEY")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
deployment_name = os.getenv("AZURE_DEPLOY_NAME")

class JapaneseFood(BaseModel):
    date: list[str] = Field(description="ユーザーが入力した日付")
    food: list[str] = Field(description="日本人がよく食べる料理")

# プロンプトテンプレートの作成
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "ユーザーが入力した日付において、日本人がよく食べる料理を教えてください"),
        ("human", "{date}")
    ]
)

model = AzureChatOpenAI(
    api_key=api_key,
    api_version=api_version,
    azure_endpoint=azure_endpoint,
    deployment_name=deployment_name,
    temperature=0
    ).bind(response_format={"type":"json_object"})

chain = prompt | model.with_structured_output(JapaneseFood)

food = chain.invoke({"date": "12月24日"})
print(type(food))
print(food)
<class '__main__.JapaneseFood'>
date=['12月24日'] food=['クリスマスケーキ', 'ローストチキン', '寿司', '天ぷら', 'うどん'] 

この書き方だとmodel.with_structured_output(JapaneseFood)とシンプルに書けるので確かにいい感じです。

コメントなど

LCELを使うとシンプルに表現できるので確かにいいなと思いましたが、複雑な処理をしない場合はそこまでありがたみを感じられないというのが偽らざる感想です。とはいえ、今後はこれがスタンダードな書き方になるので、どんどん使っていきたいなと思いました。

ヘッドウォータース

Discussion