top of page

RAGで精度が出ない?その理由はRetrievalだった

  • iLect
  • 7月24日
  • 読了時間: 11分
ree

はじめに:RAGの「Retrieval」がなぜ重要なのか?

最近、ChatGPTのような大規模言語モデル(LLM)を使って、独自のデータに基づいた質問応答システムを構築したいというニーズが増えています。そこで注目されているのが「RAG(Retrieval-AugmentedGeneration)」です。

RAGは、「検索(Retrieval)」と「生成(Generation)」の2つのステップで構成されます。


  1. Retrieval(検索):ユーザーの質問に関連する情報を、あらかじめ用意された知識ベース(ドキュメント、データベースなど)から探し出します。

  2. Generation(生成):検索で得られた情報をLLMに与え、その情報を基にユーザーの質問に対する回答を生成させます。


この2つのステップのうち、LLMの生成能力がいくら高くても、最初の「Retrieval(検索)」ステップで適切な情報を見つけられなければ、最終的な回答の精度は著しく低下してしまいます。AI界隈でよく聞く「ゴミを入れればゴミが出る(GarbageIn,GarbageOut)」という言葉の通りです。RAGの精度に悩む多くのケースで、その根本原因はRetrievalにあると言っても過言ではありません。


Retrievalの役割と重要性

Retrievalの主な役割は、LLMが質の高い回答を生成するために必要な「文脈(Context)」を提供することです。RetrievalはLLMに与える情報の質を直接左右し、RAGシステムの精度、信頼性、そしてユーザー体験に決定的な影響を与えます。


質の高いRetrievalを実現するためには、以下のような要素が重要になります。

  • 適切なチャンキング戦略:ドキュメントをLLMが処理しやすい適切なサイズに分割すること。

  • 高品質な埋め込みモデル:テキストの意味的な類似度を正確に捉えるベクトルを生成すること。

  • 適切なベクトルストア:大量の埋め込みベクトルから効率的に関連情報を検索できること。

  • 多様な検索戦略:質問の意図やデータの特性に応じた検索方法を選択できること。

特に最後の「多様な検索戦略」が、今日のテーマである「複数のRetrieverを使う工夫」に繋がります。


本記事では得意分野の違う2つのRetrieverを組み合わせる実装例を解説していきます。異なる検索戦略を持つRetrieverを組み合わせることで、より多様なタスクに対応できるRAGを実現しましょう。


複数のRetrieverを使う実装例

準備

以下2つを準備していることを想定して以下を実装していきます。

  • APIキー :OpenAITavily Search API

  • 実装環境 :GoogleClabratory

  • PDF :本記事内に用意したあるテキストをPDF化したもの


実装内容の確認

今から実装する生成AIはでは、2つの特性の異なるRetrieverを使用します。

① 独自データベースから検索を行い過去の知見を引き出すRetriever

② WEB検索を行い新しい情報を引き出すRetriever

どちらのRetrievalがユーザーの入力した質問に適しているか選択し、選択したRetrievalで回答を生成するよう実装していきます。


実装に必要な主要なパーツとして、

  • 2つのRetrieval

    • ドキュメント検索:PDFから独自データベースを作成

    • WEB検索:Tavilyというウェブ検索サービスを利用(Tavily Search API)

  • どちらのRetrievalを使用するか判断する機能

  • 選択したRetrievalを使用して回答を生成する機能

を意識して実装を行っていきましょう。


APIキーの管理方法

GoogleColaboratoryでAPIキーを使用する際はセキュリティリスクの観点から「シークレット機能」を使用することを強く推奨します。以下の実装についても「シークレット機能」を使用していることが前提となったコードとなっておりますのでご注意ください。


【シークレットへのAPIキー登録方法】

①左のアイコンから「シークレット」を開く。

②「新しいシークレットを追加」をクリック。

③プログラム内で使用する名称を「名前」に入力(GOOGLE_API_KEY・TAVILY_API_KEY)

④所持しているAPIキーを「値」に入力(OpenAIAPIキー・Tavily Search APIキー)

⑤「ノートブックからのアクセス」を有効にする

ree

PDFの準備

今回は「桃太郎」の物語に手を加えた文章を使用します。以下の文章を任意のテキストエディタなどでPDF化してご用意ください。※本記事内ではPDFは「test.pdf」というファイル名を使用します。


【桃太郎】
昔々、おじいさんとおばあさんが川で大きな桃を拾いました。家に持ち帰ると桃が割れ、中から元気な男の子が飛び出しました。彼らはその子を桃太郎と名付け、大切に育てました。ある日、浜辺を歩いていた桃太郎は、いじめられている亀を助けました。亀はお礼に彼を竜宮城へ誘い、桃太郎は乙姫様にもてなされました。楽しい時間はあっという間に過ぎ、帰る際に乙姫様から玉手箱を渡されました。地上に戻り玉手箱を開けると、桃太郎はたちまちおじいさんになってしまったのです。


実装

まずは単一Retrieval(ドキュメント検索)のみでRAGの動作確認をしてみましょう。そのあとでもう一つのRetrieval(WEB検索)を追加することで回答がどう変わるか試していきます。


なお、実装に使用するLangChainの基礎やRAGの基礎実装については過去の記事で詳細を解説していますので、良ければご参照ください。




  1. 必要なライブラリのインストール・インポート

始めに実装に必要なライブラリを準備します。インストール作業に数分時間を要する場合があります。


!pip install langchain_openai
!pip install -U langchain-community
!pip install pypdf
!pip install -q -U sentence-transformers langchain
!pip install faiss-cpu
!pip install tavily-python

from google.colab import userdata
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import re
import os
from langchain.document_loaders import PyPDFLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain.embeddings import HuggingFaceEmbeddings

from langchain_community.retrievers import TavilySearchAPIRetriever
from enum import Enum
from pydantic import BaseModel
from typing import Any
from langchain_core.documents import Document


  1. APIキーの取得

「シークレット機能」に登録したAPIキーを「API_KEY」という変数に用意しておきます。


# API_KEYの取得
API_KEY = userdata.get("GOOGLE_API_KEY")

# 取得できていなかった時にメッセージを表示
if API_KEY is None:
    raise ValueError("OPENAI_API_KEYが設定されていません。")


  1. 単一のRetriever(ドキュメント検索)を使用したRAGの実装

準備したPDFはColab上のセッションストレージ内にアップロードしておきましょう。

アップロードしたPDFを使用してRAGを実装していきます。


# PDFの読み込み~テキスト前処理
loader = PyPDFLoader("/content/test.pdf")
pages = loader.load_and_split()
def clean_text(text):
    text = text.replace("\n", " ")
    text = re.sub(r"\s+", " ", text)
    text = re.sub(r"^\d+$", "", text)
    text = text.strip()
    return text
for page in pages:
    page.page_content = clean_text(page.page_content)

# テキスト分割
text_splitter = CharacterTextSplitter(chunk_size=30, chunk_overlap=0)
docs = text_splitter.split_documents(pages)

# テキストのベクトル化~データベース作成
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
db = FAISS.from_documents(
    docs,
    embedding=embeddings,
)

# Retriever(ドキュメント検索)を準備する
retriever = db.as_retriever(search_kwargs={"k": 3})

作成したRetriever(ドキュメント検索)を使用してChainを作成します。


# prompt(ChatPromptTemplate)を準備する
prompt_template = """\
以下の内容を参照して質問に適切に回答してください:
```{context}```

質問: {question}
"""
prompt_rag = ChatPromptTemplate.from_template(prompt_template)
# model(ChatOpenAI)を準備する
model = ChatOpenAI(model="gpt-4o-mini", api_key=API_KEY)
# promptとmodelをつないでchain_ragを作成する
chain_rag = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt_rag
    | model
)

完成したChainを実行してみましょう。PDF内に書かれた「改変された桃太郎」の内容に準じた出力が返されたら成功です。


query = "桃太郎を100文字以内で要約してください。"
output_rag = chain_rag.invoke(query)

print(output_rag.content)

続いてもう一つ別の質問もしてみましょう。これはPDFからは読み取れない質問です。「答えられない」と回答されるか、誤った回答が出力されることを想定して実行します。


query = "今日は何月何日で,秋田の今日の天気は?"
output_route_rag = chain_rag.invoke(query)

print(output_route_rag.content)

実行結果が確認出来たら、次はこの「今日」というPDFにはない情報に回答できるように2つ目のRetriever(WEB検索)を追加することで解決していきましょう。

なお、複数のRetrieverを使用する際はそれぞれに名前(run_name)をつけるようにします。Retrieverが識別しやすくなり、実行ログの追跡などを行う際にどのRetrieverが動いているか確認することにも役立ちます。


# Retriever(ドキュメント検索)に名前を付ける
langchain_document_retriever = retriever.with_config(
    {"run_name": "langchain_document_retriever"}
)
  • .with_config():LangChainのコンポーネント(ここではretriever)に対して設定を変更するためのメソッドです。

    • run_name:このretrieverが実行される際の名前を設定できます。今回は「langchain_document_retriever」と名前をつけています。


  1. 2つのRetriever(ドキュメント検索・WEB検索)を使用したRAGの実装

2つ目のRetriever(WEB検索)を用意します。


os.environ["TAVILY_API_KEY"] = userdata.get("TAVILY_API_KEY")
web_retriever = TavilySearchAPIRetriever(k=3).with_config(
    {"run_name": "web_retriever"}
)
  • os.environ["TAVILY_API_KEY"] = userdata.get("TAVILY_API_KEY"):Tavily Search APIを利用するために必要な「APIキー」をシークレット機能から取得して環境変数に設定します。

  • TavilySearchAPIRetriever():Tavily Search APIを呼び出してWeb検索を実行するためのオブジェクトを作成します。

    • k=3:検索結果として取得するドキュメントの数を指定します。今回は関係性の高い順に3件取得する設定です。

    • .with_config({"run_name": "web_retriever"}):このRetrieverに「web_retriever」という名前をつけます。


つづいて、ユーザーの質問内容に応じて、どの情報源(Retriever)を使うべきかを自動的に判断する仕組みを実装します。


class Route(str, Enum):
    langchain_document = "langchain_document"
    web = "web"
    
class RouteOutput(BaseModel):
    route: Route

route_prompt = ChatPromptTemplate.from_template("""\
質問に回答するために適切なRetrieverを選択してください。

質問: {question}
""")

route_chain = (
    route_prompt
    | model.with_structured_output(RouteOutput)
    | (lambda x: x.route)
)
  • class Route(str, Enum):Retrieverの選択リストの定義をしています。選べる項目をリストアップして、それぞれの項目に名前を付ける役割があります。今回は2つのRetrieverとして「langchain_document」か「web」を設定しました。

  • class RouteOutput(BaseModel):PydanticライブラリのBaseModelを継承したクラスです。少しややこしい設定ですが、「Route」で設定した「langchain_document」か「web」をPythonで扱いやすい決まった構造(スキーマ)で出力する処理を定義していると思ってください。

  • route_chain = ():ユーザーが入力した質問にどちらのRetrieverが適しているかを判定するLLMを定義します。

    • .with_structured_output(RouteOutput):この設定により上で定義したスキーマ(route: Route)に従った出力となります。

    • (lambda x: x.route):LLMからの出力の内、route属性だけを取り出します。これにより、ドキュメント検索が適しているならば「langchain_document」を、WEB検索に適しているならば「web」を文字列で次の処理に渡すことができます。


次は「ユーザーからの質問」と「route_chain」の出力結果を受け取り「2つのRetriever」のどちらかを実行する関数を作ります。この関数の出力は質問への回答を作成するための「情報」として次の処理に使用されます。


def routed_retriever(inp: dict[str, Any]) -> list[Document]:
    question = inp["question"]
    route = inp["route"]

    if route == Route.langchain_document:
        print("langchain_document_retriever")
        return langchain_document_retriever.invoke(question)
    elif route == Route.web:
        print("web_retriever")
        return web_retriever.invoke(question)

    raise ValueError(f"Unknown route: {route}")
  • routed_retriever

    • (inp: dict[str, Any]):関数に入力される形を指定しています。今回は辞書の形式でキーには文字列であることが示されています。実際にこの関数を実行する時は「route_chain」の実行結果がinpという関数内の変数名に入力されます。

    • -> list[Document]:関数の返り値の形を指定しています。関数を実行するとRetrieverから検索された情報がこの形式で出力されます。今回は情報はリスト型で出力されます。

    • question = inp["question"]:入力された情報のうち「ユーザーからの質問」を変数に入力しています。

    • route = inp["route"]:入力された情報のうち「2つのRetrieverのどちらでるか」を変数に入力しています。

    • if route == Route.langchain_document::Retrieverにドキュメント検索が採用された時の処理です。

      • print("langchain_document_retriever"):ドキュメント検索が採用されたことの確認用print文です。

      • return langchain_document_retriever.invoke(question):ドキュメント検索(langchain_document_retriever)を使用してユーザーからの質問(question)にあった情報をこの関数の出力とします。

    • elif route == Route.web::RetrieverにWEB検索が採用された時の処理です。

      • print("web_retriever"):WEB検索が採用されたことの確認用print文です。

      • return web_retriever.invoke(question):WEB検索(web_retriever)を使用してユーザーからの質問(question)にあった情報をこの関数の出力とします。

    • raise ValueError(f"Unknown route: {route}")routeに想定外の値が入っていた場合エラーを出力するための設定です


最後に出来上がったパーツを1つにつなげて、ユーザーからの質問を受け取り、回答を出力する一連の流れを定義します。

ree

route_rag_chain = (
    {
        "question": RunnablePassthrough(),
        "route": route_chain,
    }
    | RunnablePassthrough.assign(context=routed_retriever)
    | prompt_rag | model 
)
  • "question": RunnablePassthrough():ユーザーからの質問が入りそのまま次の処理に渡されます。

  • "route": route_chain:どちらのRetrieverを使用するか判断するためのLLM(route_chain)が実行され、その結果を次の処理に渡します。

  • RunnablePassthrough.assign(context=routed_retriever)"question""route"を受け取り、routed_retriever関数を実行します。結果である「情報」はcontextという名前で次のLLMによる回答を生成する際に使用されます。

  • prompt_rag:最初の入力「question」と前の処理で作られた「context」を使用してプロンプトを作成します。

  • modelprompt_ragを受け取り回答を生成します。


では、route_rag_chainを「単一のRetriever(ドキュメント検索)を使用したRAG」の時と同じ質問で試してみましょう。使用されたRetrieverの種類と回答が出力されたら成功です。


query = "桃太郎を100文字以内で要約してください。"
output_route_rag = route_rag_chain.invoke(query)

print(output_route_rag.content)

「langchain_document_retriever」という単語と、前回と同様にPDF内に書かれた「改変された桃太郎」の内容に準じた出力が表示されるはずです。

さて、最後に前回はうまく回答できなかった次の質問も実行してみてください。


query = "今日は何月何日で,秋田の今日の天気は?"
output_route_rag = route_rag_chain.invoke(query)

print(output_route_rag.content)

いかがでしょうか?「web_retriever」という単語と今日の日付・秋田の天気は回答されましたか?


RAGの最終的な精度は、LLMの能力だけでなく、その手前に存在する「Retrieval(検索)」ステップの品質に大きく依存します。不適切な情報が渡されれば、いくら高性能なLLMでも正しい回答を生成することはできません。

Retrievalの精度を向上させるためには、単一の検索戦略に固執するのではなく、質問の意図やデータの構造を考慮し、複数のRetrieverを柔軟に組み合わせることが非常に有効です。今回のLangChainを使用すればこのような複数のRetrieverを組み合わせることも柔軟に対応できることが実感できたことと思います。今回紹介したWEB検索は数あるRetrieverの内の1つです。ぜひ様々なRetriever機能を調べて、追加・連結させてみてください。





本記事はAI総合研究所として活動するNABLAS株式会社が提供する、法人向けAI人材育成iLectが提供する新講座【ゼロから始めるRAG:開発・改善・運用まで】から一部抜粋したものです。講座内ではさらに実践的な内容を経験豊富な講師・メンターのサポートのもと学ぶことができます。

ree

Comments


bottom of page