AIチャットの会話履歴管理とトークン削減テクニック|スライディングウィンドウと要約圧縮
AI LLM トークン節約 Web開発 JavaScript
結論
AIチャットの会話履歴管理は 「直近のメッセージ配列(スライディングウィンドウ)」 と 「過去会話の要約テキスト」 を組み合わせて構成します。トークン消費を定数に抑え、応答速度を維持できます。
マルチターン(複数回の会話)に対応したAIチャットでは、やりとりが長くなるにつれて送信するリクエストのトークン数が雪だるま式に増加します。対策を行わないと、LLMのコンテキスト上限に達してエラーになるだけでなく、レスポンス速度の悪化やAPI従量課金コストの急増を引き起こします。
会話履歴管理手法の比較マトリクス
トークン消費量を抑えるための代表的な3つの手法と、それぞれのメリット・注意点の比較です。
| 管理手法 | 仕組み | トークン節約効果 | 文脈の保持精度 | 実装難易度 |
|---|---|---|---|---|
| 全件送信(対策なし) | 過去の全メッセージを毎回送信 | ❌ 悪化し続ける (O(N^2)) | 〇 完全保持 | ★☆☆ 低い |
| スライディングウィンドウ | 直近 N 件(例: 6件)のみ保持して送信 | 〇 定数に固定 (O(1)) | △ 直近以外の文脈は消失 | ★★☆ 中程度 |
| コンテキスト要約 | 古い履歴を LLM で要約文に圧縮して保持 | 〇 大幅削減 (要約分のみ) | 〇 要説を維持 | ★★★ 高い |
トークン削減の導入手順
会話履歴の増大を抑えるための実装ステップです。
- 受信した履歴配列から直近 N 件のメッセージをスライス抽出します。
- 抽出後の概算トークン数が上限値を超えていないか計算して切り詰めます。
- 要約テキストをシステムプロンプトのコンテキスト領域へ挿入して LLM へ送信します。
1. 直近メッセージのスライディング抽出
クライアントから送られてきた履歴配列から、最新のやりとりN件(例: 直近3往復=6件)を抽出します。
interface ChatMessage {
role: "user" | "assistant" | "system";
content: string;
}
export function truncateHistory(history: ChatMessage[], maxMessages = 6): ChatMessage[] {
// 直近 maxMessages 件のみを取得
if (history.length <= maxMessages) {
return history;
}
return history.slice(-maxMessages);
}
2. トークン数の上限チェックと切り詰め
文字数や概算トークン数を計算し、設定した最大トークン制限(例: 2000トークン)を超過している場合は古いメッセージから順に除去します。
// 日本語文字数による概算(1文字 ≒ 1.5〜2 トークン換算)
function estimateTokenCount(text: string): number {
return Math.ceil(text.length * 1.8);
}
export function fitHistoryWithinTokenLimit(
history: ChatMessage[],
maxTokens = 2000
): ChatMessage[] {
const result: ChatMessage[] = [];
let currentTokens = 0;
// 最新のメッセージから逆順に積算
for (let i = history.length - 1; i >= 0; i--) {
const msgTokens = estimateTokenCount(history[i].content);
if (currentTokens + msgTokens > maxTokens) {
break; // 上限を超えたら打ち切り
}
currentTokens += msgTokens;
result.unshift(history[i]); // 配列の先頭へ追加
}
return result;
}
3. 要約テキストのシステムプロンプト結合
やりとりが10往復を超えた段階で、過去の文脈を1〜2文の「要約文」として抽出し、システムプロンプトのコンテキスト領域へ動的に注入します。
// 送信メッセージ構造の組み立て例
const systemPrompt = `あなたはアシスタントです。
これまでの会話の要約:
${summaryText || "なし"}
上記の文脈を踏まえて回答してください。`;
const finalPayload = [
{ role: "system", content: systemPrompt },
...fitHistoryWithinTokenLimit(truncateHistory(rawHistory)),
];
現場で発生する失敗境界(Failure Boundary)
会話履歴の削減処理で発生しやすい境界問題と障害例です。
[障害ログ例]: Error: 400 Invalid argument: role sequence must alternate between 'user' and 'model'
[原因]: スライディングウィンドウで配列を切り取った結果、先頭が 'assistant' になり、LLM のロール交互ルールに違反した。
先頭メッセージのロール不整合
履歴配列を単純に .slice(-5) などの奇数で切り取ると、切り出された配列の先頭が assistant(AI側の返答)になり、APIのバリデーション(user から始まる交互メッセージ構造)に違反してエラーとなります。
- 対策: スライディング切り出し後の先頭要素が
assistantである場合は、その要素を除去して必ずuserメッセージから始まるように配列を調整します。
「それ」「さっきの話」の代名詞参照切れ
スライディングウィンドウで過去ログを切り捨てすぎると、ユーザーが「さっき言ってたやつ」「それ」と返した際に指し示す対象がコンテキストから消えており、AIがまったく関係のない回答をする現象が発生します。
- 対策: 最低でも直近2往復(4件)はスライディング領域に残し、重要な固有名詞はシステムプロンプト内の要約領域へ保持させます。