GMOグローバルサイン・ホールディングスCTO室の@zulfazlihussinです。
私はhakaru.aiの開発チームにてAI開発を担当しております。今回は、LLM Wikiについて共有したいと思います。
最近、業務効率化でRAGのチューニングをひたすらやっていました。chunkサイズの調整、embeddingの最適化、re-ranking——ある程度はうまくいきます。しかし、RAGを使っているうちに、違和感を覚えました。「結局、LLMが毎回ゼロから考えているだけでは?」
と思いました。そんなとき、Andrej KarpathyのLLM Wiki(GitHub Gist)がとても参考になりましたので、共有したいと思います。
LLM Wikiとは
LLMに答えさせるのではなく、知識をコンパイルさせる。
LLM Wikiとは、質問に答えるたびにその知識をMarkdownファイルとして蓄積し、次の回答をより豊かにしていくアーキテクチャです。RAGが毎回ゼロから推論するのに対し、LLM Wikiは使うほど賢くなるという特性を持ちます。Karpathy氏はこの蓄積効果を「複利(compound interest)」と表現しています。
RAGとLLM Wikiの違い
まず、それぞれのアーキテクチャを整理します。
RAGは生データをチャンク単位に分割し、ベクトルDBに格納します。質問が来たとき、類似度検索で関連チャンクを取得し、それをコンテキストとしてLLMに渡して回答を生成します。
Raw → チャンク分割 → ベクトル化 → 類似度検索 → 回答(→ 消える)
一方、LLM Wikiはまず生データからLLMが構造化知識を構築し、Markdownファイルとして保存します。質問への回答はこのWikiを参照して行い、回答から得られた知見はWikiに書き戻されます。
Raw → 知識コンパイル → Wiki(Markdown) → 回答 → Wiki更新(→ 蓄積される)
RAGとLLM Wikiの最大の違いは、回答が蓄積されるかどうかです。RAGは回答のたびにゼロから推論を行いますが、LLM WikiはWikiを更新し続けるため、使うほど回答品質が向上します。また、ベクトルDBを使わないためデータの中身が人間にとって透明であり、すべてGitで管理できる点も特徴です。具体的な出力の違いは、後述のデモ結果で示します。
LLM Wikiの3層アーキテクチャ
LLM Wikiは3つの層で構成されています。設計思想を理解するために全体像をご紹介します。
| 層 | 役割 | 書き込む主体 |
|---|---|---|
| Layer 1: raw | 論文・ログ・記事などの原始データ。不変 | 人間 / Ingestion処理 |
| Layer 2: Wiki | LLMが構築・保守する構造化知識(Markdown) | LLMエージェント |
| Layer 3: Schema | CLAUDE.md等の運用ガイドライン。エージェントの行動規約 | 人間 |
ポイントは2つあります。まず、LLMはrawには書き込みを行いません。元データへの汚染を防ぐためです。次に、Wikiはすべて人間が読めるMarkdownです。ベクトルDBのように中身が不透明にならないため、可読性が確保されます。
3つの運用サイクル
LLM Wikiが「使うほど賢くなる」のは、3つのサイクルが回るからです。
Ingest(取り込み)
新しいソースをrawに追加すると、LLMが関連する複数のWikiページを同時更新します。1件のソースから10〜15ページが更新されることもあります。「読んで終わり」ではなく、読んだ内容が即座にWikiに反映されます。
Query(照会・拡張)
質問に回答した際、その回答・知見をWikiに書き戻します。例えば、「Nginx 413エラーの原因は?」という質問に回答すると、その回答がWikiのInsightセクションに追記されます。回答そのものがWikiを成長させます。
Lint(保守・整合性チェック)
定期的にLLMがWiki全体をスキャンし、矛盾・古くなった記述・重複エントリを検出して修正します。人間が手動でメンテナンスしなくても、知識品質が自律的に維持される仕組みです。
┌─ Ingest ──→ raw追加 → Wiki複数ページ更新
│
サイクル ─┼─ Query ───→ 質問 → 回答 → Wikiに書き戻し
│
└─ Lint ────→ Wiki全体スキャン → 矛盾修正・重複排除
実装(最小構成)
今回のデモでは、上記のうちIngestとQueryの2サイクルを最小構成で再現しました。LintサイクルとSchema層は省略しています。ディレクトリ構成は以下の通りです。
/data
raw_logs.md # Layer 1: 生ログ・ドキュメント(不変)
raw_logs_2.md # Layer 1: 追加生データ(1ヶ月後の別環境)
/wiki
nginx_413.md # Layer 2: 構造化された知識(LLMが更新)
/src
rag.py # RAG(比較用)
wiki.py # Wiki構築(Ingestサイクル)
updater.py # Wiki更新(Queryサイクル)
llm.py # LLMラッパー
demo.py # デモ実行スクリプト
今回のデモで想定しているシステム構成は、Webアプリケーションによく見られる
以下の構成です。
クライアント(ブラウザ・アプリ)
↓ POST /api/v1/upload(ファイルアップロード)
Nginx(リバースプロキシ)
↓ リクエストをバックエンドに転送
Django(バックエンドAPI)※ Kubernetes Pod 上で動作
また、後半で登場するEnvironment Bでは、Nginxの前段にAWS ALBと
WAF(Web Application Firewall)が追加された構成を想定しています。
クライアント
↓
AWS ALB(ロードバランサー)+ WAF(Webアプリケーションファイアウォール)
↓ WAFのbody inspection制限を超えると、ここで413が返される
Nginx(リバースプロキシ)
↓
Django(バックエンドAPI)※ Kubernetes Pod 上で動作
この構成では、NginxのclientMaxBodySizeが正しく設定されていても、
WAFがより前段でリクエストを拒否するため、413エラーが発生します。
WAFの存在を知らないと「Nginxの設定は正しいのになぜ?」という混乱が
生じやすく、RAGではこのレイヤーの違いを整理した回答が難しい場面です。
本来のLLM Wikiの設計では、Layer 3としてSchemaをCLAUDE.md(Claudeを利用する場合)に置き、エージェントの行動規約を定義します。また、Wikiの検索には、単語出現回数 + ベクトル + LLM再ランキングのハイブリッド検索のような流れで行われますが、今回は最小構成のためどちらも省略しました。
デモデータ:生データと構造化知識の比較
ここからが今回のデモの核心です。同じ情報がRAGとLLM Wikiでどう異なる形で扱われるかを、実際のデータで見ていきます。
Before:raw_logs.md(生データ)
まず、Ingestする前の生データです。Nginxエラーログ、Slackのやりとり、監視ダッシュボードの情報がそのまま記録されています。
# Nginx 413 Error - Raw Logs & Reports
## Error Log (2024-03-15 14:23:01)
[error] 1234#0: *5678 client intended to send too large body: 15728640 bytes,
client: 192.168.1.100, server: api.example.com, request: "POST /api/v1/upload HTTP/1.1"
## Slack Thread - #backend-alerts
@tanaka: またUpload APIで413が出てます。10MB以上のファイルがアップロードできない状態です。
@suzuki: Djangoの設定確認しました。UPLOAD_MAX_SIZEは100MBに設定されてます。問題ないはずなんですが。
@tanaka: バックエンドのメモリ不足ですかね? Podのリミット上げたほうがいいですかね?
@yamada: 先月も同じことありましたよね。あのときPodのメモリを4GBに増やしたけど直らなかったやつ。
@suzuki: Djangoのログ見てるんですが…エラーログに何も出てないです。リクエスト自体がDjangoに届いてないかも。
@tanaka: え、じゃあアプリに到達する前に弾かれてるってこと?
## Monitoring Dashboard
- Endpoint: POST /api/v1/upload
- Status: 413 Request Entity Too Large
- Response source: Nginx (not proxied to upstream)
- Nginx config: client_max_body_size not explicitly set (default: 1m)
- Backend config: UPLOAD_MAX_SIZE = 100MB
- Affected users: ~230/day
## Previous Fix Attempts
1. Increased backend pod memory: 2GB → 4GB (no effect)
2. Added retry logic in frontend (no effect - 413 is not retriable)
3. Checked Django middleware ordering (irrelevant)
RAGはこの生データをチャンク単位で検索します。「Slackの会話」「Nginxのログ」「監視ダッシュボード」が別々のチャンクに分断されるため、「BackendはそもそもRequestに到達していない」という因果関係が失われやすくなります。
After:nginx_413.md(LLM Wikiが生成した構造化知識)
上記の生データをIngestした後、LLMが実際に生成したWikiです。断片的だった情報が、因果関係を持つ構造化知識に変換されていることがわかります。
# Nginx 413 Request Entity Too Large — ファイルアップロード失敗
## Root Cause
Nginxの `client_max_body_size` ディレクティブが明示的に設定されておらず、
デフォルト値の1MBが適用されていた。
因果関係は以下の通り:
1. クライアントが15MB のファイルをアップロードしようとする
2. Nginxがリクエストボディのサイズを client_max_body_size(1MB)と比較する
3. 制限を超えているため、Nginxはリクエストをupstream(Django)に転送せず、即座に413を返す
4. Django側は UPLOAD_MAX_SIZE = 100MB に設定されているが、
リクエストがそもそも到達しないため無意味
## Misdiagnosis
1. バックエンドPodのメモリ増設(2GB → 4GB)
413はリクエストボディのサイズ制限に起因するステータスコードであり、
メモリ量とは無関係である。この誤解は「大きなファイル = メモリが必要」
という直感的な連想から生じやすい。
2. フロントエンドへのリトライロジック追加
413は一時的な障害ではなく、リクエスト自体が拒否されているため、
同じリクエストを何度送信しても結果は変わらない。
リトライが有効なのは5xx系のサーバーエラーや一部の408/429などであり、413は対象外。
3. Djangoミドルウェアの順序確認
Djangoのエラーログに何も出力されていない時点で、問題がDjangoに
到達していないことを示唆していた。
## Insight(診断手順)
1. レスポンスの発生元を特定する — どのレイヤーからエラーが返されているかを確認する
2. 各レイヤーのログを確認する — バックエンドのログにエラーがなければ到達していない可能性が高い
3. リクエスト経路上の全レイヤーの設定を確認する
4. デフォルト値に注意する — client_max_body_size(デフォルト1MB)のように
明示的に設定しない場合に想定外に低い制限が適用されるケースがある
生データとWikiの違いに注目してください。生データには「Podメモリ増設は効果なし」という事実しかありません。一方、LLM Wikiには「なぜ効果がないか(413はサイズ制限でありメモリと無関係)」まで構造化されています。この「なぜ」が回答品質の差に直結します。
デモ実行結果:回答の具体的な差
実際に「Nginx 413エラーが発生しています。原因と解決方法を教えてください。」という質問を使って、RAGとLLM Wikiの回答を比較してみます。
RAGの回答
RAGは生データのチャンクを参照して、以下のような回答を生成しました。
Nginxの client_max_body_size が未設定でデフォルト1MBが適用されています。設定ファイルに client_max_body_size 100m; を追加してください。なお、Podメモリの増設やリトライ、Djangoの調査は効果がありませんでした。
原因と解決策は正しく導出できています。しかし、「なぜPodメモリ増設が効果なかったのか」「なぜリトライが意味ないのか」という説明がなく、診断手順の提示もありません。また、後述するWAFが原因のパターンについては言及がありません。
LLM Wikiの回答
一方、LLM Wikiは構造化された知識を参照するため、誤診パターンについて理由まで含めた回答が生成されました。例えば、リトライについては次のように説明されています。
「リトライが無意味な理由は、413が一時的な障害ではなくリクエスト拒否だからです。リトライが有効なのは5xx系や408/429のような一時障害に対してであり、413は構造的に対象外です」
また、Podメモリについては以下のように説明されています。
「Podメモリ増設が効果なかった理由は、413がメモリ不足ではなくリクエストボディのサイズ制限に起因するためです。『大きなファイル = メモリが必要』という連想は誤りです」
2つの回答を並べると、違いがより明確になります。
| 観点 | RAG | LLM Wiki |
|---|---|---|
| 因果関係の明確さ | △ 断片的な情報からの推論 | ◎ 構造化された因果チェーン |
| 誤診パターンの説明 | △ 言及はあるが理由が浅い | ◎ なぜ誤りかを明確に説明 |
| 診断手順の提示 | × なし | ◎ 体系的な手順を提示 |
知識が育つ瞬間:2回目のIngest後が最も差が出た
1ヶ月後、別の環境(Environment B)で同じ413エラーが発生しました。今度はNginxの client_max_body_size は正しく設定済みです。調査した結果、原因はNginxの前段に置かれたAWS WAFのbody inspection制限でした。
新しい生データ(raw_logs_2.md)
## Slack Thread - #infra-alerts
@kobayashi: Environment Bでまた413出てます。でも client_max_body_size は100mに設定してあるんですよね。
@yamada: nginx.conf 再確認しました。確かに100mになってます。
@kobayashi: Environment Aと同じ設定なのになんで出るんだろう…
@sato: Environment BってNginxの前にAWS ALBがいますよね?そっちが原因じゃないですかね?
@sato: 見つけました。先週ステージングのNLBがALB + WAF構成に変わってたんです。
WAFのルールにbody inspectionの10MB制限がありました。
@kobayashi: じゃあNginxに到達する前にWAFで弾かれてるってこと?
## Investigation Result
- AWS WAF body inspection limit: 8KB (default) to 64KB for core rule set
- Nginx client_max_body_size: correctly set to 100m
- Root cause: WAF layer, not Nginx layer
このraw_logをIngestすると、LLMは既存のnginx_413.mdを更新します。WAFという新しいパターンが既存の知識に統合されます。
更新後のWiki(nginx_413.md)
## Root Cause
### パターン1:Nginx `client_max_body_size` のデフォルト値
Nginxの client_max_body_size が未設定でデフォルト1MBが適用。
リクエストはDjangoに到達せず、Nginxが即座に413を返す。
### パターン2:AWS WAF の body inspection 制限 ← NEW
Nginxの設定は正しいが、前段のAWS WAFのbody inspectionルールがリクエストを拒否。
リクエストはNginxにすら到達していない。
ステージング環境でNLBからALB + WAF構成への変更が直接の契機だった。
## Misdiagnosis(更新後 — 4つに増加)
4. Nginx設定のみの確認で調査を打ち切る(パターン2で発生) ← NEW
client_max_body_size が正しく設定されていることを確認した時点で混乱が生じた。
リクエスト経路全体(WAF → ALB → Nginx → アプリケーション)のうち
Nginxより上流を見落としたためである。
## Insight(更新後 — 8ステップに拡張)
4. Nginx設定が正しい場合、前段にWAF/CDN/LBが存在するか確認する ← NEW
5. 直近のインフラ構成変更を調査する ← NEW
6.「同じ設定なのに」を疑う ← NEW
これがRAGとの決定的な差です。RAGは2回目も「Nginxの client_max_body_size を設定してください」と回答します。Environment BにWAFがあることを知らないからです。一方、LLM Wikiは2回のIngestを通じてNginxパターンとWAFパターンを統合し、次に同じ質問が来たときには両方の可能性を考慮した回答が可能になります。これが複利効果です。
| 観点 | RAG | LLM Wiki(2回のIngest後) |
|---|---|---|
| 複数パターンの提示 | × Nginxパターンのみ | ◎ Nginx + WAFの2パターン |
| 因果関係 | △ 断片的 | ◎ パターンごとに因果チェーン |
| 誤診パターン | △ 3つ(理由が浅い) | ◎ 4つ(各誤りの理由を含む) |
| 診断手順 | × なし | ◎ 8ステップの体系的手順 |
| 知識の蓄積 | × 毎回リセット | ◎ 2回のIngestで統合済み |
実装コード(最小構成)
実際に動かしたコードをご紹介します。LLMにはOpenAIのAPIを使用しています。
LLMラッパー(llm.py)
from openai import OpenAI
import os
client = OpenAI()
MODEL = os.getenv("MODEL", "gpt-4o-mini")
def chat(prompt: str, temperature: float = 0.2) -> str:
res = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
)
return res.choices[0].message.content
RAG(比較用)(rag.py)
比較用のRAGは、生データの先頭チャンクをコンテキストとして渡します。
def answer_rag(question: str, raw: str) -> str:
chunks = "\n".join(raw.split("\n")[:6])
prompt = f"""Use only the given context to answer.
Q: {question}
Context:
{chunks}
"""
return chat(prompt)
Wiki構築 — Ingestサイクル(wiki.py)
生データを受け取り、構造化されたWikiを生成します。Problem、Root Cause、Fix、Context、Misdiagnosis、Insightの6セクションを指定することで、LLMが因果関係を整理して出力します。
def build_wiki(raw: str) -> str:
prompt = f"""Convert the following raw data into structured knowledge.
Use these sections:
## Problem
## Root Cause
## Fix
## Context
## Misdiagnosis
## Insight
Raw data:
{raw}
"""
return chat(prompt)
Wiki更新 — Queryサイクル(updater.py)
新しい生データが追加されたとき、既存のWikiを保持しながら新しい知識を統合します。
def update_wiki(raw: str, wiki: str, answer: str) -> str:
prompt = f"""Improve the existing knowledge base using the new answer.
Merge new insights, fix inaccuracies, and preserve existing knowledge.
Raw data:
{raw}
Latest answer:
{answer}
Current wiki:
{wiki}
"""
return chat(prompt)
RAG・GraphRAG・LLM Wikiの比較
ここまでRAGとLLM Wikiを比較してきましたが、RAGの進化形としてGraphRAGも存在します。以前、RAG vs GraphRAGの比較記事で検証したように、GraphRAGはグラフ構造でエンティティ間の関係性をモデル化することで、従来のRAGの弱点を補います。では、GraphRAGはLLM Wikiとどう違うのでしょうか。
RAGはベクトル類似度でチャンクを検索します。「工具」と「ドライバー」のような類似度が高い単語は拾えますが、「どこで使われるか」のような関係性には弱くなります。例えば、「写真の工具はどこで使われるか?」という質問に対して、RAGは「家庭修理やDIY」と的外れな回答をしました。
GraphRAGはNeo4jなどのグラフDBでエンティティ間の関係(ドライバー → 車の整備)を構造化し、全文検索とベクトル検索を組み合わせます。同じ質問に対して「車の整備」と正確に回答できました。エンティティ間の関係性を明示的に保持する点が強みです。
LLM Wikiはさらに異なるアプローチをとります。検索の精度を上げるのではなく、回答のたびに知識そのものをコンパイルし直します。グラフDBやベクトルDBを使わず、人間が読めるMarkdownに知識を構造化します。
RAG: Raw → ベクトル化 → 類似度検索 → 回答(→ 消える)
GraphRAG: Raw → グラフ構造化 → 関係性検索 → 回答(→ 消える)
LLM Wiki: Raw → 知識コンパイル → Wiki → 回答 → Wiki更新(→ 蓄積される)
| RAG | GraphRAG | LLM Wiki | |
|---|---|---|---|
| アプローチ | ベクトル類似度検索 | グラフ構造 + 全文検索 | 知識コンパイル + 継続的更新 |
| 関係性の表現 | なし(類似度のみ) | エッジで明示的に保持 | Markdown内で文脈として記述 |
| データ形式 | ベクトル(不透明) | グラフDB(半透明) | Markdown(完全に透明) |
| 知識の寿命 | リクエスト単位(揮発) | リクエスト単位(揮発) | 永続(蓄積・複利) |
| 回答後の学習 | しない | しない | 回答がWikiに書き戻される |
| 監査性 | 限定的 | 中程度(グラフは参照可能) | 完全(ファイル単位 + Git) |
| 適正スケール | 数百万件の大量ドキュメント | 数万〜数十万件 | 数百〜数万件の高品質ドキュメント |
| 導入の複雑さ | 低い | 中程度(グラフDB運用が必要) | 中程度(LLMエージェント設計が必要) |
GraphRAGが解決したのは検索の精度です。しかし、GraphRAGもRAGと同じくステートレスです。どれだけ優れた検索をしても回答は蓄積されず、同じ質問が来たら同じ検索を繰り返します。LLM Wikiが解決するのは知識の寿命です。つまり、3者は競合ではなく、GraphRAGで検索精度を高めつつ、LLM Wikiで知識を蓄積するという組み合わせが、将来的には最も強力なアーキテクチャになるのは理想的だと考えました。
検索精度: RAG < GraphRAG
知識の蓄積: RAG = GraphRAG < LLM Wiki
デメリットと限界
メリットだけ語るのはフェアではありません。導入前に知っておくべき点を整理します。
まず、幻覚汚染リスクがあります。LLMが誤った情報をWikiに書き込むと、それ以降のすべてのクエリがその誤情報を参照しえます。Lintで矛盾は検出できますが、LLMが一貫して間違っている場合は検出が難しくなります。人間のレビューサイクルを組み込む必要があると思います。
次に、スケール限界があります。適正規模は数百〜数万件の高品質ドキュメントです。数百万件の異種ドキュメントを扱うエンタープライズ用途にはRAGの方が適しています。二者択一ではなく使い分けが現実的です。
将来展望:Knowledge Distillation
Karpathy氏が示している今後のロードマップには、LLM Wikiから合成データを作成し、それをドメイン特化モデルのファインチューニングに活用するという道筋が含まれています。
Wiki → 合成データ作成 → ファインチューニング → ドメイン特化モデル
知識のコンパイルが、最終的にはモデル自体の改善につながります。このパス(知識の伝達のフロー)はGistに明示的に記述されています。
まとめ
今回のデモで実際に確認できたのは、RAGが「Podメモリ増設は効果なし」という事実しか返せない場面で、LLM Wikiは「なぜ効果がないか(413はメモリ不足ではなくサイズ制限)」まで回答したことです。そして2回目のIngest後、LLM WikiはWAFパターンという新しい知識を統合し、RAGが永遠に知らないままの情報を回答に含めました。
RAGが「倉庫から検索して答える」、GraphRAGが「関係性を理解して検索する」なら、LLM Wikiは「図書館を育てる司書」です。回答のたびに知識が積み上がり、複利のように次の回答が豊かになります。今回のデモは最小構成ですが、Karpathy氏は同じアーキテクチャで約100記事のWikiをすでに運用しています。
終わり
ここで、この記事は以上になります。
最後までお読みいただきましてありがとうございました。
参考
[1] Andrej Karpathy; LLM Knowledge Bases; GitHub Gist[2] RAG vs GraphRAG の比較; GMO グローバルサイン・ホールディングス Tech Blog