アーキテクチャ
全体パイプライン
フォルダがディスクから根拠付き回答に変わるまでの道のり。
LocalLens はフォルダと質問を受け取り、引用付きの回答を返します。 その 2 点の間にあるのは、小さく固定されたパイプラインだけです。
インデックス用パイプライン
インデックス処理は brain を作成するときに 1 回だけ走ります。
コード上のステップは次のとおりです。
discoverTextDocuments(rootPath)(src/files.ts)はフォルダを 走査し、安全なテキストファイル以外をふるい落としてLocalDocument[]を返します。chunkDocuments(documents, { brainId })(src/rag.ts)はragChunkを呼んで各ドキュメントを約 220 トークンのチャンクに分割 (40 トークンのオーバーラップ付き)し、TextChunk[]を返します。QvacGateway.ingestChunks(workspace, chunks)(src/qvac.ts)は 各チャンクをGTE_LARGE_FP16で embedding に変換し、名前付きの QVAC ワークスペースに書き込みます。LocalLensStore.saveBrainとsaveChunks(src/store.ts)が brain とそのチャンクを.locallens/store.jsonに保存します。
最初の 3 ステップは入力フォルダを対象に動きます。4 つ目は brain の レコードを残すための処理で、次回アプリを開いたときに再インデックス なしで質問できるようにするためのものです。
質問用パイプライン
すべてのチャット往復は同じ 4 ステップをたどります。
コード上では次のようになります。
QvacGateway.search(workspace, question, 5)(src/qvac.ts)は 質問を embedding にしてragSearchを実行し、上位 5 件のSearchHit[]を返します。buildGroundedHistory(question, hits)(src/rag.ts)は、 システムルールと番号付き抜粋を含む 2 メッセージ構成のChatMessage[]を生成します。QvacGateway.answer(history)(src/qvac.ts)は QVAC のcompletion()をstream: trueで呼び出し、各contentDeltaをAsyncGenerator<string>として yield します。LocalLensApp.askBrain(src/locallens.ts)がストリームを蓄積し、{ answer, citations }を返します。
各ステップの所在
| 工程 | モジュール | 主な呼び出し |
|---|---|---|
| ファイル検出 | src/files.ts | discoverTextDocuments |
| チャンク化 | src/rag.ts | chunkDocuments → ragChunk |
| Embedding と取り込み | src/qvac.ts | ingestChunks → ragIngest |
| 永続化 | src/store.ts | saveBrain, saveChunks |
| 検索 | src/qvac.ts | search → ragSearch |
| プロンプト | src/rag.ts | buildGroundedHistory |
| テキスト生成 | src/qvac.ts | answer → completion |
| ワークフロー | src/locallens.ts | LocalLensApp.askBrain |
このパイプラインに含まれないもの
- 質問パスでの再取り込みなし。 質問するときにファイルシステムや JSON ストアに触ることはありません。QVAC のワークスペースを読むだけです。
- 質問ごとに 2 度目のモデル呼び出しはなし。 チャットモデルとの往復は 1 回。検索用の embedding 呼び出しが質問ごとに 1 回。それだけです。
- 会話状態を保持しない。 各質問は独立した検索ラウンドです。チャット 履歴は毎回ゼロから組み立てられ、その質問に対する検索結果のみを 根拠とします。
最後の性質が、フォローアップの回答を根拠から外させない理由です。
マルチターンの対話が必要なら、buildGroundedHistory の上に組み立てて
ください。中に入れてはいけません。RAG と LLM
の章で理由を説明しています。