今日の目的
今日の目的はChatBot
です。memoryです。愛のメモリーは松崎しげるさんです。熱い歌に胸キュンです。暑い見かけに僕と同じおっさんを感じます。(かっこいいけど)
ChatBot
として機能するには過去の会話履歴の記録が必須です。その機能を作っていきます。
といっても一部はLangGraphの中でもやっていたんですけどね。
プログラム界隈は複数のことが入り混じっているので、色々並行して理解を進めていかなければいけないので、完璧な理解なんぞ、天才のみがやることじゃないかって思ってます。笑
目標
- 過去の履歴を参照して回答する
ここまでの経過
プロンプトテンプレートや、LCEL、LangGraphなどをやってきたので、基礎はできてきていると思います。
いきなりここにきちゃった方は過去の記事を参照くださいませ。
バージョン関連
Python 3.10.8
langchain==0.3.7
python-dotenv
langchain-openai==0.2.5
langgraph>0.2.27
langchain-core
※LLMのAPIはAzureOpenAIのgpt-4o-miniを使いました
ライブラリのインポートと環境変数の設定
これはいつも通り
import os
from dotenv import load_dotenv
from langchain_openai import AzureChatOpenAI, AzureOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, trim_messages
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, END, MessagesState, StateGraph
load_dotenv('.env')
os.environ["LANGCHAIN_TRACING_V2"]="true"
os.environ["LANGCHAIN_ENDPOINT"]="https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"]=os.getenv("LANG_SMITH_API")
os.environ["LANGCHAIN_PROJECT"]="langchain_test"
model = AzureChatOpenAI(
azure_deployment='gpt-4o-mini',
azure_endpoint=os.getenv("END_POINT"),
api_version=os.getenv("API_VERSION"),
api_key=os.getenv("API_KEY"),
max_tokens=150
)
まずは素のLLMを確認
model.invoke([HumanMessage(content="僕の名前は池駄賃です。")])
# AIMessage(content='池駄賃さん、こんにちは!お話ししたいこと・・・)
model.invoke([HumanMessage(content="僕の名前を知ってる?")])
# AIMessage(content='ごめんなさい、あなたの名前はわかりません。・・・)
はい。単にモデルを呼び出しただけでは会話履歴が反映されません。
これをステートレスと言ったりするらしい。📝
ということで、Memory機能を追加していきましょう。
Memory機能を追加してChatBot化
ということで過去の会話履歴を参照できる様にしてChatBot化しましょう。
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": response}
workflow1 = StateGraph(state_schema=MessagesState)
workflow1.add_node("model", call_model)
workflow1.add_edge(START, "model")
workflow1.add_edge("model", END)
memory = MemorySaver()
graph1 = workflow1.compile(checkpointer=memory)
########### graphの可視化
from IPython.display import Image, display
try:
display(Image(graph1.get_graph().draw_mermaid_png()))
except Exception:
pass
すごーくシンプルなグラフを組みました。
肝はLangGraphのMemorySaver
を使うところですね。グラフをコンパイルするときに渡してあげればいいみたい。
さて、瀬戸内で大活躍した村上水軍になったつもりで試してみましょう。
query = "僕は海賊の村上です。瀬戸内で暴れてます。"
input_messages = [HumanMessage(query)]
output = graph1.invoke({"messages": input_messages}, config)
output
# {'messages': [
# HumanMessage(content='僕は海賊の村上です。瀬戸内で暴れてます。'・・・),
# AIMessage(content='村上さん、海賊としての冒険は楽しそうですね!・・・)
# ]
query = "ところで、僕の名前はなに?"
input_messages = [HumanMessage(query)]
output = graph1.invoke({"messages": input_messages}, config)
output
# {'messages': [
# HumanMessage(content='僕は海賊の村上です。瀬戸内で暴れてます。'・・・),
# AIMessage(content='村上さん、海賊としての冒険は楽しそうですね!・・・),
# HumanMessage(content='ところで、僕の名前はなに?'・・・),
# AIMessage(content='あなたの名前は「村上」ですよ!海賊としての・・・),
# ]
ということで、ちゃんと会話の履歴を参照しながら回答することができました。
結構簡単だったな。w
ま、前回やったLangGraphでも会話履歴は残せてましたね。
PromptTemplateを使う場合
システムプロンプトを定義するような場合ですね。これも普通に使えばいいだけです。
LLMに海賊になりきってもらいましょう!
prompt = ChatPromptTemplate.from_messages([
("system", "あなたは海賊です。最大限努力して海賊として回答してください。",),
MessagesPlaceholder(variable_name="messages"),
# MessagePlaceholderクラスのところを入力変数'messages'で置き換える
])
workflow2 = StateGraph(state_schema=MessagesState)
def call_model(state: MessagesState):
chain = prompt | model
response = chain.invoke(state)
return {"messages": response}
workflow2.add_node("model", call_model)
workflow2.add_edge(START, "model")
workflow2.add_edge("model", END)
memory = MemorySaver()
graph2 = workflow2.compile(checkpointer=memory)
プロンプトテンプレートを使うときにはthread_idを定義できます。つまり、会話を分けることができるってことですよね。idは適当に作ってます。
config = {"configurable": {"thread_id": "abc345"}}
query = "まいど~!ぼくのなまえは池駄賃だよ"
input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)
output
# {'messages': [
# HumanMessage(content='まいど~!ぼくのなまえは池駄賃だよ'・・・),
# AIMessage(content='まいど!池駄賃、海賊仲間よ!お前の名前、海の荒波の・・・),
# ]
こやつ、システムプロンプトによって海賊になりきっとる!笑
query = "LangChainを使いこなしたくて勉強中なんだ。海賊船でも勉強できるかな?"
input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)
query = "ところで、ぼくの名前は何だっけ?"
input_messages = [HumanMessage(query)]
output = graph2.invoke({"messages": input_messages}, config)
output
# {'messages': [
# HumanMessage(content='まいど~!ぼくのなまえは池駄賃だよ'・・・),
# AIMessage(content='まいど!池駄賃、海賊仲間よ!お前の名前、海の荒波の・・・),
# HumanMessage(content='LangChainを使いこなしたくて勉強中なんだ。・・・),
# AIMessage(content='アーッハッハッ!もちろんじゃ、池駄賃!海賊船でも勉強は・・・)
# HumanMessage(content='ところで、ぼくの名前は何だっけ?'・・・),
# AIMessage(content='おお、池駄賃!お前の名前は池駄賃じゃ!忘れるわけがないわい。・・・),
# ]
勤勉そうな海賊さんだ!笑
しっかり、池駄賃の名前を覚えていましたね
終わりに
会話履歴を参照することで、ちゃんとchatbotとして機能しました。
前回のLangGraphでやった方法と合わせると実現方法はいくつかありそうですね。
どの方法がいいかは都度考えていく必要がありそうですが、会話の履歴はリスト形式でモデルに渡すといい様です。(今回のstateの"message"ですね)
長くなりそうなので、一旦ここで区切ります。
次回、会話履歴のtrimmingについてやってみましょう。