LangChain v0.2の使い方
はじめに
ChatGPT などの AI API を活用し、AI エージェントなどを構築しやすくしてくれるフレームワークである LangChain 。これの最新のバージョンである v0.2
の使い方について、内部の実装なども交えながら解説していきたいと思います。
内部構造も把握しておくことで、どういうときにどこを見たらいいかなども分かるようになります。それ以外でも、LangChain Expression Language(LCEL)と言われる記法があり、それがどうして実現しているのかについても書こうと思います。
なお、今回扱うのは Python 版の解説となります。
LangChain v0.2 について
LangChain は v0.1
から大幅に改修が入り、利用の仕方なども大きく変わっています。しかし、 v0.1
との互換性を極力維持した形で開発が進んでいるためすでに v0.1
で開発したことがある場合はなにもせずに動くと思います。
公式の記事から引用(翻訳)すると以下の要素が盛り込まれているようです。
特に注目すべきは、 langchain
と langchain-community
が完全に分離されたことでしょう。これによって利用するパッケージが変わります。 v0.1
で開発したことがある人はこのあたりを重点的にチェックするとよいでしょう。
インストール
インストールは以下のように pip
を使います。
$ pip install langchain
$ pip install langchain-community
今回は OpenAI の ChatGPT を利用するため、以下も合わせてインストールします。
$ pip install -qU langchain-openai
セットアップ
ChatGPT など、外部サービスを利用する場合は API Key を設定する準備・設定する必要があります。今回は OpenAI の API Key を準備・設定します。
環境変数に設定する
以下のように os
モジュールを利用してAPI Keyを設定します。
import os
os.environ['OPENAI_API_KEY'] = "<YOUR_API_KEY>"
しかし、毎回こうして設定するのは手間ですし、GitHub にアップするなどする場合に、API Key がコミットされてしまうのも問題と言えます。そのため、個人的には以下の dotenv
などを利用してキーを別ファイルで管理し、コミットしない形にするのがオススメです。
dotenvで設定する
毎回上記のように設定するのは非効率ですし、色々なところにAPI Keyを保持することになってしまうので、 dotenv
という機能を利用すると設定ファイルにまとめて保存しておくことができます。( API Key だけでなく、その他のトークンなども保存することができます)
まずはパッケージをインストールします。
$ pip install python-dotenv
そして .env
ファイルを準備し、 KEY=VALUE
の形で値を保存しておきます。
HOGE_KEY=xg-hogehoge-fugafuga-foo-bar
FUGA_KEY=bot-fugafuga-piyopiyo
.env
ファイルには複数の値を保存できるので改行区切りで列挙します。
そして以下のようにロード処理を実行します。
import dotenv
dotenv.load_dotenv()
すると自動的に環境変数に値がロードされます。
以下のようにチェックすると値が出力されるのが分かります。
import os
print(os.environ.get("HOGE_KEY")) # => xg-hogehoge-fugafuga-foo-bar
ChatGPTとやり取りしてみる
まずは基本的な、ChatGPT とのやり取りを実装する方法を見ていきましょう。
フローとしては、
- ChatGPT のモデルを作成する
- メッセージを準備する
- 実行
というステップです。
それぞれ見ていきましょう。
モデルを作成する
OpenAI の API を利用するためには API Key が必要になるため、そのためのセットアップを行ってからモデルを作成します。
# API Key を環境変数に読み込む
import dotenv
dotenv.load_dotenv()
# モデルの作成
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
メッセージの準備
LLM に送信するメッセージを準備します。ここでは、いわゆるシステムプロンプトと呼ばれる事前の設定と、ユーザの発言を準備します。
from langchain_core.messages import HumanMessage, SystemMessage
messages = [
SystemMessage(content="次の発言を日本語から英語に翻訳してください。"),
HumanMessage(content="こんにちは!")
]
実行する
準備したモデルとメッセージを使って実行してみます。実行するには invoke
を呼び出すだけです。
result = model.invoke(messages)
print(result)
実行すると以下のような結果になりました。ChatGPT からのレスポンスは JSON 形式になっているのが分かります。
content='Hello!' response_metadata={'token_usage': {'completion_tokens': 2, 'prompt_tokens': 28, 'total_tokens': 30}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None} id='run-0915b48e-bff0-4546-adf2-7a0a83c94de1-0' usage_metadata={'input_tokens': 28, 'output_tokens': 2, 'total_tokens': 30}
パースして利用しやすくする
前述の通り、レスポンスは JSON で返ってきます。このままだとやや扱いづらいですね。シンプルにメッセージだけを取り出したい場合はすでに準備されている機能を利用して簡単に取り出すことができます。
パースするには StrOutputparser
モジュールを利用します。
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
parsed_result = parser.invoke(result)
print(parsed_result)
実行すると、JSON から必要な部分だけが取り出されているのが分かります。(JSON で言うと content
部分が抜き出されている)
Hello!
ここまでのまとめ
API Keyの準備とモデルの作成をしたら、あとは AI に処理してもらいたいメッセージを準備して invoke
メソッドで呼び出すだけで結果を得ることができます。また、 invoke
メソッドはチェインしていく前提になっているため、様々な部分で見かけることになるでしょう。結果的に、色々なモジュールの invoke
の結果をつなぎ合わせていくと望みの結果を得ることができます。
以下に、ここまでのコード全部を掲載しておきます。
コード全文
import dotenv
dotenv.load_dotenv()
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
from langchain_core.messages import HumanMessage, SystemMessage
messages = [
SystemMessage(content="次の発言を日本語から英語に翻訳してください。"),
HumanMessage(content="こんにちは!")
]
result = model.invoke(messages)
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
parsed_result = parser.invoke(result)
print(parsed_result)
LCEL のチェイン機能を利用して構築する
前段では LangChain の簡単な使い方について解説しました。ここからは、 v0.2
の強みである LCEL を使って構築していきます。
まず、先程の処理を LCEL を使って再構築してみると以下のようになります。( model
や parser
は前段で作成したものと同一のものです)
chain = (model | parser)
result = chain.invoke(messages)
print(result)
呼び出し( invoke
)が一度だけになり、とてもシンプルな見た目になりました。処理の連結には |
を利用しているのが特徴的です。イメージ的にはシェルのパイプでしょうか。
ここでは model
と parser
のふたつだけですが、例えば以下のように複数の処理を |
を使って簡単に連結することができます。
# LLM実行前にプロンプトテンプレートを適用してから実行するイメージ
chain = (prompt | model | parser)
チェインの使い方がなんとなくイメージできたでしょうか。
上で「 invoke
の呼び出しがチェインしていく」と話していたのはまさにこの部分です。すべての処理が invoke
可能になっているため、それを連結することが容易になっている、というわけなんですね。
チェインの仕組み
ここはちょっと脱線して、なぜ |
でこうしたチェインが実現できるかについて書きたいと思います。
Python に用意されたオーバーロード機能を利用している
Python には、色々な演算子をオーバーロードする機能が備わっています。今回のチェインを実現するにあたって利用されているのは or
演算のオーバーロードです。思い出してください。Python など通常のプログラミング言語では |
はビットの or
演算をしていますよね。これをオーバーロードしているわけです。
オーバーロードするには __or__
メソッドを実装する必要があります。また、LCEL の秀逸な点として、チェイン可能なクラス以外もチェインに組み込めるように設計されています。例えば、適切に設定された辞書オブジェクト( dict
)なども組み込むことができます。これは __ror__
メソッドをオーバーロードすることで実現しています。
__or__
は左側に置かれたオブジェクトがチェイン可能クラスの場合に呼びされます。一方、 __ror__
は右側に置かれた場合に呼び出されます。(おそらく right
の r
なのだと思います)
どういうことかというと、疑似コードで示すと、
any_dict = { """... 必要な設定 ...""" }
chain = (model | any_dict )
model
が左側に置かれているのが分かります。この場合は model
インスタンスのクラスの __or__
が呼び出されます。
any_dict = { """... 必要な設定 ...""" }
chain = (any_dict | model)
一方、 model
を右側に置いた場合かつ any_dict
が通常の dict
などの「本来は実行可能ではないオブジェクト」が左側に来ると __ror__
が呼び出される、というわけです。
ちなみに、実際に LangChain で実装されているコードの一部を抜粋すると、以下のようなオーバーロードになっていました。
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(coerce_to_runnable(other), self)
これを見ると、左側に置かれるオブジェクトは複数の型を許容する形になっているのが分かりますね。( Union
によって定義されている)
プロンプトテンプレートを利用する
ChatGPT とのシンプルなやり取りは説明した通りです。しかし、LangChain を使うようなアプリケーションの場合、シンプルなやり取りで済むことはないでしょう。それだけであればそもそも OpenAI のモジュールを利用して API とやり取りしたほうが依存が少なくてよいです。
ここではプロンプトテンプレートを利用して、ユーザの入力などに応じてプロンプトを生成して、より柔軟に ChatGPT とやり取りできるようにしていきます。
まずは簡単に利用するコードを見てみましょう。
import dotenv
dotenv.load_dotenv()
from langchain_core.prompts import ChatPromptTemplate
system_template = "次の内容を {language} に翻訳してください。"
prompt_template = ChatPromptTemplate.from_messages([
("system", system_template),
("user", "{text}")
])
result = prompt_template.invoke({
"language": "English",
"text": "LangChain は便利です。",
})
print("Prompt:")
print(result)
# Message に変換もできる
print("Messages:")
print(result.to_messages())
実行すると以下の出力が得られます。
Prompt:
messages=[SystemMessage(content='次の内容を English に翻訳してください。'), HumanMessage(content='LangChain は便利です。')]
Messages:
[SystemMessage(content='次の内容を English に翻訳してください。'), HumanMessage(content='LangChain は便利です。')]
to_messages()
によってただの配列に変換されているのが分かります。
テンプレートもチェインに組み込める
実はこのテンプレートの処理もチェインに組み込むことができます。チェイン版は以下のようになります。
chain = prompt_template | model | parser # model などは前段で作成したものと同じ
llm_result = chain.invoke({
"language": "English",
"text": "LangChain は便利です。",
})
print(llm_result) # => LangChain is convenient.
チェインの最初の入力が、テンプレートへの入力となっている点に注目です。チェインの最初の入力がなにになるかはしっかりと把握しておく必要があります。
テンプレートに会話履歴を挿入する方法
さて、プロンプトテンプレートを利用して ChatGPT とやり取りする方法を見てきました。しかし、LLM は本来会話履歴を保持しません。いわば LLM は記憶を持っていないようなものです。そのためチャットボットを作った場合、前に話していた内容を覚えていないので人間が会話するとチグハグした内容になってしまいます。例えば、最初に名前を名乗っていたのに、途中で名前を聞いても「どちら様ですか?」となってしまうわけです。
これを防ぐためには、過去の会話の履歴を保持し、それを ChatGPT に毎回送ることで文脈を維持します。会話履歴を保存する方法について公式のチュートリアルを参考に見ていきましょう。
ということで、会話履歴を挿入する方法を見ていきましょう。会話履歴部分の挿入には MessagePlaceholder
を利用します。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant. Answer all questions to the best of your ability."),
MessagesPlaceholder(variable_name="messages"),
]
)
引数に指定している variable_name
は、 invoke
時に渡される引数名となります。
呼び出すときは以下のように invoke
メソッドの引数に、指定した名称を指定します。
# AIMessage と HumanMessage をインポート
from langchain_core.messages import HumanMessage, AIMessage
chain = prompt | model | parser
result = chain.invoke({
"messages": [
HumanMessage(content="こんにちは!"),
AIMessage(content="こんにちは! なにかお手伝いできますか?"),
HumanMessage(content="私はえどといいます。覚えておいてね"),
]
})
print(result) # => こんにちは、えどさん!お名前を覚えておきますね。今日はどんなことをお手伝いしましょうか?
messages
に渡した配列の最後が最終のインプットとなり、それに対するレスポンスが得られているのが分かるかと思います。
会話履歴(メモリ)を保存する
先ほどの例では自分でメモリ配列を準備して呼び出していました。しかし実際は自動でやり取りを保持しないとなりません。ここではそれを実現する方法を見ていきます。
チュートリアルからコードを抜粋します。主なクラスは RunnableWithMessageHistory
です。
from langchain_core.messages import HumanMessage, AIMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
with_message_history = RunnableWithMessageHistory(model, get_session_history)
session_id
は文字列で、「どの会話か(セッションか)」を判断するために用いられます。任意の文字列です。
そして RunnableWithMessageHistory
クラスに LLM のモデルと、会話履歴を取り出す関数を渡すことで会話履歴を参照する Runnable
が作られます。( Runnable
については後日、別記事で解説します)
会話履歴を利用して会話する
実際に ChatGPT にメッセージを送信する部分を見ていきましょう。会話する際に、 session_id
を指定する必要があるためそれ用のコンフィグオブジェクトを作成します。
config = {
"configurable": {
"session_id": "abcd",
}
}
このコンフィグオブジェクトを利用して呼び出します。
response = with_message_history.invoke(
[HumanMessage(content="こんにちは! 私はえどです。")],
config=config,
)
print(response.content) # => こんにちは、えどさん!お会いできて嬉しいです。今日はどんなことをお手伝いできるでしょうか?
response = with_message_history.invoke(
[HumanMessage(content="私の名前はなんでしょう?")],
config=config,
)
print(response.content) # => あなたの名前は「えど」さんですね。どうぞよろしくお願いします!今日は何か特別なことをお話ししたいですか?
ちゃんと名前を覚えていてくれました。
LCEL と RunnableWithMessageHistory と組み合わせる
次は LCEL 、つまりチェインに組み込む方法を見ていきます。これについては以下の記事を参考にさせていただきました。
また、公式ドキュメントにイメージ図が掲載されています。
緑枠の中央にある赤い文字で書かれている Your Runnable が、達成したいチェイン部分となります。以下、実際のコード例です。
prompt_template = ChatPromptTemplate.from_messages(
messages=[
SystemMessage(content=system_prompt),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{user_input}"),
]
)
chain = prompt_template | model | parser
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="user_input",
history_messages_key="history",
)
result = with_message_history.invoke(
{"user_input": user_input},
config={
"configurable": {
"session_id": "abcd",
}
},
)
print(result)
MessagesPlaceholder
を利用する場合は、 RunnableWithMessageHistory
のコンストラクタにhistory_messages_key
を指定し、プレースホルダーに設定した名称を知らせる必要があります。
またユーザーのインプットに当たる部分は HumanMessagePromptTemplate
を利用し、挿入する名前を user_input
にしています。(これは任意の文字列)
そして呼び出し時にユーザーインプットとして user_input
に値を設定しています。
実際にはやり取りをループ処理して会話できるようにしてやる必要があります。以下に、実際にやり取りができるコード全体を掲載しておきます。
実際の全体のコード
import dotenv
dotenv.load_dotenv()
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
system_prompt = "あなたは AI アシスタントとしてユーザーの質問に答えてください。"
while True:
user_input = input("質問は?\n")
if user_input.lower() == "exit":
print("さようなら!")
break
if user_input is None:
continue
prompt_template = ChatPromptTemplate.from_messages(
messages=[
SystemMessage(content=system_prompt),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{user_input}"),
]
)
chain = prompt_template | model | parser
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="user_input",
history_messages_key="history",
)
result = with_message_history.invoke(
{"user_input": user_input},
config={
"configurable": {
"session_id": "abcd",
}
},
)
print(result)
最後に
LangChain はかなり大規模なフレームワークで、実装も複雑になっています。表面的に使うだけならそこまで大変ではないですが、実際のアプリを作成しようとすると途端に内部を把握していないとうまく構築できないことも多いです。
今回は簡単な使い方にとどめましたが、別の記事では(上でも書いた通り) Runnable
についてや、その他の機能( Retriever と呼ばれる機能など)も解説していきたいと思います。
LLM はうまく活用すれば今までになかったアプリが作成できるので、ぜひみなさんも活用してみてください。
エンジニア絶賛募集中!
MESONではUnityエンジニアを絶賛募集中です! XRのプロジェクトに関わってみたい! 開発したい! という方はぜひご応募ください!
MESONのメンバーページからご応募いただくか、TwitterのDMなどでご連絡ください。
書いた人
比留間 和也(あだな:えど)
カヤック時代にWEBエンジニアとしてリーダーを務め、その後VRに出会いコロプラに転職。 コロプラでは仮想現実チームにてXRコンテンツ開発に携わる。 DAYDREAM向けゲーム「NYORO THE SNAKE & SEVEN ISLANDS」をリリース。その後、ARに惹かれてMESONに入社。 MESONではARエンジニアとして活躍中。
またプライベートでもAR/VRの開発をしており、インディー部門でTGSに出展など公私関わらずAR/VRコンテンツ制作に精を出す。プライベートな時間でも開発しているように、新しいことを学ぶことが趣味で、最近は英語を学んでいる。
MESON Works
MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。
Discussion