80% の問いに固定答えがあるなら、なぜ毎回 embedding 検索して 5 片を LLM に送るのか。L1 Wiki は逆発想:事前に答えを組み立て、クエリ時はそのまま返す。
Wikipedia には見過ごされがちな洞察がある:人間の知識の 80% は固定構造を持つ。「Apple 社」には必ず設立年、本社、CEO、主力製品がある。従来 RAG はこれを 500 token 片に分解し、クエリごとにパズルを組み直す — 同一パズルを 1,000 回解くのは無駄。
L1 Wiki の逆転:オフラインでパズルを組み立て、クエリ時にそのまま返す。夜間バッチが:
documents をスキャン(kb_id, slug) をキーに wiki_pages へ保存クエリ時、問いが slug にマップできれば wiki body を直接返す — L2 不使用。
CREATE TABLE wiki_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
kb_id UUID NOT NULL,
slug TEXT NOT NULL,
title TEXT NOT NULL,
aliases TEXT[] NOT NULL DEFAULT '{}',
body TEXT NOT NULL,
summary TEXT NOT NULL,
source_chunks UUID[] NOT NULL,
token_count INT NOT NULL,
compiled_at TIMESTAMPTZ NOT NULL,
compiled_by TEXT NOT NULL,
compiled_prompt TEXT NOT NULL,
lint_status TEXT NOT NULL DEFAULT 'pending',
lint_errors JSONB,
version INT NOT NULL DEFAULT 1,
UNIQUE(kb_id, slug)
);
CREATE INDEX idx_wiki_aliases ON wiki_pages USING GIN(aliases);
ALTER TABLE wiki_pages ENABLE ROW LEVEL SECURITY;
3 つの設計:aliases を GIN インデックス付き配列、source_chunks で追跡、compiled_prompt でバージョン A/B。
async function compileWiki(kb) {
const slugs = await planSlugs(kb);
for (const slug of slugs) {
const existing = await findPage(kb.id, slug);
const chunks = await findRelevantChunks(kb, slug);
const fp = hashChunks(chunks);
if (existing?.fingerprint === fp) continue; // 変更なしスキップ
const page = await llmCompile({
model: 'claude-sonnet-4-6',
prompt: COMPILE_PROMPT_V2,
slug, chunks,
existingBody: existing?.body,
});
await upsertPage(kb.id, slug, page, fp);
await enqueueLint(page.id);
}
}
Fingerprint スキップでコスト制御、既存 body 提供で差分更新、Lint 非同期で worker 解放。
[SYSTEM]
あなたは知識コンパイラ。複数 chunks を構造化 Wiki ページに統合する。
規則:
1. Markdown 出力
2. 最初の文は 80 字以内のサマリー
3. 見出し、リスト、表で本体構成
4. 事実主張末尾に [chunk_id] 付記
5. chunks 間矛盾は「注意事項」に明記
6. chunks 外の情報を作らない
slugs:
- slug: return-policy
title: "返品ポリシー"
aliases: ["返金フロー", "返品方法", "返品期間"]
category: policy
「このナレッジベースで最も問われそうなトピック 30–50 個を JSON で出力せよ」。
flowchart LR
Q[質問] --> KW[キーワード照合<br/>aliases]
Q --> EMB[embedding<br/>vs title+summary]
Q --> LLM[LLM 分類<br/>GPT-4o-mini]
KW --> V{投票}
EMB --> V
LLM --> V
V -->|2+ 一致| HIT[L1 命中]
V -->|分岐| MISS[L2 に fall back]
Fig 3-1: 3 路投票
flowchart TB
W[Wiki ページ] --> E1[E1: 事実重複]
W --> E2[E2: 引用欠落]
W --> E3[E3: ページ内矛盾]
W --> E4[E4: ページ間矛盾]
W --> E5[E5: ソース削除]
E1 & E2 & E3 & E4 & E5 --> R[Lint レポート]
Fig 3-2: 5 種チェック
| エラー | 検出 | 対応 |
|---|---|---|
| E1 重複 | LLM セクション比較 | 警告のみ |
| E2 引用欠落 | Regex カバレッジ | ブロック + 再コンパイル |
| E3 内部矛盾 | NLI 三値分類 | ブロック + 人手審査 |
| E4 ページ間矛盾 | ページ間 NLI | 警告 + レビュー |
| E5 ソース失効 | JOIN soft-delete | ブロック + 再予定 |
L1 命中 3 条件:slug 投票、lint_status=passed かつ compiled_at >= chunks.updated_at、テナント / KB 一致。
命中応答:
{"from_wiki": true, "answer": "...", "tokens": {"prompt": 0, "completion": 0}}
純 L1 命中で LLM 呼び出しゼロ。
Wiki body 長(>500 token)時は LLM に要約依頼。L2 より 80% トークン節約。
| 指標 | 単層 L2 | L1 + L2 | 差 |
|---|---|---|---|
| 平均レイテンシ | 2.8 s | 1.2 s | −57% |
| P95 レイテンシ | 6.5 s | 3.2 s | −51% |
| 月 Token 費 | USD 15,000 | USD 4,800 | −68% |
| L1 命中率 | N/A | 38–52% | — |
| 幻覚率 | 4.2% | 1.8% | −57% |
Wiki ページは lint 検証済みのため、幻覚率も下がる。
(kb_id, slug) キー、aliases GIN インデックス