8. 会話の状態とメモリ
これまで構築してきたすべてのLLMインタラクションはステートレスでした。ステートレスとは、各リクエストが独立していることを意味します。つまり、モデルは以前の会話を記憶していません。これは、ドキュメントの要約や単一の質問に対するQ&Aのような一度限りのタスクには問題なく機能します。
しかし、会話型エージェントを構築するのは別の話です。エージェントが以前に議論された内容を思い出し、「それ」や「あれ」のような代名詞を理解し、対話全体を通してコンテキストを維持する必要があります。これを実現するには、状態(state)を明示的に管理する必要があります。
(ここで「状態」とは、プログラムが記憶する情報を指します。会話型エージェントの場合、以前の会話履歴が状態です。)
この章では、以下の内容を扱います:
- LLMが会話を「記憶」しない理由
- LangChainのメッセージ履歴ツールを使用して会話メモリを実装する方法
- コンテキストオーバーフローを防ぐためにトークン予算を管理する方法
8.1) LLMが忘れる理由
LLMにはメモリがない
LLMには重要な特性があります:以前の会話から何も記憶しません。
LLM APIを呼び出すと、モデルは入力を処理して応答を生成します。しかし、その記録をどこにも保存しません。状態を維持するモデル内部のメモリはなく、会話履歴もありません。各API呼び出しは完全に独立しています。毎回新しく始めるようなものです。
これは設計によるものです。LLMはステートレスな関数のように動作します:入力を提供すると出力を生成しますが、何も保持されません。現在OpenAIのサーバーで実行されているモデルには、あなたが今尋ねたことの記録がありません。
LLMが記憶しているように見える理由
しかし待ってください。ChatGPTやClaudeを使用すると、会話を記憶しているように感じます。「パリについて教えて」と言った後、「人口は?」とフォローアップすると、モデルはまだパリについて話していることを理解します。どのように機能するのでしょうか?
答えは:アプリケーションが各新しいメッセージと一緒に以前の会話履歴を送信しているからです。
実際に起こっていることは次のとおりです:
LLMは以前にパリについて尋ねられたことを「記憶」していません。アプリケーションが新しいメッセージと一緒に以前の会話履歴を送信したため、知っているだけです。最終的に、状態を管理するのはモデルではなくアプリケーションです。
「状態」はモデルではなくアプリケーションで管理する必要がある理由
LLMを純粋関数と考えてください:入力が与えられると、出力を生成します。提供するメッセージ以外に、LLMが管理する内部状態はありません。これは設計によるものです。
会話型エージェントの場合、これは状態をアプリケーションで管理する必要があることを意味します。
状態(会話履歴)は、モデルではなくアプリケーションコードに存在します。これはあなたが責任を持つことを意味します:
- 会話履歴を保存する
- 各新しいリクエストと一緒に関連する履歴を送信する
- 履歴のサイズを管理する(セクション8.3で説明)
セクション8.2では、LangChainのメッセージ履歴ツールを使用してこれを実装します。
状態を管理しないとどうなるか?
状態を管理しないと、エージェント(アプリケーション)は一貫した会話を維持できません。最も一般的な失敗は次のとおりです:
1. 以前の会話を記憶できない
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
# 最初の質問
response1 = llm.invoke([HumanMessage(content="私の名前はアリスです")])
print(response1.content) # 出力: はじめまして、アリスさん!
# 2番目の質問(履歴が送信されていない)
response2 = llm.invoke([HumanMessage(content="私の名前は何ですか?")])
print(response2.content) # 出力: お名前は分かりません...2回目の呼び出しでその情報を送信しなかったため、モデルはあなたの名前がアリスだと言ったことを知りません。
2. 代名詞が何を指すか分からない
# ユーザーがトピックについて尋ねる
response1 = llm.invoke([HumanMessage(content="Pythonについて教えて")])
print(response1.content)
# 出力: Pythonは読みやすさで知られる高水準プログラミング言語です...
# ユーザーが代名詞でフォローアップ
response2 = llm.invoke([HumanMessage(content="その主な機能は何ですか?")])
print(response2.content)
# 出力: 喜んでお手伝いします! 何について尋ねているか指定していただけますか?以前のメッセージがないため、モデルは「その」が何を指すか分かりません。
実際の影響:
状態管理なしでカスタマーサポートエージェントを構築することを想像してください:
ユーザー: "注文番号#12345に問題があります"
エージェント: "申し訳ございません。どのような問題でしょうか?"
ユーザー: "配送先住所が間違っています"
エージェント: "お手伝いできます。注文番号を教えていただけますか?"
ユーザー: "今言ったばかりですが..."この体験はユーザーをイライラさせ、信頼を失います。状態管理は会話型エージェントにとってオプションではありません。一貫性のある有用なインタラクションを作成するために不可欠です。
セクション8.2では、LangChainのメッセージ履歴ツールを使用して状態管理を実装し、第3章のCLIチャットに会話メモリを追加します。
8.2) 会話状態の管理
状態管理が重要である理由を理解したので、実装方法を学びましょう。LangChainの組み込みメッセージ履歴ツールを使用して会話履歴を管理します。最後に、学んだことを第3章のCLIチャットに適用して状態管理を追加します。
メッセージタイプの理解: HumanMessage、AIMessage、SystemMessage
会話状態の管理方法を学ぶ前に、会話状態で使用されるメッセージタイプを知る必要があります。LangChainは会話を表すために3つのメッセージタイプを使用します:HumanMessage(ユーザー入力)、AIMessage(モデル応答)、SystemMessage(指示)。各メッセージにはrole(メッセージタイプ)とcontent(実際のテキスト)があります。
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
# SystemMessage: モデルの動作に対する指示
system_msg = SystemMessage(content="あなたはPythonプログラミングを専門とする親切なアシスタントです。")
# HumanMessage: ユーザー入力
user_msg = HumanMessage(content="Pythonでファイルを読み込むにはどうすればいいですか?")
# AIMessage: モデルの応答
# (実際には、LangChainがモデルの応答をこのオブジェクトでラップします - ここでは説明のために示しています)
ai_msg = AIMessage(content="コンテキストマネージャーで `open()` 関数を使用できます...")なぜメッセージタイプを分けるのか?
モデルが会話履歴を効果的に理解するには、各メッセージの目的と誰が言ったかを知る必要があります。3つのメッセージタイプは異なる目的を果たします:
- SystemMessage: モデルがどのように動作すべきかを定義する指示(例:「簡潔に」、「あなたはPythonチューターです」)
- HumanMessage: ユーザーが言ったこと
- AIMessage: モデルが以前に応答したこと
この構造により、モデルは指示、ユーザーの質問、自身の過去の回答を区別できます。これは一貫した複数ターンの会話を維持するために不可欠です。
会話履歴を手動で構築する
3つのメッセージタイプを理解したので、会話履歴を手動で構築する方法を見てみましょう:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
llm = ChatOpenAI(model="gpt-4o-mini")
# 会話履歴を構築
# SystemMessageは指示を設定します(開始時に一度)
# その後: ユーザー入力 → AI応答 → ユーザー入力(自然な会話の流れ)
messages = [
SystemMessage(content="あなたは簡潔なPythonチューターです。"),
HumanMessage(content="リスト内包表記とは何ですか?"),
AIMessage(content="リスト内包表記はリストを作成する簡潔な方法です: [x*2 for x in range(5)]"),
HumanMessage(content="もっと複雑な例を見せてもらえますか?")
]
# 新しい質問と一緒に履歴全体を送信
response = llm.invoke(messages)
print(response.content)出力:
もちろんです! フィルタリングと変換を行うリスト内包表記です:
[x**2 for x in range(10) if x % 2 == 0]
これは次を生成します:
[0, 4, 16, 36, 64]完全な会話履歴を送信したため、モデルは「もっと複雑な例」がより複雑なリスト内包表記の例を指すことを理解します。
InMemoryChatMessageHistoryの使用
会話が増えるにつれて、メッセージリストを手動で構築するのは面倒になります。LangChainのInMemoryChatMessageHistoryは、メッセージを追加して完全な履歴を取得するメソッドを提供することで、これを簡素化します。
主要なメソッド:
add_message(message): 単一のメッセージを追加します(HumanMessage、AIMessage、SystemMessage)add_messages(messages): 複数のメッセージを一度に追加しますmessages: 完全なメッセージリストを返すプロパティclear(): すべてのメッセージを削除します(新しく始めるのに便利)
例:
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
# メッセージ履歴ストアを作成
history = InMemoryChatMessageHistory()
# 複数のメッセージを一度に追加
history.add_messages([
SystemMessage(content="あなたは親切なPythonチューターです。"),
HumanMessage(content="Pythonのデコレータとは何ですか?")
])
# メッセージを1つずつ追加
history.add_message(AIMessage(content="デコレータは別の関数の動作を変更する関数です..."))
history.add_message(HumanMessage(content="例を見せてもらえますか?"))
# すべてのメッセージを取得
messages = history.messages実践例: InMemoryChatMessageHistoryを使用した状態管理:
from langchain_openai import ChatOpenAI
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import SystemMessage, HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
history = InMemoryChatMessageHistory()
# システムメッセージを設定して履歴に追加(開始時に一度実行)
system_msg = SystemMessage(content="あなたは親切なPythonチューターです。")
history.add_message(system_msg)
# チャット関数
def chat(user_input):
"""ユーザー入力を処理し、LLMに送信し、会話履歴を自動的に管理します"""
# ユーザー入力を履歴に追加
history.add_message(HumanMessage(content=user_input))
# 以前の会話履歴と一緒にユーザー入力を送信
response = llm.invoke(history.messages)
# モデル応答を履歴に追加
history.add_message(response)
return response.content
# 会話をシミュレート
print(chat("ラムダ関数とは何ですか?"))
print(chat("例を見せてください")) # モデルはコンテキストを記憶
print(chat("通常の関数との違いは何ですか?")) # まだ記憶している出力:
ラムダ関数はlambdaキーワードで定義される無名関数です...
例を示します: square = lambda x: x**2
次のように使用できます: square(5) # 25を返します
ラムダ関数は単一の式に制限されていますが、通常の関数は...chat()関数は状態管理を自動的に処理します:各ユーザーリクエストを以前の会話履歴と一緒にLLMに送信し、リクエストと応答の両方を履歴に追加します。単にchat()を呼び出すだけで、追加の管理なしに会話状態を維持します。
第3章のリファクタリング: CLIチャットにメモリを追加
第3章のストリーミングCLIチャットを取り上げて、会話メモリを追加しましょう。元のステートレスバージョンは次のとおりです:
# chapter3_cli.py (元のバージョン - ステートレス)
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
def chat_loop():
print("チャットを開始しました。終了するには 'quit' と入力してください。\n")
while True:
user_input = input("あなた: ")
if user_input.lower() == "quit":
break
# ステートレス - 履歴なし
response = llm.stream([HumanMessage(content=user_input)])
print("AI: ", end="", flush=True)
for chunk in response:
print(chunk.content, end="", flush=True)
print("\n")
if __name__ == "__main__":
chat_loop()メモリ付きのリファクタリング版:
# chapter8_cli.py (メモリ付き)
from langchain_openai import ChatOpenAI
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
llm = ChatOpenAI(model="gpt-4o-mini")
def chat_loop():
print("チャットを開始しました。終了するには 'quit' と入力してください。\n")
# 履歴を作成してシステムメッセージを追加
history = InMemoryChatMessageHistory()
history.add_message(SystemMessage(content="あなたは親切なアシスタントです。"))
while True:
user_input = input("あなた: ")
if user_input.lower() == "quit":
break
# ユーザーメッセージを履歴に追加
history.add_message(HumanMessage(content=user_input))
# 応答をストリーミング
print("AI: ", end="", flush=True)
full_response = ""
for chunk in llm.stream(history.messages):
print(chunk.content, end="", flush=True)
full_response += chunk.content
print("\n")
# AI応答を履歴に追加
history.add_message(AIMessage(content=full_response))
if __name__ == "__main__":
chat_loop()変更点:
- 履歴ストレージを追加:
chat_loop()内でInMemoryChatMessageHistory()インスタンスを作成 - システムメッセージ: 開始時に一度履歴に追加
- 履歴と一緒にユーザー入力を送信:
llm.stream(history.messages)は以前の会話を含む - 会話を追跡: ユーザー入力とLLM応答の両方を履歴に追加
リファクタリングされたチャットのテスト:
チャットを開始しました。終了するには 'quit' と入力してください。
あなた: 私の名前はアリスです
AI: はじめまして、アリスさん! 今日はどのようにお手伝いできますか?
あなた: 私の名前は何ですか?
AI: あなたの名前はアリスです。
あなた: 今何を聞きましたか?
AI: あなたの名前が何かと聞かれました。
あなた: quitモデルは会話全体を通してコンテキストを維持するようになりました。あなたの名前、以前の質問を記憶し、対話の以前の部分を参照できます。
永続ストレージ: インメモリオプションを超えて
InMemoryChatMessageHistoryはローカル開発には便利ですが、本番環境に移行するには、永続性を保証するストレージソリューションに置き換える必要があります。
InMemoryの技術的制限:
- 揮発性RAM: サーバープロセスが終了または再起動すると、メモリに保存されているすべての会話履歴が即座に削除されます。更新やエラー回復により、ユーザーコンテキストが完全に失われます。
- 水平スケーリング不可: サービスが複数のサーバーインスタンスにスケールすると、各サーバーは独自の分離されたメモリを維持します。異なるサーバーに接続するユーザーは会話履歴を共有できません。
- リソースの非効率性: すべての会話履歴をRAMに保存することはメモリ集約的であり、同時ユーザーが増加するとシステムの安定性を脅かします。
プロフェッショナルな代替案:
PostgresChatMessageHistory(推奨): 最も堅牢で広く採用されている選択肢。PostgreSQLを永続ストレージに使用し、複雑なクエリとデータ分析に優れています。SQLChatMessageHistory: MySQLなどの既存のSQLデータベースを活用します。変更なしで現在のインフラストラクチャを使用できます。RedisChatMessageHistory: 応答速度が重要なサービスに最適です。永続化オプション付きのメモリベースで、高トラフィック処理に特化しています。
「ストレージは変わっても、コードは同じまま」
LangChainはすべてのストレージバックエンドで統一されたインターフェースを提供します。InMemoryChatMessageHistoryで使用したadd_message()やadd_messages()などの同じメソッドは、他のストレージオプションでも同じように機能します。これは、ストレージを切り替える際にビジネスロジック(会話処理コード)を変更する必要がないことを意味します。
# [開発] ローカルインメモリ
# from langchain_core.chat_history import InMemoryChatMessageHistory
# history = InMemoryChatMessageHistory()
# [本番] PostgreSQL永続ストレージ
import psycopg
from langchain_postgres import PostgresChatMessageHistory
# PostgreSQL接続を作成
sync_connection = psycopg.connect(
"postgresql://user:password@10.1.1.100:5432/agent_db",
autocommit=True,
)
# DB接続とセッションIDを指定
# session_id: 会話を識別する一意のキー
# - ユーザーごとの管理: session_id = user_id (ユーザーごとに1つの会話)
# - セッションごとの管理: session_id = uuid (会話ごとに新しいID)
history = PostgresChatMessageHistory(
table_name="chat_history",
session_id="user_123",
sync_connection=sync_connection
)
# --- ストレージタイプに関係なく同じインターフェース ---
history.add_message(HumanMessage(content="以前の注文を見せてください。"))
print(history.messages)重要: このガイドは迅速な進行のためにInMemoryを使用していますが、本番環境のデプロイではPostgresChatMessageHistoryのような永続ストレージに切り替える必要があります。
8.3) 会話の長さの管理
前のセクションで実装した会話メモリ機能には重要な問題があります:履歴にメッセージを追加するだけです。これは履歴が増え続けることを意味し、2つの主要な問題を引き起こします:
- コスト: 履歴全体がすべてのリクエストと一緒に送信されるため、履歴が増えるにつれてリクエストごとのコストが増加し続けます
- コンテキストウィンドウの制限: モデルには単一のリクエストで処理できる最大入力サイズがあります(例:GPT-5の場合400Kトークン)。会話履歴がこの制限を超えると、モデルはリクエストを適切に処理できません。
最も単純な解決策の1つはスライディングウィンドウパターンです。
スライディングウィンドウパターン(最新のNメッセージのみを保持)
スライディングウィンドウパターンは、会話履歴の最新のNメッセージのみを保持することで、上記の2つの問題を解決します。古いメッセージを破棄することで履歴サイズを管理し、次の利点を提供します:
- コスト管理: 会話の長さに関係なく履歴を一定のサイズ以下に保つことで、リクエストごとのコストが無限に増加するのを防ぎます
- オーバーフローなし: 入力サイズをモデルの最大制限内に保ちます
概念図:
ウィンドウサイズが4の場合、最新の4つのメッセージ(3、4、5、6)のみを保持し、古いメッセージ(1、2)を破棄します。新しいメッセージ(7)が到着すると、ウィンドウは最新のメッセージに向かって移動し、最も古いメッセージ(3)を削除してメッセージ4、5、6、7を保持します。
スライディングウィンドウのトレードオフ:
- 長所: 履歴サイズを制限してコストを一定に保ち、コンテキストウィンドウのオーバーフローを防ぎます
- 短所: ウィンドウサイズを超えたメッセージは破棄されるため、モデルはそれらを参照できません
このトレードオフは問題になる可能性があります。解決策は、最近の会話にスライディングウィンドウを使用しながら、必要に応じて別のストレージから必要な過去の情報を取得することです。これはRAG(Retrieval-Augmented Generation)を使用して実装でき、第9章で説明します。
ウィンドウサイズの単位: メッセージ数 vs トークン数
上の図はメッセージ数でウィンドウサイズを設定する例を示しています。しかし、本番環境では、メッセージサイズが異なるため、トークンベースのウィンドウサイズ設定がより一般的に使用されます:
メッセージ数ベースのトリミング:
メッセージ数でウィンドウサイズを制限します(例:最新の20メッセージのみを保持)。
- 特性: 固定メッセージ数ですが、総トークン数は依然として変動する可能性があります
- 使用時: メッセージサイズが制御されている場合(SMS、文字数制限のあるチャット)
- リスク: 1つの長いメッセージでもコンテキストウィンドウを超える可能性があります
トークン数ベースのトリミング(本番環境推奨):
LLMが処理する入力単位であるトークン数でウィンドウサイズを制限します(例:最新の5,000トークンのみを保持)。
- 特性: メッセージの長さに関係なくコンテキストウィンドウを超えません
- 使用時: メッセージサイズが変動する場合
トークンベースのトリミングの実装: trim_messages()
LangChainは、トークンベースのスライディングウィンドウパターンを実装するtrim_messages()ユーティリティを提供します。
trim_messages()の動作:
この関数は完全なメッセージリストと最大トークン数を受け取り、トークン制限内に収まる最新のメッセージのみを返します。
主要なパラメータ:
messages: トリミングするメッセージリストmax_tokens: 維持する最大トークン数token_counter: 各メッセージのトークン数を計算する関数(モデルのトークナイザーを使用してメッセージのトークン数を返します)include_system: SystemMessageを常に保持するかどうか(通常True)
トークンカウンターの設定:
import tiktoken
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_core.messages.utils import trim_messages
# モデルのトークナイザーを取得(異なるモデルファミリーは異なるトークナイザーを使用)
# 適切なトークナイザーを取得するためにモデル名を提供
# Claudeモデル: Anthropicのトークナイザーを使用(tiktokenはOpenAI専用)
enc = tiktoken.encoding_for_model("gpt-4o")
def token_counter(msg: BaseMessage) -> int:
"""単一のメッセージのトークンをカウントします。"""
return len(enc.encode(msg.content or ""))
# 会話履歴を作成
messages = [
SystemMessage(content="あなたは親切なアシスタントです。"),
HumanMessage(content="こんにちは!"),
AIMessage(content="こんにちは! どのようにお手伝いできますか?"),
HumanMessage(content="2+2は何ですか?"),
AIMessage(content="2+2は4です。"),
HumanMessage(content="3+3は何ですか?"),
AIMessage(content="3+3は6です。"),
HumanMessage(content="4+4は何ですか?"),
]
# 最大トークン数内のメッセージのみを保持
trimmed = trim_messages(
messages,
max_tokens=30,
token_counter=token_counter,
include_system=True,
)
print(f"元のメッセージ数: {len(messages)}")
print(f"トリミング後のメッセージ数: {len(trimmed)}")
for m in trimmed:
print(f"{type(m).__name__}: {m.content}")注意: 保持されるメッセージ数は、max_tokensの値と各メッセージの実際のトークン数に依存します。上記の例では、max_tokens=30はテスト目的で選択された非常に小さい値です。本番環境では、平均メッセージサイズと希望する会話範囲を考慮して適切な値を設定する必要があります。
例: チャット関数へのトリミングの適用
import tiktoken
from langchain_openai import ChatOpenAI
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage
from langchain_core.messages.utils import trim_messages
llm = ChatOpenAI(model="gpt-4o-mini")
history = InMemoryChatMessageHistory()
enc = tiktoken.encoding_for_model("gpt-4o-mini")
def token_counter(msg: BaseMessage) -> int:
"""単一のメッセージのトークンをカウントします。"""
return len(enc.encode(msg.content or ""))
def chat_with_trimming(user_input: str, max_tokens: int = 1000) -> str:
"""自動履歴トリミング付きのチャット。"""
history.add_message(HumanMessage(content=user_input))
system_msg = SystemMessage(content="あなたは親切なアシスタントです。")
all_messages = [system_msg] + history.messages
# 最大トークン数にトリミング
trimmed_messages = trim_messages(
all_messages,
max_tokens=max_tokens,
token_counter=token_counter,
include_system=True,
)
response = llm.invoke(trimmed_messages)
history.add_message(response)
return response.content
# 使用例
print(chat_with_trimming("日本への旅行を計画しています"))
print(chat_with_trimming("東京で何を訪れるべきですか?"))
print(chat_with_trimming("何日間滞在すべきですか?"))
# 履歴が増えても、最大トークン数内の最近の会話のみがLLMに送信されます次のステップ: 会話状態管理の制限と解決策(第9章: RAG)
会話履歴をモデルに提供することは、会話コンテキストを維持するのに役立ちます。しかし、これだけでは不十分な場合があります。例えば:
- 会社の文書やマニュアルで情報を見つける必要がある場合
- スライディングウィンドウから押し出された古い会話履歴を参照する必要がある場合
ここでRAG(Retrieval-Augmented Generation)が必要になります。RAGは次のように機能します:
- ストレージ: セマンティック検索のためにベクトルデータベースに情報を保存
- 検索: 探しているものと類似した意味を持つ情報をクエリ
スライディングウィンドウパターンの制限を補完するためにRAGを使用する場合:
- スライディングウィンドウ: 最新の20メッセージを保持(最近のコンテキスト)
- RAG: ウィンドウから押し出されたメッセージから関連コンテンツを検索して取得
単に最近の会話を記憶するだけでなく、RAGを使用すると長期記憶システムを作成できます。
RAGにより、エージェントは外部知識の活用と過去の会話の取得を通じて、より広範な知識とより長いコンテキストを活用できます。
第9章では、RAGの実装方法を詳しく説明します。
章のまとめ:
この章では、次のことを学びました:
- LLMが忘れる理由: モデルはステートレスです。メモリは会話履歴を再送信することで作られる錯覚です
- メッセージタイプ: SystemMessage(指示)、HumanMessage(ユーザー入力)、AIMessage(モデル応答)
- 状態管理:
InMemoryChatMessageHistoryを使用した会話履歴の管理 - 会話の長さ管理: 無制限の履歴がコストとコンテキストウィンドウの問題を引き起こす理由
- スライディングウィンドウ: 履歴が無限に増加するのを防ぐために最近のメッセージのみを保持するパターン
- トークンベースのトリミング:
trim_messages()とトークンカウントを使用したスライディングウィンドウの実装