Buffer MemoryとWindow Memory
ConversationBufferMemoryとConversationBufferWindowMemoryを実装して直近Nターンをコンテキストに保持し、ウィンドウサイズが一貫性とコストに与える影響を測定します。
「Buffer MemoryとWindow Memory」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
バッファメモリ:すべてを保持する
バッファメモリは最も単純な戦略です。すべてのターンのすべてのメッセージをリストに保存し、APIを呼び出すたびに完全な履歴を含めます。完全なコンテキストが保持されるため、モデルはいつ発言された内容でも参照できます。一方で、上限がないままコンテキストが線形に増加するという欠点があります。1回のタスク完了のような短いセッションでは、バッファメモリは非常に適しており、実装も最も簡単です。
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
return_messages=True, # return Message objects, not a string
memory_key='history' # key to inject into prompt template
)
# Add messages manually
memory.chat_memory.add_user_message('What is a neural network?')
memory.chat_memory.add_ai_message('A neural network is a system of layers...')
# Load what will be injected
print(memory.load_memory_variables({}))チェーンへのバッファメモリの統合
LCELでバッファメモリを使用するには、RunnableWithMessageHistoryを経由させるか、従来の方法としてConversationChainを使用します。メモリオブジェクトが履歴を保持し、チェーンはプロンプトテンプレート内のMessagesPlaceholderを使って履歴を注入します。呼び出しのたびに、メモリが新しいターンを自動的に追加します。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chain = (
ChatPromptTemplate.from_messages([
('system', 'You are helpful.'),
MessagesPlaceholder('history'),
('human', '{input}')
])
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
with_memory = RunnableWithMessageHistory(
chain, get_history,
input_messages_key='input',
history_messages_key='history'
)無制限バッファの問題
バッファメモリは、会話が長くなってモデルのコンテキストウィンドウを超えるまで機能します。GPT-4o-miniの128Kコンテキストでは、一般的なチャットはあふれるまでに200~400ターン続くことがあります。より現実的には、50ターンでもリクエストごとに50K以上のトークンを送ることになり、大きなコストが発生します。そのため、履歴のサイズに上限を設ける戦略が必要です。
import tiktoken
enc = tiktoken.encoding_for_model('gpt-4o-mini')
def count_history_tokens(messages: list) -> int:
total = 0
for msg in messages:
total += len(enc.encode(msg.content))
total += 4 # per-message overhead
return total
# Check how big the history has grown
history = store.get('session-1')
if history:
token_count = count_history_tokens(history.messages)
print(f'History size: {len(history.messages)} messages, {token_count} tokens')ウィンドウメモリ:直近Nターンを保持する
ウィンドウメモリは、会話の直近Kターンだけを保持し、古いメッセージを破棄します。これにより、会話がどれだけ長くなっても、コンテキストをK * avg_tokens_per_turnに制限できます。その代わり、非常に初期のコンテキストは失われるため、モデルは何ターンも前にユーザーが話した内容を忘れる可能性があります。ほとんどの汎用チャットボットでは、5~10ターンのウィンドウにすると、許容できるコストで十分な一貫性を保てます。
from langchain.memory import ConversationBufferWindowMemory
# Keep last 5 turns (10 messages: 5 user + 5 assistant)
memory = ConversationBufferWindowMemory(
k=5, # number of TURNS to keep (each turn = user + AI)
return_messages=True,
memory_key='history'
)
# After 10 turns, only turns 6-10 will be in context
# Turns 1-5 are silently droppedInMemoryChatMessageHistoryによるウィンドウメモリの実装
LangChainのInMemoryChatMessageHistoryはすべてのメッセージを保持しますが、プロンプトに注入する前に履歴を切り詰めることができます。よくあるパターンは、ログ記録用に完全な履歴を保存しつつ、LLMには直近N件のメッセージだけを渡す方法です。history.messagesにPythonのスライス記法を使うと、最も新しいウィンドウを取得できます。
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables import RunnableLambda
WINDOW_SIZE = 10 # last 10 messages (5 turns)
def get_windowed_history(session_id: str):
full_history = store.get(session_id, InMemoryChatMessageHistory())
store[session_id] = full_history
return full_history
# In the chain, trim before injecting
def trim_history(messages):
return messages[-WINDOW_SIZE:] if len(messages) > WINDOW_SIZE else messages
# Use in prompt with trimming
from langchain_core.messages import trim_messages
trimmer = trim_messages(
max_tokens=2000,
strategy='last',
token_counter=ChatOpenAI(model='gpt-4o-mini'),
include_system=True
)トークン単位とターン単位のウィンドウ
ウィンドウサイズは、ターン(直近K個の人間とAIのペア)またはトークン(履歴の直近Tトークン)で指定できます。ターンの長さは異なるため、トークン単位のウィンドウの方が信頼性に優れています。コードを含むターンは、「はい」だけのターンより10倍長くなることがあります。LangChainのtrim_messages()ユーティリティは両方の戦略に対応しており、履歴を切り詰める際にシステムプロンプトを保持することもできます。
from langchain_core.messages import trim_messages, SystemMessage, HumanMessage, AIMessage
# Token-based trimming — keep last 1000 tokens of conversation
trimmer = trim_messages(
max_tokens=1000,
strategy='last', # keep most recent messages
token_counter=len, # approximate: count characters / 4
include_system=True, # always include the system prompt
allow_partial=False, # don't split a message in half
start_on='human' # start window on a human message
)
trimmed = trimmer.invoke(all_messages)
print(f'Trimmed to {len(trimmed)} messages')ウィンドウサイズと一貫性の測定
適切なウィンドウサイズを選ぶには、ウィンドウを小さくしたときに会話の一貫性がどのように低下するかを測定する必要があります。ターンNがターンN-5、N-10、N-20の情報を参照するテスト会話を複数用意します。ウィンドウが5、10、20ターンの場合でもモデルが正しく回答できるかをテストします。その結果から、用途に必要な最小ウィンドウサイズが分かります。
def test_reference_at_distance(chain_with_memory, distances=[5, 10, 20]):
results = {}
for distance in distances:
session_id = f'test-dist-{distance}'
# Fill with filler turns
for i in range(distance):
chain_with_memory.invoke(
{'input': f'Turn {i}: filler message'},
config={'configurable': {'session_id': session_id}}
)
# Ask about something said at the start
first_msg = 'Recall that the user said the magic word is AZURE.'
response = chain_with_memory.invoke(
{'input': 'What was the magic word?'},
config={'configurable': {'session_id': session_id}}
)
results[distance] = 'AZURE' in response.upper()
return resultsシステムプロンプトとウィンドウメモリの組み合わせ
ウィンドウメモリを使用するときは、必ずシステムプロンプトを保持してください。システムプロンプトはAIのペルソナとルールを定義します。システムプロンプトがないと、ウィンドウが最初のターンを通り過ぎた後にモデルがペルソナを失う可能性があります。トリミング関数でinclude_system=Trueオプションを使用するか、プロンプトテンプレート内でシステムメッセージをウィンドウ化された履歴の前に必ず挿入します。
# Always put system prompt BEFORE the windowed history
prompt = ChatPromptTemplate.from_messages([
('system', 'You are a Python tutor. Always explain with code examples.'),
MessagesPlaceholder('history'), # windowed history injected here
('human', '{input}'),
])
# The system prompt is never trimmed — only the history window is managed
# This ensures the model's persona is always present regardless of windowバッファメモリとウィンドウメモリの選択ガイド
バッファメモリは、会話が短く上限がある場合(1回限りのタスク、フォームウィザード)、完全なコンテキストが重要な場合(法的分析、コードレビュー)、トークン予算を気にしなくてよい場合に使用します。ウィンドウメモリは、会話が際限なく長くなり得る場合(カスタマーサポート、汎用アシスタント)、遠い過去より最近のコンテキストが重要な場合、トークンコストを予測可能な範囲に制限する必要がある場合に使用します。
# Decision matrix in code
def choose_memory_strategy(
expected_turns: int,
max_context_tokens: int = 128000,
avg_tokens_per_turn: int = 200
) -> str:
buffer_tokens = expected_turns * avg_tokens_per_turn
if buffer_tokens < max_context_tokens * 0.5:
return 'buffer' # Safe to keep everything
elif expected_turns <= 20:
return 'window_10' # Keep last 10 turns
else:
return 'summary' # Need summarization for long convos
print(choose_memory_strategy(5)) # 'buffer'
print(choose_memory_strategy(50)) # 'window_10'
print(choose_memory_strategy(200)) # 'summary'ウィンドウのRedisへの永続化
本番環境では、完全な会話履歴をRedis(高速な取得が可能)に保存し、読み取り時にウィンドウサイズまで切り詰めます。TTLを設定したRedisを使うと、古いセッションが自動的に期限切れになります。ソート済みセット、またはLRANGEを使うリストを使用すると、履歴全体を読み込まずに直近N件のメッセージだけを効率的に取得できます。
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_windowed_redis_history(session_id: str, window: int = 10):
# RedisChatMessageHistory stores all messages
history = RedisChatMessageHistory(
session_id=session_id,
url='redis://localhost:6379',
ttl=3600 # 1 hour TTL
)
# Trim to window size in-memory before use
all_msgs = history.messages
if len(all_msgs) > window * 2: # window turns = window*2 messages
history.messages = all_msgs[-(window * 2):]
return history本番環境でのメモリ健全性の監視
メモリ関連の問題を早期に検出するため、本番環境では次の指標を追跡します。セッションが大きくなりすぎていないかを検出するリクエストあたりの平均履歴トークン数、コンテキストウィンドウの使用率(80%超で警告)、セッションストアのメモリリークを検出するストア内のセッション数、Redisから再読み込みされたセッションのキャッシュヒット率です。平均履歴トークン数が設定可能なしきい値を超えたらアラートを発生させます。
import time
from langchain_core.callbacks import BaseCallbackHandler
class MemoryMonitorCallback(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
history = inputs.get('history', [])
token_estimate = sum(len(m.content.split()) * 1.3 for m in history)
if token_estimate > 50000:
print(f'WARNING: Large history {token_estimate:.0f} estimated tokens')
def on_chain_end(self, outputs, **kwargs):
# Log usage for dashboards
passクイックチェック
バッファメモリとウィンドウメモリの戦略を理解できているか確認しましょう。
レッスンのまとめ
このレッスンでは、次のことを学びました。バッファメモリは完全な履歴を保持しますが上限なく増加するため、短く上限のある会話にのみ適しています。ウィンドウメモリは直近Kターンだけを保持し、遠い過去のコンテキストを犠牲にする代わりに、コストを予測可能な範囲に制限できます。また、メッセージの長さは異なるため、trim_messages()によるトークン単位のトリミングは、ターン単位のウィンドウより信頼性に優れています。次は、終わりのない長い会話に適した要約メモリを見ていきます。
よくある質問
「Buffer MemoryとWindow Memory」レッスンは無料ですか?
はい。「Buffer MemoryとWindow Memory」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「Buffer MemoryとWindow Memory」で何を学びますか?
ConversationBufferMemoryとConversationBufferWindowMemoryを実装して直近Nターンをコンテキストに保持し、ウィンドウサイズが一貫性とコストに与える影響を測定します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「Buffer MemoryとWindow Memory」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- StatelessなLLMに外部メモリが必要な理由
- Buffer MemoryとWindow Memory
- Summary MemoryとTokenを考慮した切り詰め
- RedisとPostgreSQLへのチャット履歴の永続化