今回はエンジニアやテクニカルリードに向けて、「ステートレスなLLMにどうやって記憶(Context)を持たせるか」というテーマで、実装レベルの深掘りを行います。
AIエージェント開発の現場において直面する「トークン制限」「コスト」「応答速度(レイテンシ)」という現実的な制約の中で、どのアーキテクチャを選ぶべきか。バッファ、要約、ベクトル検索という3つの主要パターンを比較し、ビジネス価値を最大化する最適な解を探求します。理論だけでなく「実際にどう動くか」を重視し、アジャイルに検証していきましょう。
1. ステートレスなLLMに「記憶」を持たせる技術的課題
LLMを用いたAIエージェント開発において、AIが直前の会話を忘れる「記憶喪失」のメカニズムを正しく理解することは、堅牢な業務システム設計の第一歩です。表面的な対話の裏側で何が起きているのか、技術的な制約から紐解いていきます。
HTTPリクエストと同様のステートレス性とは
Web開発におけるHTTPプロトコルと同様に、OpenAIのモデルやClaudeなどのLLMも、本質的にステートレスなアーキテクチャを採用しています。APIへのプロンプト送信と、それに対するコンプリーション(応答)の返却という1往復の処理が終わると、モデルはセッションの記憶を一切保持しません。
あたかもAIが文脈を覚えているように見せるには、システム側で過去のやり取りを毎回プロンプトに含めて再送信する必要があります。
User: こんにちは、私の名前はHARITAです。
AI: こんにちは、HARITAさん。
(次のターン)
User: 私の名前を覚えていますか?
(裏側で送信されるプロンプト)
"User: こんにちは、私の名前はHARITAです。\nAI: こんにちは、HARITAさん。\nUser: 私の名前を覚えていますか?"
このように、過去の履歴(History)を現在の入力に連結して渡すことで、初めて文脈理解が成立します。これがLLMの対話における「記憶」の正体です。
コンテキストウィンドウの物理的限界とコスト
ここで直面する最大の壁が、LLMが一度に処理できるトークン量の上限であるコンテキストウィンドウ(Context Window)の制限です。
技術の進歩により、モデルが扱えるコンテキスト長は拡大を続けています。例えばOpenAIの環境では、2026年2月13日をもってGPT-4oがChatGPT UIから完全に廃止(レガシーモデルとして終了)されました。API経由では引き続きGPT-4oを利用可能ですが、日常業務や高速処理の新たな標準としてはGPT-5.2(無料プラン標準)が推奨されています。また、推論に特化したタスクではoシリーズ(o1やo3など)を使い分けるアプローチが主流となっています。
しかし、ウィンドウが広くなったからといって、全履歴をそのまま送るアプローチには、エンタープライズ環境での運用において2つの大きな課題が残ります。
- APIコストの増大: 多くのLLM APIは入出力トークン数に基づく従量課金制です。会話が長引き過去ログが数万トークン単位で肥大化すれば、リクエストごとのコストは経営的にも無視できない規模に膨れ上がります。
- レイテンシの悪化: 入力トークンが増加すると処理時間(Time to First Token)も長くなります。膨大な履歴データの処理によるタイムラグは、ユーザー体験を著しく損なう要因となります。
さらに、入力過多によりモデルが重要な指示を見落とす「Lost in the Middle」現象のリスクも高まります。最新のベストプラクティスでは、Claudeの運用においてシステムプロンプト(CLAUDE.mdなど)のトークン数を2500以内に抑え、タスクを分割して計画から実行へと移すワークフローや、MCP(Model Context Protocol)を活用した外部ツール連携が推奨されています。単に膨大な履歴を投げ込むのではなく、必要な情報を精査して渡す仕組みが不可欠です。
「短期記憶」と「長期記憶」の技術的な定義
効率的なシステム設計を実現するためには、「記憶」を大きく2つに分類して管理するアプローチが有効です。
- 短期記憶(Short-term Memory): 現在進行中のセッション内での会話履歴を指します。直前の文脈を維持し、ユーザーとのスムーズな対話を実現するために不可欠な要素です。LangChainのバッファメモリなどがこれに該当します。
- 長期記憶(Long-term Memory): セッションを超えて永続的に保持される情報です。ユーザーのプロフィール、過去の会話の要約、社内ドキュメントなどの外部ナレッジベースが該当し、主にベクトルデータベースを用いたRAG(検索拡張生成)アーキテクチャによって実現されます。
最新LLMの高度な推論能力を引き出しつつ、コストとパフォーマンスの最適解を導き出すことが、システム設計者に求められる役割です。単純な履歴の丸投げから脱却し、コンテキストを適切に制御するアーキテクチャを構築していく必要があります。
2. パターンA:バッファメモリ方式(ConversationBufferMemory)の実装と限界
開発初期によく使われる最もシンプルな手法が「バッファメモリ方式」です。LangChainではConversationBufferMemoryクラスで提供されています。
基本原理:プロンプトへの履歴全注入
会話の履歴をリストや文字列としてメモリ(RAMやRedisなど)に保存し、次のリクエスト時にそのすべてをプロンプトに展開します。情報の欠落が一切ないため、直近の会話に対する精度は最も高く、細かい参照も完璧にこなせます。
Pythonによる実装コード例
LangChainを使った実装イメージは以下の通りです。
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI
# LLMの初期化
llm = ChatOpenAI(temperature=0, model_name="ChatGPT")
# メモリの初期化(ここですべての履歴を保持する)
memory = ConversationBufferMemory()
# 会話チェーンの作成
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
# 会話の実行
conversation.predict(input="こんにちは、私はPythonが得意です。")
# AI: こんにちは!Pythonが得意なんですね。素晴らしいです。
conversation.predict(input="おすすめのライブラリはありますか?")
# AI: Pythonが得意なには、データ分析ならPandas、Web開発ならFastAPIなどがおすすめです。
裏側では、memoryオブジェクトが会話履歴を蓄積し、ConversationChainがそれを自動的にプロンプトに埋め込んでいます。
適用すべきユースケースと明確な限界点
パターンAは以下のケースに適しています。
- 短い対話: FAQ対応や、数ターンで完結するタスク指向のBot。
- 高精度が求められる場面: 前の文言を一言一句正確に参照する必要がある場合。
- PoC(概念実証)段階: まずは動くものを作って仮説検証したい場合。
しかし、会話が10ターン、20ターンと続くとプロンプト長がトークン上限に達し、InvalidRequestError(コンテキスト長超過エラー)が発生します。APIコストも線形に増加するため、本番環境への無計画な投入は危険です。リソースは有限であるため、次に「要約」というアプローチが登場します。
3. パターンB:要約メモリ方式(ConversationSummaryMemory)による圧縮
人間が過去の会話を要点(Summary)で記憶するように、AIにも要約を行わせるのがパターンBです。
LLM自身による中間要約の生成プロセス
ConversationSummaryMemoryは、会話の進行に伴いバックグラウンドでLLMを呼び出し、会話履歴を要約して圧縮します。
例えば、以下のやり取りを想定します。
User: 昨日はカレーを食べたんだ。すごく辛くて美味しかった。
AI: いいですね。どこのお店ですか?
User: 駅前の「スパイス天国」という店だよ。ナンが巨大で有名なんだ。
AI: ああ、あのお店ですね!私も知っています。
要約メモリはこれを次のように圧縮します。
Summary: ユーザーは昨日、駅前の「スパイス天国」で辛いカレーを食べた。その店は巨大なナンで有名である。
次のターンでは生の会話履歴の代わりにこのSummaryのみがプロンプトに含まれ、会話が長引いてもトークン消費量を一定範囲に抑えられます。
「情報の粒度」と「文脈維持」のトレードオフ
この手法にもトレードオフが存在します。
- 詳細情報の欠落: 要約の過程で細かいニュアンスや固有名詞が削ぎ落とされるリスクがあります。「巨大なナン」は残っても、「昨日」という時間軸や些細な感情表現が消える可能性があります。
- レイテンシとコストの二重負担: 要約作成のためにメインの回答生成とは別にLLMを呼び出す必要があります。1回のユーザー発言に対して裏側で2回(要約用と回答用)のAPIコールが発生し、応答速度が低下する可能性があります。
実装コード:要約チェーンの組み込み
from langchain.memory import ConversationSummaryMemory
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
# 要約用と回答用で同じLLMを使うことも、分けることも可能
llm = ChatOpenAI(temperature=0)
# 要約メモリの初期化
memory = ConversationSummaryMemory(llm=llm)
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
conversation.predict(input="進行中のプロジェクトの進捗はどう?")
# 裏側でこれまでの会話を要約し、それを踏まえて回答
このパターンは、長期間にわたるセッションや、大まかな文脈の維持で十分な雑談Botに適しています。一方、正確な数値やコードスニペットを扱うプログラミング支援Botには不向きです。
4. パターンC:ベクトル検索方式(VectorStoreRetrieverMemory)による長期記憶
さらに高度なアプローチとして、RAG(Retrieval-Augmented Generation)の技術を記憶管理に応用するパターンCを紹介します。これは「必要なことだけ思い出す」という、人間に最も近い記憶の仕組みです。
Embeddings(埋め込み表現)とベクトルDBの基礎
この方式では、会話の各ターンをベクトル(数値の配列)に変換してベクトルデータベースに保存します。これをEmbedding(埋め込み)と呼びます。ベクトル空間上では意味の近い言葉同士が近くに配置され、「Python」と「プログラミング」は近く、「Python」と「料理」は遠くなります。
セマンティック検索による「関連性」に基づいた記憶の呼び出し
ユーザーの発言時、システムは内容をベクトル化し、データベースから意味的に関連性の高い過去の会話(Top-k件)のみを検索(Retrieve)します。
例えば「以前話したLinuxのコマンド何だっけ?」という質問に対し、過去の膨大なログから「Linux」「コマンド」に関連するやり取りだけを抽出しプロンプトに注入します。これにより、数ヶ月前の会話でもトークン制限を気にせず「思い出す」ことが可能になります。これが長期記憶の実装です。
RAGアーキテクチャを応用した会話履歴管理の実装
LangChainでの実装例です。軽量なベクトルストアとしてFAISSを使用しますが、本番環境ではPineconeやWeaviateなどが推奨されます。
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.memory import VectorStoreRetrieverMemory
# 1. ベクトルストアの初期化
embeddings = OpenAIEmbeddings()
# 実際の運用では永続化されたDBを使用する
vectorstore = FAISS.from_texts(["初期知識"], embedding=embeddings)
retriever = vectorstore.as_retriever(search_kwargs=dict(k=2))
# 2. ベクトルメモリの作成
memory = VectorStoreRetrieverMemory(retriever=retriever)
# 3. 会話への適用
# メモリに保存する際、テキストをベクトル化してインデックスに追加する
memory.save_context(
{"input": "私の好きな果物はリンゴです"},
{"output": "承知しました。リンゴですね。"}
)
memory.save_context(
{"input": "私の好きな色は青です"},
{"output": "青色、素敵ですね。"}
)
# 4. 検索の実行(必要な情報だけが取得される)
print(memory.load_memory_variables({"prompt": "好きな果物は何だっけ?"}))
# 結果: {'history': 'User: 私の好きな果物はリンゴです\nAI: 承知しました。リンゴですね。'}
この方式の最大のメリットは、理論上無限の記憶容量を持てることです。過去数年分のログがあっても検索時間はわずかで、プロンプトに含めるのは検索ヒットした数件のみのため、トークン消費も最小限に抑えられます。
デメリットとしては、インフラの複雑化(ベクトルDBの管理)と、文脈の断絶(検索漏れにより直前の会話がつながらないリスク)が挙げられます。
5. 比較と選定:3つのアーキテクチャの評価マトリクス
3つのパターンについて、ビジネス要件とシステム要件に応じた判断基準となるマトリクスを用意しました。
コスト・精度・実装難易度の比較表
| 特徴 | パターンA:バッファ | パターンB:要約 | パターンC:ベクトル検索 |
|---|---|---|---|
| 実装難易度 | 低(初心者向き) | 中 | 高(インフラ構築が必要) |
| トークン消費 | 高(履歴量に比例) | 中(要約分のみ) | 低(抽出分のみ・一定) |
| 情報の精度 | 最高(完全保持) | 低(詳細欠落リスク) | 中(検索精度に依存) |
| 長期記憶 | 不可(上限あり) | 可能だが粗い | 最適(無限に近い) |
| レイテンシ | 入力増に伴い悪化 | 要約生成分遅延 | 検索オーバーヘッドあり |
ユースケース別推奨構成
- カスタマーサポート(短時間完結): パターンA(バッファ)
- ユーザーの問題解決が目的で会話が短いため、正確さを優先します。
- メンターBot・コーチングAI(長期間継続): パターンB(要約) + パターンC(ベクトル検索)のハイブリッド
- 直近の会話はバッファで保持し、古い会話は要約してベクトルDBに格納します。LangChainの
ConversationSummaryBufferMemoryなどをベースに拡張します。
- 直近の会話はバッファで保持し、古い会話は要約してベクトルDBに格納します。LangChainの
- 社内ナレッジ検索Bot: パターンC(ベクトル検索)
- 会話履歴と社内Wikiなどのドキュメントを同じベクトル空間に格納し、会話と知識をシームレスに扱います。
実運用におけるデータベース選定ガイド
パターンCを採用する場合のデータベース選定ガイドです。
- Redis (RediSearch): キャッシュとしてRedisを利用中なら、モジュール追加でベクトル検索が可能です。高速で低遅延です。
- Pinecone: フルマネージドなベクトルDBです。スケーラビリティが高く運用が容易ですが、コストがかかります。
- pgvector (PostgreSQL): リレーショナルデータと一元管理したい場合に最適です。既存のPostgres環境を活用できます。
6. 実践演習:ユーザーの好みを学習し続けるパーソナライズBotの構築
最後に、単なる「記憶」を超えて「学習」するAIエージェントのアイデアを共有します。会話ログをそのまま保存するのではなく、そこから「ユーザープロファイル」を抽出して更新し続けるアーキテクチャです。
単なる履歴保持を超えた「プロファイル学習」への応用
ユーザーが「最近、健康のために糖質制限をしていてね」と発言した場合、これを単なるログとして残さず、LLMを用いて構造化データに変換します。
- Entity Extraction (固有表現抽出)
DietaryPreference:LowCarbInterest:Health
このメタデータをKey-Valueストア(ユーザーDB)に保存し、次回のプロンプト生成時に「System Prompt」として注入します。
System Prompt: あなたは栄養士AIです。ユーザーは糖質制限中です(LowCarb)。これを考慮してレシピを提案してください。
サンプルコード:ユーザー情報の動的更新
# 概念的な擬似コード
def update_user_profile(user_id, conversation_text):
# LLMに分析させるプロンプト
extraction_prompt = f"""
以下の会話から、ユーザーの好みや属性を抽出してJSON形式で出力せよ。
会話: {conversation_text}
出力項目: [食べ物の好み, 趣味, 職業]
"""
# 抽出実行
profile_data = llm.predict(extraction_prompt)
# DB更新
db.users.update(user_id, profile_data)
# メインの処理フロー
current_profile = db.users.get(user_id)
system_prompt = f"あなたは親切なアシスタントです。ユーザー情報: {current_profile}"
response = llm.chat(system_prompt, user_input)
# バックグラウンドでプロファイルを更新
update_user_profile(user_id, user_input + response)
このように、「会話履歴(Flow)」と「ユーザー状態(State)」を分けて管理することで、トークンを無駄遣いせず高度なパーソナライズが可能になります。
まとめ:最適な記憶設計がビジネスを変える
AIエージェントにおける「記憶」の実装パターンを解説しました。
- バッファ方式: 短期決戦、高精度。
- 要約方式: 全体像の維持、トークン節約。
- ベクトル検索方式: 長期記憶、大規模ナレッジ活用。
これらに優劣はありません。重要なのは、開発するサービスの「ユーザー体験のゴール」と「ビジネス要件」に合わせて、これらを適切に選択し組み合わせることです。まずはプロトタイプを作り、実際の挙動を確かめながら最適なアーキテクチャを描き出してください。
コメント