メインコンテンツへスキップ

問題

LLMには有限のコンテキストウィンドウがあります。128Kトークンモデルは寛容に聞こえますが、出力予算、システムプロンプト、ツール説明、マルチターン会話の蓄積された履歴を差し引くと話は別です。長い会話、大きなツール結果、マルチステップエージェントループはすべてこの制限に対して圧力をかけます。多くの場合、単一のセッション内でです。 素朴なソリューションは切り詰めです。ウィンドウが満杯になったら古いメッセージを削除します。これは高速で予測可能ですが、コンテキストを無差別に破壊します。ユーザーの元々の意図、以前のターンからの重要な決定、重要なデータポイントはすべて、単純な文字切り詰めが当たると消えてしまいます。反対の極端 — すべてのターンでLLM駆動の要約 — は意味的なコンテンツを保持しますが、高コストで遅く、独自の障害モード(幻覚的な要約、数値精度の喪失)を導入します。 真の課題は「ウィンドウに収まる」ことではありません。それは:重要な情報を失わずに段階的に劣化し、不要な圧縮にトークンを浪費せず、ユーザーが感じることができるレイテンシを追加しない。 FIM Oneはこれを5層の多層防御アーキテクチャで解決します。各層は問題の異なるスケールに対処し、きれいに構成されます — 単一の層が完璧である必要はありません。次の層がそれが見落とすものをキャッチするからです。

5つの防御層

コンテキスト管理は単一のメカニズムではありません。これは積み重ねられた層であり、各層は特定の粒度で特定の関心事を処理します。
コンポーネント機能動作時期
5Budget Configurationモデル仕様から使用可能な入力トークン予算を計算起動時 / リクエストごと
4DbMemory永続化された履歴を読み込み、読み込み時にコンパクト化リクエストごとに1回
3ContextGuard反復ごとの予算強制すべてのReAct反復
2CompactUtilsトークン推定、スマート切り詰め、LLMコンパクト化層3と4によって呼び出し
1Memory Implementations抽象インターフェース + 具体的な戦略フレームワークレベル
層は下から上へ番号が付けられています。これは上位層が下位層に依存しているためです。層5が予算を設定します。層4は初期ロード時のコンパクト化を行います。層3はすべての反復で予算を強制します。層2と1は層3と4が使用するプリミティブを提供します。

Layer 5 — 予算設定

予算は3つの値から計算されます:
usable_input_tokens = context_size - max_output_tokens - system_prompt_reserve
デフォルト値:128,000 - 64,000 - 4,000 = 60,000 tokens 4,000トークンのシステムプロンプト予約は、エージェントのシステムプロンプト、ツール説明、およびフォーマットのオーバーヘッドをカバーします。これは固定定数です — 実際にはシステムプロンプトのクリッピングを避けるのに十分な余裕があり、予算を無駄にしないほど小さいです。 予算値は3つのソースから取得でき、優先順位順に解決されます:
  1. データベース ModelConfig — 管理者が設定したモデルごとの context_sizemax_output_tokens
  2. 環境変数LLM_CONTEXT_SIZELLM_MAX_OUTPUT_TOKENS
  3. ハードコードされたデフォルト — 128Kコンテキスト、64K出力。
メインLLMと高速LLMは独立した予算を持ちます。DAGステップ実行は高速LLMの予算を使用します。ReActモードはメインLLMの予算を使用します。これが重要な理由は、オペレーターが履歴が蓄積するReAct用の大規模コンテキストモデルと、各ステップが新たに開始するDAGステップ用の小規模で高速なモデルをペアリングすることが多いためです。 4,000トークンの下限が適用されます — 設定ミスの値がより小さい予算を生成する場合、システムは無言で失敗するのではなく4Kにクランプします。

Layer 4 — DbMemory

DbMemory は本番環境のメモリ実装です。データベースから永続化された会話履歴を読み込み、エージェントが見る前にトークン予算に合わせてコンパクト化します。 設計は意図的に読み取り専用です。永続化は chat.py で処理されます。これは API レイヤーで、メッセージのライフサイクル全体(メタデータ、使用状況追跡、画像添付を含む)を管理します。DbMemory は読み取りのみを行います。その add_message()clear() メソッドは何もしません。この分離により、二重書き込みを防ぎ、永続化ロジックを一箇所に保ちます。 読み込み時に、DbMemory は以下を実行します:
  1. 会話のすべての userassistant メッセージをクエリし、作成時間順に並べます。
  2. 末尾のユーザーメッセージ(現在のクエリで、エージェントが再度追加します)を削除します。
  3. 画像添付を再構築します。画像を含むユーザーメッセージはメタデータ(file_idmime_type)をデータベースに保存し、DbMemory はディスクから base64 データ URL を再構築して、LLM が以前のターンからの画像を「見る」ことができるようにします。
  4. コンパクト化: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 段階のプロセスに従います:
  1. サイズが大きすぎる個別メッセージを切り詰める。 50K 文字を超える単一メッセージは、[Truncated] サフィックス付きでハード切り詰めされます。これは暴走するツール出力をキャッチします — Web スクレイプが Web ページ全体を返す場合、ファイル読み取りが大規模なデータセットをダンプする場合。
  2. 総トークン数を推定する。 合計が予算内に収まる場合は、すぐに返します。ほとんどの反復はここで成功します — 圧縮は例外であり、常ではありません。
  3. 予算を超えた場合は圧縮する。 compact_llm が利用可能な場合は、ヒント固有のプロンプトで LLM 駆動の圧縮を使用します。それ以外の場合は、smart_truncate にフォールバックします。
ヒント システム は、ContextGuard を万能ではなくコンテキスト認識にするものです。状況が異なれば、圧縮戦略も異なります:
ヒント使用者保持削除
react_iterationReAct エージェント ループ最近の推論チェーン、現在の目標、重要なデータ古い冗長なステップ、失敗した再試行、詳細なツール出力
planner_inputDAG エンリッチ クエリユーザー インテント進化、主要な決定、制約対話の詳細、挨拶、ツール呼び出しメカニクス
step_dependencyDAG ステップ コンテキスト主要なデータ、数値、結論推論プロセス、失敗した試行、詳細なフォーマット
generalデフォルト フォールバック主要な事実、決定、ツール結果挨拶、フィラー、冗長なやり取り
各ヒントは、圧縮 LLM に何を保持し、何を破棄するかを指示する慎重に作成されたシステム プロンプトにマップされます。プロンプトは「会話と同じ言語で書く」で終わります — これは CJK ユーザーの場合、要約がそうでなければ英語にデフォルト設定されるという詳細が重要です。 LLM 圧縮が失敗した場合(ネットワーク エラー、空の応答、例外)、ContextGuard は静かに 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 またはヒューリスティック圧縮を使用します。
WindowMemory と SummaryMemory はコードベースに残っています。これらはテストに役立つプリミティブとして機能し、Web レイヤーなしで FIM One のコアライブラリを組み込むユーザーにとって有用だからです。これらはデッドコードではなく、アーキテクチャが成長した単純なケースです。

ReActを通じたコンテキストフロー

ReActエージェントは、ロード時と反復時の2つの異なるフェーズでコンテキスト管理を使用します。 ツール反復は高速化のため非ストリーミングchat()を使用し、回答合成はストリーミングstream_chat()stream_answer()経由で使用します。このツーフェーズ分割(高速ツールループの後にストリーミング合成)は、レイテンシとユーザー体験の両方を最適化します。デュアルモード実行とツール選択を含むReActエンジンの完全なアーキテクチャについては、ReActエンジンを参照してください。 重要な洞察:DbMemoryは履歴コンテキストの問題(前のリクエストからのターン)を処理し、ContextGuardはリクエスト内の成長の問題(エージェントループ中に蓄積するツール結果)を処理します。 これらは異なるタイムスケールで動作し、異なる障害モードをキャッチします。 ユーザーの現在のクエリは常にpinned=Trueとしてマークされます。これにより、すべてのコンパクション(smart_truncatellm_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つのカテゴリのメッセージがピン留めされます:
  1. 現在のユーザークエリ — 常にピン留めされます。ユーザーが質問をして履歴が長すぎる場合、システムは履歴を圧縮し、質問は圧縮しません。
  2. ストリーム中に注入されたメッセージ — ユーザーがエージェント実行中にフォローアップを送信すると、注入されたメッセージはピン留めされ、エージェントは次の反復でそれを認識します。
ピン留めのリスクは蓄積です。多くの注入されたメッセージがある長いセッションでは、ピン留めされたコンテンツが予算の大部分を消費し、実際の会話履歴の余地がなくなる可能性があります。ContextGuardはハードキャップでこれに対処します: ピン留めされたトークンが予算の50%を超える場合、最も古い注入されたメッセージはピン留めが解除され、圧縮可能なプールに移動されます。 最新のピン留めされたメッセージ(現在のクエリ)のみが保持されます。 これはトレードオフです。古い注入されたメッセージのピン留めを解除すると、それらが要約または切り詰められる可能性があります。しかし、別の方法 — ピン留めされたメッセージが他のすべてのコンテキストを圧倒することを許可する — はさらに悪いです。システムは最新のコンテキストを保持することを優先し、これはほぼ常に古い注入よりも関連性があります。

トークン推定

FIM One は実際のトークナイザーではなくヒューリスティックトークン推定を使用しています。これは明確なトレードオフを伴う意図的な選択です。 なぜ実際のトークナイザーを使わないのか? 3つの理由があります:
  1. 依存関係のコスト。 tiktoken(OpenAIのトークナイザー)は15MBのコンパイル済みRustバインディングです。sentencepiece(一部のオープンソースモデルで使用)には独自のビルド要件があります。複数のLLMプロバイダーをターゲットとするフレームワークの場合、単一の正しいトークナイザーは存在しません — 各モデルファミリーは異なるものを使用しています。
  2. 速度。 ヒューリスティック推定は文字列に対する単一パスです。実際のトークン化には語彙参照、BPEマージ操作、特殊トークン処理が含まれます。ContextGuardは反復ごとに推定を呼び出し、時には複数回呼び出します — 速度の違いは重要です。
  3. 十分な精度。 ヒューリスティックは混合言語テキスト用に調整されています(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トークンロールマーカー、デリミタ、フォーマット
推定は空でないコンテンツに対して常に最低1トークンを返します。これは予算計算のゼロ除算エッジケースを防ぎます。

ユーザーが見るもの

コンテキスト管理は、一般的なケースでは見えないように設計されており、アクティベートされるときは最小限の干渉に留まります。ユーザーに見える信号は以下の通りです: CompactDivider。 DbMemory が読み込み時に履歴をコンパクト化すると、フロントエンドは「Earlier context (N messages) was summarized by AI.」というテキスト付きの破線区切り線をレンダリングします。これは要約と保持された最近のメッセージの間に表示され、ユーザーに古いコンテキストが圧縮されたことを視覚的に示しながら、会話フローを中断しません。 トークン使用量表示。 各応答の最後の done カードには「X.Xk in / X.Xk out」が表示されます。これは消費された入出力トークンの合計です。これにはコンパクト化に費やされたトークン(要約のための高速 LLM 呼び出し)が含まれます。トークン消費を監視するユーザーは、コンパクト化がどのようなオーバーヘッドを追加しているかを確認できます。 グレースフルなエラーハンドリング。 コンテキスト管理が完全に失敗した場合(フォールバックチェーンを考えると起こらないはずのシナリオですが、理論的には可能です)、エラーは応答内のエージェントエラーテキストとして表示され、システムクラッシュとしては表示されません。会話は続行され、ユーザーは再試行または言い換えることができます。 目標は、ほとんどのユーザーがコンテキスト管理について考えないようにすることです。長い会話ができ、システムが予算を透過的に処理し、唯一の目に見える成果物は時々表示されるコンパクト区切り線です。トークン効率を気にするパワーユーザーとオペレーターにとって、使用量表示と設定可能な予算パラメーターは必要な制御を提供します。