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. モデル

次に、PromptValuemodel に渡します。この例では、modelChatModel であり、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. 全体的なプロセス

実行プロセスは以下の通りです:

  1. chain.invoke({"topic": "ice cream"}) を呼び出し、これは定義したワークフローを開始し、パラメータ {"topic": "ice cream"} を渡して "ice cream" に関するジョークを生成することに相当します。
  2. 呼び出しパラメータ {"topic": "ice cream"} を最初のチェーン要素である prompt に渡し、プロンプトテンプレートをフォーマットしてプロンプト Tell me a little joke about ice cream を取得します。
  3. プロンプト Tell me a little joke about ice creammodel(gpt4 モデル)に渡します。
  4. model によって返された結果を output_parser 出力パーサーに渡し、モデル結果をフォーマットして最終コンテンツを返します。

各コンポーネントの出力に興味がある場合は、いつでも promptprompt | 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

簡単に言うと、上記のプロンプトテンプレートは contextquestion を受け入れてプロンプト内で置き換える値として受け入れます。プロンプトテンプレートを構築する前に、文脈の一部として使用する関連文書を取得したいと考えています。

テストとして、メモリベースのベクトルデータベースをシミュレートするために 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

プロセスは以下の通りです:

  1. まず、2 つのエントリを含む RunnableParallel オブジェクトを作成します。最初のエントリ context には、リトリーバによって抽出されたドキュメント結果が含まれます。2 番目のエントリ question には、ユーザーの元の質問が含まれます。質問を渡すために、このエントリをコピーするために RunnablePassthrough を使用します。
  2. 前の手順で生成した辞書を prompt コンポーネントに渡します。これにより、ユーザー入力(つまり question)と取得したドキュメント(つまり context)を受け入れてプロンプトを構築し、PromptValue を出力します。
  3. model コンポーネントは生成されたプロンプトを取り、OpenAI の LLM モデルに渡して評価を行います。モデルが生成した出力は ChatMessage オブジェクトです。
  4. 最後に、output_parser コンポーネントが ChatMessage を取り、Python 文字列に変換し、invoke メソッドから返します。