コンテキストエンジンの設計 — スコープ×グレイン×トークン予算で「何をLLMに見せるか」を計画する
RAGの暗黙の選択をユーザーが操作できるデータ構造に変える
Key Takeaways
- コンテキスト選択を暗黙のtop-kからスコープ×グレインの明示的なデータ構造に変える
- デフォルト(ノートブック内=full、リンク近傍=summary)でユーザー意図の強さとトークン消費を比例させる
- 見積もりは4文字=1トークンで十分。精度の要件は用途(判断支援)が決める
- 計画と実行の分離(ContextPlan / SummaryPlan / ReindexPlan)はテスト・可観測性・キャンセル安全性を同時に買う
課題: コンテキスト選択はRAG最大の暗黙知
LLMアプリの品質を決める最大の変数は、モデルでもプロンプト文言でもなくコンテキストに何を入れたかです。ところが多くのRAG実装では、この選択は検索スコアとtop-kに埋め込まれた暗黙の決定で、ユーザーには制御も観察もできません。「なぜこの答えになったのか」の半分はコンテキスト選択の説明なのに、です。
Marianのコンテキストエンジンは、open-notebookスタイルの設計でこれを明示化します。中核は2つの軸の直積です。
- スコープ:
notebook(選んだソース集合のみ)かvault(ノートブックのソースに加え、そこからリンクされた近傍ノートも候補化) - グレイン: ソースごとに
full(全文)/summary(要約)/excluded(除外)
// lib/marian-data/context.ts
type ContextGrain = 'full' | 'summary' | 'excluded'
type ContextScope = 'notebook' | 'vault'
interface ContextSourcePlan {
id: NoteId
title: string
grain: ContextGrain
tokens: number // このソースの見積もり消費
external?: boolean // scope=vault時、ノートブック外から来た近傍
}
interface ContextPlan {
scope: ContextScope
sources: ContextSourcePlan[]
totalTokens: number
includedCount: number
}デフォルトの妙: 中は全文、外は要約
buildContextPlanはスコープとグレイン指定からContextPlanを組み立てますが、グレインのデフォルトに設計判断が入っています。ノートブック内のソースはfull、vault拡張で入ってきた近傍ノートはsummaryが初期値です。
ユーザーが明示的に選んだものは詳細に、リンクグラフを辿って自動で入ってきたものは薄く。トークン消費の主導権を「ユーザーの意図の強さ」に比例させる、という方針をデフォルト値だけで表現しています。近傍ノートにはexternal: trueが立つため、UIは「これは自動で追加された」と区別して見せられます。
トークン見積もり: 4文字=1トークンで十分
計画段階の見積もりは正確なトークナイザを使いません。
estimateTokens(id, grain):
excluded → 0
summary → max(8, round(summaryText.length / 4))
full → round((title.length + body.length) / 4)
CONTEXT_TOKEN_BUDGET = 8000 // ソフト予算(UI表示用)なぜ正確さを捨てられるのか。この数字の用途が課金でも切り詰めでもなく、ユーザーの意思決定支援だからです。「全文で入れると予算の60%をこの1ノートが食う」が分かれば、summaryに落とす判断ができます。tiktoken相当の依存を足して得られる精度向上は、この用途には寄与しません。予算の8,000トークンも強制値ではなくソフトリミットで、超過はUIの警告として現れます。
見積もりの精度は用途が決める。人間の判断支援なら±20%の誤差は無害で、依存ゼロ・同期計算という性質の方が価値が高い。
同じ思想がもう1つ: 要約プランナー
「計画をデータとして返し、実行と分離する」パターンは要約パイプラインにも貫かれています。lib/marian-summarizeのプランナーは、入力テキストとモードからSummaryPlanを返す純粋関数です。
lengthSpecs = {
short: { targetTokens: 80, format: 'TL;DR — 1–2文または3点以内' },
medium: { targetTokens: 250, format: 'セクション付きの1ページ要旨' },
long: { targetTokens: 700, format: '構造化された複数セクション要約' },
}
// auto: 入力 <= 400トークンならshort、<= 2000ならmedium、それ以上はlong
interface SummaryPlan {
strategy: 'single' | 'map-reduce'
chunks: SummaryChunkPlan[] // 文字レンジ [startChar, endChar)
perChunkTargetTokens: number // map段の各チャンク予算(targetの約60%)
contextWindow: number // 既定8000
reserveTokens: number // 既定1500(プロンプト+出力の取り分)
}入力がcontextWindow - reserveTokensに収まればsingle、収まらなければチャンク分割してmap-reduceに切り替えます。map段の各チャンクには最終目標の約60%のトークン予算を割り、reduce段でマージする余白を残します。プランはLLMを1回も呼ばずに計算されるため、「この10万字のドキュメントは何チャンクのmap-reduceになるか」がテストで断言できます。
計画と実行の分離が買うもの
コンテキスト計画・要約計画・(別記事で扱った)再インデックス計画。Marianの基盤レイヤーには「まず計画をデータとして作り、実行は別の層がやる」という同型のパターンが繰り返し現れます。得られる性質は共通です。
- テスト容易性: LLM・DB・クロックなしで計画ロジックを検証できる
- 可観測性: 実行前に「何が起きるか」をUIやログに出せる
- キャンセル安全性: 計画段階では何も起きていないので、捨てるのが自由
まとめ
コンテキストエンジンは、(1)スコープ×グレインの直積で「何を見せるか」を型のあるデータにする、(2)デフォルト値(中はfull・外はsummary)でユーザー意図とトークン消費を整合させる、(3)4文字=1トークンの粗い見積もり+8,000のソフト予算で判断支援に徹する、(4)計画と実行の分離を要約パイプラインまで貫く、という設計です。RAGの説明可能性は、検索スコアの可視化よりも先にコンテキスト選択の可視化から始まります。
FAQ
- コンテキストエンジンのスコープとグレインとは何か?
- スコープはコンテキスト候補の範囲で、notebook(選択したソースのみ)とvault(ソースからリンクされた近傍ノートも候補に含む)の2種類。グレインはソースごとの粒度で、full(全文)/summary(要約)/excluded(除外)の3段階です。この直積からトークン見積もりつきのContextPlanが構築されます。
- トークン見積もりはなぜ正確なトークナイザを使わないのか?
- 用途が課金や強制切り詰めではなく、ユーザーの意思決定支援(このノートを全文で入れると予算の何%か)だからです。4文字=1トークンの近似で±20%程度の誤差は判断に影響せず、依存ライブラリなしの同期計算という性質の方が価値があります。予算8,000トークンもソフトリミットで、超過は警告として表示されます。
- 長文の要約はどう計画されるのか?
- 要約プランナーが入力トークン数(4文字=1トークン見積もり)とコンテキストウィンドウ(既定8,000、予約1,500)を比較し、収まればsingle、収まらなければmap-reduce戦略を選びます。map段の各チャンクには最終目標トークン(short=80/medium=250/long=700)の約60%を割り当て、チャンク分割は文字レンジとして計画に含まれます。LLMを呼ばずに計画が確定するためテスト可能です。