LLM Frontline
開発・エージェント

LLMの会話履歴をどう設計するか|全量・直近N・要約・検索の7方式と壊れやすい境界

ミナト・開発・API担当
・ 約33分で読めます
LLMの会話履歴をどう設計するか|全量・直近N・要約・検索の7方式と壊れやすい境界

チャットやエージェントを本番に出すと、だいたい同じ順番で問題が出ます。最初は品質、次にレイテンシ、そして「会話が長くなると急におかしくなる」という報告です。5往復目までは正確だったのに、15往復目には最初に伝えた制約を無視する。あるいは、料金だけが会話の長さに比例して膨らんでいく。この記事では、会話履歴(コンテキスト・メモリ)の設計を、7つの方式の比較と、実装で実際に壊れる境界の話として整理します。判断の根拠になる部分は各社の公式ドキュメントと論文を確認しながら書いています。

症状:長い会話で何が起きているのか

現場で報告される「長い会話だと変になる」は、少なくとも4種類の別の現象が混ざっています。

1つ目は、指示の消失です。会話の冒頭で与えた制約(出力形式、禁止事項、対象範囲)が、後半で無視されます。履歴を要約で圧縮している場合、要約器が「制約」を重要な情報だと判断せず落としているケースが典型です。

2つ目は、コストとレイテンシの膨張です。ステートレスなAPIでは、毎ターン過去の全履歴を送り直します。Anthropicのコンテキストウィンドウのドキュメントは、リクエストに含まれるものはすべてコンテキストウィンドウに数えられるとしています。システムプロンプト、messages内の全メッセージ(ツール結果・画像・ドキュメントを含む)、そしてツール定義です。加えて、そのターンでモデルが生成した出力も、拡張思考を含めて次のターンの入力になります。つまり会話が続くほど入力トークンは単調増加します。

3つ目は、途中で会話が破綻して回復しないパターンです。序盤に立てた誤った前提を後半まで引きずり、訂正しても戻らない。これは後述する論文が正面から扱っています。

4つ目は、上限への衝突です。Anthropicのドキュメントによれば、入力だけでコンテキストウィンドウを超える場合は400 invalid_request_error(prompt is too long)が返ります。Claude 4.5以降のモデルでは、入力トークンとmax_tokensの合計が超える場合はリクエスト自体は受理され、生成中に上限に達するとstop_reason: "model_context_window_exceeded"で停止します。以前のモデルではバリデーションエラーになりますが、model-context-window-exceeded-2025-08-26のベータヘッダを付けると同じ挙動にオプトインできる、とも書かれています。

「短い会話では完璧なのに、長くなると品質が落ちる」という報告は、モデルの問題として扱われがちです。ただ実際には、こちら側が履歴に何を積み、何を落としているかの設計が原因であることのほうが多いというのが実感です。
LLMチャットを運用するエンジニア

なぜ起きるのか:容量の問題ではない

まず押さえておきたいのは、コンテキストウィンドウが広ければ解決するわけではない、という点です。

Anthropicのコンテキストウィンドウのドキュメントは、より大きなコンテキストウィンドウはより複雑で長いプロンプトを扱えるようにするが、コンテキストは多ければ自動的に良いというものではない、と明記しています。そして、トークン数が増えるにつれて正確さと想起(accuracy and recall)が劣化する現象をcontext rotと呼び、「コンテキストに何を入れるかを吟味することは、どれだけの容量が使えるかと同じくらい重要になる」としています。

Anthropicのエンジニアリングブログ「Effective context engineering for AI agents」は、これをattention budget(注意の予算)という言い方で説明しています。同記事はコンテキストエンジニアリングを「LLMの推論中に、最適なトークン(情報)の集合をキュレーションし維持するための戦略の集合」と定義したうえで、Transformerがトークン間の総当たりの関係を計算する構造上、コンテキストは有限の資源として扱うべきで、限界効用は逓減すると述べています。

Googleの長文脈のドキュメントも同じ方向の注意を書いています。Geminiモデルは各種のneedle-in-a-haystack(干し草の中の針)型の検索評価で高い性能を出すとしつつ、複数の情報を同時に取り出す必要がある場合には性能が落ちうるとし、性能は文脈次第で大きく変動しうると述べています。また、実務的な助言として、クエリや質問はプロンプトの末尾(他のすべてのコンテキストの後)に置いたほうが性能が良くなる、としています。

マルチターン特有の劣化:能力ではなく「ばらつき」

もう1つ、容量とは別の要因があります。情報が複数ターンに分散していること自体が難しい、という問題です。

Laban, Hayashi, Zhou, Nevilleによる論文「LLMs Get Lost In Multi-Turn Conversation」(arXiv:2505.06120)は、単一ターンとマルチターンを大規模シミュレーションで比較しています。同論文の要旨は、テストしたトップクラスのオープン/クローズドのLLMがいずれもマルチターンで単一ターンより有意に低い性能を示し、6つの生成タスクで平均39%の低下があったと報告しています。

興味深いのはその内訳です。同論文は20万件を超えるシミュレーション会話の分析から、性能低下を「aptitude(能力)のわずかな低下」と「unreliability(不安定さ)の大幅な増加」の2成分に分解しています。論文中の図1では、マルチターン設定について能力が-15%、不安定さが+112%(2倍以上)と示されています。ここでの能力は最良ケースのシミュレーション結果、不安定さは最良ケースと最悪ケースの差として定義されています。なお本文側の記述では、能力の低下はFullからShardedへの平均で16%と有意でない水準にとどまる一方、不安定さは平均112%増加した、とされています。検証対象は8つのモデルファミリから選んだ15のLLMです。

原因についても踏み込んでいます。同論文は、LLMは早いターンで仮定を置き、最終的な解を早すぎるタイミングで出そうとし、それに過度に依存する傾向があるとしています。要旨の言葉を借りれば「会話で誤った方向に進むと、道に迷って戻ってこない」という現象です。

メモ

同論文は緩和策の検証も行っています。各ターンでそれまでの要件をすべて言い直すSnowball設定は、単一ターン(Full)からマルチターン(Sharded)への性能低下を15〜20%緩和できたとされます。ただしSnowballの性能はFullやConcatには届かない、とも書かれています。一方で、生成温度を下げる対処はマルチターンでは有効でないと報告されています。全部を言い直しても完全には戻らない、という点が設計上は重要です。

7つの方式を比較する

以上を踏まえて、履歴の扱い方を7つに分けて整理します。実務ではこれらを組み合わせますが、単独の性質を把握しておくと組み合わせの判断がしやすくなります。

方式やること向く場面壊れる場面コスト・レイテンシ
(a) 全量そのまま履歴を一切加工せず全部送る数往復で終わる問い合わせ、監査上の再現性が最優先長い会話、ツール結果が多いエージェント入力が単調増加。キャッシュは最も効かせやすい
(b) 直近Nターン直近N往復だけ残す前後関係が近接している雑談的な用途冒頭の制約や設定が消える。ツール呼び出しと結果の対が割れる上限が読める。切り落とし位置が動くとキャッシュが崩れる
(c) トークン予算で切り詰め予算に収まるまで古い順に落とす予算を厳密に守る必要がある本番(b)と同じ。境界がターン途中に来ると壊れやすい上限が読める。予算計算のオーバーヘッドは小さい
(d) 古い部分を要約古い履歴を要約文に置換長時間の対話、エージェントの長期セッション要約器が制約や数値を落とす。要約の要約で情報が痩せる要約のための追加呼び出しが発生。以後の入力は縮む
(e) 要約+直近全文古い部分は要約、直近数ターンは原文実務での標準形として扱われることが多い要約と原文の境界付近で矛盾が起きる(d)と同様。品質とコストのバランスは取りやすい
(f) ベクトル検索で引く過去ターンを検索して必要な分だけ戻す参照が散発的で、履歴が非常に長い場合検索が外れると「知っているのに答えない」。時系列の前後関係が壊れる検索の追加レイテンシ。入力は小さく保てる
(g) 構造化した状態決定事項・制約をJSON等で別管理予約・見積・フォーム入力など状態が明確な業務状態のスキーマから漏れた情報は永久に失われる入力が安定する。実装コストは最も高い

各方式の補足

(a)全量そのままは、実は最初の選択肢として悪くありません。数往復で完結する問い合わせなら、加工しないほうが安全で、しかもプロンプトキャッシュの恩恵を最大化できます。Anthropicのプロンプトキャッシュのドキュメントは、自動キャッシュを使うと会話が伸びるにつれてキャッシュ位置も自動的に前進し、各リクエストで最後のキャッシュ可能ブロックまでがキャッシュに書かれ、それ以前は読み出しになる、という逐次キャッシュの動作を説明しています。履歴を加工しない限り、この形が素直に効きます。

(b)直近Nターンと(c)トークン予算での切り詰めは、実装が最も簡単な代わりに、いちばん静かに壊れます。LangChainの短期メモリのドキュメントは、メッセージを削除する際は結果として残るメッセージ履歴が妥当であることを必ず確認するよう明記し、ほとんどのプロバイダはツール呼び出しを含むassistantメッセージの後に対応するtool結果メッセージが続くことを要求する、と注意しています。機械的に古い順に落とすと、この対が割れます。

(d)(e)の要約は、実務で広く使われる形です。プロバイダ側の機能としても提供され始めています。Anthropicのcompaction(ベータ、ベータヘッダcompact-2026-01-12)は、コンテキストウィンドウの上限に近づくとサーバ側で古い文脈を要約する機能で、ドキュメントによればClaude 4.6以降のモデル向けにベータ提供されています。既定のトリガ閾値は入力15万トークン(設定可能な最小値は5万トークン)です。仕組みとしては、要約を含むcompactionブロックが応答に返り、以降のリクエストではそのブロックより前のコンテンツブロックがAPI側で自動的に落とされます。ドキュメントは、compactionブロックを次のリクエストに渡し返す必要があると明記しています。応答のテキストだけを取り出して積むと、この状態が黙って失われます。

(f)ベクトル検索は、履歴が非常に長く、参照が散発的な場合に効きます。ただし会話履歴を検索対象にすると、文書検索とは違う難しさが出ます。「さっき言ったやつ」のような指示語が多く、単独のターンだけを取り出すと意味が確定しないためです。ターン単位ではなく話題の区切り単位でまとめる、直近数ターンは検索を通さず必ず原文で入れる、といった補強が要ります。

(g)構造化した状態は、実装コストが最も高い代わりに、いちばん壊れにくい形です。Anthropicのcontext engineeringの記事も、長時間タスクの手法としてcompaction・structured note-taking(構造化されたノート取り)・sub-agentアーキテクチャの3つを挙げており、2つ目がこれに当たります。同記事は、エージェントがコンテキストウィンドウの外に永続化するノートを定期的に書き、後で読み戻すことで、稼働中のコンテキストを消費せずに長時間タスクの記憶を保てる、と説明しています。

あわせて読みたい

コンテキストエンジニアリングとは|プロンプトの次に来る設計の基本

選び方の判断軸

方式そのものより、選ぶための軸を先に決めるほうが実務では速く進みます。4つの質問で絞れます。

  1. 1

    会話の典型的な長さはどれくらいか

    実測します。p50が3往復で、p95が8往復なら、そもそも凝った圧縮は不要です。p95が40往復を超えるなら、要約か構造化状態が要ります。「長くなるかもしれない」ではなく分布を見ます。
  2. 2

    後半で参照する情報は、前半のどこにあるか

    冒頭の制約だけを参照するのか、任意のターンを参照するのか。前者なら(e)ハイブリッドで足ります。後者なら(f)検索か(g)構造化状態が要ります。
  3. 3

    ツール結果が履歴を占める割合はどれくらいか

    エージェントでは、人間の発話よりツール結果のほうがはるかに大きいのが普通です。ここが支配的なら、要約ではなくツール結果のクリアが先です。
  4. 4

    間違えたときに誰がどう気づくか

    履歴の圧縮は、失敗しても例外を投げません。品質が静かに落ちるだけです。気づく仕組み(後述の回帰セットと監視)を作れないなら、圧縮の導入自体を遅らせるほうが安全です。

実務上の出発点としては、(a)全量から始めて、実測で困ってから(e)要約+直近全文に移り、エージェントならツール結果の扱いを別建てにする順序を勧めます。最初から(f)や(g)を作り込むと、たいてい過剰設計になります。

壊れやすい境界

ここからがこの記事の本題です。会話履歴の設計は、動かないバグではなく「静かに品質が落ちるバグ」を生みます。実際に踏みやすい境界を6つ挙げます。

1. 要約でシステム指示や制約が消える

要約器に「これまでの会話をまとめて」と投げると、要約器は会話の内容を要約します。ユーザーが途中で述べた制約(「金額は税抜きで」「社外秘の情報は出さないで」)は、流れとしては些末に見えるため落ちやすい。

対処は、要約の対象と保護対象を分けることです。システムプロンプトは要約に含めず常に原文で先頭に置き、ユーザーが述べた制約は要約とは別に「制約リスト」として構造化して保持します。つまり(e)と(g)の併用です。要約プロンプト側でも、決定事項・未解決事項・守るべき制約を項目立てで出させると落ちにくくなります。

2. ツール結果が履歴を肥大させる

エージェントでは、ファイル内容や検索結果がそのまま履歴に積まれます。Anthropicのcontext editingは、この問題に特化した機能です。clear_tool_uses_20250919ストラテジは、ドキュメントの表現では「Claudeが処理し終えたあとの古いツール結果(ファイル内容や検索結果など)はもう必要ない」という前提に立ち、古いものから順にツール結果をクリアし、削除したことを示すプレースホルダのテキストに置き換えます。

パラメータの既定値は、公式ドキュメントによればtriggerが入力10万トークン(tool_usesでの指定も可)、keepが直近3組のツール呼び出しと結果、clear_tool_inputsはfalse(既定ではツール結果だけをクリアし、Claude自身のツール呼び出しは残す)です。加えて、消してはいけないツールをexclude_toolsで除外できます。

自前で実装する場合も考え方は同じです。ツール結果は本文と同じ重みで扱わず、別のライフサイクルを持たせます。

3. 途中を削るとプロンプトキャッシュのプレフィックスが崩れる

これがコストとレイテンシに直撃する境界です。

Anthropicのプロンプトキャッシュのドキュメントは、キャッシュのプレフィックスがtools->system->messagesの順に作られるとし、キャッシュのブレークポイント以前の内容を変更すると、そのキャッシュエントリと以降のすべてが無効になると説明しています。ブレークポイントまでのプレフィックス全体の累積ハッシュを計算しているため、どこか1文字でも変われば別のハッシュになる、という仕組みです。過去のユーザーメッセージやアシスタントメッセージを編集した場合も同様に、以降のキャッシュが無効になります。

OpenAIのプロンプトキャッシュのドキュメントも同じ性質を書いています。利用可能なブレークポイントを後ろ向きにたどって最長の一致プレフィックスを探す仕組みであり、ブレークポイント以前で内容や関連設定が変わると、その変更以降のプレフィックスは一致できない、としています。モデル、ツール定義、スキーマ、推論設定、出力形式、コンテキスト管理の設定を変えるとキャッシュはリセットされます。

つまり、「トークンを減らすために古いターンを削る」という操作は、同時に「キャッシュ読み出しを捨てて全量を再びキャッシュ書き込みする」という操作でもあります。削って浮くトークンより、キャッシュを失って増える課金のほうが大きい、という逆転が起こりえます。

Anthropicのcontext editingのドキュメントは、この点を正面から扱っています。ツール結果のクリアはキャッシュされたプロンプトのプレフィックスを無効化するため、キャッシュ無効化に見合うだけのトークンをクリアするべきであり、そのために毎回最低限クリアするトークン数を保証するclear_at_leastパラメータを使う、と説明しています。クリアのたびにキャッシュ書き込みのコストが発生するが、以降のリクエストは新しくキャッシュされたプレフィックスを再利用できる、とも書かれています。

注意

「入力トークンを削ったのに請求が減らない、むしろ増えた」という事象では、まずここを疑う価値があります。キャッシュ読み出しトークンは通常の入力より大幅に安いため、切り詰めの効果はキャッシュヒット率とセットでなければ評価できません。削る前と後で、入力トークン・キャッシュ読み出し・キャッシュ書き込みの3つを分けて記録してください。

あわせて読みたい

プロンプトキャッシュ(Prompt Caching)とは|LLM APIのコストとレイテンシを下げる仕組みと実務

4. 途中の指示変更が要約で丸められる

会話の途中で運用側が指示を追加したい場面があります(モード切り替え、注意事項の注入など)。素朴な実装は、トップレベルのシステムプロンプトを書き換えることです。しかしプレフィックス階層の先頭を変えるので、キャッシュは全滅します。

Anthropicのプロンプトキャッシュのドキュメントは、一部のモデルで、トップレベルのsystemフィールドを編集する代わりに{"role": "system"}のメッセージをmessagesに追加することで、キャッシュされたプレフィックスを変えずに会話の途中でシステム指示を追加できる、と説明しています(ドキュメントによればClaude Sonnet 5では利用できず、トップレベルのsystemフィールドを使うことになります)。使えるモデルかどうかは公式の対応表で確認してください。

この機能が使えない場合でも、設計の考え方は変わりません。運用側の指示は、ユーザー発話と同じ層に混ぜて要約対象にしない。要約に飲まれると「途中で切り替えたはずのモードが後半で元に戻る」という再現しにくい不具合になります。

5. PIIや権限外の情報が履歴に残り続ける

会話履歴は、放っておくと個人情報の貯蔵庫になります。しかも永続化され、保存先によっては保持期間の設定も別建てです。

OpenAIのconversation stateのドキュメントは、レスポンスオブジェクトが既定で30日間保存されること、storeをfalseにすると自動保存を無効化できることを説明しています。加えて、Conversationオブジェクトとその中のアイテムは30日のTTLの対象ではない、とされています。会話に紐づけたものは、明示的に消さない限り残るということです。

Anthropicのmemory toolのように、クライアント側でファイルとして永続化する仕組みではさらに直接的です。同ドキュメントは、メモリツールはクライアント側で動作し、Claudeはファイル操作を要求するだけで実行するのはアプリケーション側だとしたうえで、セキュリティ上の考慮として、機微情報の書き込みを防ぐ検証、ファイルサイズの上限、長期間アクセスされていないメモリファイルの定期削除、パストラバーサル対策(/memories/../../secrets.envのような経路を検証で弾く)を挙げています。

実務的には、履歴に入れる前にマスキングする、権限が変わったら該当ターンを引かない、会話単位の保持期間を業務要件から決める、の3点を先に決めておくのが安全です。ここは法務やセキュリティ要件と直結するため、自社の状況に合わせて確認してください。

あわせて読みたい

LLMに渡す前にPIIを外す|マスキング処理をパイプラインとして設計する

6. 履歴の機械的な加工がAPIの制約に触れる

最後に、実装レベルで即座にエラーになる境界です。

Anthropicのコンテキストウィンドウのドキュメントは、ツール結果を返す際には、対応するツール要求に付随するthinkingブロックを署名込みで一切変更せずに含める必要があるとし、APIは暗号署名でthinkingブロックの真正性を検証するため、thinkingブロックを変更するとエラーを返す、と明記しています。履歴を再構築する処理がブロックを正規化したり、空白を整えたりすると、ここで落ちます。

同様に、過去のthinkingブロックがコンテキストに残るかどうかはモデルによって異なります。同ドキュメントによれば、あるモデル群では既定で保持されて入力トークンとして数えられ、別のモデル群では渡し返しても自動的に除去されます。履歴管理コードが、モデルによって異なる前提の上に立っていないかは確認する価値があります。

OpenAI側にも似た注意があります。conversation stateのドキュメントは、推論モデルをステートレスに(履歴を自前で組み立てて)呼ぶ場合、応答のoutput配列の全アイテムを保持するよう述べています。Responses APIは既定で暗号化された推論アイテムを返すため、テキストだけを抜き出して積むとこれが失われます。

ヒント

共通する教訓は「履歴は自分で組み立てたデータ構造ではなく、APIとの契約である」ということです。読みやすい形に整形してから送り直す実装は、たいていどこかで契約を破ります。加工は追加と削除に限定し、既存ブロックの中身には触らないのが安全側です。

会話状態をどこに置くか

方式が決まったら、次は置き場所です。選択肢は3つあります。

クライアント側に持つ形は、ブラウザやアプリが履歴を保持して毎回サーバに送ります。実装は軽い一方、送られてきた履歴を信用することになるため改竄への耐性がなく、システムプロンプトや制約をサーバ側で再付与する前提でなければ使えません。

サーバ側に持つ形は、自前のDBに会話を保存し、リクエストごとに組み立てます。もっとも自由度が高く、監査ログや保持期間の管理もやりやすい形です。LangGraphのpersistenceのドキュメントは、この層を明示的にモデル化しています。checkpointerがグラフの状態スナップショットを保存し、thread_idで識別されるスレッド単位の短期メモリを提供する構成で、実装としてInMemorySaver(プロセス再起動で消える)、SqliteSaver(開発向け)、PostgresSaver/AsyncPostgresSaver(本番向け)が挙げられています。さらにスレッドをまたぐ長期メモリとしてstoreがあり、ユーザーの好みや事実といったアプリケーション定義のkey-valueデータを保持できる、とされています。この「スレッド内の短期」と「スレッドをまたぐ長期」の分離は、フレームワークを使わない場合でも設計の型として有用です。

プロバイダ側に持つ形は、APIの状態管理機能に預けます。OpenAIのconversation stateのドキュメントは、メッセージ配列を自分で組み立てて渡す方法、previous_response_idで応答を数珠つなぎにする方法、そしてConversationsという永続オブジェクトに状態を保持する方法の3つを挙げています。手数は最も少なくなりますが、保持期間の扱いが異なる点と、プロバイダを移す際の移植性は評価に入れておく必要があります。

再現性とデバッグのためのログ設計

会話履歴のバグは、再現できないと直せません。最低限、次の項目をターンごとに残すことを勧めます。

会話ターンごとに記録したい項目

  • 会話ID(スレッドID)とターン番号
  • そのリクエストで実際に送った履歴の識別情報(全文が無理ならメッセージID列とハッシュ)
  • 適用した圧縮方式と、その時点のパラメータ(残したターン数、要約のトリガ閾値など)
  • 圧縮や切り詰めが発生したか、発生したなら何トークン削ったか
  • 入力トークン・出力トークン・キャッシュ読み出しトークン・キャッシュ書き込みトークンの内訳
  • 使用モデルと停止理由(上限到達やコンテキスト超過を判別するため)
  • 要約を生成した場合は、その要約文そのもの

とくに最後の項目は軽視されがちですが、「なぜ制約が消えたのか」を調べるとき、要約文が残っているかどうかで調査時間が桁で変わります。要約は再生成すると別物になるため、後から復元できません。

あわせて読みたい

LLMアプリのオブザーバビリティ|トレーシングとログ設計で「なぜこの出力になったか」を後から追えるようにする

評価:マルチターンの回帰セットを作る

履歴設計の変更は、単一ターンの評価セットでは検出できません。マルチターン専用の回帰セットが要ります。

作り方の骨子は、前述の論文の手法がそのまま参考になります。同論文は、完全に指定された1つの指示を複数の「シャード」に分割してターンごとに開示するSharded設定と、同じシャードを箇条書きで1つの指示に連結したConcat設定を比較しています。Concatは検証用のベースラインで、FullとConcatの両方に成功するのにShardedで失敗するなら、原因は言い換えではなくマルチターンであること自体にある、と切り分けられます。自社で作るときも同じ形にできます。

  1. 1

    実際の会話ログから代表的な会話を選ぶ

    長さの分布を見て、p50付近とp95付近の両方から取ります。長いものだけを集めると、短い会話での劣化(圧縮の副作用)を見落とします。
  2. 2

    最終的に満たすべき要件を列挙する

    最後に出るべき回答が満たすべき条件を、チェック可能な形で書き出します。「税抜きで表記されている」「指定した3項目がすべて含まれる」といった粒度です。
  3. 3

    1つの指示を分割してターンに配る

    要件を複数ターンに分けて開示する形に組み替えます。序盤にだけ出てくる制約を必ず1つ以上入れておくと、要約による消失を検出できます。
  4. 4

    連結版(1ターン版)も同時に作る

    同じ要件を1つのメッセージにまとめた版を用意します。マルチターン版だけ失敗するなら、原因は履歴設計側にあると切り分けられます。
  5. 5

    同じ会話を複数回まわして分散を見る

    平均だけでなく最良と最悪の差を記録します。マルチターンでの劣化は主に不安定さとして現れるため、1回だけ流しても見えません。

切り詰めの前後を比較するときは、指標を2つ並べます。1つは正答率(またはルーブリックのスコア)、もう1つは1会話あたりの合計入力トークンです。この2軸でプロットすると、「トークンを3割削ったら正答率が1割落ちた」という交換条件が見えます。どこで妥協するかは業務側の判断ですが、材料がないまま圧縮率だけを上げていく状態は避けられます。

なお、要約に使うモデルは本番の応答に使うモデルと分けて記録してください。安いモデルに任せると入力コストは下がりますが、制約の取りこぼしが増えることがあります。

あわせて読みたい

LLMアプリの評価(eval)の作り方|「動いた気がする」で止めないための実務手順

監視:本番で見る数字

評価セットは変更前の判断に使い、監視は変更後の劣化検知に使います。見るべき数字は多くありません。

会話の長さの分布(ターン数とトークン数のp50/p95)は、設計の前提が崩れていないかを見るためのものです。想定より長い会話が増えていれば、圧縮の発生頻度も上がっています。圧縮・切り詰めの発生率が全リクエストの1割を超えるようなら、加工が例外ではなく通常運転になっているということなので、方式そのものを見直す合図です。

キャッシュ読み出しトークンの比率は、コストの健康診断です。Anthropicのドキュメントによれば、cache_creation_input_tokensとcache_read_input_tokensの両方が0なら、そのプロンプトはキャッシュされていません(モデルごとに最小のキャッシュ可能トークン数があり、それ未満だとエラーにならずキャッシュされないだけ、とされています)。OpenAIもGPT-5.6以降で1,024の可視入力トークン、それ以前のモデルで2,048という下限を示しています。Googleの暗黙キャッシュにもモデル世代ごとの最小入力トークン数があり、ドキュメントではGemini 2.5世代が2,048、Gemini 3世代が4,096とされています。会話が短いうちはキャッシュが効かないのが正常なので、会話の長さと合わせて見る必要があります。

停止理由の分布は、上限への衝突を検知するためのものです。コンテキスト超過に気づいていない状態は避けたいところです。

あわせて読みたい

LLMのコスト管理|トークン課金の考え方と削減の定石

小さく始める順番

一度に全部は作れないので、効果と工数の比で並べます。

  1. 1

    計測を入れる

    ターン数・入出力トークン・キャッシュ内訳・停止理由を会話IDで紐づけて記録します。これが無いと以降の判断が全部推測になります。
  2. 2

    全量のまま、キャッシュを効かせる

    まず加工しません。システムプロンプトとツール定義を安定させ、可変部分を後ろに置いて、キャッシュ読み出しが出ることを確認します。
  3. 3

    ツール結果だけを別扱いにする

    エージェントなら、ここが最も効きます。古いツール結果のクリアを入れ、削る量がキャッシュ破棄に見合うかを実測します。
  4. 4

    マルチターンの回帰セットを作る

    代表会話を10本程度、序盤の制約を含む形で用意します。ここまで来て初めて、圧縮を安全に導入できます。
  5. 5

    要約+直近全文を導入する

    古い部分だけを要約し、直近数ターンは原文で残します。要約文はログに残します。回帰セットで、圧縮前後の正答率とトークン数を比較します。
  6. 6

    制約と決定事項を構造化して外に出す

    要約で落ちやすい情報を、要約とは別のフィールドで保持します。ここまでやると、要約の失敗が致命傷になりにくくなります。
  7. 7

    必要なら検索を足す

    履歴が非常に長く、参照が散発的な場合にだけ検討します。直近ターンは検索を通さず必ず入れる、という原則は崩さないでください。

よくある質問

コンテキストウィンドウが広いモデルを使えば、履歴の設計は不要になりますか
不要にはなりません。Anthropicのコンテキストウィンドウのドキュメントは、より大きなコンテキストウィンドウは複雑で長いプロンプトを扱えるようにするが、コンテキストは多ければ自動的に良いというものではないとし、トークン数が増えると正確さと想起が劣化する現象をcontext rotと呼んでいます。同ドキュメントは、コンテキストに何を入れるかを吟味することは、どれだけの容量が使えるかと同じくらい重要だとしています。Googleの長文脈のドキュメントも、needle-in-a-haystack型の検索では高い性能を出す一方で、複数の情報を同時に取り出す場合には性能が落ちうるとしています。加えて、履歴が長いほど毎ターンの入力トークンが増えるため、コストにも効きます。容量が広がることは前提を楽にしますが、設計そのものは残ります。
直近Nターンだけ残す実装は、なぜ危険なのですか
3つの理由があります。1つ目は、冒頭で与えた制約や設定が静かに消えることです。エラーにはならず、品質だけが落ちます。2つ目は、ツール呼び出しと結果の対が割れることです。LangChainの短期メモリのドキュメントは、メッセージを削除する際は残る履歴が妥当であることを必ず確認するよう明記し、ほとんどのプロバイダはツール呼び出しを含むassistantメッセージの後に対応するtool結果メッセージが続くことを要求するとしています。3つ目は、プロンプトキャッシュのプレフィックスが毎ターン変わることです。切り落とし位置が動くと先頭が変わるため、キャッシュヒットが得られません。どうしても使うなら、システムプロンプトと序盤の制約は別枠で常に先頭に置き、切り落とし位置をターン境界に合わせ、キャッシュのヒット率を監視してください。
履歴を要約で圧縮したら、料金はどれくらい下がりますか
一概には言えず、キャッシュの効き方次第で逆に増えることもあります。プロンプトキャッシュは先頭からのプレフィックス一致で成立するため、履歴の途中を要約に置き換えると、その位置以降のキャッシュが無効になります。Anthropicのcontext editingのドキュメントは、ツール結果のクリアがキャッシュされたプレフィックスを無効化するため、キャッシュ無効化に見合うだけのトークンをクリアすべきだとし、そのためのclear_at_leastパラメータを説明しています。実測の手順としては、圧縮の導入前後で、入力トークン・キャッシュ読み出しトークン・キャッシュ書き込みトークン・要約生成に使ったトークンの4つを分けて記録し、1会話あたりの合計で比較してください。トークン数だけを見ると判断を誤ります。
要約はいつ、どのタイミングで走らせるべきですか
閾値で走らせるのが一般的です。プロバイダ側の機能を参考にすると、Anthropicのcompaction(ベータ)は既定のトリガ閾値が入力15万トークン(設定可能な最小値は5万トークン)、context editingのツール結果クリアは既定のtriggerが入力10万トークンで直近3組のツール呼び出しと結果を残す設定です。自前で実装する場合も、ターンの長さは大きくばらつくため、ターン数ではなくトークン数を基準にするほうが安定します。あわせて、要約の直後は必ずキャッシュ書き込みが発生する前提でコストを見積もってください。頻繁に走らせると、要約のコストとキャッシュ再構築のコストが重なります。
マルチターンでの精度劣化は、履歴設計だけで解決できますか
完全には解決できません。arXiv:2505.06120は、6つの生成タスクで単一ターンからマルチターンへの平均39%の性能低下を報告し、劣化を能力のわずかな低下と不安定さの大幅な増加に分解しています(同論文の図1では能力-15%・不安定さ+112%)。同論文は、各ターンでそれまでの要件をすべて言い直すSnowball設定でも、単一ターンからマルチターンへの低下を15〜20%緩和するにとどまり、FullやConcatの水準には届かないとしています。生成温度を下げる対処も、マルチターンでは有効でないと報告されています。実務的には、履歴設計で悪化を防ぎつつ、要件が固まった時点で1つの指示に統合し直す(同論文の言葉ではconsolidate)導線をUI側に用意する、という併用が現実的です。同論文自身も、会話がうまくいかない場合に新しい会話を始めるほうが良い結果になりうる、と利用者向けの推奨として書いています。
会話状態はクライアント・サーバ・プロバイダのどこに置くべきですか
監査や権限管理の要件があるならサーバ側が基本です。クライアント側に持たせる形は実装が軽い代わりに、送られてきた履歴を信用することになるため、システムプロンプトや制約はサーバ側で必ず再付与する前提が要ります。プロバイダ側に預ける形は手数が最少で、OpenAIのconversation stateのドキュメントは、メッセージ配列を自分で渡す方法、previous_response_idで数珠つなぎにする方法、Conversationsという永続オブジェクトを使う方法の3つを挙げています。ただし同ドキュメントによれば、レスポンスオブジェクトは既定で30日保存される一方、Conversationオブジェクトとその中のアイテムは30日のTTLの対象ではありません。保持期間の要件や移植性は、法務・セキュリティ要件に関わる部分でもあるため、預ける前に自社の状況に合わせて確認してください。

まとめ

会話履歴の設計は、「何トークンまで送るか」という容量の話に見えて、実際は「何を捨てられるか」を決める判断です。そして捨てる判断を間違えても、システムは動き続けます。落ちるのは品質だけです。

順番としては、まず計測、次に全量のままキャッシュを効かせる、それからツール結果を別扱いにする、回帰セットを作ってから要約を入れる、という並びが安全です。最初から凝った圧縮を作り込むと、効果を測れないまま複雑さだけが増えます。

そして、境界の話を覚えておいてください。要約は制約を落とします。切り詰めはツール呼び出しの対を割ります。途中の書き換えはキャッシュのプレフィックスを壊します。履歴の永続化はPIIを残します。thinkingブロックの加工はエラーになります。どれも、実装した瞬間には気づけない種類の問題です。

最後に、マルチターンでの劣化は履歴設計だけでは消えません。論文が示したのは、能力そのものより不安定さが大きく増えるという構図でした。であれば、長い会話を長いまま完璧に扱おうとするより、要件が固まった時点で一度整理し直す導線を業務フローとUIの側に用意するほうが、実務としては現実的な着地点だと考えています。

あわせて読みたい

トークンとは/コンテキストウィンドウとは|LLMの入出力を測る単位と上限の仕組み

出典・参考

この記事をシェア

関連する記事

開発・エージェント

トークンとは/コンテキストウィンドウとは|LLMの入出力を測る単位と上限の仕組み

LLMが文章を扱う単位である「トークン」と、入力と出力の合計を縛る「コンテキストウィンドウ」の仕組みを、実務目線で解説します。サブワードによるトークナイズ、日本語と英語でのトークン数の違い、上限超過時の挙動、トークン単位の課金、そして「上限が大きいほど良いとは限らない」理由と、長文の前処理・見積り・コスト試算といった勘所までを整理します。

プロンプト実務

コンテキストエンジニアリングとは|プロンプトの次に来る設計の基本

プロンプトエンジニアリングの次の設計単位として注目される「コンテキストエンジニアリング」を、実務目線で整理します。コンテキストの劣化(context rot)という前提、渡す情報の構成と順序、長時間動くエージェントで使う圧縮・メモ・サブエージェントといった手法までをまとめます。