LangChain の新記法「LangChain Expression Language (LCEL)」入門
LangChain Advent Calendar 2023 の 2 日目の記事です。
LangChain Expression Language (LCEL) とは
LangChain Expression Language (LCEL) は、LangChain でのコードの新しい記述方法です。
公式ドキュメント: https://python.langchain.com/docs/expression_language/
LCEL ではプロンプトや LLM を |
で繋げて書き、処理の連鎖 (Chain) を実装します。
2023 年 10 月後半頃から、LangChain では LCEL を使う実装が標準的となっています。
この記事では LCEL の基本的な使い方を紹介していきます。
LCEL の基本的な使い方
prompt と model をつなぐ
まず、LCEL を使う最もシンプルな例として、prompt と model をつないでみます。
はじめに、prompt (PromptTemplate) と model (ChatOpenAI) を準備します。
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
prompt = PromptTemplate.from_template("""料理のレシピを考えてください。
料理名: {dish}""")
model = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
そして、これらをつないだ chain を作成します。
chain = prompt | model
この chain を実行します。
result = chain.invoke({"dish": "カレー"})
print(result.content)
すると、以下のように LLM の生成した応答が表示されます。
材料:
- 牛肉または鶏肉 300g
- 玉ねぎ 1個
<以下略>
prompt (PromptTeamplte) の穴埋めと、model (ChatOpenAI) の呼び出しが連鎖的に実行された、ということです。
LCEL では、chain = prompt | model
のように、プロンプトや LLM を |
で繋げて書き、処理の連鎖 (Chain) を実装します。
LCEL 以前は chain = LLMChain(prompt=prompt, llm=llm)
のように書いて Chain を実装していました。
これらを比較すると、LCEL のほうが直感的なコードに見えると思います。
output_parser も繋ぐ
2 つ目の例として、prompt と model に加えて、output_parser も繋いでみます。
LLM に料理のレシピを生成させて、その結果を Recipe クラスのインスタンスに変換する、という流れを実施してみます。
まず、Recipe クラスを定義し、output_parser (PydanticOutputParser) を準備します。
from dotenv import load_dotenv
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from pydantic import BaseModel, Field
class Recipe(BaseModel):
ingredients: list[str] = Field(description="ingredients of the dish")
steps: list[str] = Field(description="steps to make the dish")
output_parser = PydanticOutputParser(pydantic_object=Recipe)
続いて、prompt (PromptTemplate) と model (ChatOpenAI) を準備します。
prompt = PromptTemplate.from_template(
"""料理のレシピを考えてください。
{format_instructions}
料理名: {dish}""",
partial_variables={"format_instructions": output_parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-3.5-turbo-1106").bind(
response_format={"type": "json_object"}
)
LCEL の記法で、prompt と model、output_parser を繋いだ chain を作成します。
chain = prompt | model | output_parser
chain を実行してみます。
result = chain.invoke({"dish": "カレー"})
print(type(result))
print(result)
すると、最終的な出力として、Recipe クラスのインスタンスが得られました。
<class '__main__.Recipe'>
ingredients=['カレールー', '肉(豚肉、鶏肉、牛肉など)', 'じゃがいも', 'にんじん', '玉ねぎ', 'にんにく', '生姜', 'トマト缶', '水', '塩', 'コショウ', 'サラダ油'] steps=['1. じゃがいもとにんじんを洗い、皮をむいて食べやすい大きさに切る。', '2. 玉ねぎ、にんにく、生姜をみじん切りにする。', '3. 鍋にサラダ油を熱し、にんにくと生姜を炒める。', '4. 肉を加えて炒め、色が変わったら玉ねぎを加える。', '5. じゃがいもとにんじんを加えて炒める。', '6. トマト缶を加え、水を注ぎ入れて煮込む。', '7. カレールーを加えて溶かし、塩とコショウで味を調える。', '8. ご飯の上にカレーをかけて完成。']
LCEL のしくみ
ここで、LCEL の記法がどのように実現されているのかを少し解説します。
LCEL は、LangChain の各種モジュールが継承している「Runnable インタフェース」などによって実現されています。
LangChain (langchain-core) のソースコードで、Runnable は抽象基底クラス (ABC) として定義されています。
class Runnable(Generic[Input, Output], ABC):
Runnable では __or__
と __ror__
が実装されています。
def __or__( self, other: Union[ Runnable[Any, Other], Callable[[Any], Other], Callable[[Iterator[Any]], Iterator[Other]], Mapping[str, Union[Runnable[Any, Other], Callable[[Any], Other], Any]], ], ) -> RunnableSerializable[Input, Other]: """Compose this runnable with another object to create a RunnableSequence.""" return RunnableSequence(first=self, last=coerce_to_runnable(other)) def __ror__( self, other: Union[ Runnable[Other, Any], Callable[[Other], Any], Callable[[Iterator[Other]], Iterator[Any]], Mapping[str, Union[Runnable[Other, Any], Callable[[Other], Any], Any]], ], ) -> RunnableSerializable[Other, Output]: """Compose this runnable with another object to create a RunnableSequence.""" return RunnableSequence(first=coerce_to_runnable(other), last=self)
Python では、__or__
や __ror__
によって |
を演算子オーバーロードすることができるため、chain = prompt | model
のような記法ができるということです。
もう少しだけ複雑な LCEL の例
LCEL のしくみをなんとなく知ったところで、もう少しだけ複雑な LCEL の例もいくつか見てみます。
ルールベースの処理 (通常の関数) をはさむ
LLM を使ったアプリケーションでは、LLM の応答に対してルールベースでさらに処理を加えたり、何らかの変換をかけたいことも多いです。
LCEL では、Chain の連鎖に任意の処理 (関数) を加えることができます。
たとえば、LLM の生成したテキストに対して、小文字を大文字に変換する処理を連鎖させたい場合は、以下のように実装できます。
from langchain.schema.runnable import RunnableLambda
def upper(inp: str) -> str:
return inp.upper()
chain = prompt | model | output_parser | RunnableLambda(upper)
独自の処理と LLM の呼び出しを連鎖させたい、というユースケースはかなり多いので、その流れを直感的に書けるのはとても嬉しいです。
RAG (Retrieval-Augmented Generation)
最後に、最近話題の RAG (Retrieval-Augmented Generation) の例です。
RAG では下図のように、ユーザの入力に関係する文書を VectorStore から検索して、プロンプトに含めて使います。
文書をベクトル検索したりしてプロンプトに含めることで、社内文書などの LLM が本来知らない情報をもとに回答させることができる、という手法です。
RAG を LCEL で実装するため、まずは retriever (LangChain における文書を検索するインタフェース) を準備します。
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.vectorstores.faiss import FAISS
texts = [
"私の趣味は読書です。",
"私の好きな食べ物はカレーです。",
"私の嫌いな食べ物は饅頭です。",
]
vectorstore = FAISS.from_texts(texts, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
prompt・model・output_parser を準備します。
prompt = ChatPromptTemplate.from_template(
"""以下のcontextだけに基づいて回答してください。
{context}
質問: {question}
"""
)
model = ChatOpenAI(model="gpt-3.5-turbo-1106", temperature=0)
output_parser = StrOutputParser()
LCEL で chain を実装します。
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| output_parser
)
この LCEL の chain では、最初に {"context": retriever, "question": RunnablePassthrough()}
と書かれています。
これは、入力が retriever に渡されつつ、prompt にも渡される、というイメージです。
この chain を実行します。
result = chain.invoke("私の好きな食べ物はなんでしょう?")
print(result)
すると、検索した独自知識に基づいて回答してくれました。
回答: カレーです。
おわりに
この記事では、LangChain の新記法「LangChain Expression Language (LCEL)」を紹介しました。
LLM を使ったアプリケーション開発において、連鎖的に処理を実行したいことは非常に多いです。
そのような処理の流れを直感的に書けることはとても嬉しく、LCEL を知って以来、「LangChain を入れるのは重いけど、LCEL は使いたいなあ」と思うことも多かったです。
そんな中、2023 年 11 月末から、LangChain のコア機能の「langchain-core」というパッケージへの分離が始まりました。
「langchain-core」には、LangChain の主要な抽象化や LCEL が含まれ、今までよりも安定したパッケージにすることを意図しているそうです。
「LangChain の抽象化や LCEL だけ使えればいいのに」と思うことも多かった自分には非常に嬉しい取り組みです。
今後の LangChain のアップデートも楽しみです。
Discussion