AIエージェントが会話を忘れる原因と解決方法【RAG・メモリ設計3つの実装パターン】
AIエージェントが「記憶喪失」になる原因
ChatGPT APIや生成AIを使ったエージェント(自動で判断して動作するプログラム)を運用していると、こんな経験はないでしょうか。
- 10分前の会話の内容をエージェントが忘れている
- ユーザーが「さっき言ったあの件」と言っても、過去のやり取りが反映されない
- チャットボットが同じ質問を何度もしてきた
これは「セッション間記憶喪失」と呼ばれる問題です。LLMは本質的にステートレス(状態を持たない)であり、エージェントが新しいセッションを開始すると、前のセッションの文脈が完全に失われてしまいます。
RAGを使っても解決しないケースが多い点が2026年の現場での共通課題です。
RAGの本質:「記憶」ではなく「検索」
RAGに対する誤解が根強くあります。RAGは記憶ではありません。
RAG(Retrieval-Augmented Generation)の実態:
質問 → テキストをベクトル化 → 類似ドキュメントを検索 → プロンプトに追加
これは Ctrl+F(検索)を賢くしただけです。
RAGが苦手なこと:
❌ 「3週間前の会議でAさんが決めた方針」を記憶する
❌ 「ユーザーが過去に拒否した選択肢」を避ける
❌ セッション間のコンテキスト継続
❌ 意思決定の記録と参照
RAGが得意なこと:
✅ 大量ドキュメントから関連箇所を検索
✅ 社内ナレッジベースの活用
✅ 最新情報の取得(学習データのカットオフを超える)
本物のメモリが必要な場合、RAGとは別の設計が必要です。
ChatGPT APIでメモリ機能を実装する理由
ChatGPT APIは基本的に「ステートレス」です。つまり、APIを呼び出すたびに、以前の会話内容は保持されません。毎回、ユーザー設定や好みを何度も入力し直す必要があります。
これを解決するのが「メモリ機能」です。メモリ機能を実装すれば、ユーザーの過去の会話や設定を自動的に記憶し、次の会話に活かすことができます。結果として:
- ユーザーが同じ指示を繰り返す必要がなくなる
- AIアシスタントがユーザーの好みや背景を理解した上で回答する
- 会話がより自然で効率的になる
これは特にチャットボット、カスタマーサポートAI、個人用コーディングアシスタントで重要です。
メモリ実装の3つの方法
ChatGPT APIでメモリを実装する方法は、大きく3つに分かれます。
1. 会話履歴の直接送信(シンプル)
最もシンプルな方法は、ChatGPT APIのmessagesパラメータに過去の会話をそのまま追加することです。ただし、この方法はトークン数が増え続けるため、長期運用には向きません。
2. LangChainのメモリ永続化(実装的)
LangChainのメモリクラスを使い、会話履歴をJSONファイルやデータベースに保存する方法です。これが今回の記事の中心となります。
3. Retrieval Augmented Generation(RAG)統合(高度)
会話履歴をベクトル化して保存し、関連する過去の会話を検索して活用する方法です。大量の会話データを効率的に管理できます。
前提環境と必要なもの
以下の環境を整備してください。
- Python: 3.9以上
- OpenAI APIキー: https://platform.openai.com/api-keys で取得
- LangChain: 0.1.0以上
- 必要なライブラリ:
python-dotenv(APIキーの管理)langchain(メモリと会話チェーン)openai(ChatGPT APIの呼び出し)
インストール手順
# Pythonバージョン確認
python --version
# 必要なライブラリをインストール
pip install langchain openai python-dotenv
LangChainのバージョン確認:
pip show langchain
0.1.0以上であることを確認してください。
解決策1:JSONファイルでセッション間メモリを実装する
最も手軽で実用的な方法です。データベース不要で始められます。
ステップ1: 環境変数の設定
.env ファイルを作成し、OpenAI APIキーを保存します。
# .env ファイル
OPENAI_API_KEY=sk-...your-api-key-here...
ステップ2: シンプルなメモリ実装コード
まずは「メモリ機能なし」と「メモリ機能あり」の違いを実感するコードです。
# memory_basic.py
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain.llms import OpenAI
from dotenv import load_dotenv
import os
# 環境変数から APIキー を読み込む
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
# ステップ1: メモリオブジェクトを作成
memory = ConversationBufferMemory()
# ステップ2: OpenAI LLM を初期化
llm = OpenAI(
api_key=api_key,
model="gpt-3.5-turbo", # または gpt-4
temperature=0.7
)
# ステップ3: ConversationChain を作成(メモリ付き)
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True # デバッグ用:やり取りを画面に表示
)
# ステップ4: 会話を実行
response1 = conversation.predict(input="私の名前はTaroです。好きなプログラミング言語はPythonです。")
print("Response 1:")
print(response1)
print("\n" + "="*50 + "\n")
# 2番目の入力:前の会話を覚えているか確認
response2 = conversation.predict(input="私の好きな言語は何だったか覚えていますか?")
print("Response 2:")
print(response2)
このコードを実行すると、2番目の質問で「Python」と答えられます。なぜなら、ConversationChainが自動的に過去の会話をメモリに保存し、それを次の質問に含めるからです。
ステップ3: メモリを永続化(保存と読み込み)
# memory_persistent.py
import json
import os
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain.llms import OpenAI
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
# メモリ保存用のファイルパス
MEMORY_FILE = "conversation_memory.json"
def load_memory():
"""ファイルからメモリを読み込む"""
memory = ConversationBufferMemory()
if os.path.exists(MEMORY_FILE):
with open(MEMORY_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
# 保存されたメモリを復元
memory.buffer = data.get('buffer', '')
return memory
def save_memory(memory):
"""メモリをファイルに保存"""
with open(MEMORY_FILE, 'w', encoding='utf-8') as f:
json.dump({'buffer': memory.buffer}, f, ensure_ascii=False, indent=2)
# ステップ1: 前回のメモリを読み込む
memory = load_memory()
# ステップ2: LLM と ConversationChain を初期化
llm = OpenAI(api_key=api_key, model="gpt-3.5-turbo", temperature=0.7)
conversation = ConversationChain(llm=llm, memory=memory, verbose=True)
# ステップ3: ユーザーと対話ループ
print("AIアシスタント(メモリ機能付き)")
print("終了: 'quit' または 'exit' を入力")
print("="*50)
while True:
user_input = input("\nあなた: ").strip()
if user_input.lower() in ['quit', 'exit']:
save_memory(memory) # 終了時にメモリを保存
print("メモリを保存しました。さようなら!")
break
if not user_input:
continue
response = conversation.predict(input=user_input)
print(f"\nAI: {response}")
# 毎回メモリを保存(オプション:セッション終了時のみでもOK)
save_memory(memory)
このコードの重要な点:
load_memory()は前回のセッションから会話履歴を復元save_memory()は毎回の会話後にファイルに保存- プログラムを再起動しても、会話の履歴が残る
実行例:
python memory_persistent.py
初回セッション:
あなた: 私はデータサイエンティストです。好きなツールはPandasです。
AI: (Pandasについて返答)
あなた: quit
次のセッション:
python memory_persistent.py
あなた: 何度も教えなくていいから、私の職業とツール選びについて簡潔に説明して
AI: あなたはデータサイエンティストで、Pandasをお好みですね。...
解決策2:グラフ検索で関係性を追跡する
RAGのベクトル検索が苦手な「関係性」の追跡をグラフDBで解決します。
# graph_memory.py
# グラフ構造でエンティティ間の関係を記録して検索する
import json
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass, field, asdict
GRAPH_FILE = Path("knowledge_graph.json")
@dataclass
class Node:
id: str
type: str # "person", "decision", "project", "concept"
label: str
properties: dict = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.now().isoformat()[:10])
@dataclass
class Edge:
source_id: str
target_id: str
relation: str # "decided_by", "depends_on", "rejected", "approved"
metadata: dict = field(default_factory=dict)
class KnowledgeGraph:
def __init__(self):
data = json.loads(GRAPH_FILE.read_text()) if GRAPH_FILE.exists() else {"nodes": {}, "edges": []}
self.nodes: dict[str, Node] = {k: Node(**v) for k, v in data["nodes"].items()}
self.edges: list[Edge] = [Edge(**e) for e in data["edges"]]
def add_node(self, node: Node) -> None:
self.nodes[node.id] = node
self._save()
def add_edge(self, edge: Edge) -> None:
self.edges.append(edge)
self._save()
def get_related(self, node_id: str, max_depth: int = 2) -> list[Node]:
"""指定ノードから深さmax_depthまで関連ノードを取得する"""
visited = set()
result = []
queue = [(node_id, 0)]
while queue:
current_id, depth = queue.pop(0)
if current_id in visited or depth > max_depth:
continue
visited.add(current_id)
if current_id in self.nodes:
result.append(self.nodes[current_id])
for edge in self.edges:
if edge.source_id == current_id and edge.target_id not in visited:
queue.append((edge.target_id, depth + 1))
elif edge.target_id == current_id and edge.source_id not in visited:
queue.append((edge.source_id, depth + 1))
return result
def get_decisions_by_person(self, person_id: str) -> list[Node]:
"""特定の人物が関わった意思決定をすべて取得する"""
decision_ids = {
e.target_id for e in self.edges
if e.source_id == person_id and e.relation == "decided_by"
}
return [self.nodes[did] for did in decision_ids if did in self.nodes]
def _save(self) -> None:
GRAPH_FILE.write_text(json.dumps({
"nodes": {k: asdict(v) for k, v in self.nodes.items()},
"edges": [asdict(e) for e in self.edges]
}, ensure_ascii=False, indent=2))
# 使用例: プロジェクトの意思決定を記録する
graph = KnowledgeGraph()
# エンティティを追加
graph.add_node(Node("user_alice", "person", "Alice(エンジニア)"))
graph.add_node(Node("proj_001", "project", "ECサイトリニューアル"))
graph.add_node(Node("dec_001", "decision", "PostgreSQL採用(MySQL→移行)"))
graph.add_node(Node("dec_002", "decision", "Next.js App Router採用"))
# 関係を追加
graph.add_edge(Edge("user_alice", "dec_001", "decided_by", {"date": "2026-04-15"}))
graph.add_edge(Edge("dec_001", "proj_001", "belongs_to"))
graph.add_edge(Edge("dec_002", "proj_001", "belongs_to"))
# クエリ: Aliceが関わった意思決定
alice_decisions = graph.get_decisions_by_person("user_alice")
print("Aliceの意思決定:", [d.label for d in alice_decisions])
解決策3:ConversationalRetrievalChainでRAG統合
LangChainの ConversationalRetrievalChain を使うと、会話履歴と外部知識(ドキュメント)を組み合わせたRAG(検索拡張生成)を実装できます。これは特にドキュメントQAやナレッジベース検索で威力を発揮します。
ステップ1: ドキュメントの準備
以下の手順でドキュメントを準備します。
# rag_setup.py
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
# ステップ1: テキストファイルをロード
# 例:sample_docs.txt を準備
loader = TextLoader('sample_docs.txt', encoding='utf-8')
documents = loader.load()
# ステップ2: テキストを小さなチャンク(小片)に分割
text_splitter = CharacterTextSplitter(
chunk_size=500, # 1チャンク500文字
chunk_overlap=50 # チャンク間で50文字重複させる
)
docs = text_splitter.split_documents(documents)
# ステップ3: OpenAI の埋め込み(embedding)を使ってベクトル化
embeddings = OpenAIEmbeddings(api_key=api_key)
# ステップ4: FAISS(ベクトルデータベース)にドキュメントを保存
vector_store = FAISS.from_documents(docs, embeddings)
# ステップ5: 検索用に保存
vector_store.save_local("faiss_index")
print("ドキュメントの準備完了。ベクトルデータベースを保存しました。")
ステップ2: ConversationalRetrievalChain の実装
# rag_conversation.py
import json
import os
from dotenv import load_dotenv
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.llms import OpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
# ステップ1: ベクトルデータベースを読み込む
embeddings = OpenAIEmbeddings(api_key=api_key)
vector_store = FAISS.load_local("faiss_index", embeddings)
# ステップ2: メモリを準備
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# ステップ3: LLM を初期化
llm = OpenAI(api_key=api_key, model="gpt-3.5-turbo", temperature=0.7)
# ステップ4: ConversationalRetrievalChain を作成
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=vector_store.as_retriever(),
memory=memory,
verbose=True
)
# ステップ5: ユーザー入力ループ
print("RAG対応のQAチャット(メモリ機能付き)")
print("="*50)
while True:
user_input = input("\nあなた: ").strip()
if user_input.lower() in ['quit', 'exit']:
print("終了します。")
break
if not user_input:
continue
# qa_chain で回答を生成
result = qa_chain({"question": user_input})
answer = result['answer']
print(f"\nAI: {answer}")
# 参照したドキュメントも表示(オプション)
if "source_documents" in result:
print("\n参考にしたドキュメント:")
for doc in result['source_documents'][:2]: # 最初の2つだけ表示
print(f" - {doc.page_content[:100]}...")
このコードの流れ:
- ベクトルデータベース(FAISS)からドキュメントを検索
- 過去の会話(メモリ)と検索結果を組み合わせて、LLMに入力
- 常に最新のドキュメント知識に基づいた回答が得られる
解決策4:より高度なメモリ管理(SQLiteベース)
大規模なアプリケーションでは、JSONファイルではなくデータベースにメモリを保存するのが適切です。
# memory_sqlite.py
import sqlite3
import json
from datetime import datetime
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain.llms import OpenAI
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
class SQLiteMemory:
"""SQLiteを使ったメモリ管理"""
def __init__(self, db_path="conversation.db", user_id="default"):
self.db_path = db_path
self.user_id = user_id
self._init_db()
def _init_db(self):
"""テーブルを作成"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS conversation_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
message TEXT NOT NULL,
role TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
def add_message(self, message, role="user"):
"""メッセージをデータベースに追加"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO conversation_memory (user_id, message, role) VALUES (?, ?, ?)",
(self.user_id, message, role)
)
conn.commit()
conn.close()
def get_history(self, limit=20):
"""会話履歴を取得"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"SELECT message, role FROM conversation_memory WHERE user_id = ? ORDER BY id DESC LIMIT ?",
(self.user_id, limit)
)
rows = cursor.fetchall()
conn.close()
# 逆順にして時系列に
return [(row[1], row[0]) for row in reversed(rows)]
# 使用例
db_memory = SQLiteMemory(user_id="user123")
# メモリから履歴を取得してLangChainメモリに組み込む
history = db_memory.get_history()
langchain_memory = ConversationBufferMemory()
# 過去のメッセージをメモリに復元
for role, message in history:
if role == "user":
langchain_memory.chat_memory.add_user_message(message)
else:
langchain_memory.chat_memory.add_ai_message(message)
# 通常通りConversationChainを実行
llm = OpenAI(api_key=api_key, model="gpt-3.5-turbo", temperature=0.7)
conversation = ConversationChain(llm=llm, memory=langchain_memory, verbose=True)
# 会話実行後、メッセージをDBに保存
user_input = "Pythonの最新トレンドを教えて"
response = conversation.predict(input=user_input)
db_memory.add_message(user_input, role="user")
db_memory.add_message(response, role="assistant")
print(response)
このアプローチの利点:
- 複数ユーザー対応:
user_idでユーザーごとの会話を分離 - 永続性: SQLiteはファイルベースで、パソコン再起動後も保持
- スケーラビリティ: ファイルではなくDBなので、大量データに対応
- クエリ可能: 特定の日時や単語で検索可能
解決策5:スキーマ強制でエージェントを信頼できる状態にする
エージェントの出力を構造化して、後処理の信頼性を上げます。
# structured_agent.py
# Pydanticでエージェント出力のスキーマを強制する
import anthropic
import json
from pydantic import BaseModel, ValidationError
from typing import Literal
client = anthropic.Anthropic()
class AgentAction(BaseModel):
action_type: Literal["search", "write", "notify", "ask_user", "done"]
target: str
content: str
confidence: float # 0.0〜1.0
requires_confirmation: bool
class AgentResponse(BaseModel):
reasoning: str
next_action: AgentAction
memory_to_save: list[str] # 次のセッションに引き継ぐ情報
def run_structured_agent(task: str, context: str = "") -> AgentResponse | None:
"""スキーマ検証付きのエージェント実行"""
system = f"""あなたはタスクを自律実行するエージェントです。
コンテキスト: {context}
必ず以下のJSON形式で出力してください:
{{
"reasoning": "判断理由",
"next_action": {{
"action_type": "search|write|notify|ask_user|done",
"target": "対象",
"content": "実行内容",
"confidence": 0.0〜1.0,
"requires_confirmation": true|false
}},
"memory_to_save": ["次のセッションに引き継ぐ情報"]
}}"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
system=system,
messages=[{"role": "user", "content": f"タスク: {task}"}]
)
cost = (response.usage.input_tokens * 3.00 + response.usage.output_tokens * 15.00) / 1_000_000
print(f"エージェントコスト: ${cost:.5f}")
try:
data = json.loads(response.content[0].text)
agent_response = AgentResponse(**data) # Pydanticで型検証
return agent_response
except (json.JSONDecodeError, ValidationError) as e:
print(f"スキーマ検証失敗: {e}")
return None
class AgentRunner:
"""エージェントの実行を管理するクラス(後処理込み)"""
def __init__(self, store: "SessionStore"):
self.store = store
self.execution_log: list[dict] = []
def run(self, task: str, max_steps: int = 10) -> None:
"""エージェントを実行する(モデル返答後の後処理まで完了させる)"""
context = self.store.build_context_prompt()
for step in range(max_steps):
print(f"\n=== Step {step + 1} ===")
result = run_structured_agent(task, context)
if not result:
print("スキーマ検証失敗 → 終了")
break
print(f"判断: {result.reasoning}")
print(f"次のアクション: {result.next_action.action_type}")
# 1. メモリを更新(後処理)
if result.memory_to_save:
self.store.memory.long_term_facts.extend(result.memory_to_save)
self.store.save()
print(f" メモリ保存: {result.memory_to_save}")
# 2. 実行ログに記録(後処理)
self.execution_log.append({
"step": step + 1,
"action": result.next_action.action_type,
"target": result.next_action.target,
})
# 3. 確認が必要なアクションは一時停止
if result.next_action.requires_confirmation:
confirm = input(f"確認: {result.next_action.content} → 実行しますか?(y/n): ")
if confirm != "y":
print("ユーザーが中断")
break
# 4. 完了判定
if result.next_action.action_type == "done":
print("\nタスク完了(後処理まで完了)")
break
print(f"\n実行ログ: {len(self.execution_log)}ステップ")
3つの解決策の使い分け
シナリオ別の最適解:
┌─────────────────────────────────────────┬──────────────────────────┐
│ シナリオ │ 推奨アプローチ │
├─────────────────────────────────────────┼──────────────────────────┤
│ 個人プロジェクト・小規模チャットボット │ JSONファイル(解決策1) │
│ 複数ユーザー・複雑な関係性追跡が必要 │ グラフDB(解決策2) │
│ 中規模: 複数ユーザー・複数プロジェ
---
## あわせて読みたい
- [Claude Codeで長期メモリ実装【複数セッション対応】会話履歴の活用法](/code/claude-code/)
- [Claude Code Context Window完全ガイド【2026年版】200K活用とContext超過エラーの解決法](/code/claude-code-context-window/)
- [LangChainで会話メモリを実装する3ステップ|生成AIの履歴保存・永続化【コード付き】](/code/ai-3/)
参考ソース
- Beyond RAG: Why I replaced similarity search with graph traversal for AI agent context
- RAG isn't memory. It's Ctrl+F with embeddings.
- Strict Schema Enforcement: The Bedrock of AI Reliability
- An Agent Run Is Not Done When the Model Stops Talking
- Your AI Agent Forgets Everything Between Sessions (Here's How to Fix It)