LangMemの3種類の長期記憶(セマンティック・エピソディック・プロシージャル)をLiteLLM経由で使い分けてみる
はじめに
データ事業本部のkobayashiです。
前回はLangGraphのCheckpoint機構による短期メモリ(スレッド内の会話履歴)を扱いました。今回はその先として、スレッドを跨いで保持される長期メモリを LangMem パッケージで実装する方法を紹介します。
LangMemはLangChainチームが提供する記憶管理ライブラリで、人間の記憶モデルに倣って セマンティック・エピソディック・プロシージャル の3種類の長期記憶をサポートします。LangGraphのStore機構と組み合わせて使います。
短期メモリと長期メモリ
| 種類 | 保存場所 | 範囲 | 用途 |
|---|---|---|---|
| 短期メモリ(前回) | Checkpointer |
1スレッド内 | 会話履歴 |
| 長期メモリ(今回) | Store |
全スレッド横断 | 事実・嗜好・過去の対応 |
長期メモリの3分類:
| 分類 | 内容 | 例 |
|---|---|---|
| セマンティック | 事実・知識 | 「ユーザーAは魚介類アレルギー」 |
| エピソディック | 経験・出来事 | 「先週、AはJSONについて質問した」 |
| プロシージャル | 振る舞い方 | システムプロンプトの改善 |
RAGとの違い
「ベクトル検索で必要な情報をLLMに渡す」という枠組みは RAG と共通です。実際 LangMem も内部では InMemoryStore(index={"dims": 1536, "embed": ...}) のように Embedding インデックスを利用しています。一方で、目的とライフサイクルが異なります。
| 観点 | RAG | LangMem(長期メモリ) |
|---|---|---|
| 対象 | 静的な知識(ドキュメント、マニュアル、社内Wiki) | 会話中に得た情報(ユーザーの事実・経験・振る舞い) |
| 書き込み元 | 事前にバッチで人間/パイプラインがインデックス化 | LLM自身が会話中に自律的に保存・更新 |
| 更新性 | 基本 read-only、再インデックスは別ジョブ | write/update/delete が日常的に発生 |
| 粒度・形 | 文書チャンク(テキスト断片) | 構造化された記憶(事実 / エピソード / プロンプト) |
| スコープ | 全ユーザー共通の知識ベース | namespace=("memories", user_id) でユーザーごとに分離 |
RAGは「就業規則を答えさせる」のように外部の固定知識を引き込むためのもので、LangMemは「Aliceがエビアレルギーだと前回言っていた」のように会話を通じて蓄積・進化するエージェント自身の記憶を扱うためのものとなります。特にプロシージャルメモリ(システムプロンプト自体を更新する)は RAG の枠組みでは表現しにくい領域になります。
実用上は併用するのが自然で、「社内ドキュメントは RAG、ユーザー個別の嗜好や過去のやり取りは LangMem」と役割分担します。
環境
Python 3.13
litellm 1.83.14
langgraph 1.1.10
langmem 0.0.21
langchain-litellm 0.6.4
langchain-core 1.3.2
$ uv pip install litellm langgraph langmem langchain-litellm langchain-core
$ export OPENAI_API_KEY="sk-..." # Embedding用
$ export ANTHROPIC_API_KEY="sk-ant-..."
セマンティックメモリ
ユーザーに関する事実を会話から自動抽出し、ベクトル検索で取り出すパターンです。LangMem の create_manage_memory_tool と create_search_memory_tool を使います。
"""セマンティックメモリ: ユーザーに関する事実をベクトル検索で取り出す。
LangMem の create_memory_store_manager / create_search_memory_tool を使い、
会話から自動的に事実を抽出してストアに保存し、関連時に検索する。
"""
from langchain.agents import create_agent
from langchain_litellm import ChatLiteLLM
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langmem import create_manage_memory_tool, create_search_memory_tool
# ストアと checkpointer
# Store: 長期記憶(スレッド横断)
# Checkpointer: 短期記憶(スレッド内の会話履歴)
store = InMemoryStore(
index={"dims": 1536, "embed": "openai:text-embedding-3-small"},
)
checkpointer = InMemorySaver()
llm = ChatLiteLLM(model="openai/gpt-5-mini")
# LangMem の標準ツール: 記憶の保存と検索
# namespace で「誰の記憶か」を指定する
NAMESPACE = ("memories", "{user_id}")
agent = create_agent(
model=llm,
tools=[
create_manage_memory_tool(namespace=NAMESPACE),
create_search_memory_tool(namespace=NAMESPACE),
],
system_prompt=(
"ユーザーに関する重要な事実は manage_memory で保存し、"
"次回以降の会話で必要なら search_memory で検索して活用してください。"
),
checkpointer=checkpointer,
store=store,
)
def chat(user_id: str, text: str) -> str:
config = {"configurable": {"thread_id": user_id, "user_id": user_id}}
result = agent.invoke(
{"messages": [{"role": "user", "content": text}]},
config=config,
)
return str(result["messages"][-1].content)
# 1日目: ユーザーが好みを伝える
print("=== Day 1 ===")
print("AI:", chat("alice", "私はマグロが好きです。アレルギーはエビです。"))
# 2日目: 別スレッドで好みを参照する(Checkpointerは短期なので参照できないが、Storeから検索される)
print("\n=== Day 2(新スレッド) ===")
config = {"configurable": {"thread_id": "alice-day2", "user_id": "alice"}}
result = agent.invoke(
{
"messages": [
{"role": "user", "content": "今日のおすすめのお寿司を教えてください"}
]
},
config=config,
)
print("AI:", result["messages"][-1].content)
ポイントは以下の3点です。
Store と Checkpointer の使い分け
Checkpointer: スレッド内の会話履歴(短期)Store: スレッド横断の長期記憶。Embedding indexを設定することでベクトル検索が可能になる
namespace パターン
("memories", "{user_id}") のように {} で囲むと、config.configurable.user_id から動的に解決されます。これによりユーザーごとに記憶を完全に分離できます。
LLMが自律的に保存・検索する
manage_memory_tool と search_memory_tool をツールとして渡すことで、LLMが「これは覚えるべき情報」「これは検索が必要」と判断します。明示的に保存処理を書く必要はありません。
実行結果は以下のようになります。
$ python semantic_memory.py
=== Day 1 ===
AI: 情報ありがとうございます。以下を記録しました:
- 好きな食べ物:マグロ
- アレルギー:エビ(甲殻類)
今後の提案や注意点でこれを考慮します。マグロのレシピ、エビを避けた外食時の注意、代替食材の提案など、何かご希望があれば教えてください。
=== Day 2(新スレッド) ===
AI: いいですね!おすすめネタ
- 鰹(かつお)── 春〜初夏の戻り鰹は香りと脂が良い。
- 鯵(あじ)── 身がしっかりして脂がのる季節。
- 中トロ(ちゅうとろ)── 脂の旨味を楽しみたいときの定番。
- 帆立(ほたて)── 甘みが強く、炙りや生のままでも美味。
(以下略)
Day 1 では manage_memory_tool が呼ばれ、ユーザーの好み(マグロ)とアレルギー(エビ)が Store に保存されます。
Day 2 は 別スレッド(thread_id=alice-day2)で起動するため、Checkpointer(短期メモリ)には会話履歴が無い状態です。それでも user_id=alice を渡しているので、namespace=("memories", "alice") の長期記憶を search_memory_tool で引き出して回答に反映できる、というのがポイントです。
なお、上の実行例では Day 2 の応答にエビへの言及が省略されていますが、これは GPT-5-mini が search_memory_tool を毎回必ず呼ぶわけではないためです。プロンプトに「ユーザーの嗜好に必ず合わせて回答を絞る」といった指示を強めることで search_memory を確実に呼ばせる挙動に寄せられます。
エピソディックメモリ
過去の会話の「やりとり全体」を記憶として残し、似たシチュエーションで参照するパターンです。LangMem の ReflectionExecutor を使うと会話の後ろでバックグラウンド処理として記憶抽出を走らせられます。
"""エピソディックメモリ: 過去の会話エピソードを丸ごと記憶し、類似シーンで参照する。
「以前ユーザーが似た質問をしたとき、どう答えたか」を記憶として残す。
ここでは LangMem の ReflectionExecutor を使って会話終了後に
バックグラウンドで記憶を抽出する例を示す。
"""
import litellm
from langchain_core.messages import HumanMessage
from langchain_litellm import ChatLiteLLM
from langgraph.store.memory import InMemoryStore
from langmem import ReflectionExecutor, create_memory_store_manager
litellm.modify_params = True
# 応答用は安価なGPT、記憶抽出(manager)はClaudeに任せる
llm = ChatLiteLLM(model="openai/gpt-5-mini")
store = InMemoryStore(
index={"dims": 1536, "embed": "openai:text-embedding-3-small"},
)
# create_memory_store_manager は会話メッセージから自動で記憶を抽出する
manager = create_memory_store_manager(
"anthropic:claude-sonnet-4-6",
namespace=("episodes", "{user_id}"),
instructions=(
"ユーザーの質問の傾向や、どのように答えると喜ばれたか、"
"といったエピソードを記憶として抽出してください。"
),
)
# ReflectionExecutor: 会話の後ろでバックグラウンドに記憶抽出を走らせる
reflection = ReflectionExecutor(manager, store=store)
def turn(messages: list, user_id: str) -> str:
"""1ターンの会話を実行し、その後エピソードを抽出する。"""
response = llm.invoke(messages)
new_messages = [*messages, response]
# バックグラウンドでエピソード抽出を依頼
config = {"configurable": {"user_id": user_id}}
reflection.submit(
{"messages": new_messages},
config=config,
after_seconds=0,
)
return str(response.content)
# サンプル会話
messages = [
HumanMessage(content="Pythonでリスト内包表記を簡潔に書く方法を教えてください")
]
ans = turn(messages, "alice")
print("AI:", ans[:200])
# 背景で動いている記憶抽出ジョブが完了するまで待つ
# wait=True でキュー内の全ジョブの完了を待ってからシャットダウンする
reflection.shutdown(wait=True)
items = store.search(("episodes", "alice"))
print(f"\n抽出されたエピソード: {len(items)} 件")
for item in items[:3]:
print(f" - {item.value}")
ReflectionExecutor は応答後の重い処理(記憶抽出)をユーザーへのレスポンスとは切り離してくれるため、レイテンシを増やさずにバックグラウンドで記憶を蓄積できます。スクリプト終了時に shutdown(wait=True) を呼ぶことで、キュー内のジョブを最後まで実行してからクリーンに終了します(呼ばないとプロセスが背景スレッドの分だけハングします)。
実行結果は以下のようになります。
$ python episodic_memory.py
AI: いいですね。Python のリスト内包表記(list comprehension)は短くて速く書けますが、
可読性を損なわないことが大事です。基本と実用的なテクニックを簡潔にまとめます。
(中略)
抽出されたエピソード: 3 件
- {'kind': 'Memory', 'content': {'content': 'ユーザーはPythonに関する質問をしている(リスト内包表記の簡潔な書き方)。Pythonの実用的なコーディングテクニックに興味がある。'}}
- {'kind': 'Memory', 'content': {'content': 'ユーザーへの回答スタイルとして、「具体的なコード例付きで網羅的に説明する」「最後に『具体的なコードを見せてくれればさらに対応する』と促す」形式が効果的だった。'}}
- {'kind': 'Memory', 'content': {'content': 'Pythonのリスト内包表記について質問された際、基本構文・よく使うパターン・ウォルラス演算子などの発展テクニック・ベストプラクティスを網羅的に例付きで説明したところ、会話が自然に進んだ。'}}
注目したいのは、抽出された3件の記憶が 単なる事実の羅列ではなく「ユーザーの興味の傾向」「うまく行った回答スタイル」 を含む点です。Manager(Claude)が会話メッセージから「次回類似の質問が来たときに役立つ知識」をエピソードとして整理しています。
プロシージャルメモリ
エージェントの「振る舞い方」自体を記憶として更新するパターンです。ユーザーフィードバックを受けてシステムプロンプトを段階的に最適化します。
"""プロシージャルメモリ: エージェントの「振る舞い方(プロンプト)」を記憶として更新する。
ユーザーフィードバックを受けてシステムプロンプトを段階的に改善していく。
LangMem の create_prompt_optimizer を使う。
"""
import litellm
from langchain_core.messages import AIMessage, HumanMessage
from langchain_litellm import ChatLiteLLM
from langmem import create_prompt_optimizer
litellm.modify_params = True
# 応答用はGPT、プロンプト最適化(optimizer)はClaudeに任せる
llm = ChatLiteLLM(model="openai/gpt-5-mini")
optimizer = create_prompt_optimizer(
"anthropic:claude-sonnet-4-6",
kind="prompt_memory",
)
initial_prompt = "あなたは丁寧なアシスタントです。質問に答えてください。"
# 過去のフィードバック付き会話
trajectories = [
(
[
HumanMessage(content="プログラミングの質問があります"),
AIMessage(content="どのような質問ですか?お気軽にどうぞ"),
],
{"feedback": "もっとコード例を示してほしかった"},
),
(
[
HumanMessage(content="JSONのパースについて"),
AIMessage(content="Pythonでは json モジュールを使います。"),
],
{"feedback": "実際のコード例があると助かる"},
),
]
# プロンプトをフィードバックを反映して最適化
better_prompt = optimizer.invoke(
{"trajectories": trajectories, "prompt": initial_prompt}
)
print("=== 最適化前のプロンプト ===")
print(initial_prompt)
print("\n=== 最適化後のプロンプト ===")
print(better_prompt)
# 最適化後のプロンプトで実際に応答してみる
response = llm.invoke(
[
{"role": "system", "content": str(better_prompt)},
{"role": "user", "content": "JSONのパースを教えてください"},
]
)
print("\n=== 最適化後のプロンプトでの応答 ===")
print(response.content)
実行結果は以下のようになります。
$ python procedural_memory.py
=== 最適化前のプロンプト ===
あなたは丁寧なアシスタントです。質問に答えてください。
=== 最適化後のプロンプト ===
あなたは丁寧なアシスタントです。質問に答えてください。プログラミングに関する質問には、必ず具体的なコード例を示してください。コード例はわかりやすく、実際に動作するものを提供してください。
=== 最適化後のプロンプトでの応答 ===
JSONのパース(解析)とは、JSON形式の文字列をプログラム内で扱えるデータ構造(オブジェクト、辞書、構造体など)に変換することです。
(中略)
2) Python(標準 json モジュール)
- 文字列/ファイル読み込み
\`\`\`python
import json
json_text = '{"name": "Bob", "age": 25, "skills": ["python", "sql"]}'
try:
obj = json.loads(json_text)
print(obj['name']) # Bob
except json.JSONDecodeError as e:
print("JSON parse error:", e)
\`\`\`
(以下略:JavaScript / Go / Java / C# のサンプルも続けて生成される)
Optimizer(Claude)が過去の feedback(「もっとコード例を示してほしかった」「実際のコード例があると助かる」)を読み取って、システムプロンプトに 「プログラミングに関する質問には、必ず具体的なコード例を示してください」 というルールを自動追加しました。最適化後のプロンプトでGPT-5-mini に応答させると、複数言語のコード例を網羅した回答に変化しているのが確認できます。
このパターンの本質は、人間からのフィードバックが直接プロンプトに反映されることです。エージェントの「振る舞い」自体が継続的に改善される長期記憶として機能します。
LiteLLMでのモデル使い分け
| ロール | スクリプト | モデル | 理由 |
|---|---|---|---|
主LLM (llm) |
semantic / episodic | openai/gpt-5-mini |
多数回呼ばれる、安価モデルで十分 |
主LLM (llm) |
procedural | openai/gpt-5-mini |
最適化されたプロンプトの動作確認用 |
| Manager(記憶抽出) | episodic | anthropic:claude-sonnet-4-6 |
抽象化・要約品質を担保 |
| Optimizer(プロンプト改善) | procedural | anthropic:claude-sonnet-4-6 |
プロンプト設計の繊細な変更を任せる |
| Embedding | semantic / episodic | openai:text-embedding-3-small |
ベクトル検索用 |
LangMem は内部的に「メイン応答用LLM」と「メタ処理用LLM(manager / optimizer)」を別々に指定できる設計になっており、LiteLLMと組み合わせると 応答はGPT、メタ処理はClaude といったプロバイダー使い分けが自然に実現できます。
まとめ
LangChainチームの LangMem とLangGraphの Store 機構を組み合わせて、スレッドを跨いで保持される長期メモリの3分類(セマンティック・エピソディック・プロシージャル)を実装しました。
セマンティックメモリは create_manage_memory_tool / create_search_memory_tool を渡すだけでLLMが会話から事実を自律的に保存・検索し、エピソディックメモリは ReflectionExecutor で記憶抽出をバックグラウンドに逃がすことでレイテンシを増やさずに過去のエピソードを蓄積できます。プロシージャルメモリは create_prompt_optimizer がフィードバックを反映してシステムプロンプト自体を書き換えるため、エージェントの「振る舞い」そのものを継続的に改善できます。
なおInMemoryStore は本番では PostgresStore などに差し替えることで、マルチノード環境でも長期記憶を共有可能です。LiteLLMで応答には安価なGPT、記憶抽出やプロンプト最適化といったメタ処理にはClaudeを割り当てることで、品質を担保しつつ全体コストを抑えられる点もこの構成の大きな利点です。
最後まで読んでいただきありがとうございました。










