LCEL イントロダクション
LCEL (LangChain Expression Language) は、複雑なタスクチェーンを基本コンポーネントから構築することを可能にし、ストリーミング処理、並列処理、ログ出力などの機能をサポートする、強力なワークフローオーケストレーションツールです。
基本例: プロンプト + モデル + 出力パーサー
この例では、LCEL (LangChain Expression Language) を使用して、プロンプトテンプレート、モデル、および出力パーサーの3つのコンポーネントをリンクして「ジョークを言う」というタスクを実装するためのワークフローを構築する方法を示します。このコードでは、チェーンの作成方法、パイプシンボル |
を使用して異なるコンポーネントを接続する方法、それぞれのコンポーネントの役割と出力結果について紹介しています。
まずは、プロンプトテンプレートとモデルを接続して特定のトピックに関するジョークを生成する方法を見てみましょう:
依存関係のインストール
%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_template("{topic} についてのジョークを教えて")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()
chain = prompt | model | output_parser
chain.invoke({"topic": "アイスクリーム"})
出力
"なぜパーティーはアイスクリームを招待しないの? 暑くなると溶けるからさ!"
このコードでは、LCEL を使用して異なるコンポーネントをチェーンに接続しています:
chain = prompt | model | output_parser
ここでの |
シンボルは、異なるコンポーネントを接続し、1つのコンポーネントの出力を次のコンポーネントの入力として渡す Unix pipe オペレータ と似ています。
このチェーンでは、ユーザー入力がプロンプトテンプレートに渡され、その後プロンプトテンプレートの出力がモデルに渡され、最後にモデルの出力が出力パーサーに渡されます。それぞれのコンポーネントを別々に見てみて、何が起こっているかをよりよく理解しましょう。
1. プロンプト
prompt
はテンプレート変数の辞書を受け入れ、PromptValue
を生成する BasePromptTemplate
です。 PromptValue
はプロンプトを含むラップされたオブジェクトで、LLM
(文字列の形式の入力として) や ChatModel
(メッセージシーケンスの形式の入力として) に渡すことができます。任意の種類の言語モデルと使用できます。それは BaseMessage
の生成および文字列の生成のためのロジックを定義しています。
prompt_value = prompt.invoke({"topic": "アイスクリーム"})
prompt_value
出力
ChatPromptValue(messages=[HumanMessage(content='{topic} についてのジョークを教えて')])
以下では、プロンプトフォーマットされた結果をチャットモデルで使用されるメッセージ形式に変換しています:
prompt_value.to_messages()
出力
[HumanMessage(content='{topic} についてのジョークを教えて')]
直接文字列に変換することもできます:
prompt_value.to_string()
出力
'Human: {topic} についてのジョークを教えて.'
2. モデル
次に、PromptValue
を model
に渡します。この例では、model
は ChatModel
であり、BaseMessage
を出力することを意味します。
モデルを直接呼び出してみましょう:
message = model.invoke(prompt_value)
message
戻り値:
AIMessage(content="{topic} がパーティーに招待されないのはなぜ?\n\nなぜなら暑くなるといつも滴ってしまうからさ!")
もしもモデルが LLM
タイプとして定義されている場合、文字列が出力されます。
from langchain_openai.llms import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
モデルの戻り値:
'\n\nBot: アイスクリームのトラックがなぜ故障したのか?メルトダウンしたからです!'
3. 出力パーサー
最後に、model
からの出力を output_parser
に渡します。これは BaseOutputParser
であり、文字列または BaseMessage
を入力として受け付けます。 StrOutputParser
は任意の入力を単純な文字列に変換します。
output_parser.invoke(message)
"{topic} がパーティーに招待されないのはなぜ?\n\nなぜなら暑くなるといつも滴ってしまうからさ!"
4. 全体的なプロセス
実行プロセスは以下の通りです:
-
chain.invoke({"topic": "ice cream"})
を呼び出し、これは定義したワークフローを開始し、パラメータ{"topic": "ice cream"}
を渡して "ice cream" に関するジョークを生成することに相当します。 - 呼び出しパラメータ
{"topic": "ice cream"}
を最初のチェーン要素であるprompt
に渡し、プロンプトテンプレートをフォーマットしてプロンプトTell me a little joke about ice cream
を取得します。 - プロンプト
Tell me a little joke about ice cream
をmodel
(gpt4 モデル)に渡します。 -
model
によって返された結果をoutput_parser
出力パーサーに渡し、モデル結果をフォーマットして最終コンテンツを返します。
各コンポーネントの出力に興味がある場合は、いつでも prompt
や prompt | model
などのより小さなバージョンのチェーンをテストして中間結果を確認できます:
input = {"topic": "ice cream"}
prompt.invoke(input)
(prompt | model).invoke(input)
RAG 検索例
次に、より複雑な LCEL 例の説明をします。質問に回答する際に背景情報を追加するために、チェーンを生成するために強化された取得を実行します。
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
vectorstore = DocArrayInMemorySearch.from_texts(
["harrison worked at kensho", "bears like to eat honey"],
embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()
template = """以下の文脈のみに基づいて質問に回答してください:
{context}
質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("where did harrison work?")
この場合、構成されたチェーンは次のとおりです:
chain = setup_and_retrieval | prompt | model | output_parser
簡単に言うと、上記のプロンプトテンプレートは context
と question
を受け入れてプロンプト内で置き換える値として受け入れます。プロンプトテンプレートを構築する前に、文脈の一部として使用する関連文書を取得したいと考えています。
テストとして、メモリベースのベクトルデータベースをシミュレートするために DocArrayInMemorySearch
を使用し、クエリに基づいて類似文書を取得できるリトリーバを定義します。これもチェーン可能な実行可能コンポーネントですが、単独で実行してみることもできます:
retriever.invoke("where did harrison work?")
次に、RunnableParallel
を使用して、プロンプトの入力を準備し、リトリーバを使用してドキュメントを検索し、RunnablePassthrough
を使ってユーザーの質問を渡します:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
要約すると、完全なチェーンは次のとおりです:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
プロセスは以下の通りです:
- まず、2 つのエントリを含む
RunnableParallel
オブジェクトを作成します。最初のエントリcontext
には、リトリーバによって抽出されたドキュメント結果が含まれます。2 番目のエントリquestion
には、ユーザーの元の質問が含まれます。質問を渡すために、このエントリをコピーするためにRunnablePassthrough
を使用します。 - 前の手順で生成した辞書を
prompt
コンポーネントに渡します。これにより、ユーザー入力(つまりquestion
)と取得したドキュメント(つまりcontext
)を受け入れてプロンプトを構築し、PromptValue
を出力します。 -
model
コンポーネントは生成されたプロンプトを取り、OpenAI の LLM モデルに渡して評価を行います。モデルが生成した出力はChatMessage
オブジェクトです。 - 最後に、
output_parser
コンポーネントがChatMessage
を取り、Python 文字列に変換し、invoke
メソッドから返します。