TECHNICAL NOTE
Knowledge Graph Collectorを開発した
背景と課題
新しい分野を学ぶとき、検索結果はたくさん出てきます。しかし地図なしの探索は、面白い記事に寄り道できる反面、「何から始めるべきか」「どの概念が前提になるか」を見失いがちです。本プロジェクトは、曖昧な自然文の学習相談を、段階的にたどれる構造へ変換することを目指しました。
たとえば「Python でデータ分析を学びたい。統計も含めて基礎から知りたい」という入力に対し、単語一覧を返すのではなく、前提・基礎・中核・応用がつながった「次の一歩が見える」グラフを出力します。
1つの問いが、学習地図になるまで
- 問いを投げる — 学びたいテーマを自然文で入力する
- 探索の作戦を立てる — ローカル LLM が学習意図を調査計画に分解する
- 材料を集める — Wikipedia から広く関連候補を収集し、Google Trends の傾向も取得する
- 道筋を描く — LLM が教育的な依存関係を持つ 15〜25 個のノードへ再構成する
- 地図を歩く — ブラウザ上でグラフを探索し、ノード詳細やレイヤーを確認する
ここで大切にしたのは、「情報量の多い地図」ではなく、「最初の一歩を選べる地図」です。
システム設計
全体アーキテクチャ
外部の生成 API に依存せず、Ollama 上で動く gemma4 を使う設計です。学習テーマや収集候補をローカル環境で扱えるため、データの取り扱いをシンプルに保てます。外の世界から知識の手がかりを集め、考える場所は手元に残す、という役割分担です。
生成パイプライン
学習構造のモデル
エッジは単なる関連リンクではなく、A を理解してから B を学ぶ という教育的な依存関係として扱います。Wikipedia の「つながっている」と、学習の「先に必要」は別物です。この区別により、表示順序がそのまま学習ロードマップになります。
工夫した点
LLM を使う部分ほど、気持ちよく動く仕組みだけでなく、期待どおりに動かないときの受け止め方が重要になります。以下は、そのための実装上の工夫です。
1. 自然文を「調査可能な問い」に変換
最初からグラフを作らず、LLM に seed_terms、含める概念、除外する概念、難易度のヒントを JSON で作らせます。いきなり答えを描かせるのではなく、先に「どこを探検するか」を決める方式です。これにより、ユーザーの「基礎から」「統計も含めたい」といった文脈を候補収集へ反映します。
2. LLM 出力をそのまま信頼しない
LLM は頼もしい共同作業者ですが、ときどき JSON の前後に説明を書いてしまいます。そこで応答から JSON 部分だけを抽出し、失敗時は最大 3 回まで JSON 専用の再生成を行います。その後 Pydantic モデルへ変換して、次を正規化します。
- ノード ID の形式と重複
layerを 0〜3 の範囲に制限- ノード種別を
FOUNDATIONAL / BASIC / CORE / APPLICATIONに統一 - 存在しないノードを参照するエッジを除外
- エッジ重みを 0.0〜1.0 に制限
3. 外部データ取得の失敗に耐える
Wikipedia はタイムアウト、HTTP 429、再試行待機を扱い、ページ取得結果をメモリにキャッシュします。Google Trends は 5 件ずつのバッチで取得し、失敗した候補は 0 として処理を継続します。
4. 依存関係を操作できる可視化 UI
標準ライブラリの http.server と HTML Canvas で、軽量なローカル UI を実装しました。生成済みグラフの切替、ノード検索、ドラッグ・ズーム、Fit、ノード詳細、レイヤー別一覧を提供します。ノードを眺めるだけで終わらず、「この概念の次は何か」を手でたどれる画面を目指しました。
技術スタック
| 区分 | 技術 | 用途 |
|---|---|---|
| 言語 | Python 3.11+ | パイプライン、HTTP サーバー、データ処理 |
| ローカル LLM | Ollama / gemma4 | 調査計画と学習グラフの JSON 生成 |
| 外部データ | MediaWiki API | 関連概念の候補収集 |
| 外部データ | pytrends / Google Trends | 候補の検索傾向取得 |
| データ検証 | Pydantic | ノード・エッジ・グラフの型定義と正規化 |
| グラフ処理 | NetworkX | グラフ処理の基盤 |
| Web UI | http.server / HTML / CSS / JavaScript Canvas | ローカルでの閲覧・操作・描画 |
| 永続化 | JSON | 生成結果の保存と再表示 |
データ設計
生成結果は output/graphs/{topic}_{timestamp}.json に保存し、既存 UI との互換用に output/knowledge_graph.json にも保存します。複数回生成した結果を UI から選択・比較できる構成です。
実装詳細
モジュール境界と責務
| モジュール | 入力 | 出力 | 責務 |
|---|---|---|---|
main.py | CLI 引数 | なし | 直接実行とパッケージ実行の入口。処理は pipeline に委譲 |
pipeline.py | 自然文テーマ | 保存済みグラフの要約 | 処理のオーケストレーション、候補数制御、ファイル保存 |
gemma_graph_builder.py | テーマ・候補語・調査計画 | KnowledgeGraph | プロンプト設計、Ollama 呼び出し、JSON 復旧、スキーマ正規化 |
wikipedia_collector.py | シード語 | 関連候補の配列 | MediaWiki API の取得・キャッシュ・候補フィルタリング |
trends_collector.py | 候補語の配列 | {候補語: 平均関心度} | 5 件単位の Trends 取得とリトライ |
graph_models.py | ノード・エッジ値 | Pydantic モデル | グラフの不変条件を型として定義 |
web_app.py | HTTP リクエスト | HTML / JSON | ローカル API、保存済み JSON の安全な参照、Canvas 描画 |
pipeline.py をユースケース層、各 collector を外部サービスへのアダプター層、graph_models.py をドメインモデル層として分離しています。料理でいえば、pipeline は進行役、collector は食材の調達役、model は完成品の形を守る型です。UI は生成ロジックを持たず、POST /generate を通して pipeline を呼び出すため、同じ生成処理を CLI と Web UI で共有できます。
候補収集のアルゴリズム
調査計画で得た seed_terms と must_include を、入力テーマと重複除去してシードにします。各シードから最大 35 件を収集し、最大 8 シード、全体で最大 180 件に制限しています。候補数を抑えることで、外部 API の負荷と後段のプロンプトサイズを制御します。
Wikipedia 候補は次の条件で絞り込みます。いずれかの条件で候補が空になった場合は、リンク先の基本候補へフォールバックします。
本文類似度は日本語・英数字をトークン化し、各文書の語頻度と文書頻度から TF-IDF ベクトルを生成してコサイン類似度を求めます。関連候補の収集段階では、Wikipedia のリンク構造を「候補発見」に使い、最終的な学習順序の根拠には使いません。
LLM 統合と出力契約
Ollama の /api/generate をストリーミングで呼び出し、受信したトークンは必要に応じて UI 用コールバックへ渡します。LLM に返させる形式は JSON オブジェクトのみで、二段階に分けています。
| 段階 | 主なフィールド | 意図 |
|---|---|---|
| 調査計画 | normalized_topic, seed_terms, must_include, must_exclude, complexity_hint | ユーザーの曖昧な意図を、情報収集の条件へ変換 |
| グラフ生成 | nodes[], edges[] | 概念・レイヤー・前提関係を持つ学習ロードマップを構成 |
JSON として解析できない応答には、コードフェンスを除去し、最初の { から最後の } を抽出する復旧処理を適用します。それでも失敗した場合は「JSON オブジェクトのみを返す」再試行プロンプトを追加し、最大 3 回試行します。
生成後には、ラベルや ID の別表記を alias_map で解決します。これにより、LLM がエッジで日本語ラベルを返した場合でも、正規化済みの node_id に接続できます。解決できない端点を持つエッジと重複エッジは保存対象から除外します。
グラフの不変条件
Pydantic モデルと正規化関数によって、保存する JSON に以下の性質を持たせます。
| 対象 | 制約 |
|---|---|
| ノード ID | 英数字とアンダースコアへ正規化し、先頭が数字の場合は接頭辞を付与 |
| レイヤー | 0..3 に丸め込み |
| ノード種別 | FOUNDATIONAL、BASIC、CORE、APPLICATION のいずれか |
| エッジ | source / target の双方が既存ノード ID であるものだけを採用 |
| エッジ重み | 0.0..1.0 に丸め込み |
| ノード数 | 通常 15〜25 を指示。25 を超えた場合は最大 20 個に絞るよう再生成 |
このガード層は、LLM の生成品質に揺らぎがあっても UI が扱えるグラフ JSON を安定して得るための境界です。自由に考えさせる部分と、壊れない形に整える部分を分けることで、創造性と運用性を両立させています。
API とローカル安全性
Web サーバーは 127.0.0.1:8000 にのみバインドし、外部ネットワークへ公開しない前提です。エンドポイントは最小限にしています。
| Method | Path | 概要 |
|---|---|---|
GET | / | 単一ページの UI を返却 |
GET | /graph-files | output/graphs/ 配下の保存済みグラフ一覧を返却 |
GET | /graph.json?file=... | 指定したグラフ JSON を返却 |
POST | /generate | { "topic": "..." } を受け、グラフを生成・保存 |
/graph.json のファイル指定はプロジェクトルートからの相対パスへ解決し、プロジェクト外へ到達するパスは拒否します。これにより、クエリ文字列を利用したプロジェクト外の任意ファイル読み取りを防ぎます。/generate は空のテーマを 400 として拒否します。
障害時の挙動
- MediaWiki API は 20 秒のタイムアウトを設定し、HTTP 429 を検知した場合は
Retry-Afterを優先して待機します。 - Google Trends は 5 キーワードずつ取得し、各バッチを最大 3 回試行します。取得できない語は値
0として後続処理を止めません。 - UI でクライアントが切断した場合は
BrokenPipeErrorなどを捕捉し、サーバー全体を停止させません。
現在の設計上の選択
Google Trends の値はパイプラインで取得している一方、現在の build_graph_with_llm() ではグラフの採点・並べ替えにはまだ反映していません(互換用引数として受け取るのみ)。候補の人気度と教育的な重要度は一致しないため、現段階では LLM が教育的な依存関係を判断する設計を優先しています。今後は、検索傾向を「補助シグナル」として重みに統合し、人気のみに引きずられないランキングへ発展させる余地があります。
実行方法
python3 -m venv myenv
source myenv/bin/activate
pip install -r requirements.txt
ollama pull gemma4
python src/collector/web_app.py
ブラウザで http://127.0.0.1:8000 を開きます。CLI からは次のように実行できます。
python src/collector/main.py "Pythonでデータ分析を学びたい。統計も含めて基礎から知りたい"
今後の展望
現在は「学習を始めるための地図」を作る段階です。次は、その地図が学習者の歩み方に合わせて育つようにします。
- 学習履歴(理解度・復習回数・認知負荷)の永続化と、次に学ぶ項目の推薦
- 生成したグラフの編集・差分比較・共有
- 長時間の生成を非同期ジョブ化し、進捗をより詳細に表示
- 候補選定における Google Trends の活用範囲を拡張
リポジトリ構成
src/
├── collector/
│ ├── pipeline.py # 主パイプライン
│ ├── gemma_graph_builder.py # LLM 呼び出しと出力正規化
│ ├── wikipedia_collector.py # Wikipedia 候補収集
│ ├── trends_collector.py # 検索傾向取得
│ └── web_app.py # Web UI / API
└── models/
└── graph_models.py # Pydantic データモデル
output/
└── graphs/ # 生成済みナレッジグラフ