はじめに
LangChainでチャットボットを構築するとき、最初にぶつかる壁が「AIが過去の会話を覚えていない」という問題です。ユーザーが「さっき言った件ですが」と戻っても、API経由のLLMには前の発言が一切伝わりません。
LangChainのメモリ機能は、会話履歴の保持と自動的なプロンプト挿入を担います。本記事では、LangChain 0.3系で推奨される3つのメモリ手法を取り上げます。
- バッファメモリ -- 全履歴をそのまま保持する基本形
- ウィンドウメモリ -- 直近N件だけを残して古い履歴を切り詰める
- サマリーメモリ -- 古い履歴をLLMで要約して圧縮する
なぜメモリが必要なのか
LLM自体は「受け取った入力だけを見て応答を生成する」仕組みです。ChatGPTのWeb画面で文脈が自然に引き継がれるのは、裏側で過去の会話履歴を毎回プロンプトに含めて送信しているからです。API経由でLLMを利用する場合も、開発者が履歴をプロンプトの一部として送る必要があります。LangChainのメモリ機能を使えば、この履歴の組み立てを自動化できます。
1. バッファメモリ -- 全履歴を保持する基本形
LangChain 0.3では ConversationChain と ConversationBufferMemory が非推奨となり、LCELを使って自前でメモリを組み込むのが標準になりました。鍵となるのが MessagesPlaceholder です。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
prompt = ChatPromptTemplate.from_messages([
("system", "あなたは親切なアシスタントです。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
history = InMemoryChatMessageHistory()
chain = prompt | model | StrOutputParser()
def chat(user_input: str) -> str:
response = chain.invoke({
"history": history.messages,
"input": user_input
})
history.add_user_message(user_input)
history.add_ai_message(response)
return response
MessagesPlaceholder が履歴を差し込む場所を指定します。chat 関数は「履歴を取り出す → プロンプトに差し込む → 実行 → 履歴に保存」というサイクルを回します。
複数ユーザーを扱う場合は、セッションIDごとに履歴オブジェクトを辞書で管理します。
session_store: dict[str, InMemoryChatMessageHistory] = {}
def get_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in session_store:
session_store[session_id] = InMemoryChatMessageHistory()
return session_store[session_id]
しかし、全履歴を保持すると会話が続くほどトークン量とAPI料金が増え続けます。50往復で約10,000トークンに達する場合もあります。この課題を解決するのが次の2つの手法です。
2. ウィンドウメモリ -- 直近N件だけを残す
LangChain 0.3では trim_messages 関数で履歴をトークン数ベースで切り詰められます。
from langchain_core.messages import trim_messages
def trimmer(messages):
return trim_messages(
messages,
max_tokens=500,
strategy="last", # 最新側を残す
token_counter=lambda m: model.get_num_tokens_from_messages(m),
include_system=True, # システムメッセージは保持
start_on="human"
)
def chat_with_window(user_input: str) -> str:
trimmed = trimmer(history.messages)
response = chain.invoke({"history": trimmed, "input": user_input})
history.add_user_message(user_input)
history.add_ai_message(response)
return response
重要な設計ポイントは「履歴オブジェクトには全件保存し続け、プロンプトに渡す直前でトリムをかける」ことです。これにより後から窓サイズを変更できます。手軽でコスト増もありませんが、古い話題に戻れなくなるのが代償です。
3. サマリーメモリ -- 履歴を要約して圧縮する
古い履歴を「捨てる」のではなく「要約して短く保つ」アプローチです。一定のタイミングでLLMに会話の要点を生成させ、要約文を履歴の先頭に置きます。
from langchain_core.messages import SystemMessage, HumanMessage
summary_prompt = ChatPromptTemplate.from_messages([
("system", "以下の会話履歴を簡潔に要約し、200字以内でまとめてください。"),
("human", "{conversation}")
])
summary_chain = summary_prompt | model | StrOutputParser()
KEEP_RECENT = 4
def chat_with_summary(user_input: str, history) -> str:
all_msgs = history.messages
if len(all_msgs) > KEEP_RECENT:
old, recent = all_msgs[:-KEEP_RECENT], all_msgs[-KEEP_RECENT:]
summary = summary_chain.invoke({
"conversation": "\n".join(f"{m.content}" for m in old)
})
hist_msgs = [SystemMessage(content=f"会話の要約: {summary}")] + recent
else:
hist_msgs = all_msgs
response = chain.invoke({"history": hist_msgs, "input": user_input})
history.add_user_message(user_input)
history.add_ai_message(response)
return response
要約の生成にはAPI呼び出しが増えるコストがありますが、古い話題の要点が残るため長時間の相談向きです。実運用では「一定の往復ごとに要約を更新する」「要約文をキャッシュする」工夫を加えます。
3つの手法の比較
| 観点 | バッファ | ウィンドウ | サマリー |
|---|---|---|---|
| トークン量 | 増え続ける | 一定に保たれる | 圧縮される |
| 古い話題 | すべて残る | 完全に失われる | 要点は残る |
| 追加コスト | なし | なし | 要約生成でAPI増 |
| 向き | 短時間の対話 | 雑談・問い合わせ | 長時間の相談 |
用途に応じて選ぶのが大切です。本番では「要約は一定往復ごとに更新し、直近はウィンドウで固定する」複合型もよく使われます。
まとめ
- LangChain 0.3では
MessagesPlaceholderとInMemoryChatMessageHistoryの組み合わせが推奨されるメモリの基本形 - バッファメモリは全履歴を保持するが、会話が長くなるとコストが増大
-
ウィンドウメモリは
trim_messagesで直近N件だけを残し、手軽でコスト増なし - サマリーメモリは古い履歴をLLMで要約して圧縮し、古い話題の要点を残せる
- 履歴オブジェクトには全件保存し、プロンプト渡す直前で圧縮をかけるのが柔軟な構成
メモリの仕組みを取り入れることで、チェーンが「文脈を理解する対話相手」として振る舞うようになります。用途に応じて3つの手法を使い分けてみてください。
本記事の内容は著書『LangChain x AI実践入門』をベースに再構成した技術解説です。