たねやつの木

Photographs, Keyboards and Programming

【ぴよログでRAG: 第9回】賢い回答を生成する:LLMとの連携とプロンプトエンジニアリング

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

前回、私たちはRAGシステムの「検索(Retrieval)」部分を完成させました。ユーザーの質問に対して、我々の知識の宝庫であるベクトルデータベースから、最も関連性の高い情報(チャンク)を瞬時に取り出すことができるようになりました。

しかし、取り出した情報をそのまま見せるだけでは、無愛想な検索結果リストに過ぎません。私たちの目標は、まるで人間と対話しているかのような、自然で賢い「AIアシスタント」です。

そこで今回は、いよいよRAG (Retrieval-Augmented Generation) の最後のピース、"G" すなわち「生成(Generation)」のフェーズを実装します。検索してきた情報を元に、LLM (大規模言語モデル) がユーザーの質問に直接、かつ自然な言葉で回答を生成する部分です。この最終工程では、LLMに的確な指示を与える「プロンプトエンジニアリング」が鍵を握ります。

前の記事

この記事でできること

  • RAGにおける生成(Generation)フェーズの役割を理解できる。
  • LLMに的確な指示を与える「プロンプトエンジニアリング」の基本がわかる。
  • GoogleのLLMである Gemini API を利用し、検索結果(コンテキスト)を基に回答を生成するPythonコードを実装できる。

事前に必要なもの

  • Pythonの実行環境
  • Google Gemini APIキー:
    1. Google AI for Developers にアクセスし、Googleアカウントでログインします。
    2. 「Get API key in Google AI Studio」をクリックしてAPIキーを生成します。
    3. 生成されたAPIキーは、後でコード内で使用するので、安全な場所に保管してください。
  • 関連ライブラリ: GoogleのAPIをPythonから簡単に利用するためのライブラリをインストールします。 bash pip install google-generativeai
  • 前回の成果物: rag_retrieval.py で作成した SimpleRAG クラス。

LLMとプロンプトエンジニアリング

LLMは非常に賢いですが、万能の魔法使いではありません。最高のパフォーマンスを引き出すには、「何を」「どのような形式で」 やってほしいのかを、明確に指示する必要があります。この指示書こそが「プロンプト」です。

RAGにおける生成フェーズのプロンプトは、一般的に以下の要素で構成されます。

  1. 役割設定(Instruction): LLMにどのような役割を演じてほしいかを指示します。「あなたは優秀な育児アシスタントです」のように。
  2. 参考資料(Context): 前回、ベクトルデータベースから検索してきた関連チャンクをここに埋め込みます。「以下の情報を参考にしてください:...」
  3. 質問(Question): ユーザーが入力した元の質問をそのまま含めます。「質問:...」
  4. 出力形式の指定(Output Format): 回答の形式を指示します。「上記の情報を基に、質問に対して日本語で簡潔に答えてください」のように。

このプロンプトをテンプレートとして用意し、実行時に関連情報と質問を動的に埋め込むことで、LLMは「参考資料の中から答えを探し、アシスタントとして振る舞い、質問に答える」という一連のタスクを正確に実行できるようになります。

実装:Gemini APIとの連携

それでは、前回の SimpleRAG クラスを拡張して、Gemini APIと連携する generate_answer メソッドを追加しましょう。

# rag_system.py (rag_retrieval.pyを改名・拡張)
import os
import chromadb
from sentence_transformers import SentenceTransformer
import google.generativeai as genai

# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
# 自身のAPIキーを設定してください
# ※コードに直接書き込むのは危険です。環境変数からの読み込みを推奨します。
os.environ['GOOGLE_API_KEY'] = "YOUR_API_KEY"
# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

class PiyologRAG:
    def __init__(self, model_name='intfloat/multilingual-e5-large', db_path="./chroma_db", collection_name="piyolog_rag_collection"):
        print("モデルとデータベースを初期化しています...")
        self.retrieval_model = SentenceTransformer(model_name)
        self.db_client = chromadb.PersistentClient(path=db_path)
        self.collection = self.db_client.get_collection(name=collection_name)

        # LLMモデルの初期化
        self.generation_model = genai.GenerativeModel('gemini-2.5-flash')
        print("初期化が完了しました。")

    def search(self, query, k=3):
        # (前回のsearchメソッドと全く同じ)
        query_embedding = self.retrieval_model.encode(query, normalize_embeddings=True)
        results = self.collection.query(
            query_embeddings=[query_embedding.tolist()],
            n_results=k
        )
        return results['documents'][0]

    def generate_answer(self, query, context):
        """
        コンテキストと質問を基に、LLMで回答を生成する
        """
        # プロンプトテンプレートの定義
        prompt_template = """
        あなたは、提供された育児記録データにのみ基づいて質問に答える、誠実なAIアシスタントです。
        以下の情報を参考にしてください。

        --- 参考情報 ---
        {context}
        ---

        上記の参考情報に基づいて、以下の質問に日本語で回答してください。
        参考情報に答えがない場合は、「分かりません」とだけ答えてください。

        質問: {question}
        """

        # テンプレートにコンテキストと質問を埋め込む
        context_str = "\n\n".join(context)
        prompt = prompt_template.format(context=context_str, question=query)

        print("\n--- LLMへの入力プロンプト ---")
        print(prompt)
        print("---------------------------\n")

        # LLMによる回答生成
        response = self.generation_model.generate_content(prompt)
        return response.text

    def ask(self, query):
        """
        質問応答のパイプライン全体を実行する
        """
        # 1. 検索 (Retrieval)
        retrieved_context = self.search(query, k=2)

        # 2. 生成 (Generation)
        answer = self.generate_answer(query, retrieved_context)
        return answer

# --- 実行部分 ---
if __name__ == '__main__':
    # RAGシステムをインスタンス化
    rag_system = PiyologRAG()

    # 質問応答を実行
    test_query = "9月1日のミルクの合計量は?"
    final_answer = rag_system.ask(test_query)

    # 最終的な回答の表示
    print(f"--- 最終的な回答 ---\n{final_answer}")

PiyologRAG クラスに generate_answerask メソッドを追加しました。

  • generate_answer: プロンプトテンプレートを定義し、search で得られた context とユーザーの query を埋め込んで最終的なプロンプトを作成します。そして generation_model.generate_content(prompt) を呼び出して、Gemini APIに回答生成を依頼します。
  • ask: これまで作ってきた searchgenerate_answer を順番に呼び出す、一連の処理の流れ(パイプライン)を定義したメソッドです。ユーザーは、この ask メソッドを呼び出すだけで、質問から回答生成までの一連の流れを実行できます。

実行結果の確認

YOUR_API_KEY の部分を自身のものに書き換えて(注意:環境変数からの読み込みを強く推奨します)、実行してみましょう。

--- LLMへの入力プロンプト ---

あなたは、提供された育児記録データにのみ基づいて質問に答える、誠実なAIアシスタントです。
以下の情報を参考にしてください。

--- 参考情報 ---
日付: 2022年06月17日
--------------------
時刻: 01:50, イベント: 起きる (睡眠時間: 205分)
時刻: 01:50, イベント: おしっこ
時刻: 01:50, イベント: 母乳 (授乳時間: 左8分, 右7分)
時刻: 02:15, イベント: うんち
時刻: 02:20, イベント: 母乳 (授乳時間: 左0分, 右15分)
時刻: 02:35, イベント: 吐く
時刻: 03:00, イベント: 寝る
時刻: 04:00, イベント: 起きる (睡眠時間: 60分)
時刻: 04:00, イベント: おしっこ
時刻: 04:30, イベント: 母乳 (授乳時間: 左10分, 右8分)
時刻: 05:15, イベント: 寝る
時刻: 07:30, イベント: 起きる (睡眠時間: 135分)
時刻: 07:35, イベント: おしっこ
時刻: 07:35, イベント: 母乳 (授乳時間: 左5分, 右6分)
時刻: 07:35, イベント: うんち
時刻: 07:45, イベント: 吐く
時刻: 08:00, イベント: おしっこ
時刻: 08:00, イベント: うんち
時刻: 08:40, イベント: 母乳 (授乳時間: 左8分, 右6分)
時刻: 09:25, イベント: 寝る
時刻: 10:00, イベント: 起きる (睡眠時間: 35分)
時刻: 10:10, イベント: おしっこ
時刻: 10:15, イベント: 母乳 (授乳時間: 左7分, 右5分)
時刻: 11:05, イベント: おしっこ
時刻: 11:05, イベント: うんち
時刻: 11:40, イベント: 寝る
時刻: 12:00, イベント: 起きる (睡眠時間: 20分)
時刻: 12:15, イベント: 母乳 (授乳時間: 左5分, 右7分)
時刻: 12:30, イベント: 寝る
時刻: 13:10, イベント: おしっこ
時刻: 13:10, イベント: 起きる (睡眠時間: 40分)
時刻: 13:15, イベント: 母乳 (授乳時間: 左5分, 右5分)
時刻: 13:45, イベント: 病院
時刻: 13:45, イベント: 体温
時刻: 14:00, イベント: 体重
時刻: 14:00, イベント: 身長
時刻: 14:00, イベント: 頭囲
時刻: 16:10, イベント: おしっこ
時刻: 16:15, イベント: 母乳 (授乳時間: 左7分, 右9分)
時刻: 16:35, イベント: 寝る
時刻: 18:30, イベント: 起きる (睡眠時間: 115分)
時刻: 18:30, イベント: 母乳 (授乳時間: 左7分, 右7分)
時刻: 19:10, イベント: おしっこ
時刻: 19:15, イベント: 母乳 (授乳時間: 左11分, 右0分)
時刻: 19:30, イベント: 寝る
時刻: 20:30, イベント: 起きる (睡眠時間: 60分)
時刻: 21:00, イベント: おしっこ
時刻: 21:00, イベント: お風呂
時刻: 21:15, イベント: 母乳 (授乳時間: 左8分, 右8分)
時刻: 21:35, イベント: 母乳 (授乳時間: 左0分, 右15分)
時刻: 22:10, イベント: 洗浄液作成
時刻: 23:05, イベント: おしっこ
時刻: 23:05, イベント: うんち
時刻: 23:10, イベント: 母乳 (授乳時間: 左8分, 右9分)
時刻: 23:40, イベント: 寝る
--------------------
1日のサマリー:
- 合計睡眠時間: 670分
- ミルク合計: 0ml
- 母乳合計: 左89分, 右107分
- おしっこ: 11回
- うんち: 5回


日付: 2022年06月15日
--------------------
時刻: 00:00, イベント: おしっこ
時刻: 00:00, イベント: 起きる (睡眠時間: 120分)
時刻: 00:00, イベント: 母乳 (授乳時間: 左8分, 右12分)
時刻: 00:40, イベント: 寝る
時刻: 03:20, イベント: おしっこ
時刻: 03:20, イベント: 起きる (睡眠時間: 160分)
時刻: 03:25, イベント: 母乳 (授乳時間: 左9分, 右13分)
時刻: 04:00, イベント: うんち
時刻: 04:30, イベント: 寝る
時刻: 06:20, イベント: 起きる (睡眠時間: 110分)
時刻: 06:20, イベント: おしっこ
時刻: 06:20, イベント: うんち
時刻: 06:25, イベント: 母乳 (授乳時間: 左8分, 右10分)
時刻: 06:45, イベント: 寝る
時刻: 10:00, イベント: 母乳 (授乳時間: 左8分, 右10分)
時刻: 10:00, イベント: 起きる (睡眠時間: 195分)
時刻: 10:00, イベント: おしっこ
時刻: 10:55, イベント: おしっこ
時刻: 10:55, イベント: うんち
時刻: 12:30, イベント: 母乳 (授乳時間: 左7分, 右6分)
時刻: 13:00, イベント: 寝る
時刻: 13:45, イベント: 起きる (睡眠時間: 45分)
時刻: 13:50, イベント: 母乳 (授乳時間: 左7分, 右7分)
時刻: 14:15, イベント: 寝る
時刻: 14:45, イベント: 起きる (睡眠時間: 30分)
時刻: 15:00, イベント: おしっこ
時刻: 15:35, イベント: 母乳 (授乳時間: 左7分, 右9分)
時刻: 16:30, イベント: 寝る
時刻: 17:30, イベント: 起きる (睡眠時間: 60分)
時刻: 17:40, イベント: おしっこ
時刻: 17:45, イベント: お風呂
時刻: 18:00, イベント: 母乳 (授乳時間: 左10分, 右10分)
時刻: 19:25, イベント: おしっこ
時刻: 19:25, イベント: うんち
時刻: 20:30, イベント: 母乳 (授乳時間: 左11分, 右11分)
時刻: 21:20, イベント: 寝る
時刻: 22:20, イベント: 起きる (睡眠時間: 60分)
時刻: 22:25, イベント: おしっこ
時刻: 22:25, イベント: 母乳 (授乳時間: 左8分, 右5分)
時刻: 22:45, イベント: うんち
時刻: 23:10, イベント: 寝る
--------------------
1日のサマリー:
- 合計睡眠時間: 780分
- ミルク合計: 0ml
- 母乳合計: 左83分, 右93分
- おしっこ: 9回
- うんち: 5回

---

上記の参考情報に基づいて、以下の質問に日本語で回答してください。
参考情報に答えがない場合は、「分かりません」とだけ答えてください。

質問: 2022年06月17日の合計睡眠時間は?

---------------------------

--- 最終的な回答 ---
2022年06月17日の合計睡眠時間は670分です。

(※合計量などは皆さんのデータによって変わります)

素晴らしい!検索結果のテキストをただ並べるのではなく、LLMがその内容を解釈し質問の意図に沿った形で、かつ自然な文章で回答を生成してくれました。

最後に

ついに、私たちの育児AIアシスタントが完成しました!

  • プロンプトエンジニアリングでLLMに的確な指示を与える方法を学んだ。
  • Google Gemini APIを使い、検索結果を基に回答を生成する機能を実装した。
  • 検索(R)→生成(G)という、RAGの一連のパイプラインを完成させた。

これにて、ぴよログデータでつくる育児AIアシスタントシリーズは一旦完結です!

当初の非構造化データから始まり、正規表現でのデータ抽出、チャンク化、ベクトル化、そしてLLMとの連携と、RAGシステムを一から構築する長い道のりでした。 最終的に、育児記録に関する質問へ自然言語で応答できるAIアシスタントが完成しました。

この連載を通じて、RAGの基本的な仕組みや実装の流れを掴んでいただけたなら幸いです。 ここからさらに、

  • StreamlitなどでUIを作成して、より対話的に使えるようにする
  • 異なるエンベディングモデルやLLMを試して、回答精度を比較する
  • チャンキング戦略をさらに洗練させて、検索精度を向上させる

など、多くの発展が考えられます。ぜひご自身のプロジェクトとして育ててみてください。

全9回にわたり、お付き合いいただき本当にありがとうございました!