LocalLens
拡張

より充実した診断を追加する

モデルの状態、インデックス進捗、ファイル/チャンク数、そして QVAC profiler の実レイテンシ値を表に出す。

置く場所: モデル状態と profiler ブリッジは src/qvac.ts、公開は src/server.ts、表示は src/ui/app.js

LocalLens はライフサイクルの大半をユーザーから隠しています。すべてが 高速なときはそれで構いません。モデル読み込みに 1 分かかっていて UI が 何も表示しない、というときには困ります。小さな診断面があれば、コアを 複雑化させずにこれを解消できます。

表に出す価値のある信号は 2 種類あります。

  • 運用状態 — 「いま何が起きているか」: ロード中かロード済みか、 ファイルとチャンクの数、アクティブなチャットモデル。ゲートウェイは すでにこれらを把握しています。あとは公開するだけです。
  • パフォーマンス指標 — 「どのくらい速いか」: モデル読み込み時間、 embedding レイテンシ、RAG 検索と取り込みのタイミング、生成の スループット。QVAC SDK には profiler ユーティリティが含まれており、 自分の側で計測コードを書かずにこれらをキャプチャできます。

両者は相補的です。運用状態は今を伝え、profiler は所要時間を伝えます。

何を公開するか

  1. モデル読み込みの状態。 Loading / loaded / failed と、どのモデルか。
  2. インデックスの進捗。 読み込んだバイト数、作成したチャンク数、 現在のファイル。
  3. brain 統計。 ファイル数、チャンク数、埋め込みモデル名。
  4. アクティブなモデル。 いま使われているチャットモデル(1.7B か、 600M フォールバックか)。
  5. レイテンシ集計。 loadModelembedcompletionragIngestragSearch の直近値と平均値。

最初の 4 つはゲートウェイから来ます。5 つ目は profiler から来ます。

レシピ

1. ゲートウェイにステータスゲッターを追加する

src/qvac.ts
type ModelState = "unloaded" | "loading" | "loaded" | "error";

export class QvacGateway {
  // existing fields
  private state: { chat: ModelState; embedding: ModelState } = {
    chat: "unloaded",
    embedding: "unloaded",
  };

  status() {
    return {
      chat: { state: this.state.chat, modelId: this.chatModelId },
      embedding: { state: this.state.embedding, modelId: this.embeddingModelId },
    };
  }

  // wrap loadModel calls to update state
  private async loadModels(): Promise<void> {
    this.state.embedding = "loading";
    try {
      this.embeddingModelId ??= await loadModel({ modelSrc: GTE_LARGE_FP16 });
      this.state.embedding = "loaded";
    } catch (e) {
      this.state.embedding = "error";
      throw e;
    }
    // … same pattern for chat …
  }
}

ステータスゲッターは読み取り専用で副作用なしなので、診断エンドポイント から呼んでもモデルライフサイクルを触る心配がありません。

2. QVAC profiler を有効化する

@qvac/sdk には profiler ユーティリティがあり、モデル読み込み、生成、 embedding、RAG 操作のタイミングを記録します。起動時に 1 回有効化すれば、 ゲートウェイのすべての呼び出しを自動的にキャプチャしてくれます。

src/qvac.ts
import { profiler } from "@qvac/sdk";

// near the top of the file, alongside chatModelConfig
profiler.enable();

profiler.enable() はグローバルスイッチです。これ以降すべての SDK 呼び出しがイベントを記録します。呼び出し単位で有効化したいなら、QVAC の 各関数も { profiling: { enabled: true } } をオプションで受け取れますし、 { profiling: { enabled: false } } で個別に無効化もできます。

エクスポート形式は 3 つあります。

メソッド戻り値用途
profiler.exportSummary()高レベルなサマリ文字列シャットダウン時のログ用。
profiler.exportTable()詳細な集計テーブルターミナルで読む用。
profiler.exportJSON(){ aggregates, recentEvents, config }UI 向けに HTTP で配信する用。

診断エンドポイントが返すべきは JSON です。UI は好きな形に整形できます。

ゲートウェイに小さなヘルパーを追加すれば、呼び出し元は profiler を 直接インポートせずに済みます。

src/qvac.ts
metrics() {
  return profiler.exportJSON();
}

profiler はすべてを測るわけではない

profiler は SDK 呼び出しのレイテンシをキャプチャします。インデックスの 進捗、ファイル/チャンク数、現在アクティブなチャットモデルを直接測ることは しません。これらはゲートウェイ自身の状態から来ます。だからこのページでは 2 つの面を両方残しています。profiler はステータスゲッターを補完するもので、 代替するものではありません。

3. /api/diagnostics ルートを追加する

src/server.ts
if (url.pathname === "/api/diagnostics" && request.method === "GET") {
  return json({
    qvac: app.diagnostics(),
    metrics: app.metrics(),
    brains: (await app.listBrains()).map((b) => ({
      id: b.id,
      name: b.name,
      status: b.status,
      fileCount: b.fileCount,
      chunkCount: b.chunkCount,
      lastIndexedAt: b.lastIndexedAt,
      lastError: b.lastError,
    })),
  });
}

app.qvac は private です。LocalLensAppdiagnostics()metrics() をプロキシメソッドとして公開するか、ゲートウェイを protected にするかです。プロキシのほうが綺麗です。サーバーが話す相手は引き続き LocalLensApp だけにしておきます。

src/locallens.ts
diagnostics() {
  return this.qvac.status();
}

metrics() {
  return this.qvac.metrics();
}

4. インデックスの進捗

インデックスは現在、単一の async 呼び出しで動いています。進捗を ストリームしたい場合は、ワークフローをジェネレータに切り替えます。

src/locallens.ts
async *createBrainFromFolderProgress(input: CreateBrainFromFolderInput): AsyncGenerator<ProgressEvent, Brain> {
  yield { type: "discovery", message: "Walking folder…" };
  const documents = await discoverTextDocuments(folderPath);
  yield { type: "discovered", fileCount: documents.length };

  yield { type: "chunking" };
  const chunks = await chunkDocuments(documents, { brainId: brain.id });
  yield { type: "chunked", chunkCount: chunks.length };

  yield { type: "ingesting" };
  await this.qvac.ingestChunks(brain.workspace, chunks);
  yield { type: "ingested" };

  return indexed;
}

そのうえで、サーバーがそれを Server-Sent Events ストリームとして公開します。

if (url.pathname === "/api/brains/from-files/stream") {
  const stream = new ReadableStream({ /* yield events */ });
  return new Response(stream, { headers: { "content-type": "text/event-stream" } });
}

UI は SSE ストリームを消費し、プログレスバーを更新します。

5. 表示する

app.js にレンダ関数を 3 つ追加します。

function renderDiagnostics(diagnostics) {
  // model state badge in the topbar
  // brain table in a side panel
}

function renderMetrics(metrics) {
  // "Last search: 230 ms · avg 280 ms"
  // "Embedding model loaded in 4.2s"
  // — pulled from metrics.aggregates and metrics.recentEvents
}

function renderIndexingProgress(event) {
  // progress bar updates from SSE events
}

パネルは折りたためる形にしてください。ほとんどのユーザーはデフォルトで 開いたままにしたくないからです。

診断は読み取り専用

これらのエンドポイントは、いずれも状態を変更してはいけません。将来、 モデルを再起動したり brain をエヴィクトしたりする機能が必要になったら、 それは別ルートとして用意します。診断は何が起きているかを示すもので、 介入はしません。

変えなくて良いもの

  • domain.ts — 型はそのまま。
  • rag.ts — チャンク化とプロンプトに影響なし。
  • store.ts — JSON の形は変わりません。診断エンドポイントはすでに そこにある情報を読みます。

外部リファレンス

  • QVAC profiler リファレンスenabledisableexportSummaryexportTableexportJSON、 呼び出し単位の profiling オプションを含む全 API。

On this page