はじめに
前回の記事ではローカルLLMをCPUだけで動かす方法を紹介しました。今回はその発展として、RAG(Retrieval-Augmented Generation) を実装してみます。RAGとは、あらかじめ用意したテキストをベクトル検索して関連箇所を取り出し、その内容をLLMへの文脈として渡すことで、より正確な回答を得る手法です。GPUなし・クラウドなし・すべてローカルで動かします。
対象者
GPUなし・クラウドなしのローカル環境でRAGを試したい人。
今回も10年ほど前のデスクトップのWSLで動かしています。
環境
OS : Ubuntu 24.04.1 LTS
CPU : Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
メモリ : 16GB
llama.cpp: version 8146 (418dea39c)
開発環境
Python の仮想環境には conda を使用しています。
conda は Anaconda/Miniconda が提供するパッケージ・環境管理ツールです。
condaを使うことでPython のバージョンや依存パッケージをプロジェクトごとに分離することができます。
conda create -n rag-env python=3.13.12
conda activate rag-env
pip install -r requirements.txt
主な依存パッケージ(requirements.txt):
| パッケージ | 役割 |
|---|---|
faiss-cpu |
ベクトルインデックス・検索 |
sentence-transformers |
テキストの埋め込みベクトル生成 |
numpy |
ベクトル演算 |
huggingface_hub |
埋め込みモデルのダウンロード |
LLM の推論には以前ビルドした llama.cpp を使用します。
全体の構造
今回実装したファイルは番号付きで実行順序が分かるようにしています。
GitHubにリポジトリをおいてみました。
GitHubリポジトリ(rag-cli)
0_setup.sh # Miniconda インストール+conda 環境構築
1_pip_install.sh # pip パッケージ一括インストール
2_get_embedded_model.sh # 埋め込みモデルのダウンロード(初回のみ)
3_make_chunk.py # テキスト → チャンク JSON
4_make_index.py # チャンク JSON → FAISS インデックス
5_search.py # クエリ → 類似チャンク取得
6_generate.py # チャンク + クエリ → llama.cpp → 回答生成
7_rag_cli.py # 5_search + 6_generate をまとめた CLI
パイプライン全体の流れ:
【前処理】事前に一度だけ実行する
【RAG実行】クエリのたびに実行する
7_rag_cli.py は検索と回答生成を import して一度に実行するラッパーです。
各スクリプトの概要
題材として青空文庫の「走れメロス」を使います。
チャンク分割
長いテキストをそのままベクトル化すると精度が落ちるため、一定サイズに分割します。分割サイズとオーバーラップ(前後の重複)をパラメータで調整できます。オーバーラップを設定することで、チャンクの境界をまたぐ文脈も拾えるようになります。
インデックス生成
チャンクを埋め込みモデル(paraphrase-multilingual-MiniLM-L12-v2)でベクトル化し、FAISSインデックスに保存します。このステップは前処理として一度だけ実行します。
ベクトル検索
クエリを同じ埋め込みモデルでベクトル化し、FAISSで類似度の高いチャンクを上位k件取得します。コサイン類似度を使っているため、同じ単語を使っていなくても意味的に近いチャンクが拾えます。
「メロスはなぜ怒ったのか」と聞くと、メロスの激怒の場面や王の暴虐を描いたチャンクが上位に来ます。
回答生成
取得したチャンクとクエリをプロンプトに組み立てて llama.cpp に渡します。LLMはテキスト全体を読んでいるわけではなく、検索で絞り込んだ関連箇所だけを文脈として受け取ります。
RAG CLI
上記の検索→回答生成を1コマンドで実行できます。
python 7_rag_cli.py -q "メロスはなぜ怒ったのか"
実行結果(抜粋):
メロスは、邪智暴虐の王に対して怒りを覚えました。...
まとめ
はじめてRAGの話を聞いた時は、ドキュメントだけいれれば、AIが自動で検索してくれる魔法の箱のようなイメージでした。ですが、実際に動かしてみて、そんなに簡単じゃないことが分かりました。
生成AIは回答をまとめている部分をうけもっているだけで、回答精度はチャンクの精度に大きく依存します。
もっといろいろ追究しようかと思っていましたが、anything-LLMという便利ツールがあることを知ったので、そちらに移行予定です。
ちなみに今回のコードはClaude Sonnetに書いてもらいました。
バイブコーディングすごいですねー。