問題
LLMには有限のコンテキストウィンドウがあります。128Kトークンモデルは寛容に聞こえますが、出力予算、システムプロンプト、ツール説明、マルチターン会話の蓄積された履歴を差し引くと話は別です。長い会話、大きなツール結果、マルチステップエージェントループはすべてこの制限に対して圧力をかけます。多くの場合、単一のセッション内でです。 素朴なソリューションは切り詰めです。ウィンドウが満杯になったら古いメッセージを削除します。これは高速で予測可能ですが、コンテキストを無差別に破壊します。ユーザーの元々の意図、以前のターンからの重要な決定、重要なデータポイントはすべて、単純な文字切り詰めが当たると消えてしまいます。反対の極端 — すべてのターンでLLM駆動の要約 — は意味的なコンテンツを保持しますが、高コストで遅く、独自の障害モード(幻覚的な要約、数値精度の喪失)を導入します。 真の課題は「ウィンドウに収まる」ことではありません。それは:重要な情報を失わずに段階的に劣化し、不要な圧縮にトークンを浪費せず、ユーザーが感じることができるレイテンシを追加しない。 FIM Oneはこれを5層の多層防御アーキテクチャで解決します。各層は問題の異なるスケールに対処し、きれいに構成されます — 単一の層が完璧である必要はありません。次の層がそれが見落とすものをキャッチするからです。5つの防御層
コンテキスト管理は単一のメカニズムではありません。これは積み重ねられた層であり、各層は特定の粒度で特定の関心事を処理します。| 層 | コンポーネント | 機能 | 動作時期 |
|---|---|---|---|
| 5 | Budget Configuration | モデル仕様から使用可能な入力トークン予算を計算 | 起動時 / リクエストごと |
| 4 | DbMemory | 永続化された履歴を読み込み、読み込み時にコンパクト化 | リクエストごとに1回 |
| 3 | ContextGuard | 反復ごとの予算強制 | すべてのReAct反復 |
| 2 | CompactUtils | トークン推定、スマート切り詰め、LLMコンパクト化 | 層3と4によって呼び出し |
| 1 | Memory Implementations | 抽象インターフェース + 具体的な戦略 | フレームワークレベル |
Layer 5 — 予算設定
予算は3つの値から計算されます:128,000 - 64,000 - 4,000 = 60,000 tokens。
4,000トークンのシステムプロンプト予約は、エージェントのシステムプロンプト、ツール説明、およびフォーマットのオーバーヘッドをカバーします。これは固定定数です — 実際にはシステムプロンプトのクリッピングを避けるのに十分な余裕があり、予算を無駄にしないほど小さいです。
予算値は3つのソースから取得でき、優先順位順に解決されます:
- データベース ModelConfig — 管理者が設定したモデルごとの
context_sizeとmax_output_tokens。 - 環境変数 —
LLM_CONTEXT_SIZEとLLM_MAX_OUTPUT_TOKENS。 - ハードコードされたデフォルト — 128Kコンテキスト、64K出力。
Layer 4 — DbMemory
DbMemory は本番環境のメモリ実装です。データベースから永続化された会話履歴を読み込み、エージェントが見る前にトークン予算に合わせてコンパクト化します。
設計は意図的に読み取り専用です。永続化は chat.py で処理されます。これは API レイヤーで、メッセージのライフサイクル全体(メタデータ、使用状況追跡、画像添付を含む)を管理します。DbMemory は読み取りのみを行います。その add_message() と clear() メソッドは何もしません。この分離により、二重書き込みを防ぎ、永続化ロジックを一箇所に保ちます。
読み込み時に、DbMemory は以下を実行します:
- 会話のすべての
userとassistantメッセージをクエリし、作成時間順に並べます。 - 末尾のユーザーメッセージ(現在のクエリで、エージェントが再度追加します)を削除します。
- 画像添付を再構築します。画像を含むユーザーメッセージはメタデータ(
file_id、mime_type)をデータベースに保存し、DbMemoryはディスクから base64 データ URL を再構築して、LLM が以前のターンからの画像を「見る」ことができるようにします。 - コンパクト化:
compact_llmが提供されている場合はCompactUtils.llm_compact()を使用します。それ以外の場合はCompactUtils.smart_truncate()にフォールバックします。
DbMemory は追跡フラグ(was_compacted、_original_count、_compacted_count)を設定し、SSE レイヤーがこれを使用してフロントエンドに compact イベントを発行します。
Layer 3 — ContextGuard
ContextGuard は反復ごとの予算実行者です。スタンドアロン ReAct モードと DAG ステップ内の各サブエージェント内の両方で、すべての ReAct 反復の最上部で呼び出されます。これはメッセージが LLM API に到達する前の最後の防衛線です。
実行は 3 段階のプロセスに従います:
-
サイズが大きすぎる個別メッセージを切り詰める。 50K 文字を超える単一メッセージは、
[Truncated]サフィックス付きでハード切り詰めされます。これは暴走するツール出力をキャッチします — Web スクレイプが Web ページ全体を返す場合、ファイル読み取りが大規模なデータセットをダンプする場合。 - 総トークン数を推定する。 合計が予算内に収まる場合は、すぐに返します。ほとんどの反復はここで成功します — 圧縮は例外であり、常ではありません。
-
予算を超えた場合は圧縮する。
compact_llmが利用可能な場合は、ヒント固有のプロンプトで LLM 駆動の圧縮を使用します。それ以外の場合は、smart_truncateにフォールバックします。
| ヒント | 使用者 | 保持 | 削除 |
|---|---|---|---|
react_iteration | ReAct エージェント ループ | 最近の推論チェーン、現在の目標、重要なデータ | 古い冗長なステップ、失敗した再試行、詳細なツール出力 |
planner_input | DAG エンリッチ クエリ | ユーザー インテント進化、主要な決定、制約 | 対話の詳細、挨拶、ツール呼び出しメカニクス |
step_dependency | DAG ステップ コンテキスト | 主要なデータ、数値、結論 | 推論プロセス、失敗した試行、詳細なフォーマット |
general | デフォルト フォールバック | 主要な事実、決定、ツール結果 | 挨拶、フィラー、冗長なやり取り |
smart_truncate にフォールバックします。エージェントは失敗を見ることはありません。これは意図的な信頼性の選択です:ヒューリスティック切り詰めによってコンテキストを失う方が、反復をクラッシュさせるよりも優れています。
Layer 2 — CompactUtils
CompactUtils はステートレスなユーティリティクラスです — インスタンスなし、状態なし、純粋な関数のみです。レイヤー 3 と 4 が構築する 3 つの機能を提供します。
トークン推定 はトークナイザーライブラリをインポートせずにテキストをおおよそのトークン数に変換します。ヒューリスティック:
- ASCII 文字:~1 トークンあたり 4 文字
- CJK / 非 ASCII 文字:~1 トークンあたり 1.5 文字
- 画像:画像あたり 765 トークン(固定コスト)
- メッセージごとのオーバーヘッド:4 トークン(ロールマーカー、デリミタ)
smart_truncate はヒューリスティックフォールバックです。ピン留めされたメッセージを無条件に保持し、ピン留めされていないメッセージを逆向きに走査して、予算が尽きるまで蓄積します。結果は会話のサフィックスで、予算内に収まります。また、結果が先行するユーザーメッセージのない孤立したアシスタントターンで始まらないようにします — これは LLM を混乱させます。
llm_compact は LLM 駆動のパスです。メッセージを 3 つのグループに分割します — システムメッセージ(常に保持)、ピン留めされたメッセージ(常に保持)、コンパクト化可能なメッセージです。最も古いコンパクト化可能なメッセージは単一の [Conversation summary] システムメッセージに要約され、最新の 4 メッセージはそのまま保持されます。コンパクト化された結果がまだ長すぎる場合は、コンパクト化された出力に対して smart_truncate にフォールバックします — 二重の安全装置です。
レイヤー 1 — メモリ実装
メモリレイヤーはBaseMemory インターフェースを定義します: add_message()、get_messages()、clear()。3 つの実装が存在します:
- WindowMemory — カウントベースのスライディングウィンドウ。最後の N 個の非システムメッセージを保持します。シンプルで予測可能、LLM 呼び出しなし。本番環境では使用されていません。テストとステートレスシナリオに役立ちます。
-
SummaryMemory — メッセージカウントがしきい値を超えたときに LLM 要約をトリガーします。古いメッセージを
[Conversation summary]システムメッセージに圧縮します。本番環境では使用されていません。より洗練された ContextGuard アプローチより前のものです。 - DbMemory — 本番実装 (レイヤー 4 で説明)。データベースバック、読み取り専用、ロード時に LLM またはヒューリスティック圧縮を使用します。
ReActを通じたコンテキストフロー
ReActエージェントは、ロード時と反復時の2つの異なるフェーズでコンテキスト管理を使用します。 ツール反復は高速化のため非ストリーミングchat()を使用し、回答合成はストリーミングstream_chat()をstream_answer()経由で使用します。このツーフェーズ分割(高速ツールループの後にストリーミング合成)は、レイテンシとユーザー体験の両方を最適化します。デュアルモード実行とツール選択を含むReActエンジンの完全なアーキテクチャについては、ReActエンジンを参照してください。
重要な洞察:DbMemoryは履歴コンテキストの問題(前のリクエストからのターン)を処理し、ContextGuardはリクエスト内の成長の問題(エージェントループ中に蓄積するツール結果)を処理します。 これらは異なるタイムスケールで動作し、異なる障害モードをキャッチします。
ユーザーの現在のクエリは常にpinned=Trueとしてマークされます。これにより、すべてのコンパクション(smart_truncateとllm_compactの両方)を通じて生き残ることが保証されます。ピン留めされたメッセージは無条件に保持されます。履歴がどれほど積極的に圧縮されても、ユーザーの実際の質問は決して失われません。
DAGを通じたコンテキストの流れ
DAGモードはReActとは根本的に異なるコンテキスト形状を持っています。1つの長い会話スレッドではなく、ツリー構造になっています:計画フェーズ、複数の並列実行ステップ、分析フェーズです。各フェーズには独自のコンテキスト管理戦略があります。 フェーズ1 — 履歴の読み込み。 DbMemoryは会話履歴を読み込んでコンパクト化します。これはReActと同じです。コンパクト化された履歴は"Previous conversation:"というプレフィックス付きのテキストブロックにフォーマットされます。
フェーズ2 — 拡張クエリの構築。 履歴テキストと現在のクエリがenriched_queryに結合されます。これが16Kトークンを超える場合、planner_inputヒントプロンプトを使用してLLMで要約されます。16Kの閾値が選択されている理由は、プランナーが単一パスでクエリ全体を読む必要があるためです。ReActとは異なり、計画中の反復的なコンパクト化はありません。
フェーズ3 — 計画。 プランナーは2メッセージのプロンプトを受け取ります:システムプロンプトと拡張クエリです。ここではContextGuardはありません。拡張クエリは既に16Kチェックでサイズが制御されています。
フェーズ4 — ステップの実行。 各DAGステップは独自のContextGuardを持つ独立したReActエージェントとして実行されます。重要なのは、これらのサブエージェントはメモリを持たないということです。タスク説明と依存関係コンテキストのみで新規に開始します。これは設計上の意図です:DAGステップは自己完結した作業単位である必要があります。依存関係の結果は_build_step_contextを介して挿入され、50K(ContextGuardのmax_message_chars制限)で文字数が切り詰められます。
フェーズ5 — 分析。 ステップ結果はアナライザーLLM用にフォーマットされ、ステップごとに10K文字で切り詰められます。これにより、単一ステップの冗長な出力が分析コンテキストを支配するのを防ぎます。
フェーズ6 — 再計画。 アナライザーが目標が達成されず、信頼度が閾値以下であると判断した場合、ステップ結果は再計画コンテキスト用に各500文字に切り詰められます。再計画は何が起こったのかと何が問題だったのかを知る必要がありますが、すべてのステップの出力の完全な詳細は必要ありません。この積極的な切り詰めにより、再計画プロンプトはプランナーが効率的に処理できるほどコンパクトに保たれます。
LLMコールマップと再計画ロジックを含むDAGパイプラインアーキテクチャの全体については、DAGエンジンを参照してください。
ピン留めされたメッセージ
ピン留めメカニズムは、圧縮によって破棄されてはならないメッセージを保護します。2つのカテゴリのメッセージがピン留めされます:- 現在のユーザークエリ — 常にピン留めされます。ユーザーが質問をして履歴が長すぎる場合、システムは履歴を圧縮し、質問は圧縮しません。
- ストリーム中に注入されたメッセージ — ユーザーがエージェント実行中にフォローアップを送信すると、注入されたメッセージはピン留めされ、エージェントは次の反復でそれを認識します。
トークン推定
FIM One は実際のトークナイザーではなくヒューリスティックトークン推定を使用しています。これは明確なトレードオフを伴う意図的な選択です。 なぜ実際のトークナイザーを使わないのか? 3つの理由があります:-
依存関係のコスト。
tiktoken(OpenAIのトークナイザー)は15MBのコンパイル済みRustバインディングです。sentencepiece(一部のオープンソースモデルで使用)には独自のビルド要件があります。複数のLLMプロバイダーをターゲットとするフレームワークの場合、単一の正しいトークナイザーは存在しません — 各モデルファミリーは異なるものを使用しています。 - 速度。 ヒューリスティック推定は文字列に対する単一パスです。実際のトークン化には語彙参照、BPEマージ操作、特殊トークン処理が含まれます。ContextGuardは反復ごとに推定を呼び出し、時には複数回呼び出します — 速度の違いは重要です。
- 十分な精度。 ヒューリスティックは混合言語テキスト用に調整されています(ASCII/CJK分割は2つの主要なケースをカバーしています)。エッジケース(句読点が多いコード、異常なUnicode)では1.5~2倍ずれる可能性がありますが、コンテキスト管理は本質的に近似的です。60Kの予算で30%ずれていても、快適なマージンが残ります。
| コンテンツタイプ | 比率 | 根拠 |
|---|---|---|
| ASCIIテキスト | 約4文字/トークン | 英語の散文とコードは、GPT/Claudeトークナイザー全体で平均3.5~4.5文字/トークン |
| CJK/非ASCII | 約1.5文字/トークン | 各CJK文字は通常1~2トークン;1.5は幾何平均 |
| 画像 | 765トークン/画像 | ビジョンAPIのbase64エンコード画像の概算コスト |
| メッセージごとのオーバーヘッド | 4トークン | ロールマーカー、デリミタ、フォーマット |
ユーザーが見るもの
コンテキスト管理は、一般的なケースでは見えないように設計されており、アクティベートされるときは最小限の干渉に留まります。ユーザーに見える信号は以下の通りです: CompactDivider。DbMemory が読み込み時に履歴をコンパクト化すると、フロントエンドは「Earlier context (N messages) was summarized by AI.」というテキスト付きの破線区切り線をレンダリングします。これは要約と保持された最近のメッセージの間に表示され、ユーザーに古いコンテキストが圧縮されたことを視覚的に示しながら、会話フローを中断しません。
トークン使用量表示。 各応答の最後の done カードには「X.Xk in / X.Xk out」が表示されます。これは消費された入出力トークンの合計です。これにはコンパクト化に費やされたトークン(要約のための高速 LLM 呼び出し)が含まれます。トークン消費を監視するユーザーは、コンパクト化がどのようなオーバーヘッドを追加しているかを確認できます。
グレースフルなエラーハンドリング。 コンテキスト管理が完全に失敗した場合(フォールバックチェーンを考えると起こらないはずのシナリオですが、理論的には可能です)、エラーは応答内のエージェントエラーテキストとして表示され、システムクラッシュとしては表示されません。会話は続行され、ユーザーは再試行または言い換えることができます。
目標は、ほとんどのユーザーがコンテキスト管理について考えないようにすることです。長い会話ができ、システムが予算を透過的に処理し、唯一の目に見える成果物は時々表示されるコンパクト区切り線です。トークン効率を気にするパワーユーザーとオペレーターにとって、使用量表示と設定可能な予算パラメーターは必要な制御を提供します。