WordPress ブログ記事を知識ソースとした GraphRAG チャットアプリ。 Blazor WASM + LadybugDB + WebLLM をブラウザ内で動かし、外部 API 不要でオフライン動作する。
azuremoe-chat/
├── src/
│ ├── AzureMoe.Chat.Core/ 共有ライブラリ (スキーマ定数・チャンク化)
│ ├── AzureMoe.Chat.Ingest/ インジェスト CLI
│ ├── AzureMoe.Chat.Verify/ 検索動作確認 CLI
│ └── AzureMoe.Chat.Web/ Blazor WASM チャットアプリ (Phase 2)
├── docs/
│ └── architecture.md 設計ドキュメント
├── poc/ Phase 0 技術検証コード
├── tools/
│ └── embed-parity/ .NET ↔ transformers.js 埋め込み一致・DB 互換性チェック
└── model/ 埋め込みモデル置き場 (git 管理外、インジェスト時に自動取得)
└── sirasagi62/
└── ruri-v3-30m-ONNX/
インジェスト・検索ツール・チャットアプリのすべてが ruri-v3-30m (ONNX, transformers.js 形式) を使用する。
手動ダウンロードは不要 — インジェスト実行時に足りないファイルを HuggingFace から自動取得し、model/<モデルID>/ に保存する。
| 項目 | 値 |
|---|---|
| HuggingFace リポジトリ | sirasagi62/ruri-v3-30m-ONNX (トークナイザー同梱の ONNX 変換版) |
| ベースモデル | cl-nagoya/ruri-v3-30m (ModernBERT-Ja ベース、日本語特化) |
| リビジョン | cdf9391f1ff2198daa8f63f7ccf97d7b3e7415a0 に固定 (EmbeddingProfile.cs) |
| パラメータ数 | 約 37M |
| 埋め込み次元 | 256 |
| 最大シーケンス長 | 8192 トークン |
| モデルファイル | onnx/model_quantized.onnx (dtype q8) — 約 37 MB |
| prefix | クエリ 検索クエリ: / 文書 検索文書: |
| JMTEB Retrieval | 78.08 (参考: multilingual-e5-small は 67.27) |
| ライセンス | Apache-2.0 |
モデル ID・リビジョン・dtype・prefix は manifest.json に記録され、チャットアプリはそれに従ってクエリを埋め込む。
DB と異なるモデルでクエリを埋め込む事故を防ぐため、appsettings.json の EmbeddingModelId / EmbeddingDtype はフォールバック扱い。
旧モデル
Xenova/multilingual-e5-smallも--EmbeddingModel Xenova/multilingual-e5-smallで引き続き利用できる。 ruri の 130m / 310m 版に差し替える場合はEmbeddingProfile.csにプロファイルを追加する。
tools/embed-parity)埋め込みモデルや transformers.js / LadybugDB のバージョンを変えたら、全件インジェストの前に次を実行する。
# 1. .NET 側でサンプル文を埋め込み、トークン ID とベクトルを書き出す
dotnet run --project src/AzureMoe.Chat.Ingest -- embed-dump tools/embed-parity/texts.json tools/embed-parity/dump.json
# 2. 少数件で試しに DB を作る
dotnet run --project src/AzureMoe.Chat.Ingest -- --MaxPosts 10 --NoLlm --OutDir out\smoke
# 3. transformers.js (ブラウザと同じ固定バージョン) と比較し、DB を wasm エンジンで開いて検索する
cd tools\embed-parity
npm install
node check.mjs dump.json ..\..\out\smoke\<blog-XXXX>.lbdb "Azure Container Apps の新機能"
確認内容:
文書 (passage) 側の不一致は参考表示のみ。transformers.js の Unigram トークナイザーは
byte_fallback未対応で、 改行が<unk>になる (.NET 側は HF tokenizers と同じ<0x0A>)。文書は .NET でしか埋め込まないため問題ない。 クエリは両側で空白・改行を 1 スペースに正規化してから埋め込む (EmbeddingProfile.NormalizeQuery/rag-worker.js normalizeQuery)。
.xml ファイルを .tmp/ フォルダに配置.tmp/
└── wordpress.2026-06-14.xml
WordPress XML を読み込んでエンティティを抽出し、GraphDB を構築して out/ に出力する。
dotnet run --project src/AzureMoe.Chat.Ingest
設定の優先順位: コマンドライン引数 > 環境変数 > appsettings.json > デフォルト値
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--XmlDir | — | .tmp | WordPress エクスポート XML が入ったディレクトリ |
--MaxPosts | — | 0 (全件) | 処理する最大投稿数。動作確認時は 10 など小さい値に |
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--EmbeddingModel | — | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID (EmbeddingProfile.cs に定義済みのもの) |
--EmbeddingDtype | — | q8 | transformers.js 準拠の dtype 名 (q8 → onnx/model_quantized.onnx、fp32 → onnx/model.onnx など)。ファイルが無い場合のフォールバックはしない |
--ModelDir | — | model/<EmbeddingModel> | モデルのディレクトリパス。足りないファイルは固定リビジョンから自動ダウンロード |
エンティティ・Azure サービス名の抽出に使用する OpenAI 互換エンドポイント。 通常記事はチャンクごとにエンティティ/関係/サービス名を LLM で抽出する。 Azure Update 記事のサービス名は HTML の H2 構造から確定的に取得するため LLM 抽出対象外。
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--LlmBaseUrl | LLM_BASE_URL | http://localhost:11434/v1 | LLM エンドポイントのベース URL |
--LlmModel | LLM_MODEL | qwen3:8b | モデル名 (サーバーに読み込まれているモデル) |
--LlmApiKey | LLM_API_KEY | (なし) | API キー。ローカルサーバーは通常不要 |
主要な LLM サーバー別設定例:
| サーバー | LlmBaseUrl | LlmModel 例 |
|---|---|---|
| Ollama | http://localhost:11434/v1 | qwen3:8b, llama3.1:8b |
| LM Studio | http://localhost:1234/v1 | ロード中のモデル名 |
| llama.cpp server | http://localhost:8080/v1 | (引数不要のことも多い) |
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--OutDir | — | out | .lbdb ファイルと manifest.json の出力先 |
機密情報以外をファイルで管理したい場合:
{
"XmlDir": ".tmp",
"EmbeddingModel": "sirasagi62/ruri-v3-30m-ONNX",
"EmbeddingDtype": "q8",
"LlmBaseUrl": "http://localhost:11434/v1",
"LlmModel": "qwen3:8b",
"OutDir": "out"
}
out/
├── blog-20260614120000.lbdb GraphDB ファイル (Ladybug 0.19.x 形式)
└── manifest.json メタデータ (モデル情報・件数・SHA-256)
各
Chunkには所属Postの日付・タイトル・年・月が非正規化保存される (チャンク単位の日付フィルタ用)。 グラフの詳細構造は「GraphDB 構造」セクションを参照。
inspect)構築済み .lbdb を読み取り専用で開き、データが期待通りかを検証する。
「2026年2月の話題」のような時期クエリで的外れな結果が返る場合の原因切り分けに使う。
# 1. 統計サマリ: ノード/エッジ件数・日付分布(月別)・次数上位のエンティティ/サービス・サンプル Chunk
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect
# DB を明示指定 (省略時は out/ → wwwroot/data/ の順に最新 .lbdb を自動検出)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect out/blog-20260614120000.lbdb
# 2. 任意の Cypher を実行 (結果をテーブル表示)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect --cypher \
"MATCH (p:Post) WHERE p.date >= '2026-02-01' AND p.date < '2026-03-01' RETURN count(p)"
# 3. 自然文クエリでサンプルベクトル検索 (埋め込みモデルが必要・期待した記事が返るか確認)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect --query "2026年2月のAzure Functionsの更新" --topk 8
| 引数 | デフォルト | 説明 |
|---|---|---|
| (位置引数) | (自動検出) | 対象 .lbdb ファイルパス |
--cypher "..." | — | 任意の Cypher を実行して結果を表示 |
--query "..." | — | 自然文を埋め込んでベクトル検索 (上位 --topk 件) |
--embedding | sirasagi62/ruri-v3-30m-ONNX | --query 用の埋め込みモデル ID (DB の manifest.json の embeddingModel に合わせる) |
--model | model/<--embedding> | --query 用モデルのディレクトリ |
--dtype | q8 | --query 用 dtype (manifest.json の embeddingDtype に合わせる) |
--topk | 8 | --query で返す件数 |
append)既存の .lbdb にブログ記事を 1 件追加する。WordPress XML 全件再インジェストをせずに
新着記事だけをインクリメンタルに反映したいときに使う。
処理の流れ:
.lbdb を日付付きファイル名でコピー (元ファイルは変更しない)manifest.json を更新# 基本: URL と元 DB を指定して追記
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb
# 既に存在する URL を上書きしたい場合
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb --Override
# 出力先を変える / LLM を切り替える
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb \
--OutDir T:\temp\output \
--LlmBaseUrl http://localhost:1234/v1 \
--LlmModel qwen3-8b
| 引数 | デフォルト | 説明 |
|---|---|---|
<url> (位置 1) | — | 取得するブログ記事の URL |
<sourceDbPath> (位置 2) | — | コピー元の .lbdb ファイルパス |
--Override | false | 同じ URL の Post が既に存在する場合に削除して上書きする |
--OutDir | out | 出力 .lbdb と manifest.json の書き出し先 |
--EmbeddingModel / --EmbeddingDtype | sirasagi62/ruri-v3-30m-ONNX / q8 | 埋め込みモデル。元 DB と同じフォルダに manifest.json があれば一致を確認し、異なる場合は中断する |
--ModelDir | model/<EmbeddingModel> | 埋め込みモデルのディレクトリ |
--LlmBaseUrl | http://localhost:11434/v1 | LLM エンドポイント |
--LlmModel | qwen3:8b | LLM モデル名 |
--LlmApiKey | (なし) | LLM API キー |
同じ URL を
--Overrideなしで追記しようとするとエラーになる。 出力ファイル名はblog-<yyyyMMdd>.lbdb(実行日の UTC 日付)。
インジェストが生成する .lbdb ファイルの内部構造。LadybugDB (グラフDB) に格納され、
ブラウザの WASM 版も同じファイルをそのまま開く。
ブログには構造的に異なる 2 種類の投稿が混在するため、タイトルで判定して処理を分岐させる。
週次アップデートまとめ記事。H2 見出しがサービス名、その配下の箇条書きが更新内容になっている。
<h2>Azure Functions</h2> ← serviceName = "Azure Functions"
<ul>
<li>Flex Consumption で新機能 ← Chunk 1 (chunkType = "update_item")
<ul><li>詳細...</li></ul> ← 子要素は親と一体で保持
</li>
<li>従量課金で改善 ← Chunk 2 (update_item)
</li>
</ul>
<h2>Azure Container Apps</h2> ← serviceName = "Azure Container Apps"
...
<li> 1 件 + その子要素(子要素は親への補足コメントとして一体保持)"Azure Functions\n\nFlex Consumption で新機能..." (サービス名をプレフィックス)特定テーマについての文章記事。
"記事タイトル\n\nチャンクテキスト"sectionTitle フィールドに記録| ノード | 主なフィールド | 説明 |
|---|---|---|
Post | id, title, url, date, year, month | ブログ記事 1 件 |
Chunk | 下表参照 | テキストチャンク(ベクトル付き) |
AzureService | name | Azure サービス名(正式名称) |
Entity | name, type, description | LLM 抽出エンティティ(人物・技術・機能など) |
Tag | name | WordPress タグ |
Chunk フィールド詳細
| フィールド | 型 | 説明 |
|---|---|---|
id | INT64 | 主キー |
postId | INT64 | 所属 Post の id |
ordinal | INT64 | Post 内での順序 |
text | STRING | チャンク本文 |
date / year / month | STRING / INT64 | Post から非正規化(日付フィルタ用) |
title | STRING | Post タイトル(引用表示用) |
sectionTitle | STRING | 直前の H2/H3 見出しテキスト |
serviceName | STRING | Azure Update 記事のみ: H2 のサービス名。通常記事は空文字 |
chunkType | STRING | "update_item" (Update 記事の箇条書き) / "prose" (通常テキスト) |
emb | FLOAT[256] | ruri-v3-30m による埋め込みベクトル (次元は manifest.json の embeddingDim) |
Post ──HAS_CHUNK──────▶ Chunk
Post ──TAGGED──────────▶ Tag
Post ──COVERS_SERVICE──▶ AzureService ← Azure Update 記事は H2 から、通常記事は LLM から収集
Chunk ──MENTIONS───────▶ Entity ← LLM 抽出(全記事)
Entity ──RELATED_TO────▶ Entity ← LLM 抽出(全記事)
| インデックス名 | 対象 | 距離計算 |
|---|---|---|
chunk_emb_idx | Chunk.emb | コサイン類似度 (HNSW) |
| 記事タイプ | 収集元 | 特徴 |
|---|---|---|
| Azure Update 記事 | H2 見出しテキスト | 確定的・LLM 不要・ブレなし |
| 通常記事 | LLM (EntityExtractor) | 本文から柔軟に抽出 |
構築した GraphDB に対してベクトル検索を対話的に試せるツール。
# out/ から最新 .lbdb を自動検出
dotnet run --project src/AzureMoe.Chat.Verify
# DB を明示指定
dotnet run --project src/AzureMoe.Chat.Verify -- --DbPath out/blog-20260614120000.lbdb
# 結果件数を変える
dotnet run --project src/AzureMoe.Chat.Verify -- --TopK 10
| 引数 | デフォルト | 説明 |
|---|---|---|
--DbPath | (自動検出) | 検索対象の .lbdb ファイルパス |
--OutDir | out | DbPath 未指定時に最新 .lbdb を探すディレクトリ |
--EmbeddingModel | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID (DB 構築時と同じもの) |
--EmbeddingDtype | q8 | 埋め込み dtype (DB 構築時と同じもの) |
--ModelDir | model/<EmbeddingModel> | モデルのディレクトリパス |
--TopK | 5 | 返す検索結果の件数 |
> 検索クエリを入力して Enter
> \stats 統計情報を再表示
> q 終了
Blazor WebAssembly でブラウザ内完結の GraphRAG チャット。 LadybugDB (WASM)・transformers.js をすべてブラウザ内で実行する。 以下の優先順位で LLM バックエンドを自動選択するため どの環境でも動作する:
| 役割 | バックエンド | モデル / エンジン | 備考 |
|---|---|---|---|
| LLM (テキスト生成) | Chrome 組み込み AI | Gemini Nano | ダウンロード不要・最優先 |
| LLM (テキスト生成) | transformers.js | onnx-community/Qwen2.5-0.5B-Instruct (q4) | Chrome AI 非対応時に自動ダウンロード |
| LLM (テキスト生成) | OpenAI 互換 HTTP | 任意 (LM Studio / Ollama 等) | /llm コマンドで実行中に切替可 |
| 埋め込み | transformers.js | sirasagi62/ruri-v3-30m-ONNX (manifest.json に従う) | 起動時に自動ダウンロード |
モデルは transformers.js が Hugging Face Hub からダウンロードし、Cache API で自動キャッシュする。 2 回目以降はオフラインでも動作する。
必要な Chrome バージョン: Chrome 138+ (Dev または Canary チャンネル)。
ハードウェア要件:
セットアップ手順:
chrome://flags/#optimization-guide-on-device-model → Enabledchrome://flags/#prompt-api-for-gemini-nano → Enabled (または "Enabled multilingual")chrome://on-device-internals を開き、Gemini Nano モデルのダウンロード状況を確認。バージョンが 0.0.0.0 の場合は "Check for update" をクリックDevTools で確認する方法 (F12 → Console):
// Chrome 138+ の場合
typeof LanguageModel // "function" なら有効
await LanguageModel.availability() // "available" なら即利用可能
// 旧ビルドの場合
typeof window.ai?.languageModel // "object" なら有効
有効になると起動時に「Chrome 組み込み AI (Gemini Nano) 利用可能 — ダウンロード不要」と表示される。
Chrome AI が使えない環境向けに appsettings.json で変更可能:
{
"LlmModelId": "onnx-community/Qwen2.5-1.5B-Instruct",
"LlmDtype": "q4"
}
| モデル | サイズ (q4) | 特徴 |
|---|---|---|
onnx-community/Qwen2.5-0.5B-Instruct | ~350 MB | 軽量・高速 (デフォルト) |
onnx-community/Qwen2.5-1.5B-Instruct | ~900 MB | 日本語品質が向上 |
npm install が csproj ビルド時に自動実行される)manifest.json と .lbdb ファイル1. インジェストで DB を生成
dotnet run --project src/AzureMoe.Chat.Ingest
2. 生成物を Web プロジェクトの data ディレクトリにコピー
# Windows
copy out\manifest.json src\AzureMoe.Chat.Web\wwwroot\data\
copy out\*.lbdb src\AzureMoe.Chat.Web\wwwroot\data\
# Linux / macOS
cp out/manifest.json src/AzureMoe.Chat.Web/wwwroot/data/
cp out/*.lbdb src/AzureMoe.Chat.Web/wwwroot/data/
3. 開発サーバーを起動
dotnet run --project src/AzureMoe.Chat.Web
ブラウザで https://localhost:5001 を開く。
src/AzureMoe.Chat.Web/wwwroot/appsettings.json で変更できる。
| キー | デフォルト | 説明 |
|---|---|---|
ManifestUrl | data/manifest.json | manifest.json の URL |
DbBaseUrl | data/ | DB ファイルのベース URL (manifest の databaseFile を結合) |
LlmModelId | onnx-community/Qwen2.5-0.5B-Instruct | LLM モデル ID (HuggingFace) |
LlmDtype | q4 | 量子化精度。q4 / q8 / fp16 など |
LlmMaxNewTokens | 4096 | 最終回答の最大生成トークン数 (上限。EOS で自然停止) |
EmbeddingModelId | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID。manifest.json に記録があればそちらを優先 (フォールバック用) |
RetrievalMode | Normal | 探索の深さ。Fast / Normal / Deep (UI の /mode でも変更可) |
RagTopK | 6 | HTTP LLM モード時の最終参照数上限 |
MaxContextChars | 6000 | HTTP LLM モード時に LLM へ渡す文脈の最大文字数 |
LocalRagTopK | 3 | ローカル WASM LLM モード時の参照数上限 (2B 級モデル向け圧縮設定) |
LocalMaxContextChars | 2500 | ローカル WASM LLM モード時の文脈最大文字数 |
LocalPerRefMaxChars | 800 | ローカル WASM LLM モード時の参照1件あたりの最大文字数 |
HistoryTurns | 3 | Deep モードで LLM に渡す直近会話ターン数 |
DeepMaxRounds | 3 | Deep モードの検索クエリ数 (round-0 + 上位タイトルでの追検索) |
VerifyGrounding | true | 生成後に、回答中の製品名・数値が参考情報に含まれるかをルールベースで検査し、不十分なら再生成・警告表示 |
SystemPrompt | (組み込み) | システムプロンプト (キャラ付け + GraphRAG ルール) |
RetrievalMode)検索の深さと応答速度のトレードオフを選べる。UI の /mode で実行中にも切替可能。
| モード | 検索の広さ | 内容 | 会話履歴 | 体感 |
|---|---|---|---|---|
Fast | 狭 | グラフ探索なし・純ベクトル検索 | 使用しない | 最軽量 |
Normal | 中 | グラフ探索あり | 使用しない (単発質問) | バランス (既定) |
Deep | 広 | 関連エンティティまで辿る + 上位記事タイトルで決定的に追検索 (複数クエリ, 最大 DeepMaxRounds) | 直近 HistoryTurns ターン | 高精度・低速 |
全モード共通の2段構え:
質問から日付 (例: 「2026年2月」「先月」) を検出した場合は、再ランクも日付窓の中で行い範囲外の記事を除外する。
応答時間について: ブラウザ内 CPU では最終回答の生成時間が支配的で、これは全モード共通。 そのため Fast と Normal の体感差は小さく (主に検索の広さと精度で差が出る)、 複数クエリを投げる Deep が明確に遅い。各ステップの進行状況はチャット画面に表示される。
グラウンディングの担保: LLM が学習知識で一般論を返さないよう、全モードで次を行う。
VerifyGrounding とは独立に常時)。VerifyGrounding=true なら、回答中の英字の語 (製品名など、3 文字以上) と 2 桁以上の数値 (年・バージョンなど) が参考情報または質問に含まれるかを検査する (GroundingChecker、LLM は使わない)。含まれない語があれば、その語を名指しした厳密なプロンプト(参考情報のみ・[n] 引用必須)で1回だけ再生成し、裏付けられない語が少ない方の回答を採用する。それでも残る場合は、該当する語を回答の下に⚠警告で表示する。WASM (CPU) 実行時は再生成に時間がかかるため、再生成せず警告のみ表示する。チャット入力欄で以下のコマンドが使える。
| コマンド | 動作 |
|---|---|
/help | コマンド一覧を表示 |
/mode [fast|normal|deep] | 探索モードを表示 / 変更 (引数なしで現在値を表示) |
/llm [endpoint [model]] | 外部 OpenAI 互換 LLM を設定 (例: /llm http://localhost:1234/v1 gpt-model)。引数なしでローカル WASM に戻す |
/debug [on|off] | デバッグ出力の表示切替。ON 時は埋め込み・Cypher・LLM プロンプト/応答をチャット内に表示 |
/info | 現在の LLM / 埋め込みモデル情報と探索モードを表示 |
/license | ライセンス情報を表示 |
/clear | 画面と会話履歴をクリア |
/reload | アプリを再起動 |
| (それ以外) | RAG クエリとして実行 |
応答生成中は入力欄の右に 「■ 停止」ボタンが表示され、クリックすると LLM の生成を即座に中断する
(緊急停止)。途中まで生成された回答は残る。長い生成 (LlmMaxNewTokens が大きい) や、まれに小型モデルが
同じ文言を繰り返すループに入った場合の保険として使える (ループ自体は no_repeat_ngram_size で抑制済み)。
URL に以下のクエリパラメータを付けると、ページを開いた時点から /llm モードが有効になる。
/llm コマンドによる手動設定と同じ効果。
| パラメータ | 必須 | 説明 |
|---|---|---|
llm | 必須 | OpenAI 互換エンドポイントの URL |
model | 任意 | モデル名。省略時はエンドポイント側のデフォルト |
# LM Studio
https://chat.azure.moe/?llm=http://localhost:1234/v1&model=qwen3-8b
# Ollama
https://chat.azure.moe/?llm=http://localhost:11434/v1&model=qwen3:8b
# OpenAI 互換 API
https://chat.azure.moe/?llm=https://api.openai.com/v1&model=gpt-4o
スマートフォンでの利用: iOS/Android ではメモリ制約によりローカル LLM・埋め込みモデルをスキップし、 GraphDB に対してキーワード検索のみ行うモードで起動する。
?llm=...を付けると外部 LLM との組み合わせで回答生成も可能になる。
Mixed Content に注意: デプロイ済みサイト (
https://) からhttp://localhostへの接続は ブラウザの Mixed Content ポリシーによりブロックされる (CORS 設定とは無関係)。 LM Studio / Ollama などローカルサーバーを使う場合は、開発用サーバー (dotnet run) 経由でhttp://localhost:5001からアクセスするか、HTTPS に対応したリバースプロキシを用意すること。
WordPress ブログ記事を知識ソースとした GraphRAG チャットアプリ。 Blazor WASM + LadybugDB + WebLLM をブラウザ内で動かし、外部 API 不要でオフライン動作する。
azuremoe-chat/
├── src/
│ ├── AzureMoe.Chat.Core/ 共有ライブラリ (スキーマ定数・チャンク化)
│ ├── AzureMoe.Chat.Ingest/ インジェスト CLI
│ ├── AzureMoe.Chat.Verify/ 検索動作確認 CLI
│ └── AzureMoe.Chat.Web/ Blazor WASM チャットアプリ (Phase 2)
├── docs/
│ └── architecture.md 設計ドキュメント
├── poc/ Phase 0 技術検証コード
├── tools/
│ └── embed-parity/ .NET ↔ transformers.js 埋め込み一致・DB 互換性チェック
└── model/ 埋め込みモデル置き場 (git 管理外、インジェスト時に自動取得)
└── sirasagi62/
└── ruri-v3-30m-ONNX/
インジェスト・検索ツール・チャットアプリのすべてが ruri-v3-30m (ONNX, transformers.js 形式) を使用する。
手動ダウンロードは不要 — インジェスト実行時に足りないファイルを HuggingFace から自動取得し、model/<モデルID>/ に保存する。
| 項目 | 値 |
|---|---|
| HuggingFace リポジトリ | sirasagi62/ruri-v3-30m-ONNX (トークナイザー同梱の ONNX 変換版) |
| ベースモデル | cl-nagoya/ruri-v3-30m (ModernBERT-Ja ベース、日本語特化) |
| リビジョン | cdf9391f1ff2198daa8f63f7ccf97d7b3e7415a0 に固定 (EmbeddingProfile.cs) |
| パラメータ数 | 約 37M |
| 埋め込み次元 | 256 |
| 最大シーケンス長 | 8192 トークン |
| モデルファイル | onnx/model_quantized.onnx (dtype q8) — 約 37 MB |
| prefix | クエリ 検索クエリ: / 文書 検索文書: |
| JMTEB Retrieval | 78.08 (参考: multilingual-e5-small は 67.27) |
| ライセンス | Apache-2.0 |
モデル ID・リビジョン・dtype・prefix は manifest.json に記録され、チャットアプリはそれに従ってクエリを埋め込む。
DB と異なるモデルでクエリを埋め込む事故を防ぐため、appsettings.json の EmbeddingModelId / EmbeddingDtype はフォールバック扱い。
旧モデル
Xenova/multilingual-e5-smallも--EmbeddingModel Xenova/multilingual-e5-smallで引き続き利用できる。 ruri の 130m / 310m 版に差し替える場合はEmbeddingProfile.csにプロファイルを追加する。
tools/embed-parity)埋め込みモデルや transformers.js / LadybugDB のバージョンを変えたら、全件インジェストの前に次を実行する。
# 1. .NET 側でサンプル文を埋め込み、トークン ID とベクトルを書き出す
dotnet run --project src/AzureMoe.Chat.Ingest -- embed-dump tools/embed-parity/texts.json tools/embed-parity/dump.json
# 2. 少数件で試しに DB を作る
dotnet run --project src/AzureMoe.Chat.Ingest -- --MaxPosts 10 --NoLlm --OutDir out\smoke
# 3. transformers.js (ブラウザと同じ固定バージョン) と比較し、DB を wasm エンジンで開いて検索する
cd tools\embed-parity
npm install
node check.mjs dump.json ..\..\out\smoke\<blog-XXXX>.lbdb "Azure Container Apps の新機能"
確認内容:
文書 (passage) 側の不一致は参考表示のみ。transformers.js の Unigram トークナイザーは
byte_fallback未対応で、 改行が<unk>になる (.NET 側は HF tokenizers と同じ<0x0A>)。文書は .NET でしか埋め込まないため問題ない。 クエリは両側で空白・改行を 1 スペースに正規化してから埋め込む (EmbeddingProfile.NormalizeQuery/rag-worker.js normalizeQuery)。
.xml ファイルを .tmp/ フォルダに配置.tmp/
└── wordpress.2026-06-14.xml
WordPress XML を読み込んでエンティティを抽出し、GraphDB を構築して out/ に出力する。
dotnet run --project src/AzureMoe.Chat.Ingest
設定の優先順位: コマンドライン引数 > 環境変数 > appsettings.json > デフォルト値
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--XmlDir | — | .tmp | WordPress エクスポート XML が入ったディレクトリ |
--MaxPosts | — | 0 (全件) | 処理する最大投稿数。動作確認時は 10 など小さい値に |
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--EmbeddingModel | — | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID (EmbeddingProfile.cs に定義済みのもの) |
--EmbeddingDtype | — | q8 | transformers.js 準拠の dtype 名 (q8 → onnx/model_quantized.onnx、fp32 → onnx/model.onnx など)。ファイルが無い場合のフォールバックはしない |
--ModelDir | — | model/<EmbeddingModel> | モデルのディレクトリパス。足りないファイルは固定リビジョンから自動ダウンロード |
エンティティ・Azure サービス名の抽出に使用する OpenAI 互換エンドポイント。 通常記事はチャンクごとにエンティティ/関係/サービス名を LLM で抽出する。 Azure Update 記事のサービス名は HTML の H2 構造から確定的に取得するため LLM 抽出対象外。
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--LlmBaseUrl | LLM_BASE_URL | http://localhost:11434/v1 | LLM エンドポイントのベース URL |
--LlmModel | LLM_MODEL | qwen3:8b | モデル名 (サーバーに読み込まれているモデル) |
--LlmApiKey | LLM_API_KEY | (なし) | API キー。ローカルサーバーは通常不要 |
主要な LLM サーバー別設定例:
| サーバー | LlmBaseUrl | LlmModel 例 |
|---|---|---|
| Ollama | http://localhost:11434/v1 | qwen3:8b, llama3.1:8b |
| LM Studio | http://localhost:1234/v1 | ロード中のモデル名 |
| llama.cpp server | http://localhost:8080/v1 | (引数不要のことも多い) |
| 引数 | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
--OutDir | — | out | .lbdb ファイルと manifest.json の出力先 |
機密情報以外をファイルで管理したい場合:
{
"XmlDir": ".tmp",
"EmbeddingModel": "sirasagi62/ruri-v3-30m-ONNX",
"EmbeddingDtype": "q8",
"LlmBaseUrl": "http://localhost:11434/v1",
"LlmModel": "qwen3:8b",
"OutDir": "out"
}
out/
├── blog-20260614120000.lbdb GraphDB ファイル (Ladybug 0.19.x 形式)
└── manifest.json メタデータ (モデル情報・件数・SHA-256)
各
Chunkには所属Postの日付・タイトル・年・月が非正規化保存される (チャンク単位の日付フィルタ用)。 グラフの詳細構造は「GraphDB 構造」セクションを参照。
inspect)構築済み .lbdb を読み取り専用で開き、データが期待通りかを検証する。
「2026年2月の話題」のような時期クエリで的外れな結果が返る場合の原因切り分けに使う。
# 1. 統計サマリ: ノード/エッジ件数・日付分布(月別)・次数上位のエンティティ/サービス・サンプル Chunk
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect
# DB を明示指定 (省略時は out/ → wwwroot/data/ の順に最新 .lbdb を自動検出)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect out/blog-20260614120000.lbdb
# 2. 任意の Cypher を実行 (結果をテーブル表示)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect --cypher \
"MATCH (p:Post) WHERE p.date >= '2026-02-01' AND p.date < '2026-03-01' RETURN count(p)"
# 3. 自然文クエリでサンプルベクトル検索 (埋め込みモデルが必要・期待した記事が返るか確認)
dotnet run --project src/AzureMoe.Chat.Ingest -- inspect --query "2026年2月のAzure Functionsの更新" --topk 8
| 引数 | デフォルト | 説明 |
|---|---|---|
| (位置引数) | (自動検出) | 対象 .lbdb ファイルパス |
--cypher "..." | — | 任意の Cypher を実行して結果を表示 |
--query "..." | — | 自然文を埋め込んでベクトル検索 (上位 --topk 件) |
--embedding | sirasagi62/ruri-v3-30m-ONNX | --query 用の埋め込みモデル ID (DB の manifest.json の embeddingModel に合わせる) |
--model | model/<--embedding> | --query 用モデルのディレクトリ |
--dtype | q8 | --query 用 dtype (manifest.json の embeddingDtype に合わせる) |
--topk | 8 | --query で返す件数 |
append)既存の .lbdb にブログ記事を 1 件追加する。WordPress XML 全件再インジェストをせずに
新着記事だけをインクリメンタルに反映したいときに使う。
処理の流れ:
.lbdb を日付付きファイル名でコピー (元ファイルは変更しない)manifest.json を更新# 基本: URL と元 DB を指定して追記
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb
# 既に存在する URL を上書きしたい場合
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb --Override
# 出力先を変える / LLM を切り替える
dotnet run --project src/AzureMoe.Chat.Ingest -- append \
https://example.com/2026/06/azure-update-june/ \
out/blog-20260618120000.lbdb \
--OutDir T:\temp\output \
--LlmBaseUrl http://localhost:1234/v1 \
--LlmModel qwen3-8b
| 引数 | デフォルト | 説明 |
|---|---|---|
<url> (位置 1) | — | 取得するブログ記事の URL |
<sourceDbPath> (位置 2) | — | コピー元の .lbdb ファイルパス |
--Override | false | 同じ URL の Post が既に存在する場合に削除して上書きする |
--OutDir | out | 出力 .lbdb と manifest.json の書き出し先 |
--EmbeddingModel / --EmbeddingDtype | sirasagi62/ruri-v3-30m-ONNX / q8 | 埋め込みモデル。元 DB と同じフォルダに manifest.json があれば一致を確認し、異なる場合は中断する |
--ModelDir | model/<EmbeddingModel> | 埋め込みモデルのディレクトリ |
--LlmBaseUrl | http://localhost:11434/v1 | LLM エンドポイント |
--LlmModel | qwen3:8b | LLM モデル名 |
--LlmApiKey | (なし) | LLM API キー |
同じ URL を
--Overrideなしで追記しようとするとエラーになる。 出力ファイル名はblog-<yyyyMMdd>.lbdb(実行日の UTC 日付)。
インジェストが生成する .lbdb ファイルの内部構造。LadybugDB (グラフDB) に格納され、
ブラウザの WASM 版も同じファイルをそのまま開く。
ブログには構造的に異なる 2 種類の投稿が混在するため、タイトルで判定して処理を分岐させる。
週次アップデートまとめ記事。H2 見出しがサービス名、その配下の箇条書きが更新内容になっている。
<h2>Azure Functions</h2> ← serviceName = "Azure Functions"
<ul>
<li>Flex Consumption で新機能 ← Chunk 1 (chunkType = "update_item")
<ul><li>詳細...</li></ul> ← 子要素は親と一体で保持
</li>
<li>従量課金で改善 ← Chunk 2 (update_item)
</li>
</ul>
<h2>Azure Container Apps</h2> ← serviceName = "Azure Container Apps"
...
<li> 1 件 + その子要素(子要素は親への補足コメントとして一体保持)"Azure Functions\n\nFlex Consumption で新機能..." (サービス名をプレフィックス)特定テーマについての文章記事。
"記事タイトル\n\nチャンクテキスト"sectionTitle フィールドに記録| ノード | 主なフィールド | 説明 |
|---|---|---|
Post | id, title, url, date, year, month | ブログ記事 1 件 |
Chunk | 下表参照 | テキストチャンク(ベクトル付き) |
AzureService | name | Azure サービス名(正式名称) |
Entity | name, type, description | LLM 抽出エンティティ(人物・技術・機能など) |
Tag | name | WordPress タグ |
Chunk フィールド詳細
| フィールド | 型 | 説明 |
|---|---|---|
id | INT64 | 主キー |
postId | INT64 | 所属 Post の id |
ordinal | INT64 | Post 内での順序 |
text | STRING | チャンク本文 |
date / year / month | STRING / INT64 | Post から非正規化(日付フィルタ用) |
title | STRING | Post タイトル(引用表示用) |
sectionTitle | STRING | 直前の H2/H3 見出しテキスト |
serviceName | STRING | Azure Update 記事のみ: H2 のサービス名。通常記事は空文字 |
chunkType | STRING | "update_item" (Update 記事の箇条書き) / "prose" (通常テキスト) |
emb | FLOAT[256] | ruri-v3-30m による埋め込みベクトル (次元は manifest.json の embeddingDim) |
Post ──HAS_CHUNK──────▶ Chunk
Post ──TAGGED──────────▶ Tag
Post ──COVERS_SERVICE──▶ AzureService ← Azure Update 記事は H2 から、通常記事は LLM から収集
Chunk ──MENTIONS───────▶ Entity ← LLM 抽出(全記事)
Entity ──RELATED_TO────▶ Entity ← LLM 抽出(全記事)
| インデックス名 | 対象 | 距離計算 |
|---|---|---|
chunk_emb_idx | Chunk.emb | コサイン類似度 (HNSW) |
| 記事タイプ | 収集元 | 特徴 |
|---|---|---|
| Azure Update 記事 | H2 見出しテキスト | 確定的・LLM 不要・ブレなし |
| 通常記事 | LLM (EntityExtractor) | 本文から柔軟に抽出 |
構築した GraphDB に対してベクトル検索を対話的に試せるツール。
# out/ から最新 .lbdb を自動検出
dotnet run --project src/AzureMoe.Chat.Verify
# DB を明示指定
dotnet run --project src/AzureMoe.Chat.Verify -- --DbPath out/blog-20260614120000.lbdb
# 結果件数を変える
dotnet run --project src/AzureMoe.Chat.Verify -- --TopK 10
| 引数 | デフォルト | 説明 |
|---|---|---|
--DbPath | (自動検出) | 検索対象の .lbdb ファイルパス |
--OutDir | out | DbPath 未指定時に最新 .lbdb を探すディレクトリ |
--EmbeddingModel | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID (DB 構築時と同じもの) |
--EmbeddingDtype | q8 | 埋め込み dtype (DB 構築時と同じもの) |
--ModelDir | model/<EmbeddingModel> | モデルのディレクトリパス |
--TopK | 5 | 返す検索結果の件数 |
> 検索クエリを入力して Enter
> \stats 統計情報を再表示
> q 終了
Blazor WebAssembly でブラウザ内完結の GraphRAG チャット。 LadybugDB (WASM)・transformers.js をすべてブラウザ内で実行する。 以下の優先順位で LLM バックエンドを自動選択するため どの環境でも動作する:
| 役割 | バックエンド | モデル / エンジン | 備考 |
|---|---|---|---|
| LLM (テキスト生成) | Chrome 組み込み AI | Gemini Nano | ダウンロード不要・最優先 |
| LLM (テキスト生成) | transformers.js | onnx-community/Qwen2.5-0.5B-Instruct (q4) | Chrome AI 非対応時に自動ダウンロード |
| LLM (テキスト生成) | OpenAI 互換 HTTP | 任意 (LM Studio / Ollama 等) | /llm コマンドで実行中に切替可 |
| 埋め込み | transformers.js | sirasagi62/ruri-v3-30m-ONNX (manifest.json に従う) | 起動時に自動ダウンロード |
モデルは transformers.js が Hugging Face Hub からダウンロードし、Cache API で自動キャッシュする。 2 回目以降はオフラインでも動作する。
必要な Chrome バージョン: Chrome 138+ (Dev または Canary チャンネル)。
ハードウェア要件:
セットアップ手順:
chrome://flags/#optimization-guide-on-device-model → Enabledchrome://flags/#prompt-api-for-gemini-nano → Enabled (または "Enabled multilingual")chrome://on-device-internals を開き、Gemini Nano モデルのダウンロード状況を確認。バージョンが 0.0.0.0 の場合は "Check for update" をクリックDevTools で確認する方法 (F12 → Console):
// Chrome 138+ の場合
typeof LanguageModel // "function" なら有効
await LanguageModel.availability() // "available" なら即利用可能
// 旧ビルドの場合
typeof window.ai?.languageModel // "object" なら有効
有効になると起動時に「Chrome 組み込み AI (Gemini Nano) 利用可能 — ダウンロード不要」と表示される。
Chrome AI が使えない環境向けに appsettings.json で変更可能:
{
"LlmModelId": "onnx-community/Qwen2.5-1.5B-Instruct",
"LlmDtype": "q4"
}
| モデル | サイズ (q4) | 特徴 |
|---|---|---|
onnx-community/Qwen2.5-0.5B-Instruct | ~350 MB | 軽量・高速 (デフォルト) |
onnx-community/Qwen2.5-1.5B-Instruct | ~900 MB | 日本語品質が向上 |
npm install が csproj ビルド時に自動実行される)manifest.json と .lbdb ファイル1. インジェストで DB を生成
dotnet run --project src/AzureMoe.Chat.Ingest
2. 生成物を Web プロジェクトの data ディレクトリにコピー
# Windows
copy out\manifest.json src\AzureMoe.Chat.Web\wwwroot\data\
copy out\*.lbdb src\AzureMoe.Chat.Web\wwwroot\data\
# Linux / macOS
cp out/manifest.json src/AzureMoe.Chat.Web/wwwroot/data/
cp out/*.lbdb src/AzureMoe.Chat.Web/wwwroot/data/
3. 開発サーバーを起動
dotnet run --project src/AzureMoe.Chat.Web
ブラウザで https://localhost:5001 を開く。
src/AzureMoe.Chat.Web/wwwroot/appsettings.json で変更できる。
| キー | デフォルト | 説明 |
|---|---|---|
ManifestUrl | data/manifest.json | manifest.json の URL |
DbBaseUrl | data/ | DB ファイルのベース URL (manifest の databaseFile を結合) |
LlmModelId | onnx-community/Qwen2.5-0.5B-Instruct | LLM モデル ID (HuggingFace) |
LlmDtype | q4 | 量子化精度。q4 / q8 / fp16 など |
LlmMaxNewTokens | 4096 | 最終回答の最大生成トークン数 (上限。EOS で自然停止) |
EmbeddingModelId | sirasagi62/ruri-v3-30m-ONNX | 埋め込みモデル ID。manifest.json に記録があればそちらを優先 (フォールバック用) |
RetrievalMode | Normal | 探索の深さ。Fast / Normal / Deep (UI の /mode でも変更可) |
RagTopK | 6 | HTTP LLM モード時の最終参照数上限 |
MaxContextChars | 6000 | HTTP LLM モード時に LLM へ渡す文脈の最大文字数 |
LocalRagTopK | 3 | ローカル WASM LLM モード時の参照数上限 (2B 級モデル向け圧縮設定) |
LocalMaxContextChars | 2500 | ローカル WASM LLM モード時の文脈最大文字数 |
LocalPerRefMaxChars | 800 | ローカル WASM LLM モード時の参照1件あたりの最大文字数 |
HistoryTurns | 3 | Deep モードで LLM に渡す直近会話ターン数 |
DeepMaxRounds | 3 | Deep モードの検索クエリ数 (round-0 + 上位タイトルでの追検索) |
VerifyGrounding | true | 生成後に、回答中の製品名・数値が参考情報に含まれるかをルールベースで検査し、不十分なら再生成・警告表示 |
SystemPrompt | (組み込み) | システムプロンプト (キャラ付け + GraphRAG ルール) |
RetrievalMode)検索の深さと応答速度のトレードオフを選べる。UI の /mode で実行中にも切替可能。
| モード | 検索の広さ | 内容 | 会話履歴 | 体感 |
|---|---|---|---|---|
Fast | 狭 | グラフ探索なし・純ベクトル検索 | 使用しない | 最軽量 |
Normal | 中 | グラフ探索あり | 使用しない (単発質問) | バランス (既定) |
Deep | 広 | 関連エンティティまで辿る + 上位記事タイトルで決定的に追検索 (複数クエリ, 最大 DeepMaxRounds) | 直近 HistoryTurns ターン | 高精度・低速 |
全モード共通の2段構え:
質問から日付 (例: 「2026年2月」「先月」) を検出した場合は、再ランクも日付窓の中で行い範囲外の記事を除外する。
応答時間について: ブラウザ内 CPU では最終回答の生成時間が支配的で、これは全モード共通。 そのため Fast と Normal の体感差は小さく (主に検索の広さと精度で差が出る)、 複数クエリを投げる Deep が明確に遅い。各ステップの進行状況はチャット画面に表示される。
グラウンディングの担保: LLM が学習知識で一般論を返さないよう、全モードで次を行う。
VerifyGrounding とは独立に常時)。VerifyGrounding=true なら、回答中の英字の語 (製品名など、3 文字以上) と 2 桁以上の数値 (年・バージョンなど) が参考情報または質問に含まれるかを検査する (GroundingChecker、LLM は使わない)。含まれない語があれば、その語を名指しした厳密なプロンプト(参考情報のみ・[n] 引用必須)で1回だけ再生成し、裏付けられない語が少ない方の回答を採用する。それでも残る場合は、該当する語を回答の下に⚠警告で表示する。WASM (CPU) 実行時は再生成に時間がかかるため、再生成せず警告のみ表示する。チャット入力欄で以下のコマンドが使える。
| コマンド | 動作 |
|---|---|
/help | コマンド一覧を表示 |
/mode [fast|normal|deep] | 探索モードを表示 / 変更 (引数なしで現在値を表示) |
/llm [endpoint [model]] | 外部 OpenAI 互換 LLM を設定 (例: /llm http://localhost:1234/v1 gpt-model)。引数なしでローカル WASM に戻す |
/debug [on|off] | デバッグ出力の表示切替。ON 時は埋め込み・Cypher・LLM プロンプト/応答をチャット内に表示 |
/info | 現在の LLM / 埋め込みモデル情報と探索モードを表示 |
/license | ライセンス情報を表示 |
/clear | 画面と会話履歴をクリア |
/reload | アプリを再起動 |
| (それ以外) | RAG クエリとして実行 |
応答生成中は入力欄の右に 「■ 停止」ボタンが表示され、クリックすると LLM の生成を即座に中断する
(緊急停止)。途中まで生成された回答は残る。長い生成 (LlmMaxNewTokens が大きい) や、まれに小型モデルが
同じ文言を繰り返すループに入った場合の保険として使える (ループ自体は no_repeat_ngram_size で抑制済み)。
URL に以下のクエリパラメータを付けると、ページを開いた時点から /llm モードが有効になる。
/llm コマンドによる手動設定と同じ効果。
| パラメータ | 必須 | 説明 |
|---|---|---|
llm | 必須 | OpenAI 互換エンドポイントの URL |
model | 任意 | モデル名。省略時はエンドポイント側のデフォルト |
# LM Studio
https://chat.azure.moe/?llm=http://localhost:1234/v1&model=qwen3-8b
# Ollama
https://chat.azure.moe/?llm=http://localhost:11434/v1&model=qwen3:8b
# OpenAI 互換 API
https://chat.azure.moe/?llm=https://api.openai.com/v1&model=gpt-4o
スマートフォンでの利用: iOS/Android ではメモリ制約によりローカル LLM・埋め込みモデルをスキップし、 GraphDB に対してキーワード検索のみ行うモードで起動する。
?llm=...を付けると外部 LLM との組み合わせで回答生成も可能になる。
Mixed Content に注意: デプロイ済みサイト (
https://) からhttp://localhostへの接続は ブラウザの Mixed Content ポリシーによりブロックされる (CORS 設定とは無関係)。 LM Studio / Ollama などローカルサーバーを使う場合は、開発用サーバー (dotnet run) 経由でhttp://localhost:5001からアクセスするか、HTTPS に対応したリバースプロキシを用意すること。