StreamlitとRAGで作る 実用的なAIチャットボット(2) RAGの基本を理解する

Streamlit
この記事は約15分で読めます。

こんにちは、JS2IIUです。

前回に引き続き、「StreamlitとRAGで作る 実用的なAIチャットボット」シリーズの第2回目。RAGについてみていきます。前回は、Streamlitを使ってPythonだけでリッチなチャットUIを作る方法を学びました。しかし、作成したボットはまだ「オウム返し」しかできない空っぽの器でした。

今回はいよいよ、この器に「知能」と「専門知識」を吹き込みます。テーマは「RAG(Retrieval-Augmentation-Generation)」です。

最近よく耳にするこの「RAG」という言葉。難しそうに聞こえますが、本質は非常にシンプルです。今回は、データベースなどの複雑な仕組みは一旦脇に置き、Pythonコードだけで完結する「最小構成のRAG」を実装してみます。これを作ることで、RAGが裏側で何をしているのかが手に取るように分かるようになります。今回もよろしくお願いします。

1. はじめに:ChatGPTは「万能」ではない?

「ChatGPTがあれば、どんな質問にも答えてくれる」
そう思っていた時期が私にもありました。しかし、業務や専門的な用途で使おうとすると、すぐに2つの大きな壁にぶつかります。

  1. 知識の期限(Cutoff Date): モデルは学習データに含まれる過去の情報しか知りません。「昨日の社内会議の結果」や「最新のAPI仕様」については無知です。
  2. 幻覚(Hallucination): 知らないことを聞かれたとき、もっともらしい嘘をつくことがあります。「○○社の新製品について教えて」と聞くと、存在しない製品スペックを自信満々に語り出すことがあります。

これらを解決するために、「社内データを追加学習(ファインチューニング)させればいいのでは?」と考えるかもしれません。しかし、学習には莫大なコストと時間がかかり、情報は日々更新されるため、メンテナンスが現実的ではありません。

そこで登場するのが RAG(Retrieval-Augmentation-Generation:検索拡張生成) です。

2. RAGの概念:Retrieval, Augmentation, Generation

RAGのアプローチは、「学習させるのではなく、カンニングさせる」という発想です。

例えば、あなたが「持ち込み不可」の試験を受けるとします。自分の記憶力(学習済みデータ)だけで勝負しなければなりません。これが通常のLLMです。
一方、RAGは「参考書持ち込み可」の試験です。答えが分からなくても、手元の教科書(外部データ)を開いて該当箇所を探し、それを見ながら答えを書くことができます。

RAGは以下の3つのステップで行われます。

  1. Retrieval(検索): ユーザーの質問に関連する情報を、外部のデータソース(PDF、社内Wikiなど)から探し出します。「教科書の該当ページを開く」作業です。
  2. Augmentation(拡張): 検索で見つかった情報(コンテキスト)を、ユーザーの質問と一緒にプロンプトに埋め込みます。「質問文に参考情報を書き添える」作業です。
  3. Generation(生成): LLMが、拡張されたプロンプトを読み、コンテキストに基づいて回答を生成します。「参考情報を元に解答用紙に答えを書く」作業です。

今回は、この流れを最もシンプルなコードで再現します。

3. 開発環境の準備

今回は、LLMを扱うためのフレームワークとして、デファクトスタンダードである LangChain を使用します。また、LLMには OpenAI API を利用します。

OpenAI API Keyの準備

OpenAIのプラットフォームでAPI Keyを取得してください。
※ API利用は従量課金となるため、クレジット登録が必要です。

API Keyの取得方法は以下の記事を参考にして下さい。

OpenAI APIのAPIキーを取得する方法
https://zenn.dev/torakm/articles/cf681a531dc835

ライブラリのインストール

以下のコマンドで必要なライブラリをインストールします。

Bash
pip install langchain langchain-openai python-dotenv
  • langchain: フレームワーク本体。
  • langchain-openai: OpenAIモデルを扱うための専用パッケージ。
  • python-dotenv: 環境変数を管理するためのライブラリ。

プロジェクトのルートディレクトリに .env ファイルを作成し、APIキーを記述しておきましょう。

Plaintext
# .envファイル
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

4. Step 1:LangChainでLLMを操作する

まずはRAGを行う前の基本動作として、LangChain経由でGPTモデルに「こんにちは」と言わせてみましょう。

Python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

# .envファイルから環境変数を読み込む
load_dotenv()

# 1. LLM(ChatModel)のインスタンス化
# model_nameには、コストと性能のバランスが良い "gpt-4o-mini" (または gpt-3.5-turbo) を指定します
llm = ChatOpenAI(
    model_name="gpt-4o-mini",
    temperature=0  # 0にすることで、毎回同じ回答が返りやすくなります(決定論的挙動)
)

# 2. LLMへの問い合わせ
# invokeメソッドを使ってメッセージを送ります
response = llm.invoke("Pythonとは何ですか?一言で教えてください。")

# 3. 結果の表示
# responseはAIMessageオブジェクトなので、.contentでテキストを取り出します
print(f"回答: {response.content}")

コード解説

  • ChatOpenAI: LangChainでOpenAIのチャットモデルを扱うクラスです。
  • temperature=0: 生成の「ランダム性」を制御するパラメータです。RAGのような事実に基づいた回答を求める場合、創造性は不要なので 0 に設定するのが定石です。
  • .invoke(): 現在のLangChainでは、チェーンやモデルの実行にこのメソッドを使います。

5. Step 2:プロンプトテンプレートによる「命令」の型化

LLMに良い回答をさせるには、指示(プロンプト)の書き方が重要です。LangChainの PromptTemplate を使うと、プロンプトの一部を変数化し、再利用可能な「ひな形」を作ることができます。

Python
from langchain_core.prompts import PromptTemplate

# 1. プロンプトのテンプレートを作成
# {topic} の部分が変数になります
template = """
あなたは優秀なテクニカルライターです。
以下のテーマについて、初心者にもわかるように3行で解説してください。

テーマ: {topic}
"""

prompt = PromptTemplate.from_template(template)

# 2. テンプレートに変数を埋め込む
# formatメソッドで変数を置換し、完成した文字列を確認してみます
formatted_prompt = prompt.format(topic="RAG (Retrieval-Augmentation-Generation)")
print("--- 生成されたプロンプト ---")
print(formatted_prompt)
print("--------------------------")

# 3. LLMに投げる
# パイプライン演算子 `|` を使って、プロンプトとLLMを繋ぐことができます(LCEL記法)
chain = prompt | llm
response = chain.invoke({"topic": "RAG (Retrieval-Augmentation-Generation)"})

print(f"回答: {response.content}")

ここがポイント

  • {variable}: 波括弧で囲むことで、そこを入力欄として定義できます。
  • prompt | llm: これが LCEL (LangChain Expression Language) と呼ばれる記法です。「プロンプトの出力をLLMの入力に流す」という処理の流れ(Chain)をUNIXのパイプのように直感的に記述できます。

6. Step 3:RAGの最小構成(手動RAG)を実装する

さて、いよいよ本題のRAGです。
本格的なRAGでは、大量のドキュメントをデータベース(Vector Store)に入れて検索しますが、ここではRAGの「Augmentation(拡張)」と「Generation(生成)」の仕組みを理解するために、あえて検索機能を使わない「手動RAG」を作ります。

「LLMが知らない架空の事実」をコンテキストとして与え、それに基づいて回答させてみましょう。

Python
from langchain_core.prompts import ChatPromptTemplate

# --- 1. LLMが知らないはずの「独自データ」を定義 ---
# 架空の製品情報です。GPT-4でもこの製品のことは知りません。
context_data = """
製品名: AI-Cat-Goggle (AI猫ゴーグル)
機能: 猫の感情を読み取り、人間の言葉に翻訳してスマホに表示する。
価格: 19,800円
発売日: 2025年12月1日
注意点: 水に濡れると故障します。気難しい猫には誤作動することがあります。
"""

# --- 2. RAG用のプロンプトテンプレートを作成 ---
# 以下の構成がRAGプロンプトの基本形です
rag_template = """
あなたはカスタマーサポートAIです。
以下の「コンテキスト情報」だけに基づいて、ユーザーの質問に答えてください。
コンテキストにない情報は、「分かりません」と答えてください。

# コンテキスト情報
{context}

# ユーザーの質問
{question}
"""

prompt = ChatPromptTemplate.from_template(rag_template)

# --- 3. チェーンの構築 (Prompt -> LLM) ---
chain = prompt | llm

# --- 4. 実行 ---
# ユーザーの質問
user_question = "AI猫ゴーグルって、いくらですか?あと、防水ですか?"

# invoke時に、質問だけでなく「コンテキスト情報」も一緒に渡します
# これが Retrieval + Augmentation の正体です
response = chain.invoke({
    "context": context_data,
    "question": user_question
})

print(f"質問: {user_question}")
print(f"回答: {response.content}")

実行結果(例)

Plaintext
質問: AI猫ゴーグルって、いくらですか?あと、防水ですか?
回答: AI-Cat-Goggleの価格は19,800円です。なお、水に濡れると故障するため、防水ではありません。

解説:何が起きたのか?

  1. Augmentation(拡張): 私たちが定義した context_data(AI猫ゴーグルの情報)が、プロンプト内の {context} に埋め込まれました。
  2. Generation(生成): LLMは、埋め込まれた情報を読み取り、それを元に回答を生成しました。

もし context_data を渡さずに「AI猫ゴーグルについて教えて」と聞いたら、LLMは「そのような製品は存じ上げません」と答えるか、あるいは適当な嘘(ハルシネーション)をつくでしょう。

これがRAGの最小構成です。
本格的なRAGアプリ開発とは、この context_data の部分を、「ユーザーの質問に合わせて、大量のドキュメントの中から自動的に検索して持ってくる」ようにシステム化することに他なりません。

7. 技術コラム:LlamaIndexとの違い

今回のタイトルにもある LlamaIndex について少し触れておきましょう。
LangChainとLlamaIndexは、どちらもLLMアプリ開発のフレームワークですが、得意分野が異なります。

  • LangChain:
    • 特徴: 汎用的。「プロンプト管理」「チャット履歴」「エージェント機能」など、LLMアプリ全体を構築するための部品が揃っています。
    • RAG: 構築可能ですが、リトリーバル(検索)部分のチューニングは自分で細かく設定する必要があります。
  • LlamaIndex:
    • 特徴: データ連携(RAG)に特化
    • RAG: 「データを読み込ませて検索する」ことに極めて特化しており、高度な検索インデックス作成が数行で書けます。

使い分けの指針:
「とにかく高精度な検索システムを作りたい」場合はLlamaIndexが強力ですが、「チャットボットとしてUIや履歴管理を含めたアプリ全体を組みたい」場合は、LangChainの方が柔軟性が高い傾向にあります。
本連載では、Streamlitとの統合やアプリ全体のフロー制御を重視するため、汎用性の高い LangChain をメインに使用します。

8. まとめと次回予告

今回は、RAGの「心臓部」であるプロンプトへの情報注入(Augmentation)と生成(Generation)を、最小限のコードで体験しました。

  • RAGの本質: 質問に関連する情報をプロンプトにコピペして、LLMに「これを見て答えて」と指示すること。
  • LangChainの役割: プロンプトのテンプレート化やLLMの呼び出しを簡単に行うツール。
  • {context}: ここに外部情報を流し込むのがRAGの鍵。

しかし、今回の方法では context_data を手書きする必要がありました。これでは数万ページの社内マニュアルには対応できません。

そこで次回、第3回「独自データを取り込む:ドキュメントローダーとチャンキングの最適化」では、PDFやテキストファイルをプログラムで自動的に読み込み、LLMが扱いやすいサイズに分割(チャンキング)する技術を解説します。いよいよ本格的なデータ処理の始まりです。

最後まで読んでいただきありがとうございます。

連載記事リンク

コメント

タイトルとURLをコピーしました