buchizo/azuremoe-chat

chat/search app for blog.azure.moe

C#

0

33 commits

updated Sep 27, 2026

See the code

README

azuremoe-chat

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/

前提条件

  • .NET 10 SDK
  • Windows x64 (LadybugDB ネイティブバインディングが win-x64 のみ)
  • WordPress エクスポート XML ファイル
  • エンティティ抽出用ローカル LLM (OpenAI 互換エンドポイント)

埋め込みモデルのセットアップ

インジェスト・検索ツール・チャットアプリのすべてが 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 Retrieval78.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 の新機能"

確認内容:

  • クエリのトークン ID が完全一致し、ベクトルの cosine 類似度が 0.999 以上であること
  • manifest の埋め込み設定が dump と一致すること
  • ネイティブで構築した DB が wasm エンジンで開け、ベクトル検索と rag-worker.js が発行する Cypher がすべて通ること

文書 (passage) 側の不一致は参考表示のみ。transformers.js の Unigram トークナイザーは byte_fallback 未対応で、 改行が <unk> になる (.NET 側は HF tokenizers と同じ <0x0A>)。文書は .NET でしか埋め込まないため問題ない。 クエリは両側で空白・改行を 1 スペースに正規化してから埋め込む (EmbeddingProfile.NormalizeQuery / rag-worker.js normalizeQuery)。


WordPress エクスポートファイルの準備

  1. WordPress 管理画面 → ツール → エクスポート
  2. 「すべてのコンテンツ」または「投稿」を選択してエクスポート
  3. ダウンロードした .xml ファイルを .tmp/ フォルダに配置
.tmp/
└── wordpress.2026-06-14.xml

インジェストツール (AzureMoe.Chat.Ingest)

WordPress XML を読み込んでエンティティを抽出し、GraphDB を構築して out/ に出力する。

基本的な実行

dotnet run --project src/AzureMoe.Chat.Ingest

引数一覧

設定の優先順位: コマンドライン引数 > 環境変数 > appsettings.json > デフォルト値

入力

引数環境変数デフォルト説明
--XmlDir—.tmpWordPress エクスポート XML が入ったディレクトリ
--MaxPosts—0 (全件)処理する最大投稿数。動作確認時は 10 など小さい値に

埋め込みモデル

引数環境変数デフォルト説明
--EmbeddingModel—sirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID (EmbeddingProfile.cs に定義済みのもの)
--EmbeddingDtype—q8transformers.js 準拠の dtype 名 (q8 → onnx/model_quantized.onnx、fp32 → onnx/model.onnx など)。ファイルが無い場合のフォールバックはしない
--ModelDir—model/<EmbeddingModel>モデルのディレクトリパス。足りないファイルは固定リビジョンから自動ダウンロード

ローカル LLM (エンティティ抽出)

エンティティ・Azure サービス名の抽出に使用する OpenAI 互換エンドポイント。 通常記事はチャンクごとにエンティティ/関係/サービス名を LLM で抽出する。 Azure Update 記事のサービス名は HTML の H2 構造から確定的に取得するため LLM 抽出対象外。

引数環境変数デフォルト説明
--LlmBaseUrlLLM_BASE_URLhttp://localhost:11434/v1LLM エンドポイントのベース URL
--LlmModelLLM_MODELqwen3:8bモデル名 (サーバーに読み込まれているモデル)
--LlmApiKeyLLM_API_KEY(なし)API キー。ローカルサーバーは通常不要

主要な LLM サーバー別設定例:

サーバーLlmBaseUrlLlmModel 例
Ollamahttp://localhost:11434/v1qwen3:8b, llama3.1:8b
LM Studiohttp://localhost:1234/v1ロード中のモデル名
llama.cpp serverhttp://localhost:8080/v1(引数不要のことも多い)

出力

引数環境変数デフォルト説明
--OutDir—out.lbdb ファイルと manifest.json の出力先

appsettings.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 件)
--embeddingsirasagi62/ruri-v3-30m-ONNX--query 用の埋め込みモデル ID (DB の manifest.json の embeddingModel に合わせる)
--modelmodel/<--embedding>--query 用モデルのディレクトリ
--dtypeq8--query 用 dtype (manifest.json の embeddingDtype に合わせる)
--topk8--query で返す件数

追記サブコマンド (append)

既存の .lbdb にブログ記事を 1 件追加する。WordPress XML 全件再インジェストをせずに 新着記事だけをインクリメンタルに反映したいときに使う。

処理の流れ:

  1. 指定 URL のブログ記事を HTTP で取得
  2. 元の .lbdb を日付付きファイル名でコピー (元ファイルは変更しない)
  3. コピーしたファイルにチャンク・埋め込み・LLM 抽出結果を追記
  4. ベクトルインデックスを再作成してから 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 ファイルパス
--Overridefalse同じ URL の Post が既に存在する場合に削除して上書きする
--OutDirout出力 .lbdb と manifest.json の書き出し先
--EmbeddingModel / --EmbeddingDtypesirasagi62/ruri-v3-30m-ONNX / q8埋め込みモデル。元 DB と同じフォルダに manifest.json があれば一致を確認し、異なる場合は中断する
--ModelDirmodel/<EmbeddingModel>埋め込みモデルのディレクトリ
--LlmBaseUrlhttp://localhost:11434/v1LLM エンドポイント
--LlmModelqwen3:8bLLM モデル名
--LlmApiKey(なし)LLM API キー

同じ URL を --Override なしで追記しようとするとエラーになる。 出力ファイル名は blog-<yyyyMMdd>.lbdb (実行日の UTC 日付)。


GraphDB 構造

インジェストが生成する .lbdb ファイルの内部構造。LadybugDB (グラフDB) に格納され、 ブラウザの WASM 版も同じファイルをそのまま開く。

投稿タイプによるチャンク方針

ブログには構造的に異なる 2 種類の投稿が混在するため、タイトルで判定して処理を分岐させる。

Azure Update 記事 (タイトルが "Azure Update" または "Azure Updates" で始まる)

週次アップデートまとめ記事。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 で新機能..." (サービス名をプレフィックス)
  • サービス名: H2 テキストから確定的に取得(LLM 不要)

通常記事

特定テーマについての文章記事。

  • チャンク単位: 段落境界での 800 文字区切り(超長段落は文末で 1,200 文字まで強制分割)
  • 埋め込み入力: "記事タイトル\n\nチャンクテキスト"
  • セクション情報: 直前の H2/H3 見出しを sectionTitle フィールドに記録

ノード一覧

ノード主なフィールド説明
Postid, title, url, date, year, monthブログ記事 1 件
Chunk下表参照テキストチャンク(ベクトル付き)
AzureServicenameAzure サービス名(正式名称)
Entityname, type, descriptionLLM 抽出エンティティ(人物・技術・機能など)
TagnameWordPress タグ

Chunk フィールド詳細

フィールド型説明
idINT64主キー
postIdINT64所属 Post の id
ordinalINT64Post 内での順序
textSTRINGチャンク本文
date / year / monthSTRING / INT64Post から非正規化(日付フィルタ用)
titleSTRINGPost タイトル(引用表示用)
sectionTitleSTRING直前の H2/H3 見出しテキスト
serviceNameSTRINGAzure Update 記事のみ: H2 のサービス名。通常記事は空文字
chunkTypeSTRING"update_item" (Update 記事の箇条書き) / "prose" (通常テキスト)
embFLOAT[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_idxChunk.embコサイン類似度 (HNSW)

サービス名の収集方針

記事タイプ収集元特徴
Azure Update 記事H2 見出しテキスト確定的・LLM 不要・ブレなし
通常記事LLM (EntityExtractor)本文から柔軟に抽出

検索確認ツール (AzureMoe.Chat.Verify)

構築した 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 ファイルパス
--OutDiroutDbPath 未指定時に最新 .lbdb を探すディレクトリ
--EmbeddingModelsirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID (DB 構築時と同じもの)
--EmbeddingDtypeq8埋め込み dtype (DB 構築時と同じもの)
--ModelDirmodel/<EmbeddingModel>モデルのディレクトリパス
--TopK5返す検索結果の件数

起動後の操作

> 検索クエリを入力して Enter
> \stats    統計情報を再表示
> q         終了

チャットアプリ (AzureMoe.Chat.Web)

Blazor WebAssembly でブラウザ内完結の GraphRAG チャット。 LadybugDB (WASM)・transformers.js をすべてブラウザ内で実行する。 以下の優先順位で LLM バックエンドを自動選択するため どの環境でも動作する:

  1. Chrome 組み込み AI (Gemini Nano) — Chrome 127+ でモデルダウンロード不要
  2. transformers.js + WebGPU — WebGPU 対応ブラウザで GPU 推論
  3. transformers.js + WASM CPU — すべての環境で動作 (低速)

使用モデル

役割バックエンドモデル / エンジン備考
LLM (テキスト生成)Chrome 組み込み AIGemini Nanoダウンロード不要・最優先
LLM (テキスト生成)transformers.jsonnx-community/Qwen2.5-0.5B-Instruct (q4)Chrome AI 非対応時に自動ダウンロード
LLM (テキスト生成)OpenAI 互換 HTTP任意 (LM Studio / Ollama 等)/llm コマンドで実行中に切替可
埋め込みtransformers.jssirasagi62/ruri-v3-30m-ONNX (manifest.json に従う)起動時に自動ダウンロード

モデルは transformers.js が Hugging Face Hub からダウンロードし、Cache API で自動キャッシュする。 2 回目以降はオフラインでも動作する。

Chrome 組み込み AI を有効にする方法

必要な Chrome バージョン: Chrome 138+ (Dev または Canary チャンネル)。

ハードウェア要件:

  • 空きストレージ 22 GB 以上
  • RAM 16 GB 以上、または GPU VRAM 4 GB 以上
  • Windows 10 / macOS 13 / Linux (ChromeOS Plusも可)

セットアップ手順:

  1. chrome://flags/#optimization-guide-on-device-model → Enabled
  2. chrome://flags/#prompt-api-for-gemini-nano → Enabled (または "Enabled multilingual")
  3. Chrome を再起動
  4. 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) 利用可能 — ダウンロード不要」と表示される。

transformers.js フォールバックモデルの変更例

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日本語品質が向上

前提条件

  • Node.js 18+ (npm install が csproj ビルド時に自動実行される)
  • インジェストで生成した manifest.json と .lbdb ファイル
  • インターネット接続 (初回: モデルダウンロード。2 回目以降は不要)

開発環境での起動手順

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 を開く。

appsettings.json 設定項目

src/AzureMoe.Chat.Web/wwwroot/appsettings.json で変更できる。

キーデフォルト説明
ManifestUrldata/manifest.jsonmanifest.json の URL
DbBaseUrldata/DB ファイルのベース URL (manifest の databaseFile を結合)
LlmModelIdonnx-community/Qwen2.5-0.5B-InstructLLM モデル ID (HuggingFace)
LlmDtypeq4量子化精度。q4 / q8 / fp16 など
LlmMaxNewTokens4096最終回答の最大生成トークン数 (上限。EOS で自然停止)
EmbeddingModelIdsirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID。manifest.json に記録があればそちらを優先 (フォールバック用)
RetrievalModeNormal探索の深さ。Fast / Normal / Deep (UI の /mode でも変更可)
RagTopK6HTTP LLM モード時の最終参照数上限
MaxContextChars6000HTTP LLM モード時に LLM へ渡す文脈の最大文字数
LocalRagTopK3ローカル WASM LLM モード時の参照数上限 (2B 級モデル向け圧縮設定)
LocalMaxContextChars2500ローカル WASM LLM モード時の文脈最大文字数
LocalPerRefMaxChars800ローカル WASM LLM モード時の参照1件あたりの最大文字数
HistoryTurns3Deep モードで LLM に渡す直近会話ターン数
DeepMaxRounds3Deep モードの検索クエリ数 (round-0 + 上位タイトルでの追検索)
VerifyGroundingtrue生成後に、回答中の製品名・数値が参考情報に含まれるかをルールベースで検査し、不十分なら再生成・警告表示
SystemPrompt(組み込み)システムプロンプト (キャラ付け + GraphRAG ルール)

探索モード (RetrievalMode)

検索の深さと応答速度のトレードオフを選べる。UI の /mode で実行中にも切替可能。

モード検索の広さ内容会話履歴体感
Fast狭グラフ探索なし・純ベクトル検索使用しない最軽量
Normal中グラフ探索あり使用しない (単発質問)バランス (既定)
Deep広関連エンティティまで辿る + 上位記事タイトルで決定的に追検索 (複数クエリ, 最大 DeepMaxRounds)直近 HistoryTurns ターン高精度・低速

全モード共通の2段構え:

  1. recall — モードに応じてベクトル+グラフ+日付で候補を広く集める。
  2. precision — 候補を**「元の質問」へのコサイン類似度で再ランクし、質問の関連プール (上位) から外れたものを足切り**する。 グラフのつながり (共有タグ/エンティティ/サービス) は同点時の微加点に留め、的外れな拡張が本筋の記事を上回らないようにする。 これにより Deep でも「件数は多いが質問に合わない」を防ぐ。

質問から日付 (例: 「2026年2月」「先月」) を検出した場合は、再ランクも日付窓の中で行い範囲外の記事を除外する。

応答時間について: ブラウザ内 CPU では最終回答の生成時間が支配的で、これは全モード共通。 そのため Fast と Normal の体感差は小さく (主に検索の広さと精度で差が出る)、 複数クエリを投げる Deep が明確に遅い。各ステップの進行状況はチャット画面に表示される。

グラウンディングの担保: LLM が学習知識で一般論を返さないよう、全モードで次を行う。

  • 関連する参考情報が1件も見つからない場合は生成を行わず、「見つからなかった」旨を返す (VerifyGrounding とは独立に常時)。
  • 生成後に VerifyGrounding=true なら、回答中の英字の語 (製品名など、3 文字以上) と 2 桁以上の数値 (年・バージョンなど) が参考情報または質問に含まれるかを検査する (GroundingChecker、LLM は使わない)。含まれない語があれば、その語を名指しした厳密なプロンプト(参考情報のみ・[n] 引用必須)で1回だけ再生成し、裏付けられない語が少ない方の回答を採用する。それでも残る場合は、該当する語を回答の下に⚠警告で表示する。WASM (CPU) 実行時は再生成に時間がかかるため、再生成せず警告のみ表示する。
  • 検索結果は「元の質問」への関連度で再ランク・足切りされるため、文脈には質問に合致した参照のみが渡る(precision 重視)。

UI コマンド

チャット入力欄で以下のコマンドが使える。

コマンド動作
/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 パラメータ

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 に対応したリバースプロキシを用意すること。

動作フロー

  1. 起動時: manifest.json 取得 → DB ダウンロード → LadybugDB WASM 初期化 → transformers.js モデル読み込み
  2. 質問入力時 (モードにより広さが変わる): クエリ解析 (日付・キーワード抽出) → ベクトル+グラフ+日付で候補を収集 → 元の質問への関連度で再ランク・足切り → URL 単位でまとめてコンテキスト構築 (本文中の URL を除去して圧縮) → LLM でストリーミング生成 → 接地検査
  3. WebGPU 非対応環境: WASM CPU にフォールバックして生成 (低速)

buchizo/azuremoe-chat

chat/search app for blog.azure.moe

C#

0

33 commits

updated Sep 27, 2026

See the code

README

azuremoe-chat

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/

前提条件

  • .NET 10 SDK
  • Windows x64 (LadybugDB ネイティブバインディングが win-x64 のみ)
  • WordPress エクスポート XML ファイル
  • エンティティ抽出用ローカル LLM (OpenAI 互換エンドポイント)

埋め込みモデルのセットアップ

インジェスト・検索ツール・チャットアプリのすべてが 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 Retrieval78.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 の新機能"

確認内容:

  • クエリのトークン ID が完全一致し、ベクトルの cosine 類似度が 0.999 以上であること
  • manifest の埋め込み設定が dump と一致すること
  • ネイティブで構築した DB が wasm エンジンで開け、ベクトル検索と rag-worker.js が発行する Cypher がすべて通ること

文書 (passage) 側の不一致は参考表示のみ。transformers.js の Unigram トークナイザーは byte_fallback 未対応で、 改行が <unk> になる (.NET 側は HF tokenizers と同じ <0x0A>)。文書は .NET でしか埋め込まないため問題ない。 クエリは両側で空白・改行を 1 スペースに正規化してから埋め込む (EmbeddingProfile.NormalizeQuery / rag-worker.js normalizeQuery)。


WordPress エクスポートファイルの準備

  1. WordPress 管理画面 → ツール → エクスポート
  2. 「すべてのコンテンツ」または「投稿」を選択してエクスポート
  3. ダウンロードした .xml ファイルを .tmp/ フォルダに配置
.tmp/
└── wordpress.2026-06-14.xml

インジェストツール (AzureMoe.Chat.Ingest)

WordPress XML を読み込んでエンティティを抽出し、GraphDB を構築して out/ に出力する。

基本的な実行

dotnet run --project src/AzureMoe.Chat.Ingest

引数一覧

設定の優先順位: コマンドライン引数 > 環境変数 > appsettings.json > デフォルト値

入力

引数環境変数デフォルト説明
--XmlDir—.tmpWordPress エクスポート XML が入ったディレクトリ
--MaxPosts—0 (全件)処理する最大投稿数。動作確認時は 10 など小さい値に

埋め込みモデル

引数環境変数デフォルト説明
--EmbeddingModel—sirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID (EmbeddingProfile.cs に定義済みのもの)
--EmbeddingDtype—q8transformers.js 準拠の dtype 名 (q8 → onnx/model_quantized.onnx、fp32 → onnx/model.onnx など)。ファイルが無い場合のフォールバックはしない
--ModelDir—model/<EmbeddingModel>モデルのディレクトリパス。足りないファイルは固定リビジョンから自動ダウンロード

ローカル LLM (エンティティ抽出)

エンティティ・Azure サービス名の抽出に使用する OpenAI 互換エンドポイント。 通常記事はチャンクごとにエンティティ/関係/サービス名を LLM で抽出する。 Azure Update 記事のサービス名は HTML の H2 構造から確定的に取得するため LLM 抽出対象外。

引数環境変数デフォルト説明
--LlmBaseUrlLLM_BASE_URLhttp://localhost:11434/v1LLM エンドポイントのベース URL
--LlmModelLLM_MODELqwen3:8bモデル名 (サーバーに読み込まれているモデル)
--LlmApiKeyLLM_API_KEY(なし)API キー。ローカルサーバーは通常不要

主要な LLM サーバー別設定例:

サーバーLlmBaseUrlLlmModel 例
Ollamahttp://localhost:11434/v1qwen3:8b, llama3.1:8b
LM Studiohttp://localhost:1234/v1ロード中のモデル名
llama.cpp serverhttp://localhost:8080/v1(引数不要のことも多い)

出力

引数環境変数デフォルト説明
--OutDir—out.lbdb ファイルと manifest.json の出力先

appsettings.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 件)
--embeddingsirasagi62/ruri-v3-30m-ONNX--query 用の埋め込みモデル ID (DB の manifest.json の embeddingModel に合わせる)
--modelmodel/<--embedding>--query 用モデルのディレクトリ
--dtypeq8--query 用 dtype (manifest.json の embeddingDtype に合わせる)
--topk8--query で返す件数

追記サブコマンド (append)

既存の .lbdb にブログ記事を 1 件追加する。WordPress XML 全件再インジェストをせずに 新着記事だけをインクリメンタルに反映したいときに使う。

処理の流れ:

  1. 指定 URL のブログ記事を HTTP で取得
  2. 元の .lbdb を日付付きファイル名でコピー (元ファイルは変更しない)
  3. コピーしたファイルにチャンク・埋め込み・LLM 抽出結果を追記
  4. ベクトルインデックスを再作成してから 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 ファイルパス
--Overridefalse同じ URL の Post が既に存在する場合に削除して上書きする
--OutDirout出力 .lbdb と manifest.json の書き出し先
--EmbeddingModel / --EmbeddingDtypesirasagi62/ruri-v3-30m-ONNX / q8埋め込みモデル。元 DB と同じフォルダに manifest.json があれば一致を確認し、異なる場合は中断する
--ModelDirmodel/<EmbeddingModel>埋め込みモデルのディレクトリ
--LlmBaseUrlhttp://localhost:11434/v1LLM エンドポイント
--LlmModelqwen3:8bLLM モデル名
--LlmApiKey(なし)LLM API キー

同じ URL を --Override なしで追記しようとするとエラーになる。 出力ファイル名は blog-<yyyyMMdd>.lbdb (実行日の UTC 日付)。


GraphDB 構造

インジェストが生成する .lbdb ファイルの内部構造。LadybugDB (グラフDB) に格納され、 ブラウザの WASM 版も同じファイルをそのまま開く。

投稿タイプによるチャンク方針

ブログには構造的に異なる 2 種類の投稿が混在するため、タイトルで判定して処理を分岐させる。

Azure Update 記事 (タイトルが "Azure Update" または "Azure Updates" で始まる)

週次アップデートまとめ記事。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 で新機能..." (サービス名をプレフィックス)
  • サービス名: H2 テキストから確定的に取得(LLM 不要)

通常記事

特定テーマについての文章記事。

  • チャンク単位: 段落境界での 800 文字区切り(超長段落は文末で 1,200 文字まで強制分割)
  • 埋め込み入力: "記事タイトル\n\nチャンクテキスト"
  • セクション情報: 直前の H2/H3 見出しを sectionTitle フィールドに記録

ノード一覧

ノード主なフィールド説明
Postid, title, url, date, year, monthブログ記事 1 件
Chunk下表参照テキストチャンク(ベクトル付き)
AzureServicenameAzure サービス名(正式名称)
Entityname, type, descriptionLLM 抽出エンティティ(人物・技術・機能など)
TagnameWordPress タグ

Chunk フィールド詳細

フィールド型説明
idINT64主キー
postIdINT64所属 Post の id
ordinalINT64Post 内での順序
textSTRINGチャンク本文
date / year / monthSTRING / INT64Post から非正規化(日付フィルタ用)
titleSTRINGPost タイトル(引用表示用)
sectionTitleSTRING直前の H2/H3 見出しテキスト
serviceNameSTRINGAzure Update 記事のみ: H2 のサービス名。通常記事は空文字
chunkTypeSTRING"update_item" (Update 記事の箇条書き) / "prose" (通常テキスト)
embFLOAT[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_idxChunk.embコサイン類似度 (HNSW)

サービス名の収集方針

記事タイプ収集元特徴
Azure Update 記事H2 見出しテキスト確定的・LLM 不要・ブレなし
通常記事LLM (EntityExtractor)本文から柔軟に抽出

検索確認ツール (AzureMoe.Chat.Verify)

構築した 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 ファイルパス
--OutDiroutDbPath 未指定時に最新 .lbdb を探すディレクトリ
--EmbeddingModelsirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID (DB 構築時と同じもの)
--EmbeddingDtypeq8埋め込み dtype (DB 構築時と同じもの)
--ModelDirmodel/<EmbeddingModel>モデルのディレクトリパス
--TopK5返す検索結果の件数

起動後の操作

> 検索クエリを入力して Enter
> \stats    統計情報を再表示
> q         終了

チャットアプリ (AzureMoe.Chat.Web)

Blazor WebAssembly でブラウザ内完結の GraphRAG チャット。 LadybugDB (WASM)・transformers.js をすべてブラウザ内で実行する。 以下の優先順位で LLM バックエンドを自動選択するため どの環境でも動作する:

  1. Chrome 組み込み AI (Gemini Nano) — Chrome 127+ でモデルダウンロード不要
  2. transformers.js + WebGPU — WebGPU 対応ブラウザで GPU 推論
  3. transformers.js + WASM CPU — すべての環境で動作 (低速)

使用モデル

役割バックエンドモデル / エンジン備考
LLM (テキスト生成)Chrome 組み込み AIGemini Nanoダウンロード不要・最優先
LLM (テキスト生成)transformers.jsonnx-community/Qwen2.5-0.5B-Instruct (q4)Chrome AI 非対応時に自動ダウンロード
LLM (テキスト生成)OpenAI 互換 HTTP任意 (LM Studio / Ollama 等)/llm コマンドで実行中に切替可
埋め込みtransformers.jssirasagi62/ruri-v3-30m-ONNX (manifest.json に従う)起動時に自動ダウンロード

モデルは transformers.js が Hugging Face Hub からダウンロードし、Cache API で自動キャッシュする。 2 回目以降はオフラインでも動作する。

Chrome 組み込み AI を有効にする方法

必要な Chrome バージョン: Chrome 138+ (Dev または Canary チャンネル)。

ハードウェア要件:

  • 空きストレージ 22 GB 以上
  • RAM 16 GB 以上、または GPU VRAM 4 GB 以上
  • Windows 10 / macOS 13 / Linux (ChromeOS Plusも可)

セットアップ手順:

  1. chrome://flags/#optimization-guide-on-device-model → Enabled
  2. chrome://flags/#prompt-api-for-gemini-nano → Enabled (または "Enabled multilingual")
  3. Chrome を再起動
  4. 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) 利用可能 — ダウンロード不要」と表示される。

transformers.js フォールバックモデルの変更例

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日本語品質が向上

前提条件

  • Node.js 18+ (npm install が csproj ビルド時に自動実行される)
  • インジェストで生成した manifest.json と .lbdb ファイル
  • インターネット接続 (初回: モデルダウンロード。2 回目以降は不要)

開発環境での起動手順

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 を開く。

appsettings.json 設定項目

src/AzureMoe.Chat.Web/wwwroot/appsettings.json で変更できる。

キーデフォルト説明
ManifestUrldata/manifest.jsonmanifest.json の URL
DbBaseUrldata/DB ファイルのベース URL (manifest の databaseFile を結合)
LlmModelIdonnx-community/Qwen2.5-0.5B-InstructLLM モデル ID (HuggingFace)
LlmDtypeq4量子化精度。q4 / q8 / fp16 など
LlmMaxNewTokens4096最終回答の最大生成トークン数 (上限。EOS で自然停止)
EmbeddingModelIdsirasagi62/ruri-v3-30m-ONNX埋め込みモデル ID。manifest.json に記録があればそちらを優先 (フォールバック用)
RetrievalModeNormal探索の深さ。Fast / Normal / Deep (UI の /mode でも変更可)
RagTopK6HTTP LLM モード時の最終参照数上限
MaxContextChars6000HTTP LLM モード時に LLM へ渡す文脈の最大文字数
LocalRagTopK3ローカル WASM LLM モード時の参照数上限 (2B 級モデル向け圧縮設定)
LocalMaxContextChars2500ローカル WASM LLM モード時の文脈最大文字数
LocalPerRefMaxChars800ローカル WASM LLM モード時の参照1件あたりの最大文字数
HistoryTurns3Deep モードで LLM に渡す直近会話ターン数
DeepMaxRounds3Deep モードの検索クエリ数 (round-0 + 上位タイトルでの追検索)
VerifyGroundingtrue生成後に、回答中の製品名・数値が参考情報に含まれるかをルールベースで検査し、不十分なら再生成・警告表示
SystemPrompt(組み込み)システムプロンプト (キャラ付け + GraphRAG ルール)

探索モード (RetrievalMode)

検索の深さと応答速度のトレードオフを選べる。UI の /mode で実行中にも切替可能。

モード検索の広さ内容会話履歴体感
Fast狭グラフ探索なし・純ベクトル検索使用しない最軽量
Normal中グラフ探索あり使用しない (単発質問)バランス (既定)
Deep広関連エンティティまで辿る + 上位記事タイトルで決定的に追検索 (複数クエリ, 最大 DeepMaxRounds)直近 HistoryTurns ターン高精度・低速

全モード共通の2段構え:

  1. recall — モードに応じてベクトル+グラフ+日付で候補を広く集める。
  2. precision — 候補を**「元の質問」へのコサイン類似度で再ランクし、質問の関連プール (上位) から外れたものを足切り**する。 グラフのつながり (共有タグ/エンティティ/サービス) は同点時の微加点に留め、的外れな拡張が本筋の記事を上回らないようにする。 これにより Deep でも「件数は多いが質問に合わない」を防ぐ。

質問から日付 (例: 「2026年2月」「先月」) を検出した場合は、再ランクも日付窓の中で行い範囲外の記事を除外する。

応答時間について: ブラウザ内 CPU では最終回答の生成時間が支配的で、これは全モード共通。 そのため Fast と Normal の体感差は小さく (主に検索の広さと精度で差が出る)、 複数クエリを投げる Deep が明確に遅い。各ステップの進行状況はチャット画面に表示される。

グラウンディングの担保: LLM が学習知識で一般論を返さないよう、全モードで次を行う。

  • 関連する参考情報が1件も見つからない場合は生成を行わず、「見つからなかった」旨を返す (VerifyGrounding とは独立に常時)。
  • 生成後に VerifyGrounding=true なら、回答中の英字の語 (製品名など、3 文字以上) と 2 桁以上の数値 (年・バージョンなど) が参考情報または質問に含まれるかを検査する (GroundingChecker、LLM は使わない)。含まれない語があれば、その語を名指しした厳密なプロンプト(参考情報のみ・[n] 引用必須)で1回だけ再生成し、裏付けられない語が少ない方の回答を採用する。それでも残る場合は、該当する語を回答の下に⚠警告で表示する。WASM (CPU) 実行時は再生成に時間がかかるため、再生成せず警告のみ表示する。
  • 検索結果は「元の質問」への関連度で再ランク・足切りされるため、文脈には質問に合致した参照のみが渡る(precision 重視)。

UI コマンド

チャット入力欄で以下のコマンドが使える。

コマンド動作
/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 パラメータ

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 に対応したリバースプロキシを用意すること。

動作フロー

  1. 起動時: manifest.json 取得 → DB ダウンロード → LadybugDB WASM 初期化 → transformers.js モデル読み込み
  2. 質問入力時 (モードにより広さが変わる): クエリ解析 (日付・キーワード抽出) → ベクトル+グラフ+日付で候補を収集 → 元の質問への関連度で再ランク・足切り → URL 単位でまとめてコンテキスト構築 (本文中の URL を除去して圧縮) → LLM でストリーミング生成 → 接地検査
  3. WebGPU 非対応環境: WASM CPU にフォールバックして生成 (低速)