AI・機械学習
LLMの過負荷を軽減するためのインメモリレイヤーによるマッピング
Mapping with In-Memory Layers to Reduce LLM Overload (ridgetext.com)
要約
RidgeTextは、GeoJSONのような大量のデータをLLMに直接渡すとコンテキストウィンドウの制限やコストの問題が発生するため、マッピング機能において「レイヤー・ファースト・パターン」を導入しました。このパターンでは、各データソース(火災情報、トレイルなど)はサーバーサイドにデータを保存し、LLMには軽量な確認応答のみを返します。LLMはこれらの確認応答に基づいて地図生成ツールを呼び出し、最終的な地図はサーバーサイドで合成されます。これにより、LLMはデータそのものではなく、レイヤーの存在と数のみを認識するため、コンテキストウィンドウを小さく保ち、効率的な処理を実現します。
全文翻訳
RidgeTextに、トレイルを重ね合わせた火災境界マップを生成するように依頼すると、その応答には、単一の画像としてレンダリングされ、SMSで送信された火災境界とトレイルルートが重ね合わされたマップが含まれるようになりました。火災境界(陰影付きポリゴン)にトレイルルートが重ね合わされています。これらはそれぞれ独立したレイヤーであり、マップがレンダリングされる前に個別にキューイングされます。このレイアリング機能の開発中に、私たちは一つのことに気づきました。GeoJSONをLLMのツール呼び出しに渡すことはできないということです。それは単に、有用すぎるにはデータ量が多すぎるのです。背景RidgeTextは、LLMの上にオーケストレーションレイヤーとして構築されています。ユーザーはSMSを通じて(アプリやUIなしで)対話します。LLMは自然言語理解を処理し、どのツールを呼び出すかを決定し、明確な応答を組み立てて返信します。それはルールエンジンではありません。LLMはあらゆるステップで判断を下します。その非決定性は会話の機能ですが、機能にとっては制約となります。LLMが触れるものはすべて、変動に対して回復力がある必要があります。ツールがデータを返しすぎると、LLMはそれを切り捨てたり、要約を捏造したり、静かに失敗したりする可能性があります。ツールのインターフェースが曖昧な場合、LLMはそれを誤って呼び出す可能性があります。優れたツール設計とは、LLMが見るものを整形し、合理的な応答の範囲がすべて正しい結果につながるようにすることです。マップ生成は、これが重要となる明確な例です。ナイーブなアプローチ(そしてなぜそれが失敗するのか)明白な実装は、火災データを取得してLLMに返すツールと、そのデータを受け取ってマップをレンダリングするセカンドツールを持つことです。例えば次のようになります:LLMがget_wildfire_data()を呼び出す → 2,000の火災ポリゴンをGeoJSONとして受信LLMがrender_map(geojson: ...)を呼び出す → そのGeoJSONを渡す実際には、適度な火災データセットは50〜500KBの生のGeoJSONです。トークンは約4バイトなので、500KBは約125,000トークンとなり、多くのコンテキストウィンドウよりも大きく、収まる場合でも高価です。LLMは、推論できないデータのパイプになります。GeoJSONを単純化することも、検証することもできず、毎回完全なコンテキストコストを支払います。レイヤー・ファースト・パターン私たちのソリューションは、Mapbox自体の仕組みを模倣しています。レイヤーは独立して追加され、レンダリング時に合成されます。データをLLMに返す代わりに、各データ取得ツールは結果をサーバーサイドに保存し、軽量な確認応答のみを返します:LLMがretrieve_wildfire_layer(location: "Cascades")を呼び出す → { status: "queued", layerId: "wildfires-0", featureCount: 847 } LLMがretrieve_trail_layer(trailName: "PCT Section J")を呼び出す → { status: "queued", layerId: "trail-1", featureCount: 1 } LLMがgenerate_map()を呼び出す → { mapUrl: "https://storage.../map-abc123.jpg" } LLMのコンテキストは、常に確認応答(小さなJSONオブジェクト)のみを見ます。GeoJSONは、generate_mapがキューを drain して画像を合成するまで、サーバーメモリ内に存在します。レイヤースタック各retrieve_*呼び出しは、リクエストコンテキストに保持されている順序付けられたレイヤー配列に追加されます:interface MapLayer { type: 'wildfire-perimeters' | 'fire-hotspots' | 'trail' | 'heatmap' | ...; data: GeoJSON.FeatureCollection; style: LayerStyle; } // プロセス内キュー — 1回のLLMターンのライフタイムで存続 const layerQueue: MapLayer[] = [];generate_mapは、挿入順にレンダリングします — Mapboxのレイヤースタックとまったく同じで、前のレイヤーが後のレイヤーの下に配置されます。LLMは、ツールの呼び出しシーケンスによって暗黙的に順序を制御します。retrieve_satellite_baseをretrieve_trailより先に呼び出すと、トレイルは衛星画像の上に描画されます。私たちの実装では、セッションIDをキーとするMapを使用し、30分のTTLを設定しているため、キューイングされたがレンダリングされなかったレイヤーは、蓄積されるのではなく自動的に削除されます。具体的なメカニズムは重要ではありません — Redis、データベース、またはリクエストスコープのコンテキストオブジェクトでも機能します。重要なのは、データがLLMのコンテキストウィンドウ以外のどこかに存在することです。なぜこれがMapboxを模倣しているのかMapboxのコアモデルは次のとおりです。ソースはデータを提供し、レイヤーはレンダリング方法を定義し、レイヤーは宣言順に合成されます。私たちのサーバーサイドキューは、ブラウザなしで実行されているのと同じ抽象化です。「Mapbox」が私たちのレンダラーで何を意味するのかを正確に述べる価値があります。ベースタイル(地形、道路、ラベル)としてMapbox Static API画像を取得し、次にcanvasを使用してデータレイヤーをその上に合成します。Mapbox自体はGeoJSONを見ることはありません。背景を提供するだけです。私たちが保存するレイヤー記述子は、レンダラーがそれを必要とするからではなく、意図的な将来志向の選択としてMapboxの形式に従っています。もし静的タイル(3D地形、複雑なブレンディング、アニメーションレイヤー)を超えていくことになった場合、レンダラーをPlaywrightブラウザで実行されるヘッドレスMapbox GL JSインスタンスに切り替えることができます。そのレンダラーは、ツールやLLMのインターフェースを変更することなく、まったく同じレイヤーキューを消費します。コストと速度のプロファイルも変化します。JavaScriptベースのレンダラーは、リクエスト間でタイルとGLアセットをキャッシュできるため、マップごとのコストを削減し、すべてのマップに対して新しいStatic API呼び出しを行うよりもコールドスタートのレイテンシを改善できる可能性があります。これは意味します:新しいデータソースの追加は、新しいretrieve_*ツールの追加を意味します — レンダリングパイプラインは変更されませんレイヤーの順序は、ツール呼び出しシーケンスを通じて自然に表現できますスタイリングは、レイヤータイプと共に配置され、LLMを介して渡されませんレンダラーは交換可能です — 今日は静的タイル、明日はヘッドレスGL — LLMレイヤーに触れることなくレンダリングステップgenerate_mapは決定的です。LLMからGeoJSONを受け取ることはありません — ズームレベルやマップスタイルなどのオプションパラメータのみです。レイヤーキューを読み取り、各フィーチャーセットをMapbox Static APIベース画像に投影し、sharpを使用してそれらを合成します。async function generateMap(options: MapOptions): Promise<string> { const layers = drainLayerQueue(); // 消費してクリア const base = await fetchMapboxBase(options); const composed = await compositeLayers(base, layers); return await uploadToStorage(composed); }結果は、LLMがSMS応答に添付できる単一の画像URLです。LLMが実際に何を見るか「PCTの近くの火災を表示して」というリクエストの場合、LLMのツール呼び出しシーケンスは次のようになります:[ { "name": "retrieve_wildfire_layer", "result": { "status": "queued", "featureCount": 847 } }, { "name": "retrieve_trail_layer", "result": { "status": "queued", "featureCount": 1 } }, { "name": "generate_map", "result": { "mapUrl": "https://..." } } ]ツール結果の合計トークン:約150。このパターンなしでは:約125,000以上。トレードオフあなたが獲得するもの:データセットのサイズに関係なく、コンテキストウィンドウは小さく保たれますレンダリングパイプラインは決定的であり、単独でテスト可能です新しいレイヤータイプの追加は、LLMがシステムと対話する方法を変更する必要はありませんあなたが失うもの:LLMは、キューイングしたレイヤーのジオメトリを推論できません。トレイルが追加され、そのフィーチャー数がわかっても、座標自体については何もわかりません。「トレイルに最も近い都市はどこか?」のような質問は、キューイングされたレイヤーデータからは正確に答えられません — LLMは、それをテキストとして返す別のツールが必要になります。ただし、後でストレージリンクを介してレンダリングされたマップ画像を表示し、合成結果について視覚的な観察を行うことはできます。レイヤーキューは一時的です — ターンをまたいで存続しないため、複数ターンのマップの洗練には再取得が必要です。2番目のトレードオフは、会話応答と同じ履歴保持ポリシーに従うデータベーステーブルにレイヤーを永続化することで対処できます。レンダリングされた各マップ応答は、参照によって関連するレイヤーを保存します。ユーザーがフォローアップして、ズームイン、スタイルの変更、または別のデータソースの追加を要求した場合、元のAPIから再取得することなく、レイヤーをデータベースから直接復元できます。これをあなたのシステムに適用するここでのパターンは、マップに関するものではありません。それは、あなたのLLMが推論エンジンの代わりにデータパイプとして機能していることを認識し、その役割から取り除くことです。探すべきシグナル:ツールAがデータを取得 → LLMがそれを受信 → LLMがそれを直接ツールBに渡す。LLMが内容に基づいて決定を下していない場合、それは保持すべきではありません。