RAG と LLM
2 つの仕事、2 つのモデル — retrieval がエビデンスを見つけ、LLM が回答を書く。
RAG の 2 文字は、2 つの異なる仕事を指しています。Retrieval がエビデンスを見つけ、Generation が回答を書きます。 LocalLens ではこの 2 つの仕事を、別々のモデル、別々のモジュールに 意図的に分けています。
| 役割 | モデル | 何をするか |
|---|---|---|
| Retrieval (RAG) | GTE_LARGE_FP16 | ドキュメントと質問を embed。最も近い top-K の chunks を返す。 |
| Generation (LLM) | QWEN3_1_7B_INST_Q4 (フォールバック QWEN3_600M_INST_Q4) | 質問 + retrieve した chunks を読み、引用付きで回答を書く。 |
RAG 側は機械的です。workspace に対するベクトル類似度のみで、 言語的な理解はありません。LLM 側は、推論 と 文章生成 が起きる 場所です。retrieve された chunks はエビデンス。回答は、LLM が そのエビデンスを材料にして作るものです。
Retrieval がエビデンスを見つける
Retrieval は QVAC の RAG レイヤーの仕事で、embedding モデルが 担います。
ragChunkは文書を、オーバーラップ付きのトークン窓に分割します。ragIngestはそれらの chunks を workspace に書き込みつつ、GTE_LARGE_FP16で 1 つずつ埋め込みます。ragSearchは同じモデルで質問を embed し、最も近い top-K の chunks を返します。
Retrieval は回答ではありません。質問とのセマンティック類似度で ランク付けされた、回答を含む かもしれない excerpts のリストです。 何もマッチしなかった検索結果でも、それ自体が有用な信号です。LLM が それを根拠に「わかりません」と素直に言えるからです。自信たっぷりに 何かをでっち上げる必要はありません。
embedding モデルはチャット履歴を読まず、テキストを生成せず、 最終回答も見ません。出力はベクトルだけです。
Generation が回答を書く
Generation は チャット LLM の仕事です。LocalLens では既定で
QWEN3_1_7B_INST_Q4、1.7B のロードに失敗した場合のフォールバックは
QWEN3_600M_INST_Q4 です。
completion()はプロンプトビルダーが作ったChatMessage[]を受け取り、contentDeltaトークンとして文字列を streaming で 返します。- system プロンプトが、retrieve した excerpts のみから答え、 ブラケットで引用するようにモデルを縛ります。
- モデルはディスクに触れず、何も embed しません。読むのは context window — system のルール、番号付き excerpts、質問 — だけです。
実際に 回答 が生まれるのはこのステップです。これ以前はすべて 「エビデンス」と「パッケージ化されたプロンプト」を作っていただけ。 ここをスキップすれば、それは検索エンジンであってチャットではあり ません。
この分離が、プロンプトビルダーを小さく、QVAC gateway をきれいに
保つ理由です。プロンプトビルダーは embeddings も generation も
知りません。gateway は両方のモデルを抱えていますが、search と
answer という独立した 2 つのメソッドで公開しています。
2 つの間にある継ぎ目
buildGroundedHistory(question, hits) が継ぎ目です。質問と hits の
リストを受け取り、次のようなプロンプトを作ります。
- excerpts に番号がついていて (
[1]、[2]…) LLM が引用できる ようになっている; - system ルールは明示的かつ短い;
- ユーザーターンはちょうど 1 つ。会話履歴は再生されません。 すべての質問は新しい retrieval のラウンドとして扱われます。
検索が有効な結果を返さなかった場合、プロンプトはそのまま LLM に
渡りますが、excerpts の代わりに
"No matching chunks were found." という文字列が入ります。
system プロンプトの第 1 ルールが効いて、LLM はそれを率直に
伝えます。
Only use facts that appear in the excerpts. If the answer is not in them, say so plainly.
これがハルシネーション対策のすべてです。モデル 2 つ、ルール 1 つ。 それで機能します。
なぜチャット履歴を持たないのか?
LocalLens は、すべての質問を独立した retrieval ラウンドとして扱い
ます。retrieval は毎回新規、LLM のコンテキストも毎回新規、それゆえ
follow-up が以前のターンの半端な情報を継承することはありません。
複数ターンの対話を実装したい場合は、buildGroundedHistory の
内部 ではなく 上 に追加してください。