こんにちは、たねやつです。
3回にわたるRAG構築連載も、いよいよ最終回です。
前回までで、ユーザーの質問に対し、関連する文書を知識源のデータベースから検索してくる「Retriever」が完成しました。今回は、このRetrieverとOllamaで動かすLLM(大規模言語モデル)を連携させ、ついに質問応答(Q&A)システムを完成させます。
この記事でできること
- OllamaのPythonライブラリの基本的な使い方がわかります。
- 検索した文書(コンテキスト)をLLMに渡すためのプロンプトを作成できます。
- RetrieverとLLMを組み合わせて、RAGシステムを完成させることができます。
準備するもの
- これまでの成果物:
faiss_index.bin,knowledgeフォルダ、Retrieverのコード。 - Ollama: インストール済みで、サービスが実行中であること。
- 生成用LLM: OllamaでLLMをpullしておく。(例:
ollama pull gemma:2b) - Pythonライブラリ:
ollamaを追加でインストールします。
pip install ollama
ステップ1: Retriever機能の準備
まず、前回作成したRetrieverのコードを関数としてまとめておくと、後の工程がスムーズになります。この関数は、質問文を受け取り、関連する文書のテキストを返すようにします。
import os import numpy as np from sentence_transformers import SentenceTransformer import faiss # --- Retrieverの準備 --- KNOWLEDGE_DIR = "knowledge" documents = [] for filename in os.listdir(KNOWLEDGE_DIR): if filename.endswith(".txt"): filepath = os.path.join(KNOWLEDGE_DIR, filename) with open(filepath, "r", encoding="utf-8") as f: documents.append(f.read()) INDEX_FILE = "faiss_index.bin" index = faiss.read_index(INDEX_FILE) model = SentenceTransformer("google/embeddinggemma-300m") def retrieve(query_text, k=2): """質問を受け取り、関連文書のテキストを返す関数""" query_vector = model.encode([query_text]) distances, indices = index.search(query_vector.astype('float32'), k) # 検索結果のテキストを結合して返す retrieved_docs = [documents[i] for i in indices[0]] return "\n\n---\n\n".join(retrieved_docs)
ステップ2: プロンプトテンプレートの作成
次に、LLMに渡す指示書(プロンプト)のテンプレートを作成します。RAGの性能は、このプロンプトの書き方で大きく変わるため、非常に重要な工程です。
ポイントは、「参考情報だけを使って答える」ように明確に指示し、「答えがない場合は『分かりません』と答える」という制約(ガードレール)を設けることです。これにより、LLMが知ったかぶりをして嘘をつく(ハルシネーション)のを防ぎます。
def create_prompt(query_text, context): """プロンプトを生成する関数""" prompt = f""" 参考情報のみに基づいて、質問に答えてください。参考情報に答えがない場合は、「分かりません」と答えてください。 # 参考情報 {context} # 質問 {query_text} # 回答 """ return prompt
ステップ3: LLMによる回答生成
いよいよ、OllamaのLLMを呼び出して回答を生成させます。ollama.chatを使い、作成したプロンプトを渡します。
import ollama # --- 実行 --- # 1. 質問を設定 query_text = "RAGとは何ですか?" # 2. Retrieverで関連文書を取得 context = retrieve(query_text) # 3. プロンプトを生成 prompt = create_prompt(query_text, context) print("---" 生成したプロンプト ---") print(prompt) # 4. LLMにプロンプトを渡して回答を生成 print("\n--- LLMの回答 ---") response = ollama.chat( model="gemma:2b", # Ollamaでpull済みのモデルを指定 messages=[{"role": "user", "content": prompt}] ) print(response['message']['content'])
実行結果
上記のコードを実行すると、まずRetrieverが検索した結果を含むプロンプトが表示され、その後にLLMからの回答が出力されます。
--- 生成したプロンプト --- 参考情報のみに基づいて、質問に答えてください。参考情報に答えがない場合は、「分かりません」と答えてください。 # 参考情報 埋め込み(Embedding)とは、単語や文章を「ベクトル」と呼ばれる数値の配列に変換する技術です。... --- RAG(Retrieval-Augmented Generation)は、LLMの回答精度を向上させるための技術です。... # 質問 RAGとは何ですか? # 回答 --- LLMの回答 --- RAG(Retrieval-Augmented Generation)は、LLMの回答精度を向上させるための技術です。ユーザーからの質問に対し、まず関連する情報をデータベースから検索(Retrieval)し、その検索結果を基にLLMが回答を生成(Generation)します。これにより、LLMが知らない情報についても、正確な回答を生成できるようになります。
Retrieverが検索してきた(少しノイズの混じった)情報から、LLMが質問の意図を正確に汲み取り、RAGに関する部分だけを的確に抜き出して回答を生成してくれました。見事にRAGシステムが機能していますね!
全体のコード
ここまでのステップをまとめた、Q&Aシステムの全体のコードは以下のようになります。
import os import numpy as np from sentence_transformers import SentenceTransformer import faiss import ollama # --- 1. Retrieverの準備 --- KNOWLEDGE_DIR = "knowledge" documents = [] for filename in os.listdir(KNOWLEDGE_DIR): if filename.endswith(".txt"): filepath = os.path.join(KNOWLEDGE_DIR, filename) with open(filepath, "r", encoding="utf-8") as f: documents.append(f.read()) INDEX_FILE = "faiss_index.bin" index = faiss.read_index(INDEX_FILE) model = SentenceTransformer("google/embeddinggemma-300m") def retrieve(query_text, k=2): query_vector = model.encode([query_text]) distances, indices = index.search(query_vector.astype('float32'), k) retrieved_docs = [documents[i] for i in indices[0]] return "\n\n---\n\n".join(retrieved_docs) # --- 2. プロンプト生成関数の準備 --- def create_prompt(query_text, context): return f""" 参考情報のみに基づいて、質問に答えてください。参考情報に答えがない場合は、「分かりません」と答えてください。 # 参考情報 {context} # 質問 {query_text} # 回答 """ # --- 3. 実行 --- def run_rag_qa(query_text): print(f"クエリ: 「{query_text}」") context = retrieve(query_text) prompt = create_prompt(query_text, context) print("--- LLMに問い合わせ中... ---") response = ollama.chat( model="gemma3:12b", messages=[{"role": "user", "content": prompt}] ) print("--- LLMの回答 ---") print(response['message']['content']) # 実行例 run_rag_qa("RAGとは何ですか?")
まとめ
3回にわたる連載で、ローカル環境にRAGシステムを構築する方法を解説しました。ベクトルデータベースの構築から始まり、Retrieverによる検索、そしてLLMとの連携まで、一連の流れを体験いただけたかと思います。
この仕組みを使うことで、単に物知りなだけではない、特定の知識体系に基づいた正確な応答ができる「専門家」として、ローカルLLMを活躍させることができます。
ここからさらに、Web UIを追加してチャット形式で使えるようにしたり、チャンキングやハイブリッド検索でRetrieverの精度を向上させたりと、改善のアイデアは尽きません。ぜひ、みなさん自身のQ&Aシステム構築に挑戦してみてください。