たねやつの木

Photographs, Keyboards and Programming

EmbeddingGemmaでRAG構築! (第3回) ~Ollama連携とQ&Aシステムの完成~

こんにちは、たねやつです。

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システム構築に挑戦してみてください。