より充実した診断を追加する
モデルの状態、インデックス進捗、ファイル/チャンク数、そして QVAC profiler の実レイテンシ値を表に出す。
置く場所: モデル状態と profiler ブリッジは src/qvac.ts、公開は
src/server.ts、表示は src/ui/app.js。
LocalLens はライフサイクルの大半をユーザーから隠しています。すべてが 高速なときはそれで構いません。モデル読み込みに 1 分かかっていて UI が 何も表示しない、というときには困ります。小さな診断面があれば、コアを 複雑化させずにこれを解消できます。
表に出す価値のある信号は 2 種類あります。
- 運用状態 — 「いま何が起きているか」: ロード中かロード済みか、 ファイルとチャンクの数、アクティブなチャットモデル。ゲートウェイは すでにこれらを把握しています。あとは公開するだけです。
- パフォーマンス指標 — 「どのくらい速いか」: モデル読み込み時間、
embedding レイテンシ、RAG 検索と取り込みのタイミング、生成の
スループット。QVAC SDK には
profilerユーティリティが含まれており、 自分の側で計測コードを書かずにこれらをキャプチャできます。
両者は相補的です。運用状態は今を伝え、profiler は所要時間を伝えます。
何を公開するか
- モデル読み込みの状態。 Loading / loaded / failed と、どのモデルか。
- インデックスの進捗。 読み込んだバイト数、作成したチャンク数、 現在のファイル。
- brain 統計。 ファイル数、チャンク数、埋め込みモデル名。
- アクティブなモデル。 いま使われているチャットモデル(1.7B か、 600M フォールバックか)。
- レイテンシ集計。
loadModel、embed、completion、ragIngest、ragSearchの直近値と平均値。
最初の 4 つはゲートウェイから来ます。5 つ目は profiler から来ます。
レシピ
1. ゲートウェイにステータスゲッターを追加する
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 回有効化すれば、
ゲートウェイのすべての呼び出しを自動的にキャプチャしてくれます。
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 を
直接インポートせずに済みます。
metrics() {
return profiler.exportJSON();
}profiler はすべてを測るわけではない
profiler は SDK 呼び出しのレイテンシをキャプチャします。インデックスの 進捗、ファイル/チャンク数、現在アクティブなチャットモデルを直接測ることは しません。これらはゲートウェイ自身の状態から来ます。だからこのページでは 2 つの面を両方残しています。profiler はステータスゲッターを補完するもので、 代替するものではありません。
3. /api/diagnostics ルートを追加する
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 です。LocalLensApp に diagnostics() と
metrics() をプロキシメソッドとして公開するか、ゲートウェイを protected
にするかです。プロキシのほうが綺麗です。サーバーが話す相手は引き続き
LocalLensApp だけにしておきます。
diagnostics() {
return this.qvac.status();
}
metrics() {
return this.qvac.metrics();
}4. インデックスの進捗
インデックスは現在、単一の async 呼び出しで動いています。進捗を ストリームしたい場合は、ワークフローをジェネレータに切り替えます。
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 リファレンス —
enable、disable、exportSummary、exportTable、exportJSON、 呼び出し単位のprofilingオプションを含む全 API。