リクエストの流れ
1 つの質問を retrieval から LLM まで端から端まで追います。
質問の流れは LocalLens のホットパスです。チャットのラウンドトリップは すべて、固定の順序で 4 つのアクターを通過します。caller、ワークフロー クラス、QVAC gateway、そしてプロンプトビルダーです。
流れは 2 つの半分に分かれます。
- Retrieval (RAG): 質問を
GTE_LARGE_FP16で埋め込み、workspace に対してragSearchを実行し、top-K の chunks を返します。この側は 回答の言葉を 1 語も書きません。 - Generation (LLM): retrieve した chunks と質問を
QWEN3_1_7B_INST_Q4(またはフォールバックのQWEN3_600M_INST_Q4) にcompletion()で渡します。これが実際に 回答を組み立てるチャットモデルで、トークンをストリームで返します。
ステップごとの流れ
1. Embed と search (RAG)
LocalLensApp.askBrain(id, question) (src/locallens.ts)
は QvacGateway.search(workspace, question, 5) を呼びます。gateway
は質問を GTE_LARGE_FP16 で embed し、brain の QVAC workspace
に対して ragSearch を実行します。結果は SearchHit のリストです。
type SearchHit = {
id: string;
relativePath: string;
chunkIndex: number;
content: string;
score?: number;
};Top-K は 5 に固定されています。これはプロンプトサイズと回答品質の バランスで、小さな brain では実際よく機能します。変える場合は チャットモデルの context window (デフォルト 4096 トークン) に 注意してください。
2. Grounded prompt を組み立てる (継ぎ目)
src/rag.ts の buildGroundedHistory(question, hits) は
2 件のメッセージからなる ChatMessage[] を返します。これが RAG 側
(hits を生成した側) と LLM (これから hits を読む側) の継ぎ目です。
- system メッセージ: ルールを与えます。excerpts の事実のみを使う、 ブラケットで引用する、ユーザーの言語で答える、隠れた chain-of-thought は出さない。
- user メッセージ: 番号付きブロックとして excerpts を並べ、 そのあとに質問を続けます。
番号付き excerpts は、モデルが引用するときに [1] [2] として
返してくるものです。LLM は embeddings や検索スコアを一切見ません。
このパッケージ化されたプロンプトだけを見ます。
3. LLM の completion をストリームする
QvacGateway.answer(history) が チャット LLM の仕事の場所です。
QVAC の completion() を stream: true 付きで
QWEN3_1_7B_INST_Q4 (デフォルトの 1.7B パラメータ、Q4 量子化の
Qwen3 instruct モデル) に対して呼び出し、各 contentDelta イベントを
AsyncGenerator<string> として yield します。askBrain はそれらの
トークンを蓄積して最終文字列にします。
1.7B をロードできないマシンでは、gateway がモデルロード時に
QWEN3_600M_INST_Q4 に透過的にフォールバックします (
QvacGateway.loadModels
を参照)。streaming generator の形はどちらでも同じなので、上流は
変化に気づきません。
captureThinking: true は内部の reasoning トークンを可視出力から
除外し、kvCache: true は follow-up のために prefix のアテンション
状態を再利用させます。
実際に回答が書かれるのはこのステップです。ここを飛ばすと、 ランク付けされた chunks はあっても、文章としての回答はありません。
4. 引用を返す
プロンプトの構築に使った hits は、ChatAnswer.citations として
そのまま caller に返ります。
type ChatAnswer = {
answer: string;
citations: { id; relativePath; chunkIndex; score? }[];
};CLI がこれを出力し、UI が回答の下にこれを描画します。回答は LLM の出力、引用は RAG 側のエビデンスリスト。両方が一緒に届くことで、 読み手は主張をソースに照らして検証できます。
この流れに ない もの
- 再 ingest は走りません。 質問のときにファイルシステムには 触れません。
- store の書き換えは起きません。 JSON store は質問パスでは read-only です。
- LLM への 2 回目の呼び出しはありません。 質問 1 件につき completion は 1 回。embedding model は 1 回 (search 用)、 chat LLM は 1 回 (回答用) です。
read path をこれだけ薄く保っているのが、follow-up が控えめな ハードウェアでも軽快に感じる理由です。モデル呼び出し 2 回、 ディスク書き込みゼロ、そして LLM が見るのは top-K chunks と質問だけ。