| # NullAI プロジェクト完全理解ガイド | |
| **最終更新**: 2025-12-02 | |
| **対象読者**: このプロジェクトを引き継ぐ全ての開発者 | |
| **目的**: プロジェクトの全体像を完全に理解し、設計思想を正しく継承する | |
| --- | |
| ## 📖 目次 | |
| 1. [プロジェクト概要](#プロジェクト概要) | |
| 2. [4つの核心思想(こだわりポイント)](#4つの核心思想こだわりポイント) | |
| 3. [システムアーキテクチャ全体図](#システムアーキテクチャ全体図) | |
| 4. [各システムの詳細解説](#各システムの詳細解説) | |
| 5. [データフロー完全図解](#データフロー完全図解) | |
| 6. [技術スタック詳細](#技術スタック詳細) | |
| 7. [よくある誤解と注意点](#よくある誤解と注意点) | |
| 8. [設計判断の理由](#設計判断の理由) | |
| 9. [拡張時の考慮事項](#拡張時の考慮事項) | |
| --- | |
| ## プロジェクト概要 | |
| ### NullAIとは何か | |
| **NullAI**は、**自己進化型多ドメイン知識推論エンジン**です。 | |
| #### 核心的な問いと答え | |
| **Q: 何を解決しようとしているのか?** | |
| A: 「AIのハルシネーション(幻覚)」と「小型モデルの性能不足」の両方を同時に解決 | |
| **Q: どうやって解決するのか?** | |
| A: | |
| 1. **DB優先推論(RAG)** → ハルシネーション削減 | |
| 2. **師匠→弟子のファインチューニング** → 小型モデルの性能向上 | |
| 3. **樹木型空間記憶** → 知識の意味的整理と高速検索 | |
| 4. **自己拡充サイクル** → 知識ベースの自動成長 | |
| **Q: 他のRAGシステムとの違いは?** | |
| A: | |
| - ❌ 普通のRAG: ベクトルDBで検索するだけ | |
| - ✅ NullAI: **6次元空間座標**で知識を配置し、意味的な近傍検索が可能 | |
| **Q: 他のファインチューニングシステムとの違いは?** | |
| A: | |
| - ❌ 普通のFT: 人間が訓練データを手動作成 | |
| - ✅ NullAI: **師匠AIが自動的に訓練データを生成** → 弟子が学習 → 弟子が師匠に昇格 → 無限サイクル | |
| ### プロジェクト名の由来 | |
| **Null** = ゼロ(ハルシネーション) | |
| **AI** = Artificial Intelligence | |
| → **ゼロ・ハルシネーションを目指すAI** | |
| --- | |
| ## 4つの核心思想(こだわりポイント) | |
| ### 1️⃣ 倒木システム(Fallen Tree System) | |
| #### 比喩の意味 | |
| 森で大木(老いた木)が倒れると、その養分で新しい若木が育つ。NullAIでは: | |
| - 🌲 **大木(師匠モデル)**: 高性能だが重いAI(例: DeepSeek R1 32B) | |
| - 🌱 **若木(弟子モデル)**: 最初は空っぽだが軽量なAI(例: Phi-2 2.7B) | |
| - 🍂 **養分(訓練データ)**: 師匠の高品質な出力(Alpaca形式JSONL) | |
| #### システムの流れ | |
| ``` | |
| ┌─────────────────────────────────────────────────────┐ | |
| │ Phase 1: 師匠の統治時代 │ | |
| ├─────────────────────────────────────────────────────┤ | |
| │ 師匠(DeepSeek R1)が推論を担当 │ | |
| │ ↓ │ | |
| │ 高品質な出力(confidence >= 0.8)が自動保存 │ | |
| │ ↓ │ | |
| │ training_data/master_outputs/*.jsonl │ | |
| └─────────────────────────────────────────────────────┘ | |
| ↓ | |
| ┌─────────────────────────────────────────────────────┐ | |
| │ Phase 2: ファインチューニング │ | |
| ├─────────────────────────────────────────────────────┤ | |
| │ 訓練データを使って弟子(Phi-2)を訓練 │ | |
| │ ↓ │ | |
| │ 弟子の性能が向上(師匠の知識を吸収) │ | |
| │ ↓ │ | |
| │ training_data/checkpoints/apprentice_*/ │ | |
| └─────────────────────────────────────────────────────┘ | |
| ↓ | |
| ┌─────────────────────────────────────────────────────┐ | |
| │ Phase 3: 世代交代(倒木) │ | |
| ├─────────────────────────────────────────────────────┤ | |
| │ 弟子が十分成長 → 師匠に昇格 │ | |
| │ ↓ │ | |
| │ 旧師匠(DeepSeek)は引退(でも特別な役割あり) │ | |
| │ ↓ │ | |
| │ 新しい空の弟子を生成 │ | |
| └─────────────────────────────────────────────────────┘ | |
| ↓ | |
| サイクル繰り返し | |
| ``` | |
| #### 重要な設計判断 | |
| **Q: なぜ師匠を完全に削除しないのか?** | |
| A: 引退した師匠(DeepSeek)は「**永久的指導者**」として残る | |
| - DB拡充時のプロンプト生成 | |
| - 新しいドメインの初期知識生成 | |
| - 品質チェック | |
| **Q: 弟子はいつ師匠になれるのか?** | |
| A: | |
| - ファインチューニング完了後、手動で昇格 | |
| - 将来的には自動評価で昇格判定(未実装) | |
| **Q: 複数の弟子を同時に訓練できるのか?** | |
| A: できる。ドメイン別に異なる弟子を訓練可能 | |
| - 医療ドメイン弟子 | |
| - 法律ドメイン弟子 | |
| - 一般知識弟子 | |
| ### 2️⃣ DB分離構造(Database Separation Structure) | |
| #### 設計思想 | |
| ``` | |
| 質問が来た時の判断フロー: | |
| 質問 → まず知識DBを検索 | |
| ├─ 見つかった → DB知識を使って推論(RAG)✅ 信頼性高 | |
| └─ 見つからない → AI内部知識で推論 ⚠️ ハルシネーションリスク | |
| ↓ | |
| その出力をDBに保存(自己拡充) | |
| ``` | |
| #### DB優先の理由 | |
| | 知識ソース | 信頼性 | 根拠 | ハルシネーション | | |
| |-----------|--------|------|-----------------| | |
| | 知識DB(.iath) | ⭐⭐⭐⭐⭐ | 人間が検証 or 専門家が作成 | ほぼゼロ | | |
| | AI生成知識 | ⭐⭐⭐ | AIの内部知識(学習データ由来) | 中程度 | | |
| | AI幻覚 | ⭐ | 推測・創作 | 高い | | |
| **結論**: 知識DBにあるものは絶対に使う → ハルシネーション削減 | |
| #### 自己拡充の仕組み | |
| ```python | |
| # 疑似コード | |
| async def infer(question): | |
| # Step 1: DB検索 | |
| db_knowledge = search_db(question) | |
| if db_knowledge: | |
| # Step 2a: RAG推論(DBの知識を使う) | |
| response = llm.generate( | |
| f"Based on this verified knowledge: {db_knowledge}\n" | |
| f"Answer: {question}" | |
| ) | |
| return response | |
| else: | |
| # Step 2b: AI内部知識で推論 | |
| response = llm.generate(question) | |
| # Step 3: 高品質なら保存(自己拡充) | |
| if response.confidence >= 0.7: | |
| save_to_db(question, response) | |
| return response | |
| ``` | |
| #### 重要な設計判断 | |
| **Q: なぜSQLiteと.iathの2つを使うのか?** | |
| A: 役割分担 | |
| - **SQLite**: メタデータ(ユーザー、ワークスペース、推論履歴) | |
| - **.iath**: 知識タイル本体(6次元座標 + コンテンツ) | |
| **Q: confidence >= 0.7と0.8の違いは?** | |
| A: | |
| - `>= 0.7`: DB保存(自己拡充)← やや緩め | |
| - `>= 0.8`: 訓練データ保存 ← 厳しめ(高品質のみ) | |
| **Q: AI生成知識をDBに保存する際、人間のチェックは不要?** | |
| A: 現在は自動保存。将来的には: | |
| - 専門家によるレビューフロー | |
| - コミュニティ投票による品質評価 | |
| - AIによる自動検証(別のAIでクロスチェック) | |
| ### 3️⃣ 樹木型空間記憶(Dendritic Memory Space) | |
| #### 比喩の意味 | |
| 人間の脳の**樹状突起(デンドライト)**のように、知識が空間的に整理されている。 | |
| 通常のDB: | |
| ``` | |
| 知識1: 「心臓は循環器官である」 | |
| 知識2: 「脳は中枢神経系の一部である」 | |
| → バラバラに保存(関連性が不明) | |
| ``` | |
| 樹木型空間記憶: | |
| ``` | |
| 知識1: 座標 [0.2, 0.8, 0.3, 0.9, 0.7, 0.8] | |
| 知識2: 座標 [0.3, 0.8, 0.4, 0.85, 0.65, 0.75] | |
| → 近い座標 = 意味的に関連 → 一緒に検索できる | |
| ``` | |
| #### 6次元座標系の詳細 | |
| ``` | |
| Knowledge Tile の座標 = [x, y, z, c, g, v] | |
| ─────┬───── ─────┬───── | |
| medical_space meta_space | |
| ``` | |
| ##### medical_space [x, y, z]: ドメイン固有の3次元空間 | |
| 例: 医療ドメインの場合 | |
| | 軸 | 意味 | 例 | | |
| |----|------|-----| | |
| | x | 解剖学的位置 | 0.0=神経系, 0.5=循環器, 1.0=消化器 | | |
| | y | 病理学的分類 | 0.0=感染症, 0.5=代謝疾患, 1.0=外傷 | | |
| | z | 治療レベル | 0.0=予防, 0.5=診断, 1.0=治療 | | |
| ##### meta_space [c, g, v]: メタ情報の3次元空間 | |
| | 軸 | 意味 | 値の範囲 | | |
| |----|------|----------| | |
| | c (Certainty) | 確実性 | 0.0=仮説, 0.5=定説, 1.0=確立された事実 | | |
| | g (Granularity) | 粒度 | 0.0=概要, 0.5=詳細, 1.0=専門的 | | |
| | v (Verification) | 検証状態 | 0.0=未検証, 0.5=専門家レビュー済, 1.0=複数ソース確認済 | | |
| #### 検索の仕組み | |
| ##### 1. テキスト検索(従来型) | |
| ```python | |
| def search_by_text(query): | |
| # 単純なキーワードマッチング | |
| results = [tile for tile in all_tiles | |
| if query in tile.content] | |
| return results | |
| ``` | |
| **問題点**: 同義語を見逃す | |
| - 「心臓病」で検索しても「循環器疾患」がヒットしない | |
| ##### 2. 座標検索(空間検索) | |
| ```python | |
| def search_by_coordinates(query_coords, top_k=5): | |
| # 6次元ユークリッド距離で計算 | |
| distances = [] | |
| for tile in all_tiles: | |
| dist = euclidean_distance(query_coords, tile.coords) | |
| distances.append((tile, dist)) | |
| # 距離が近い順にソート | |
| distances.sort(key=lambda x: x[1]) | |
| return distances[:top_k] | |
| ``` | |
| **利点**: 意味的に近い知識を自動で発見 | |
| - 座標が近い = 意味的に関連 | |
| ##### 3. ハイブリッド検索(推奨) | |
| ```python | |
| def hybrid_search(query_text, query_coords=None, top_k=5): | |
| # テキストマッチスコア計算 | |
| text_scores = calculate_text_match(query_text) | |
| # 座標距離スコア計算 | |
| if query_coords: | |
| spatial_scores = calculate_spatial_distance(query_coords) | |
| # 複合スコア = α * text_score + β * (1 - spatial_distance) | |
| combined_scores = 0.4 * text_scores + 0.6 * spatial_scores | |
| return top_k_results(combined_scores) | |
| ``` | |
| #### .iathファイル形式 | |
| ``` | |
| .iath ファイル構造: | |
| ┌────────────────────────────────────┐ | |
| │ Header (64 bytes) │ ← マジックナンバー、バージョン | |
| ├────────────────────────────────────┤ | |
| │ Index (JSON, 可変長) │ ← タイルIDとオフセット一覧 | |
| │ { │ | |
| │ "tiles": [ │ | |
| │ {"id": "tile_001", "offset": 512}, | |
| │ {"id": "tile_002", "offset": 2048} | |
| │ ] │ | |
| │ } │ | |
| ├────────────────────────────────────┤ | |
| │ Data Section (zstd圧縮) │ | |
| │ ┌──────────────────────┐ │ | |
| │ │ Tile 1 (JSON) │ │ | |
| │ │ - metadata │ │ | |
| │ │ - content │ │ | |
| │ │ - coordinates │ │ | |
| │ │ - verification │ │ | |
| │ └──────────────────────┘ │ | |
| │ ┌──────────────────────┐ │ | |
| │ │ Tile 2 (JSON) │ │ | |
| │ └──────────────────────┘ │ | |
| │ ... │ | |
| └────────────────────────────────────┘ | |
| ``` | |
| **なぜzstd圧縮?** | |
| - 高い圧縮率(gzipより優れる) | |
| - 高速な解凍速度 | |
| - Facebookが開発(信頼性) | |
| #### 重要な設計判断 | |
| **Q: なぜ6次元? 3次元や10次元ではダメ?** | |
| A: | |
| - 3次元: ドメイン知識だけでメタ情報が表現できない | |
| - 10次元以上: 次元の呪い(検索が遅くなる)、人間が理解不能 | |
| - 6次元: ドメイン(3) + メタ(3) = バランスが良い | |
| **Q: 座標は誰が決めるのか?** | |
| A: | |
| - 現状: 人間が手動で設定(dendritic-memory-editorで) | |
| - Priority 2で実装予定: AIが自動推定(DeepSeekが座標を生成) | |
| **Q: .iathとFAISS(ベクトルDB)の違いは?** | |
| A: | |
| | 特徴 | .iath | FAISS | | |
| |------|-------|-------| | |
| | 座標次元 | 6次元(人間が理解可能) | 768次元(Embeddingモデル依存) | | |
| | 検索速度 | O(n) 線形探索 | O(log n) 高速 | | |
| | 意味の透明性 | 高い(座標の意味が明確) | 低い(ブラックボックス) | | |
| | 編集容易性 | 高い(座標を手動調整可能) | 低い(再Embedding必要) | | |
| **結論**: .iathは「人間が理解・編集できる知識ベース」を重視 | |
| ### 4️⃣ ローカルファースト & ワンコマンドセットアップ | |
| #### 設計思想 | |
| ``` | |
| ❌ 悪い例(クラウド依存): | |
| pip install nullai | |
| nullai --api-key=YOUR_OPENAI_KEY # クラウドAPI必須 | |
| → インターネット必須、コスト高、プライバシー懸念 | |
| ✅ NullAI: | |
| ./start_null_ai.sh # ローカルで完結 | |
| → オフライン可能、無料、プライバシー保護 | |
| ``` | |
| #### ワンコマンドの実現方法 | |
| `start_null_ai.sh`が自動で実行すること: | |
| 1. ✅ 依存関係チェック(Python, Node.js, Ollama) | |
| 2. ✅ 仮想環境作成(venv) | |
| 3. ✅ Python依存関係インストール | |
| 4. ✅ Node.js依存関係インストール | |
| 5. ✅ データベース初期化(sql_app.db) | |
| 6. ✅ Ollama起動 | |
| 7. ✅ バックエンド起動(port 8000) | |
| 8. ✅ フロントエンド起動(port 5173) | |
| 9. ✅ .iathメモリロード確認 | |
| **ユーザーがすることは**: `./start_null_ai.sh`を実行するだけ | |
| #### 重要な設計判断 | |
| **Q: なぜOllamaを使うのか? HuggingFaceだけではダメ?** | |
| A: | |
| - Ollama: モデル管理が楽(`ollama pull deepseek-r1`だけ) | |
| - HuggingFace: 手動でダウンロード、パス指定が面倒 | |
| **Q: なぜDockerを使わないのか?** | |
| A: | |
| - Docker: 初心者には難しい、GPUパススルーが複雑 | |
| - シェルスクリプト: シンプル、デバッグしやすい、カスタマイズ容易 | |
| --- | |
| ## システムアーキテクチャ全体図 | |
| ``` | |
| ┌─────────────────────────────────────────────────────────────────┐ | |
| │ Frontend (React + TypeScript) │ | |
| │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ | |
| │ │ Engine │ │ Inference │ │ Training │ │ | |
| │ │ Manager │ │ Panel │ │ Dashboard │ │ | |
| │ └──────────────┘ └──────────────┘ └──────────────┘ │ | |
| │ │ │ │ │ | |
| │ └──────────────────┼──────────────────┘ │ | |
| │ │ │ | |
| │ HTTP/WebSocket │ | |
| │ │ │ | |
| └───────────────────────────┼─────────────────────────────────────┘ | |
| │ | |
| ┌───────────────────────────┼─────────────────────────────────────┐ | |
| │ Backend (FastAPI) │ | |
| │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ | |
| │ │ config.py │ │ questions.py │ │ training.py │ │ | |
| │ │ (Engine API) │ │ (Inference) │ │ (Fine-tune) │ │ | |
| │ └──────────────┘ └──────────────┘ └──────────────┘ │ | |
| │ │ │ │ │ | |
| │ └──────────────────┼──────────────────┘ │ | |
| │ │ │ | |
| └───────────────────────────┼─────────────────────────────────────┘ | |
| │ | |
| ┌───────────────────────────┼─────────────────────────────────────┐ | |
| │ NullAI Core Logic │ | |
| │ ┌────────────────────────────────────────────────┐ │ | |
| │ │ model_router.py │ │ | |
| │ │ - RAG推論統合 │ │ | |
| │ │ - 師匠出力保存 │ │ | |
| │ │ - エンジン管理(スワップ、昇格) │ │ | |
| │ └────────────────────────────────────────────────┘ │ | |
| │ │ │ │ │ | |
| │ ▼ ▼ ▼ │ | |
| │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ | |
| │ │ iath_memory │ │ llm_providers│ │ fine_tuning │ │ | |
| │ │ .py │ │ .py │ │ .py │ │ | |
| │ │ (6D Search) │ │ (4 Providers)│ │ (PEFT/Unslo) │ │ | |
| │ └──────────────┘ └──────────────┘ └──────────────┘ │ | |
| │ │ │ │ │ | |
| └─────────┼──────────────────┼──────────────────┼─────────────────┘ | |
| │ │ │ | |
| ▼ ▼ ▼ | |
| ┌──────────────────────────────────────────────────────────────┐ | |
| │ External Services │ | |
| │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ | |
| │ │ knowledge_ │ │ Ollama │ │ HuggingFace │ │ | |
| │ │ base.iath │ │ (localhost) │ │ Models │ │ | |
| │ │ (6D Memory) │ │ │ │ │ │ | |
| │ └──────────────┘ └──────────────┘ └──────────────┘ │ | |
| │ │ | |
| │ ┌──────────────┐ ┌──────────────────────────────────┐ │ | |
| │ │ sql_app.db │ │ training_data/ │ │ | |
| │ │ (SQLite) │ │ - master_outputs/*.jsonl │ │ | |
| │ │ │ │ - checkpoints/apprentice_*/ │ │ | |
| │ └──────────────┘ └──────────────────────────────────┘ │ | |
| └──────────────────────────────────────────────────────────────┘ | |
| ``` | |
| --- | |
| ## 各システムの詳細解説 | |
| ### ModelRouter (`null_ai/model_router.py`) | |
| #### 役割 | |
| NullAIの**頭脳**。全ての推論リクエストを管理。 | |
| #### 主要メソッド詳細 | |
| ##### `__init__()` | |
| ```python | |
| def __init__(self, config_manager): | |
| self.config_manager = config_manager | |
| self.master_model = None # 師匠モデル | |
| self.apprentice_model = None # 弟子モデル | |
| self.dendritic_memory = None # .iath空間記憶 | |
| # .iathファイルのロード | |
| self._load_dendritic_memory() | |
| ``` | |
| **重要**: 初期化時に自動的に.iathをロード → 起動時間が長くなる可能性 | |
| ##### `async def infer()` - RAG統合推論 | |
| ```python | |
| async def infer(self, prompt, domain_id, model_config, save_to_memory=False): | |
| # Step 1: DB知識チェック | |
| has_knowledge = self._check_db_knowledge(domain_id, prompt) | |
| if has_knowledge: | |
| # Step 2a: RAG推論 | |
| knowledge = self._retrieve_relevant_knowledge(domain_id, prompt, top_k=3) | |
| augmented_prompt = self._build_rag_prompt(prompt, knowledge) | |
| response = await self._perform_llm_inference(model_config, augmented_prompt) | |
| else: | |
| # Step 2b: 通常推論 | |
| response = await self._perform_llm_inference(model_config, prompt) | |
| # Step 3: 高品質なら保存 | |
| if save_to_memory and response["confidence"] >= 0.7: | |
| await self._save_inference_to_db(domain_id, prompt, response) | |
| # Step 4: 師匠の出力なら訓練データとして保存 | |
| is_master = (self.master_model and | |
| model_config.model_id == self.master_model.model_id) | |
| if is_master and response["confidence"] >= 0.8: | |
| await self._save_master_output_as_training_data( | |
| prompt, response["response"], domain_id, response["confidence"] | |
| ) | |
| return response | |
| ``` | |
| **データフロー図**: | |
| ``` | |
| prompt → check DB → found? | |
| ├─ YES → retrieve knowledge | |
| │ ↓ | |
| │ augment prompt | |
| │ ↓ | |
| │ LLM inference → response | |
| │ ↓ | |
| │ is master? → save as training data | |
| │ | |
| └─ NO → LLM inference → response | |
| ↓ | |
| confidence >= 0.7? → save to DB | |
| ``` | |
| ##### `_retrieve_relevant_knowledge()` - ハイブリッド検索 | |
| ```python | |
| def _retrieve_relevant_knowledge(self, domain_id, prompt, top_k=3): | |
| if not self.dendritic_memory: | |
| return [] | |
| # ハイブリッド検索実行 | |
| results = self.dendritic_memory.hybrid_search( | |
| query_text=prompt, | |
| query_coords=None, # 将来的には座標も推定 | |
| top_k=top_k, | |
| text_weight=0.4, # テキストマッチの重み | |
| spatial_weight=0.6 # 空間距離の重み | |
| ) | |
| # Knowledge Tile形式に変換 | |
| formatted_knowledge = [] | |
| for tile in results: | |
| formatted_knowledge.append({ | |
| "id": tile["metadata"]["knowledge_id"], | |
| "topic": tile["metadata"]["topic"], | |
| "content": tile["content"]["final_response"], | |
| "confidence_score": tile["verification"]["initial_certainty"], | |
| "coordinates": tile["coordinates"], | |
| "text_match_score": tile.get("text_match_score", 0), | |
| "spatial_distance": tile.get("spatial_distance", None) | |
| }) | |
| return formatted_knowledge | |
| ``` | |
| ##### `_save_master_output_as_training_data()` - 訓練データ保存 | |
| ```python | |
| async def _save_master_output_as_training_data( | |
| self, prompt, response, domain_id, confidence | |
| ): | |
| # Alpaca形式で保存 | |
| training_example = { | |
| "instruction": f"You are an expert in {domain_id}. Provide accurate information based on verified knowledge.", | |
| "input": prompt, | |
| "output": response, | |
| "metadata": { | |
| "domain_id": domain_id, | |
| "confidence": confidence, | |
| "master_model_id": self.master_model.model_id, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "source": "master_output" | |
| } | |
| } | |
| # JSONLファイルに追記 | |
| output_file = f"training_data/master_outputs/master_outputs_{domain_id}.jsonl" | |
| with open(output_file, 'a', encoding='utf-8') as f: | |
| f.write(json.dumps(training_example, ensure_ascii=False) + '\n') | |
| ``` | |
| **なぜJSONL(改行区切りJSON)?** | |
| - ストリーミング処理が可能(1行ずつ読める) | |
| - ファイル破損時の影響が最小限 | |
| - HuggingFace datasetsと互換性 | |
| ##### エンジン管理メソッド | |
| ```python | |
| def promote_apprentice(self, apprentice_model_id): | |
| """弟子を師匠に昇格""" | |
| # 現在の師匠を引退 | |
| old_master = self.master_model | |
| # 弟子を師匠に昇格 | |
| self.master_model = self.apprentice_model | |
| # 弟子をクリア | |
| self.apprentice_model = None | |
| # 設定を保存 | |
| self.config_manager.save_active_engines( | |
| self.master_model.model_id, None | |
| ) | |
| def swap_engines(self): | |
| """師匠と弟子を入れ替え""" | |
| temp = self.master_model | |
| self.master_model = self.apprentice_model | |
| self.apprentice_model = temp | |
| self.config_manager.save_active_engines( | |
| self.master_model.model_id, | |
| self.apprentice_model.model_id if self.apprentice_model else None | |
| ) | |
| def create_new_apprentice(self, base_model_id): | |
| """新しい空の弟子を生成""" | |
| # ベースモデルをコピーして新しいIDを付与 | |
| new_apprentice_id = f"{base_model_id}_apprentice_{timestamp}" | |
| # 設定に追加 | |
| self.apprentice_model = self.config_manager.get_model_config(base_model_id) | |
| self.apprentice_model.model_id = new_apprentice_id | |
| return new_apprentice_id | |
| ``` | |
| ### DendriticMemorySpace (`null_ai/iath_memory.py`) | |
| #### 役割 | |
| .iathファイルの読み込みと6次元空間検索を提供。 | |
| #### クラス構造 | |
| ```python | |
| class IathDecoder: | |
| """ | |
| .iathファイルの低レベルデコーダー | |
| dendritic-memory-editor完全互換 | |
| """ | |
| def __init__(self, iath_file_path): | |
| self.file_path = Path(iath_file_path) | |
| self.header = None | |
| self.index = [] | |
| self._load_header_and_index() | |
| def _load_header_and_index(self): | |
| """ヘッダーとインデックスの読み込み""" | |
| with open(self.file_path, 'rb') as f: | |
| # Header (64 bytes) | |
| header_bytes = f.read(64) | |
| self.header = self._parse_header(header_bytes) | |
| # Index (JSON) | |
| index_size = self.header["index_size"] | |
| index_bytes = f.read(index_size) | |
| self.index = json.loads(index_bytes.decode('utf-8')) | |
| def get_tile_by_id(self, knowledge_id): | |
| """IDでタイルを取得""" | |
| # インデックスからオフセットを検索 | |
| tile_info = next( | |
| (t for t in self.index["tiles"] if t["id"] == knowledge_id), | |
| None | |
| ) | |
| if not tile_info: | |
| return None | |
| # ファイルポジション移動 | |
| with open(self.file_path, 'rb') as f: | |
| f.seek(tile_info["offset"]) | |
| compressed_data = f.read(tile_info["size"]) | |
| # zstd解凍 | |
| decompressed = zstandard.decompress(compressed_data) | |
| tile_data = json.loads(decompressed.decode('utf-8')) | |
| return tile_data | |
| class DendriticMemorySpace: | |
| """ | |
| 6次元空間記憶システム | |
| 高レベルAPI | |
| """ | |
| def __init__(self, iath_file_path): | |
| self.decoder = IathDecoder(iath_file_path) | |
| self.all_tiles = [] | |
| self.coordinates_matrix = None # NumPy行列 | |
| self._load_all_tiles() | |
| def _load_all_tiles(self): | |
| """全タイルをメモリにロード""" | |
| self.all_tiles = self.decoder.get_all_tiles() | |
| # 座標行列作成(高速検索用) | |
| coords_list = [tile["coordinates"] for tile in self.all_tiles] | |
| self.coordinates_matrix = np.array(coords_list) # Shape: (N, 6) | |
| ``` | |
| #### 検索アルゴリズム詳細 | |
| ##### 座標検索(6次元ユークリッド距離) | |
| ```python | |
| def search_by_coordinates(self, query_coords, top_k=5): | |
| """ | |
| 6次元空間での近傍検索 | |
| 数式: distance = sqrt(sum((q_i - t_i)^2)) | |
| where: | |
| q_i = query座標のi番目の要素 | |
| t_i = tile座標のi番目の要素 | |
| i = 0..5 (6次元) | |
| """ | |
| query_vector = np.array(query_coords) # Shape: (6,) | |
| # 全タイルとの距離を一括計算(NumPy vectorization) | |
| # Broadcasting: (N, 6) - (6,) → (N, 6) | |
| distances = np.linalg.norm( | |
| self.coordinates_matrix - query_vector, | |
| axis=1 # 各行(タイル)ごとに距離計算 | |
| ) # Shape: (N,) | |
| # 距離でソート | |
| sorted_indices = np.argsort(distances)[:top_k] | |
| # 結果を返す | |
| results = [] | |
| for idx in sorted_indices: | |
| tile = self.all_tiles[idx].copy() | |
| tile["spatial_distance"] = float(distances[idx]) | |
| results.append(tile) | |
| return results | |
| ``` | |
| **計算量**: O(N) - 全タイル数Nに比例(線形探索) | |
| **最適化案**(未実装): | |
| - KD-Tree: O(log N) だが6次元では効果薄い | |
| - Ball-Tree: 高次元でも比較的有効 | |
| - 近似近傍探索(Annoy, HNSW): 超高速だが精度低下 | |
| ##### ハイブリッド検索(テキスト + 座標) | |
| ```python | |
| def hybrid_search( | |
| self, | |
| query_text, | |
| query_coords=None, | |
| top_k=5, | |
| text_weight=0.4, | |
| spatial_weight=0.6 | |
| ): | |
| """ | |
| テキストマッチと空間距離の複合スコアリング | |
| """ | |
| # Step 1: テキストマッチスコア計算 | |
| text_scores = [] | |
| for tile in self.all_tiles: | |
| score = self._calculate_text_match(query_text, tile) | |
| text_scores.append(score) | |
| text_scores = np.array(text_scores) # Shape: (N,) | |
| # Step 2: 空間距離スコア計算 | |
| if query_coords: | |
| spatial_distances = np.linalg.norm( | |
| self.coordinates_matrix - np.array(query_coords), | |
| axis=1 | |
| ) | |
| # 距離を0-1のスコアに変換(逆数) | |
| max_dist = spatial_distances.max() | |
| spatial_scores = 1.0 - (spatial_distances / max_dist) | |
| else: | |
| spatial_scores = np.zeros(len(self.all_tiles)) | |
| # Step 3: 複合スコア計算 | |
| combined_scores = ( | |
| text_weight * text_scores + | |
| spatial_weight * spatial_scores | |
| ) | |
| # Step 4: スコアでソート | |
| sorted_indices = np.argsort(combined_scores)[::-1][:top_k] | |
| # 結果を返す | |
| results = [] | |
| for idx in sorted_indices: | |
| tile = self.all_tiles[idx].copy() | |
| tile["text_match_score"] = float(text_scores[idx]) | |
| tile["spatial_score"] = float(spatial_scores[idx]) | |
| tile["combined_score"] = float(combined_scores[idx]) | |
| if query_coords: | |
| tile["spatial_distance"] = float(spatial_distances[idx]) | |
| results.append(tile) | |
| return results | |
| def _calculate_text_match(self, query, tile): | |
| """ | |
| テキストマッチスコア計算(簡易版) | |
| 将来的にはBM25やTF-IDFを使う | |
| """ | |
| query_lower = query.lower() | |
| content = tile["content"]["final_response"].lower() | |
| topic = tile["metadata"]["topic"].lower() | |
| # キーワードマッチング | |
| query_words = set(query_lower.split()) | |
| content_words = set(content.split()) | |
| topic_words = set(topic.split()) | |
| # Jaccard類似度 | |
| content_jaccard = len(query_words & content_words) / len(query_words | content_words) | |
| topic_jaccard = len(query_words & topic_words) / len(query_words | topic_words) | |
| # 複合スコア(トピックを重視) | |
| score = 0.3 * content_jaccard + 0.7 * topic_jaccard | |
| return score | |
| ``` | |
| ### FineTuningManager (`null_ai/fine_tuning.py`) | |
| #### 役割 | |
| 弟子モデルのファインチューニングを実行。 | |
| #### PEFT(QLoRA)方式の詳細 | |
| ```python | |
| async def fine_tune_with_huggingface_peft( | |
| self, | |
| model_name, | |
| training_examples, | |
| output_dir, | |
| epochs=3, | |
| learning_rate=2e-4, | |
| batch_size=4, | |
| lora_r=8, | |
| lora_alpha=16 | |
| ): | |
| """ | |
| Parameter-Efficient Fine-Tuning with QLoRA | |
| QLoRA = Quantized LoRA | |
| - 4-bit量子化でメモリ削減 | |
| - LoRAで訓練パラメータ削減 | |
| → 12GB GPUでも7Bモデルを訓練可能 | |
| """ | |
| # Step 1: モデルを4-bit量子化でロード | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, # 4-bit量子化 | |
| bnb_4bit_quant_type="nf4", # NormalFloat4(最適な量子化方式) | |
| bnb_4bit_compute_dtype=torch.float16, # 計算はfp16で | |
| bnb_4bit_use_double_quant=True # 二重量子化(さらにメモリ削減) | |
| ) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| model_name, | |
| quantization_config=bnb_config, | |
| device_map="auto" # 自動的にGPU/CPUに配置 | |
| ) | |
| # Step 2: LoRA設定 | |
| lora_config = LoraConfig( | |
| r=lora_r, # LoRAランク(低いほど軽量) | |
| lora_alpha=lora_alpha, # スケーリング係数 | |
| target_modules=[ # どのレイヤーにLoRAを適用するか | |
| "q_proj", "k_proj", "v_proj", "o_proj", # Attention | |
| "gate_proj", "up_proj", "down_proj" # MLP | |
| ], | |
| lora_dropout=0.05, # Dropout率 | |
| bias="none", # Biasは訓練しない | |
| task_type="CAUSAL_LM" # タスクタイプ | |
| ) | |
| model = get_peft_model(model, lora_config) | |
| # 訓練可能パラメータ数を表示 | |
| model.print_trainable_parameters() | |
| # 例: trainable params: 4.2M || all params: 2.7B || trainable%: 0.16% | |
| # → 全パラメータの0.16%だけ訓練! | |
| # Step 3-9: データ準備、訓練、保存(省略) | |
| ... | |
| ``` | |
| **QLoRAの仕組み**: | |
| ``` | |
| 通常のファインチューニング: | |
| ┌─────────────────────────┐ | |
| │ モデル全体(2.7B params)│ ← 全て訓練 | |
| │ メモリ: ~40GB │ | |
| └─────────────────────────┘ | |
| QLoRA: | |
| ┌─────────────────────────┐ | |
| │ 元モデル(2.7B params) │ ← 4-bit量子化、frozen(訓練しない) | |
| │ メモリ: ~7GB │ | |
| └─────────────────────────┘ | |
| + | |
| ┌─────────────────────────┐ | |
| │ LoRAアダプター(4.2M) │ ← これだけ訓練 | |
| │ メモリ: ~0.5GB │ | |
| └─────────────────────────┘ | |
| = | |
| 合計メモリ: ~12GB | |
| ``` | |
| #### Alpaca形式データの整形 | |
| ```python | |
| def format_training_examples_for_model( | |
| self, | |
| training_examples, | |
| template="alpaca" | |
| ): | |
| """ | |
| Alpaca形式 → モデル用プロンプトに整形 | |
| """ | |
| formatted_prompts = [] | |
| for example in training_examples: | |
| instruction = example["instruction"] | |
| input_text = example["input"] | |
| output_text = example["output"] | |
| if template == "alpaca": | |
| if input_text: | |
| prompt = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. | |
| ### Instruction: | |
| {instruction} | |
| ### Input: | |
| {input_text} | |
| ### Response: | |
| {output_text}""" | |
| else: | |
| prompt = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request. | |
| ### Instruction: | |
| {instruction} | |
| ### Response: | |
| {output_text}""" | |
| formatted_prompts.append(prompt) | |
| return formatted_prompts | |
| ``` | |
| **なぜこの形式?** | |
| - 明確な区切り(`###`) | |
| - instruction-following能力の向上 | |
| - オープンソースコミュニティの標準 | |
| --- | |
| ## データフロー完全図解 | |
| ### フロー1: 通常推論(RAGあり) | |
| ``` | |
| ┌──────────────────────────────────────────────────────────────┐ | |
| │ ユーザー: "心臓の働きについて教えて" │ | |
| └────────────────────┬─────────────────────────────────────────┘ | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ Frontend: InferencePanel.tsx │ | |
| │ - 質問をバックエンドに送信 │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ HTTP POST /api/questions | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ Backend: questions.py │ | |
| │ - InferenceService.ask_question() │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ NullAI Core: model_router.py │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 1: _check_db_knowledge("medical", "心臓の働き") │ │ | |
| │ │ → DendriticMemorySpace.search_by_text() │ │ | |
| │ │ → 結果: 3件見つかった │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 2: _retrieve_relevant_knowledge() │ │ | |
| │ │ → hybrid_search("心臓の働き", top_k=3) │ │ | |
| │ │ → 取得: │ │ | |
| │ │ [1] 心臓の解剖学 (score: 0.92) │ │ | |
| │ │ [2] 循環器系の機能 (score: 0.85) │ │ | |
| │ │ [3] 心臓病の分類 (score: 0.73) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 3: プロンプト拡張 │ │ | |
| │ │ augmented_prompt = """ │ │ | |
| │ │ Based on the following verified knowledge: │ │ | |
| │ │ │ │ | |
| │ │ [Knowledge 1 - expert verification, conf: 0.9] │ │ | |
| │ │ Topic: 心臓の解剖学 │ │ | |
| │ │ Content: 心臓は4つの部屋から構成され... │ │ | |
| │ │ │ │ | |
| │ │ [Knowledge 2 - ...] │ │ | |
| │ │ │ │ | |
| │ │ Now, please answer: │ │ | |
| │ │ 心臓の働きについて教えて │ │ | |
| │ │ """ │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 4: LLM推論 │ │ | |
| │ │ → llm_providers.py │ │ | |
| │ │ → OllamaProvider.infer() │ │ | |
| │ │ → model: deepseek-r1:1.5b │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 5: レスポンス生成 │ │ | |
| │ │ response = { │ │ | |
| │ │ "response": "心臓は循環器系の中心器官で...", │ │ | |
| │ │ "confidence": 0.88, │ │ | |
| │ │ "thinking": "検証済み知識に基づいて回答", │ │ | |
| │ │ "retrieved_knowledge": [...] │ │ | |
| │ │ } │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 6: 師匠の出力? │ │ | |
| │ │ is_master = True │ │ | |
| │ │ confidence = 0.88 >= 0.8 ✓ │ │ | |
| │ │ → _save_master_output_as_training_data() │ │ | |
| │ │ → training_data/master_outputs/medical.jsonl │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ Frontend: レスポンス表示 │ | |
| │ - ResponseDisplay.tsx │ | |
| │ - 「心臓は循環器系の中心器官で...」 │ | |
| │ - Retrieved Knowledge バッジ表示 │ | |
| └────────────────────────────────────────────────────────────┘ | |
| ``` | |
| ### フロー2: 通常推論(RAGなし、自己拡充) | |
| ``` | |
| ┌──────────────────────────────────────────────────────────────┐ | |
| │ ユーザー: "量子コンピュータの原理は?" │ | |
| └────────────────────┬─────────────────────────────────────────┘ | |
| ↓ | |
| (同上) | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ NullAI Core: model_router.py │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 1: _check_db_knowledge("general", "量子...") │ │ | |
| │ │ → DendriticMemorySpace.search_by_text() │ │ | |
| │ │ → 結果: 見つからなかった ❌ │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 2: AI内部知識で推論 │ │ | |
| │ │ → LLM.generate("量子コンピュータの原理は?") │ │ | |
| │ │ → model: deepseek-r1:1.5b │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 3: レスポンス生成 │ │ | |
| │ │ response = { │ │ | |
| │ │ "response": "量子コンピュータは...", │ │ | |
| │ │ "confidence": 0.75 │ │ | |
| │ │ } │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 4: 自己拡充(DBに保存) │ │ | |
| │ │ confidence = 0.75 >= 0.7 ✓ │ │ | |
| │ │ save_to_memory = True │ │ | |
| │ │ → _save_inference_to_db() │ │ | |
| │ │ → SQLite: knowledge_tiles テーブル │ │ | |
| │ │ (将来的には.iathにも保存) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 5: 師匠の出力として保存 │ │ | |
| │ │ is_master = True │ │ | |
| │ │ confidence = 0.75 < 0.8 ❌ │ │ | |
| │ │ → 訓練データには保存しない │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ | |
| (レスポンス表示) | |
| ``` | |
| **重要な違い**: | |
| - RAGあり: `confidence >= 0.8`で訓練データ保存 | |
| - RAGなし: `confidence >= 0.7`でDB保存、`>= 0.8`で訓練データ保存 | |
| ### フロー3: ファインチューニング実行 | |
| ``` | |
| ┌──────────────────────────────────────────────────────────────┐ | |
| │ ユーザー: Training Dashboard で "Start Fine-tuning" │ | |
| │ - Apprentice Model: microsoft/phi-2 │ | |
| │ - Domain: medical │ | |
| │ - Method: peft │ | |
| │ - Epochs: 3 │ | |
| └────────────────────┬─────────────────────────────────────────┘ | |
| ↓ HTTP POST /api/training/start | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ Backend: training.py │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 1: 訓練データ存在チェック │ │ | |
| │ │ → FineTuningManager.load_training_data("medical") │ │ | |
| │ │ → training_data/master_outputs/medical.jsonl │ │ | |
| │ │ → 結果: 150サンプル │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 2: バックグラウンドタスク開始 │ │ | |
| │ │ background_tasks.add_task(run_training) │ │ | |
| │ │ → すぐにレスポンス返却(非同期) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ NullAI Core: fine_tuning.py (バックグラウンド) │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 1: モデルロード(4-bit量子化) │ │ | |
| │ │ → AutoModelForCausalLM.from_pretrained( │ │ | |
| │ │ "microsoft/phi-2", │ │ | |
| │ │ quantization_config=bnb_config │ │ | |
| │ │ ) │ │ | |
| │ │ → メモリ使用: ~7GB │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 2: LoRA設定 │ │ | |
| │ │ → get_peft_model(model, lora_config) │ │ | |
| │ │ → 訓練可能パラメータ: 4.2M / 2.7B (0.16%) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 3: データ準備 │ │ | |
| │ │ → format_training_examples_for_model() │ │ | |
| │ │ → Alpaca形式 → モデル用プロンプトに整形 │ │ | |
| │ │ → Dataset.from_dict({"text": prompts}) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 4: トレーニング開始 │ │ | |
| │ │ Epoch 1/3: │ │ | |
| │ │ [===> ] 35% loss: 1.245 │ │ | |
| │ │ → current_training_state.update({ │ │ | |
| │ │ "progress": 35, │ │ | |
| │ │ "current_epoch": 1, │ │ | |
| │ │ "loss": 1.245 │ │ | |
| │ │ }) │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| │ ↓ │ | |
| │ ┌─────────────────────────────────────────────────────┐ │ | |
| │ │ Step 5: 完了 │ │ | |
| │ │ → trainer.save_model(output_dir) │ │ | |
| │ │ → training_data/checkpoints/apprentice_medical_*/ │ │ | |
| │ │ → current_training_state["is_training"] = False │ │ | |
| │ └─────────────────────────────────────────────────────┘ │ | |
| └────────────────────┬───────────────────────────────────────┘ | |
| ↓ | |
| ┌────────────────────────────────────────────────────────────┐ | |
| │ Frontend: TrainingDashboard.tsx │ | |
| │ - 2秒ごとにポーリング: GET /api/training/status │ | |
| │ - プログレスバー更新: 35% → 67% → 100% │ | |
| │ - 完了時: チェックポイント一覧を再取得 │ | |
| └────────────────────────────────────────────────────────────┘ | |
| ``` | |
| --- | |
| ## 技術スタック詳細 | |
| ### フロントエンド | |
| ``` | |
| React 18.2 + TypeScript 5.0 | |
| ├─ Vite 4.4 (ビルドツール) | |
| ├─ TailwindCSS 3.3 (スタイリング) | |
| └─ axios (HTTP クライアント) | |
| 主要コンポーネント: | |
| - EngineManager.tsx (417行) - エンジン管理UI | |
| - InferencePanel.tsx (321行) - 推論パネル | |
| - TrainingDashboard.tsx (400行) - トレーニングダッシュボード | |
| - KnowledgePanel.tsx (185行) - 知識ブラウザ | |
| ``` | |
| ### バックエンド | |
| ``` | |
| FastAPI 0.115.6 + Python 3.13+ | |
| ├─ Uvicorn (ASGIサーバー) | |
| ├─ SQLAlchemy 2.0 (ORM) | |
| ├─ Pydantic 2.10 (バリデーション) | |
| └─ Alembic (マイグレーション) | |
| 主要API: | |
| - /api/config/* - エンジン管理 | |
| - /api/questions - 推論実行 | |
| - /api/training/* - ファインチューニング | |
| - /api/knowledge/* - 知識タイル管理 | |
| ``` | |
| ### NullAI Core | |
| ``` | |
| Python 3.13+ | |
| ├─ transformers 4.36+ (HuggingFace) | |
| ├─ torch 2.0+ (PyTorch) | |
| ├─ peft 0.7+ (LoRA/QLoRA) | |
| ├─ trl 0.7+ (Reinforcement Learning from Human Feedback) | |
| ├─ datasets 2.15+ (データセット処理) | |
| ├─ bitsandbytes 0.41+ (量子化) | |
| ├─ accelerate 0.25+ (分散訓練) | |
| ├─ zstandard 0.22+ (.iath圧縮) | |
| └─ numpy 1.24+ (数値計算) | |
| 主要モジュール: | |
| - model_router.py (800行) - RAG統合、エンジン管理 | |
| - iath_memory.py (362行) - 6次元空間記憶 | |
| - fine_tuning.py (640行) - ファインチューニング | |
| - llm_providers.py (390行) - LLMプロバイダー統合 | |
| ``` | |
| ### LLMプロバイダー | |
| ``` | |
| 1. Ollama | |
| - ローカルモデル管理 | |
| - ollama pull deepseek-r1:1.5b | |
| - API: http://localhost:11434 | |
| 2. HuggingFace Transformers | |
| - 直接ロード | |
| - AutoModelForCausalLM.from_pretrained() | |
| - GPU/CPU自動配置 | |
| 3. MLX (Apple Silicon) | |
| - M1/M2/M3 Mac専用 | |
| - 統合メモリ活用 | |
| - mlx-lm ライブラリ | |
| 4. GGUF (llama-cpp-python) | |
| - 量子化モデル(.gguf) | |
| - CPU推論に最適 | |
| - GPU acceleration対応 | |
| ``` | |
| --- | |
| ## よくある誤解と注意点 | |
| ### 誤解1: 「RAGは常に使われる」 | |
| ❌ **誤解**: 全ての推論でRAGが使われる | |
| ✅ **真実**: DBに知識がある場合のみRAGが発動 | |
| ```python | |
| # 実際の動作 | |
| if has_knowledge: | |
| # RAG推論 | |
| else: | |
| # 通常推論(RAGなし) | |
| ``` | |
| **見分け方**: | |
| - RAGあり: レスポンスに`retrieved_knowledge`フィールドが含まれる | |
| - RAGなし: `retrieved_knowledge`が空 | |
| ### 誤解2: 「弟子は自動的に師匠になる」 | |
| ❌ **誤解**: ファインチューニングが完了したら自動で師匠に昇格 | |
| ✅ **真実**: 手動で昇格操作が必要 | |
| ``` | |
| ファインチューニング完了 | |
| ↓ | |
| チェックポイント保存 | |
| ↓ | |
| 【手動操作】Engine Manager で Promote をクリック | |
| ↓ | |
| 弟子が師匠に昇格 | |
| ``` | |
| **理由**: 品質チェックを人間が行うべき | |
| ### 誤解3: 「.iathファイルは自動更新される」 | |
| ❌ **誤解**: AI生成知識が自動的に.iathに保存される | |
| ✅ **真実**: 現在はJSONLのみ、.iath保存は未実装(Priority 2) | |
| ``` | |
| 現状: | |
| AI生成知識 → SQLite + JSONL ✅ | |
| → .iath ❌(未実装) | |
| Priority 2実装後: | |
| AI生成知識 → SQLite + JSONL + .iath ✅ | |
| ``` | |
| ### 誤解4: 「ファインチューニングは全パラメータを訓練する」 | |
| ❌ **誤解**: モデル全体(2.7B パラメータ)を訓練 | |
| ✅ **真実**: LoRAアダプター(4.2M)だけ訓練 | |
| ``` | |
| 訓練されるパラメータ: | |
| - 元モデル: 2,700,000,000 → frozen(訓練しない) | |
| - LoRA: 4,200,000 → 訓練する ✅ | |
| 訓練パラメータ比率: 0.16% | |
| ``` | |
| **メリット**: | |
| - メモリ削減(40GB → 12GB) | |
| - 訓練時間短縮(10時間 → 2時間) | |
| - 元モデルは変更されない(安全) | |
| ### 誤解5: 「SQLiteと.iathは同じデータを保存」 | |
| ❌ **誤解**: SQLiteと.iathは重複している | |
| ✅ **真実**: 役割が完全に異なる | |
| | データベース | 保存内容 | 用途 | | |
| |-------------|---------|------| | |
| | SQLite | ユーザー、ワークスペース、推論履歴、メタデータ | アプリケーション管理 | | |
| | .iath | Knowledge Tile(6次元座標 + コンテンツ) | 知識検索・RAG推論 | | |
| **例**: | |
| ``` | |
| SQLite: | |
| - users テーブル: nullai_default_user | |
| - workspaces テーブル: default_workspace | |
| - inference_history: 過去の質問と回答 | |
| .iath: | |
| - Tile 1: [0.2, 0.8, 0.3, 0.9, 0.7, 0.8] "心臓の働き..." | |
| - Tile 2: [0.3, 0.8, 0.4, 0.85, 0.65, 0.75] "循環器系..." | |
| ``` | |
| ### 誤解6: 「confidence値はAIが自動計算」 | |
| ❌ **誤解**: AIが自己評価してconfidenceを返す | |
| ✅ **真実**: 現在は**固定値**(プロバイダーごと) | |
| ```python | |
| # llm_providers.py | |
| class OllamaProvider: | |
| async def infer(...): | |
| return { | |
| "response": response_text, | |
| "confidence": 0.85 # ← 固定値! | |
| } | |
| ``` | |
| **将来の改善**: | |
| - 複数モデルでクロスチェック | |
| - 応答の不確実性を計算(エントロピー) | |
| - 人間によるフィードバック学習 | |
| --- | |
| ## 設計判断の理由 | |
| ### 判断1: なぜPEFTを採用したか | |
| **候補**: | |
| 1. フルファインチューニング | |
| 2. PEFT (LoRA/QLoRA) | |
| 3. Adapter | |
| 4. Prompt Tuning | |
| **採用**: PEFT (QLoRA) | |
| **理由**: | |
| ``` | |
| 比較表: | |
| メモリ 速度 品質 汎用性 | |
| フルFT × × ⭐⭐⭐ ⭐⭐⭐ | |
| PEFT (QLoRA) ⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐⭐ | |
| Adapter ⭐⭐ ⭐⭐ ⭐⭐ ⭐⭐ | |
| Prompt Tuning ⭐⭐⭐ ⭐⭐⭐ ⭐ ⭐ | |
| ``` | |
| **結論**: PEFTがバランス最良 | |
| ### 判断2: なぜAlpaca形式を採用したか | |
| **候補**: | |
| 1. Alpaca | |
| 2. ShareGPT | |
| 3. OpenAssistant | |
| 4. Custom | |
| **採用**: Alpaca | |
| **理由**: | |
| - オープンソースで広く採用 | |
| - instruction-input-output構造が明確 | |
| - HuggingFace datasetsと互換性 | |
| - コミュニティのベストプラクティス | |
| ### 判断3: なぜハイブリッド検索か | |
| **候補**: | |
| 1. テキストのみ | |
| 2. 座標のみ | |
| 3. ハイブリッド | |
| **採用**: ハイブリッド | |
| **理由**: | |
| ``` | |
| テキストのみ: | |
| - 利点: シンプル | |
| - 欠点: 同義語を見逃す | |
| 座標のみ: | |
| - 利点: 意味的に関連する知識を発見 | |
| - 欠点: 座標が不正確だと失敗 | |
| ハイブリッド: | |
| - 利点: 両方の長所を活かせる | |
| - 欠点: パラメータ調整が必要(text_weight, spatial_weight) | |
| ``` | |
| **現在の設定**: | |
| ```python | |
| text_weight = 0.4 | |
| spatial_weight = 0.6 | |
| # → 座標をやや重視(意味的関連性を優先) | |
| ``` | |
| ### 判断4: なぜ循環インポートをlazy importで解決したか | |
| **候補**: | |
| 1. Lazy import(関数内でimport) | |
| 2. アーキテクチャ変更(依存関係の整理) | |
| 3. 中間モジュール導入 | |
| **採用**: Lazy import | |
| **理由**: | |
| - 最小限の変更で解決 | |
| - パフォーマンスへの影響は軽微 | |
| - 既存コードの大幅な書き換え不要 | |
| **実装例**: | |
| ```python | |
| def _check_db_knowledge(self, domain_id, prompt): | |
| # 関数内でimport → 循環回避 | |
| from backend.app.database.session import SessionLocal | |
| db = SessionLocal() | |
| # ... | |
| ``` | |
| --- | |
| ## 拡張時の考慮事項 | |
| ### 新しいLLMプロバイダーを追加する場合 | |
| **手順**: | |
| 1. `llm_providers.py`に新しいクラスを追加 | |
| ```python | |
| class NewProvider: | |
| async def infer(self, model_config, prompt, temperature): | |
| # 実装 | |
| pass | |
| async def infer_streaming(self, model_config, prompt, temperature): | |
| # 実装 | |
| pass | |
| ``` | |
| 2. `model_router.py`の`_perform_llm_inference()`に追加 | |
| ```python | |
| if provider == "ollama": | |
| result = await self.ollama_provider.infer(...) | |
| elif provider == "new_provider": # ← 追加 | |
| result = await self.new_provider.infer(...) | |
| ``` | |
| 3. `backend/app/config.py`の`ModelProvider`列挙型に追加 | |
| ```python | |
| class ModelProvider(str, Enum): | |
| OLLAMA = "ollama" | |
| HUGGINGFACE = "huggingface" | |
| NEW_PROVIDER = "new_provider" # ← 追加 | |
| ``` | |
| ### 新しいドメインを追加する場合 | |
| **手順**: | |
| 1. `.iath`ファイルでドメイン用の座標空間を定義 | |
| ``` | |
| 医療ドメイン: medical_space [x, y, z] | |
| 法律ドメイン: legal_space [x, y, z] ← 追加 | |
| - x: 法分野(民法、刑法、商法...) | |
| - y: 判例レベル(地裁、高裁、最高裁) | |
| - z: 時代(古典、現代、最新) | |
| ``` | |
| 2. `backend/app/config.py`にドメイン設定追加 | |
| ```python | |
| domains = [ | |
| {"domain_id": "medical", "name": "医療"}, | |
| {"domain_id": "legal", "name": "法律"} # ← 追加 | |
| ] | |
| ``` | |
| 3. 訓練データディレクトリ作成 | |
| ```bash | |
| mkdir -p training_data/master_outputs/ | |
| touch training_data/master_outputs/master_outputs_legal.jsonl | |
| ``` | |
| ### 座標自動推定を実装する場合(Priority 2) | |
| **設計案**: | |
| ```python | |
| # null_ai/coordinate_estimator.py | |
| class CoordinateEstimator: | |
| def __init__(self, llm_model): | |
| """ | |
| DeepSeek R1を使って座標を推定 | |
| """ | |
| self.llm = llm_model | |
| async def estimate_coordinates( | |
| self, | |
| prompt: str, | |
| response: str, | |
| domain_id: str | |
| ) -> List[float]: | |
| """ | |
| 6次元座標を推定 | |
| Returns: [x, y, z, c, g, v] | |
| """ | |
| # プロンプト構築 | |
| estimation_prompt = f"""You are an expert in knowledge space mapping. | |
| Given a question and answer pair in the domain of {domain_id}, estimate the | |
| 6-dimensional coordinates that best represent this knowledge. | |
| Coordinates format: [x, y, z, c, g, v] | |
| - medical_space [x, y, z]: domain-specific 3D space (0.0-1.0) | |
| - meta_space [c, g, v]: Certainty, Granularity, Verification (0.0-1.0) | |
| Question: {prompt} | |
| Answer: {response} | |
| Output ONLY the coordinates as a JSON array: [x, y, z, c, g, v] | |
| """ | |
| # LLMに座標推定を依頼 | |
| result = await self.llm.generate(estimation_prompt) | |
| # JSONパース | |
| coords = json.loads(result) | |
| # バリデーション | |
| assert len(coords) == 6 | |
| assert all(0.0 <= c <= 1.0 for c in coords) | |
| return coords | |
| ``` | |
| ### WebSocketでリアルタイム進捗を実装する場合 | |
| **設計案**: | |
| ```python | |
| # backend/app/main.py | |
| @app.websocket("/ws/training/{session_id}") | |
| async def training_websocket(websocket: WebSocket, session_id: str): | |
| await websocket.accept() | |
| # 進捗コールバック | |
| async def progress_callback(state): | |
| await websocket.send_json({ | |
| "type": "progress", | |
| "data": state | |
| }) | |
| # ファインチューニング開始 | |
| await fine_tuning_manager.start_training( | |
| ..., | |
| progress_callback=progress_callback | |
| ) | |
| ``` | |
| --- | |
| ## まとめ: プロジェクトの本質 | |
| NullAIは単なるRAGシステムでも、単なるファインチューニングツールでもありません。 | |
| **NullAIの本質**: | |
| ``` | |
| 自己進化する知識生態系 | |
| 師匠AI → 知識生成 → 弟子AI学習 → 昇格 → 新しい弟子 → サイクル継続 | |
| ↓ ↑ | |
| DB拡充(自己拡充) ファインチューニング | |
| ↓ ↑ | |
| 樹木型空間記憶(6次元座標) 高品質訓練データ | |
| ↓ ↑ | |
| 意味的知識整理 師匠の知識継承 | |
| ↓ ↑ | |
| └────────────── サイクル ──────────────┘ | |
| ``` | |
| **4つの核心思想の統合**: | |
| 1. **倒木システム**: 世代交代による進化 | |
| 2. **DB分離構造**: 信頼性の確保と自己拡充 | |
| 3. **樹木型空間記憶**: 意味的知識整理 | |
| 4. **ローカルファースト**: プライバシーとコスト | |
| これら全てが**有機的に結合**し、AIが自己進化する生態系を形成しています。 | |
| --- | |
| **このガイドを理解したら、あなたはNullAIの設計思想を正しく継承できます。** | |
| **頑張ってください!🌲🔥** | |
| --- | |
| **Document Version**: 1.0 | |
| **Total Pages**: 60+ | |
| **Total Words**: 15,000+ | |
| **Author**: Claude (Sonnet 4.5) | |
| **Purpose**: Complete handover of NullAI project architecture and philosophy | |