8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PDFデータを活用したLangChainでのRAG構築

Last updated at Posted at 2025-01-29

第1章 はじめに

1.1 本記事の概要と目的

本記事では、大規模言語モデル(LLM)をより効果的に活用する手法として注目されている「RAG(Retrieval-Augmented Generation)」の概要と、Python向けフレームワークであるLangChainを使った実装方法について解説します。特に、PDFデータを外部情報源として扱う具体的な方法を取り上げ、「データ検索と回答生成の流れ」 を順を追って説明します。

本記事の目的は、次の3点です。

  • RAGの基本概念・メリットを理解する
  • LangChainを使ったPDFデータの登録・検索・回答生成を実装する
  • 実装の注意点や精度向上のコツをつかむ

この記事を参考にしていただくことで、PDFドキュメントを活用したRAG構築のアイデアを形にするためのヒントを得られることを目指しています。

なお、使用しているコード及びデータはこちらのGitリポジトリに公開しています。必要に応じてダウンロードやクローンしてご利用ください。

1.2 想定読者

本記事は、次のような方を対象にしています。

  • 大規模言語モデル(LLM)の活用に関心がある方
  • LangChainを使ったシステム開発に興味がある方
  • PDFデータを活用して情報検索や回答生成を試してみたい方

Pythonでの開発経験がある程度あり、機械学習や自然言語処理に触れたことがある方であれば、スムーズに進められる内容となっています。

1.3 前提知識

  • Pythonの基礎

    • Pythonスクリプトの実行、仮想環境の作成(venvやcondaなど)、pipによるライブラリのインストールを理解している。
    • 変数、関数、リスト、辞書などの基本的な文法を理解している
  • LLM(大規模言語モデル)の概要

    • GPTやBERTなどのモデルに関して、テキスト生成の仕組みをおおまかに把握している。
  • Embeddings(埋め込み)の概念

    • 単語や文をベクトル空間に埋め込む手法(Word2VecやBERTのような考え方)に馴染みがある。

もし不明点があれば、随時公式ドキュメントや他の入門記事を参照いただくとスムーズです。

また、以下の2冊の書籍はLLMについて網羅的にまとめられており、非常に参考になりました。

第2章 RAG(Retrieval-Augmented Generation)とは

2.1 RAGの概要

RAG (Retrieval-Augmented Generation) は、検索拡張生成と訳される技術で、近年注目を集めている生成AIの応用手法の一つです。
生成AI、特に大規模言語モデル (LLM) は、膨大な知識を学習していますが、その知識は学習データに含まれる情報に基づいています。そのため、

  • 最新情報に対応できない
  • 専門的な知識や特定のドメイン知識が不足している
  • 事実に基づかない内容 (ハルシネーション) を生成してしまう

といった課題がありました。

RAGは、これらの課題を克服するために、LLMが外部の知識源を参照しながら回答を生成する仕組みを取り入れています。具体的には、質問内容に関連する情報を検索し、その情報をLLMに与えることで、より正確で信頼性の高い回答生成を可能にします。
RAGを活用することで、

  • 常に最新の情報に基づいた回答
  • 専門知識や社内データなど、特定の知識に基づいた回答
  • 根拠のある、ハルシネーションを抑制した回答

を生成することが期待できます。

2.2 RAGの仕組み

RAGの仕組みは、大きく分けて 「検索 (Retrieval)」(下図①から③) と 「生成 (Generation)」(下図④から⑥) の2つのステップから構成されます。

参考:生成AIのビジネス活用で注目されるRAG(検索拡張生成)とは? - 仕組みや活用例、精度向上のノウハウなどを紹介

20240723092206.png

<検索 (Retrieval) ステップ>

RAGシステムは、まず ユーザーからの質問(上図①) を受け付けます。すると、システムは質問内容に関連する情報を 外部の知識源 から探し出す検索を行います。

本記事では、知識源として PDFデータ を活用します。PDFデータは、あらかじめ ベクトルデータベース という、意味に基づいた検索 が得意な特殊なデータベースに登録しておきます。

検索ステップでは、以下の3つの処理を行います。

1. 質問文をベクトル化(上図①): 質問文を 埋め込みモデル (例:OpenAI Embeddings、HuggingFace Transformers) というAIモデルで、意味 を捉えた数値データ (ベクトル) に変換します。
2. ベクトル検索(上図②): ベクトル化された質問文と、ベクトルデータベース(例:FAISS、Chroma、Pinecone)に登録されたPDFドキュメントのベクトルを比較し、意味的に近い ドキュメントを検索します。
3. 関連情報を取得(上図③): 検索されたドキュメントの中から、質問に関連性の高い箇所を抽出します。


<生成 (Generation) ステップ>

検索ステップで取得した関連情報と、質問文を組み合わせて、LLMに入力します。

LLMは、与えられた情報 (関連情報と質問文) をもとに、質問に対する回答文を生成します。この際、LLMは検索された関連情報を参照しながら回答を生成するため、外部知識に基づいた、より正確で根拠のある回答を生成することが可能になります。

生成ステップでは、以下の2つの処理を行います。

1. プロンプト作成(上図④): 質問文と検索された関連情報を組み合わせ、LLMへの指示文 (プロンプト) を作成します。
2. 回答生成と出力(上図⑤&⑥): 作成されたプロンプトをLLMに入力し、回答文を生成させた後。ユーザーに解答を表示します。

このように、RAGは 「検索」 と 「生成」 のステップを組み合わせることで、LLMが持つ生成能力と、外部知識源の情報を効果的に統合し、より高度な質問応答システムを構築するための技術です。

第3章 LangChainとは 

3.1 LangChainの概要

LangChainは、大規模言語モデル(LLM)の活用をより効果的にするためのPythonライブラリおよびフレームワークです。このフレームワークは、テキスト生成、データ検索、エージェントの管理といったタスクを柔軟に組み合わせて、さまざまなアプリケーションを構築できるよう設計されています。

Langchainについての詳細な実装は、LangChain完全入門 生成AIアプリケーション開発がはかどる大規模言語モデルの操り方の本で非常にわかりやすくまとめられており、参考になりました。

3.2 LangChainの特徴

LangChainは、次のような特徴を持っています。

1. LLMを基盤にしたタスク管理

  • テキスト生成や質問応答だけでなく、検索やデータ操作を含む高度なワークフローを構築できます。

2. モジュール性

  • 各コンポーネント(例:データ読み込み、埋め込み生成、検索処理)を独立して管理できるため、目的に応じた柔軟な構成が可能です。

3. 豊富なインテグレーション

  • OpenAIやHugging Faceのモデルだけでなく、FAISSやChromaなどのベクトルデータベース、PDFリーダーなど、さまざまな外部ツールと連携可能です。

4. エージェントのサポート

  • 「エージェント」と呼ばれる機能を活用することで、ユーザーの指示に基づいて動的にタスクを切り替えたり、複数の機能を組み合わせて実行できます。

3.3 ユースケース

LangChainは以下のようなシナリオで活用されています。

  • 質問応答システム
    外部データを元に、LLMを活用した精度の高いQAシステムを構築。

  • ドキュメント要約
    長い文書をチャンク化して要約を生成するワークフローを実装。

  • カスタムチャットボット
    特定のドメインデータを利用した対話型アプリケーションの開発。

  • データ統合ツール
    様々なデータソースを取り込み、ベクトル検索とLLMを組み合わせた分析。

第4章 構築手順 - LangChain × PDFデータ RAG実装ステップ

4.1 環境構築

LangChainを使ったRAG実装を始めるために、Python環境を整えます。本記事では、Python 3.11.9を使用して動作確認を行っています。


<仮想環境の作成>

# 仮想環境の作成
$ python -m venv langchain_env

# 仮想環境の有効化(Windows)
$ langchain_env/Scripts/activate

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

$ pip install PyMuPDF langchain langchain-openai langchain-chroma python-dotenv

以下の表は各ライブラリの簡単な説明です。

ライブラリ名 説明
PyMuPDF PDFやその他のドキュメント形式を操作するためのライブラリ。fitzモジュールを通じて利用可能。
langchain 大規模言語モデル(LLM)を活用したアプリケーションを構築するためのフレームワーク。
langchain-openai LangChainでOpenAIのAPIを利用するための拡張ライブラリ。
langchain-chroma LangChainとChroma(ベクトルデータベース)を連携させるためのライブラリ。
python-dotenv .envファイルから環境変数を読み込むためのライブラリ。

<APIキーの設定>
プロジェクトのルートディレクトリに .env ファイルを作成し、使用するLLMのAPIキーやエンドポイントを設定しておきます。本記事では、Azure OpenAIのGPT-4o miniを使用する例を示します。他のサービスを利用する場合は、適宜設定内容を変更してください。

.env
OPENAI_API_KEY=your_openai_api_key_here
API_ENDPOINT=https://your-resource-name.openai.azure.com/
API_VERSION=2023-05-15  # Azure OpenAIの場合の例
DEPLOYMENT_ID_FOR_CHAT_COMPLETION=your_chat_completion_deployment_id
DEPLOYMENT_ID_FOR_EMBEDDING=your_embedding_deployment_id

.env ファイルの設定項目(Azure OpenAIの場合)

項目名 説明
OPENAI_API_KEY OpenAIまたはAzure OpenAIで発行されたAPIキー。
API_ENDPOINT Azure OpenAIサービスのエンドポイントURL。
API_VERSION 使用するAPIのバージョン。Azureでは一般的に 2023-05-15など を指定します。
DEPLOYMENT_ID_FOR_CHAT_COMPLETION Chat Completion用にデプロイされたモデルのID。
DEPLOYMENT_ID_FOR_EMBEDDING Embedding用にデプロイされたモデルのID。

⚠️ 注意事項: .env ファイルの管理

.env ファイルにはAPIキーやエンドポイントURLなどの機密情報が含まれます。そのため、必ず .gitignore に追加し、Gitでバージョン管理しないようにしてください。

4.2 外部データの登録

1. PDFデータの準備

まず、RAGシステムの知識源とするPDFファイルを用意します。本記事では、動作確認用のPDFファイルとして、国税庁が公開した「令和6年分 年末調整のしかた」を活用しました。こちらからダウンロードできます。

活用したいPDFファイルを選んだら、テキストやグラフ、表の情報の抽出を行います。RAG構築において、この情報をいかに正確に抽出できるかが非常に重要となるため、詳しく解説していきます。本記事では、PDFデータの抽出方法を6つ紹介し、それぞれのメリットとデメリット、そして用途に応じた選択のポイントをまとめます。


<Ⅰ. Pythonライブラリの活用>

PyMuPDF(fitz)pdfminerPyPDF2などの Pythonライブラリを利用して、プログラムを通じてテキストや構造情報を抽出する方法です。
メリット

  • 大量のPDFを自動で一括処理できる。
  • Pythonスクリプトで独自の前処理・後処理を組み込み可能。
  • オープンソースのライブラリなら無料で利用可能。

デメリット

  • スクリプトを書くための知識が求められる。
  • PDFのレイアウトや文字コードの扱いによって抽出精度に差がでる。
  • グラフや表の読み取り精度が低い。

こんなときにおすすめ

  • RAG用に大量のPDFデータを定期的に収集・更新する必要があるとき。
  • 自由度の高い前処理や後処理(特定のセクションだけ抽出、マーカー付きテキストだけ取り出すなど)が必要なとき。

<Ⅱ. 生成AIサービスの活用>
ChatGPTやGoogle DeepMindのGeminiなどの生成AIを利用して、PDFの内容を要約・解析し、必要な情報を取り出す方法です。(PDF自体を直接アップロードできるサービスもあれば、テキストを貼り付けて要約させるアプローチもあります。)

メリット

  • 単なるテキスト抽出にとどまらず、要約や意図理解などの高度な自然言語解析も得意。
  • プログラミング不要で、Webインターフェースからすぐ使える場合が多い。
  • 多言語対応: さまざまな言語のテキストを高精度で解析できる。

デメリット

  • 一定以上のボリュームを処理する場合、API利用料がかさむことがある。
  • プライバシー/セキュリティの懸念: 機密データをクラウドへ送信する際は要注意。
  • グラフや表などの正確な位置関係までは再現しにくい。

こんなときにおすすめ

  • 文章の意味を理解した要約や「このテキストで質問に回答できるようにしたい」とき。
  • 自社データを特定のトピックに沿って要約・分類したい場合。

<Ⅲ. OCRの活用>
スキャンした画像ベースのPDFや、文字情報が埋め込まれていないPDFからテキストを抽出する場合に OCR (Optical Character Recognition) を利用します。Tesseract OCRなどが代表的です。

メリット

  • 画像化されたPDFにも対応: スキャンデータや画像形式のドキュメントを扱える。
  • 多言語対応: 設定次第で複数言語を認識可能。

デメリット

  • 精度が画像品質に左右される: 解像度や文字の読みやすさで結果が大きく変わる。
  • 処理時間: 大量のファイルを扱う場合は長時間のバッチ処理が必要になることも。

こんなときにおすすめ

  • スキャンされた古い文書や紙資料をデジタル化したPDFから情報を抽出したいとき。
  • 文字列として取り出せない図表やスペックシートなどの画像内文字を読み取りたいとき。

<Ⅳ. 手作業(マニュアル抽出)>
人間がPDFを開き、必要な箇所をコピー&ペーストや書き写しで抽出するアナログな方法。

メリット

  • 人が直接確認しながら作業するため、誤抽出が少ない。
  • 表や特殊な図版など、アルゴリズムでは難しい内容も対応可能。

デメリット

  • 大量のファイルを扱うときは時間と人件費がかかる。
  • 熟練度や集中力に左右される。

こんなときにおすすめ

  • データ量がごく少量で、かつ正確性が最優先な場合。
  • 極端に複雑なレイアウトや、特殊文字・手書き要素が混在する場合。

<Ⅴ. 専用PDF解析ツールの利用>
Adobe AcrobatPDF Expertなど、有料・無料問わず商用/専用ソフトを活用してPDF情報を解析する方法です。

メリット

  • 市販ツールは独自のアルゴリズムを搭載していることが多く、抽出精度が高い。
  • コードを書かずにGUIで簡単にファイル操作を行える。
  • テキスト編集、注釈、比較、セキュリティ設定など、周辺機能が充実。

デメリット

  • 商用ソフトの場合はライセンス料が必要。
  • 特殊な処理を行いたい場合に独自のカスタマイズが難しいことも。

こんなときにおすすめ

  • プログラミングは苦手だけどGUIでサクッと抽出したい。
  • 社内規定でAdobe製品のみ利用可能など、特定ツールが標準化されている環境。

<Ⅵ. クラウドサービスの活用>
AWS TextractGoogle Cloud VisionAzure Form RecognizerなどのクラウドAIサービスを使い、PDFからテキストや構造を抽出する方法です。

メリット

  • 大量のファイルを同時処理可能。
  • 単なるテキスト抽出だけでなく、表やレイアウト構造を自動的に識別。
  • サーバーの保守やアップデートはクラウドベンダー側が実施するため、管理が容易。

デメリット

  • APIコール数やファイルサイズに応じて費用がかかる。
  • 機密情報の取り扱いに注意が必要。

こんなときにおすすめ

  • 大規模・高負荷のプロジェクトで、オンプレミスでの処理が困難な場合。
  • 表やフォームなど構造化データの抽出をクラウドで一括して行いたい場合。

以上6つの抽出手法をテキスト抽出精度、表や図の読み取り精度、コスト、処理速度の観点で(個人的に)評価した表を以下に示します。

方法 テキスト抽出精度 表や図の読み取り精度 コスト 処理速度
1. Pythonライブラリ
2. 生成AIサービス △ ~ ○
3. OCRの活用 △ ~ ○
4. 手作業 × ×
5. 専用PDF解析ツール
6. クラウドサービス

これらの特徴を踏まえて、使用するPDFファイルの特徴に応じて適切な方法を選択、もしくは組み合わせて使用することが重要です。以下にいくつかの例を示します

  • 図や表が非常に多いPDFファイル
    → PDF解析ツールやクラウドサービスを利用して抽出し、必要に応じて手作業で修正する。

  • 図や表がほぼない文章中心のPDFファイル
    → Pythonライブラリを活用して効率よく抽出処理を行う。

  • スキャンされた画像ベースのPDF
    → OCRを用いてテキストを抽出した後、生成AIサービスで要約や情報解析を行う。


<実装>

PDFデータを用いて情報抽出を行います。今回は、国税庁が公開した資料「年末調整のしかた」を使用します。この資料は表やグラフが少ないため、以下の手順で処理を進めました

  1. Pythonライブラリを活用した抽出
    表やグラフの数が少ないため、Pythonライブラリを使用してテキストを抽出しました。

  2. Azure OpenAIを使用したテキスト処理
    抽出したデータをAzure OpenAIで処理し、文章の体裁の整理、簡易的な表を生成を行いました。

  3. 手作業での修正
    数値やテキストの誤り、体裁の細かい崩れを確認し、必要な部分を手作業で修正しました。

PDFのデータ抽出を行うpythonコードを以下に示します。

pdf_text_extraction.py
import fitz  # PyMuPDF
from langchain_openai import AzureChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
from dotenv import load_dotenv
import os
from typing import Optional
from tqdm import tqdm
import time

# 環境変数の読み込み
load_dotenv()

# Azure OpenAI の設定
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
API_VERSION = os.getenv("API_VERSION")
DEPLOYMENT_ID_FOR_CHAT_COMPLETION = os.getenv("DEPLOYMENT_ID_FOR_CHAT_COMPLETION")

# LLMのインスタンス作成
llm = AzureChatOpenAI(
    deployment_name=DEPLOYMENT_ID_FOR_CHAT_COMPLETION,
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
)

def extract_and_restructure_pdf(
    pdf_path: str,
    output_md_path: str,
    output_raw_text_path: str,
    first_page: Optional[int] = 1,
    last_page: Optional[int] = None,
    max_retries: int = 3,
    retry_delay: int = 2
) -> None:
    
    
    """PDFの内容を抽出して整形し、Markdownと生テキストファイルに保存します。

    Args:
            pdf_path (str): 処理するPDFファイルのパス。
            output_md_path (str): 整形結果を保存するMarkdownファイルのパス。
            output_raw_text_path (str): 抽出された生テキストを保存するファイルのパス。
            first_page (Optional[int]): 処理を開始するページ番号(1始まり)。デフォルトは1。
            last_page (Optional[int]): 処理を終了するページ番号。デフォルトは最終ページ。
            max_retries (int): LLM呼び出しの最大リトライ回数。デフォルトは3。
            retry_delay (int): LLM呼び出し失敗時のリトライ間隔(秒)。デフォルトは2秒。
    """    
        
    try:
        # PDFを開く
        doc = fitz.open(pdf_path)
        total_pages = len(doc)

        # ページ範囲の計算
        first_page = first_page or 1
        last_page = last_page or total_pages

        raw_texts = []  # 生テキストを保存するリスト
        markdown_results = []  # 整形後のテキストを保存するリスト

        # PDFページを順に処理
        for page_num in tqdm(range(first_page - 1, last_page), desc="Processing PDF Pages"):
            page = doc[page_num]
            extracted_text = page.get_text("text")  # テキスト抽出

            # LLMへの指示を作成
            messages = [
                SystemMessage(content=(
                    "あなたは優れたアシスタントです。以下に与えられるテキストはPDFから抽出された内容であり、体裁が崩れている可能性があります。\n\n"
                    "以下の指示に従って、テキストの整形を行ってください:\n\n"
                    "1. 句読点や改行の位置を適切に整え、誤字脱字を修正してください(文脈に基づく範囲内で)。\n"
                    "2. 元のテキストに含まれる情報を削除しないでください。\n"
                    "3. 表形式のデータは可能な限り元のレイアウトを維持してください。\n"
                    "4. グラフの軸の数値関係を確認し、適切に説明してください。\n\n"
                    "最終結果はMarkdown形式で出力してください。"
                )),
                HumanMessage(content=f"## ページ {page_num + 1}\n\n### 抽出されたテキスト:\n\n{extracted_text}")
            ]

            # LLMを使用して整形
            for attempt in range(max_retries):
                try:
                    response = llm.invoke(messages)
                    markdown_results.append(response.content)
                    break
                except Exception as e:
                    print(f"Error during OpenAI API call for page {page_num + 1} (attempt {attempt + 1}): {e}")
                    if attempt == max_retries - 1:
                        print(f"Failed after max retries for page {page_num + 1}.")
                    time.sleep(retry_delay)

            # 抽出されたテキストを保存
            raw_texts.append(f"## ページ {page_num + 1}\n{extracted_text}")

        # 生テキストをファイルに保存
        with open(output_raw_text_path, "w", encoding="utf-8") as raw_file:
            raw_file.write("\n\n".join(raw_texts))
        print(f"抽出されたテキストが保存されました: {output_raw_text_path}")

        # 整形結果をMarkdownで保存
        with open(output_md_path, "w", encoding="utf-8") as md_file:
            md_file.write("\n\n".join(markdown_results))
        print(f"整形結果がMarkdownファイルに保存されました: {output_md_path}")

    except Exception as e:
        print(f"Error processing PDF: {e}")

if __name__ == '__main__':
    # 使用例
    pdf_path = "nencho_all.pdf"  # 処理するPDFファイルのパス
    output_md_path = "output/nencho_al_test.md"  # 整形結果の保存先
    output_raw_text_path = "output/nencho_all_test.txt"  # 抽出されたテキストの保存先

    # 出力ディレクトリの作成
    os.makedirs('output', exist_ok=True)

    # PDF処理の実行
    extract_and_restructure_pdf(
        pdf_path=pdf_path,
        output_md_path=output_md_path,
        output_raw_text_path=output_raw_text_path,
        first_page= None,  # 最初のページから処理
        last_page= None    # 最後のページまで処理
    )

本記事ではPyMuPDFGPT4o-miniを使用していますが、PDFの特徴に応じて他のライブラリや生成AIを活用し、それらを比較してみるのも有効なアプローチです。また、PDFの内容や構造に応じた適切なプロンプト設計を行うことで、より正確で有用な出力を得ることが可能です。
出力結果は以下のようになりました。

出力結果
## ページ 1

### 令和6年分 年末調整のしかた

法人番号: 700000120500002

「年末調整がよくわかるページ」をご覧ください!

国税庁ホームページには、「年末調整がよくわかるページ」を掲載しています。このページには、本年の定額減税を含めた年末調整の手順等を解説した動画やパンフレット、扶養控除等申告書など各種申告書、従業員向けの説明用リーフレットや各種申告書の記載例など、年末調整の際に役立つ情報を掲載していますので、ご活用ください。

なお、動画による説明はYouTubeにも掲載していますので、ご活用ください。

※ 令和6年分の各種情報については、令和6年10月頃に掲載いたします。

年末調整に係る源泉徴収をした所得税及び復興特別所得税の納期限は、令和7年1月10日(金)(納期の特例の承認を受けている場合は、令和7年1月20日(月))です。

その他、給料や報酬などについて源泉徴収をした所得税及び復興特別所得税の納期限については、2ページを確認してください。

(よくわかるページ)

(YouTube)

年末調整に関する相談は、国税庁ホームページからチャットボットの「税務職員ふたば」をお気軽にご利用ください。年末調整の各種申告書の書き方や添付書類に関することなどについて、AIが自動で回答します。

※ 公開期間は令和6年10月頃から令和7年1月下旬までの予定です。年末調整でお困りのときは、“ふたば” にご相談ください。

(チャットボット)

### 年末調整手続の電子化で業務の効率化!

年末調整手続の電子化を行うと、給与の支払者(勤務先)及び給与所得者(従業員)それぞれにおいて、書類の作成や確認、保管などの業務全般が大幅に効率化されるなど、双方に大きなメリットがあります。

また、国税庁では「年末調整控除申告書作成用ソフトウェア」(年調ソフト)を無償で提供しております。年末調整手続の電子化や年調ソフトについて、詳しくは国税庁ホームページをご覧ください。

テキストはほぼ正確に抽出できていますが、順番が逆になっていたり、表の読み取りが誤っている部分があったので手作業で修正しました。この修正版のMarkdownファイル(mdファイル)は、Gitレポジトリ内のdata_sourcesフォルダに保存されています。(revised_nencho_all.md)

また、表やグラフに含まれる情報をRAGで活用する場合、LLMが内容を正しく理解できるよう、データの構造を明示的にする必要があります。特に、表やグラフ形式のデータは、数値や文字情報の縦・横の関係性をLLMに伝える工夫が重要です。

以下に、PDFから抽出した表データをLLM向けに変換する例を示します。

元の表(PDFのイメージ):

年度 売上(億円) 利益(億円)
2020年 150 20
2021年 200 30

LLM向けに変換した形式:

  • 2020年度の売上は150億円、利益は20億円です。
  • 2021年年度の売上は200億円、利益は30億円です。

上記のように、表の各行を文章形式で表現し、列の情報をキーワードとして明示的に記述することで、LLMは表データの構造と内容を効果的に理解できます。この形式に変換することで、LLMは「〇〇年度の売上はいくらですか?」といった質問に対して、表データに基づいた正確な回答を生成することが期待できます。


長くなりましたが、以上がPDFからのデータ抽出方法となります。今回のRAG構築において、このPDFデータの前処理に最も時間を費やしました。RAGで期待通りの正確な回答を得るためには、元となるPDFデータから、いかに情報を正確に、そして適切に抽出できるかが非常に重要です。 どんなに高性能なLLMやRAGフレームワークを使ったとしても、知識源となるデータの精度が低ければ、精度の高い回答は望めません。
特に、PDFデータは、レイアウトの複雑さや文字認識の課題など、データ抽出の難易度が高い場合があります。だからこそ、RAG構築の成否を左右すると言っても過言ではない、データの前処理には丁寧に時間をかけることが重要だと、今回の経験を通して改めて感じました。

2. ドキュメント分割 (Text Splitting)

PDFファイルからテキストデータを抽出したら、次にテキストをドキュメント分割 (Text Splitting) する必要があります。ドキュメント分割とは、抽出したテキストをチャンクと呼ばれる小さな塊に分割する処理です。

ドキュメント分割を行う主な目的は以下の2点です。

  • 検索精度の向上:
    質問文とPDFドキュメント全体を比較するよりも、細かく分割されたチャンク単位で比較する方が、質問内容との関連性が高い箇所を特定しやすくなります。

  • LLMのコンテキストウィンドウ制限への対応:
    大規模言語モデル (LLM) が一度に処理できるテキスト量には上限 (コンテキストウィンドウ) があります。長文のままLLMに入力すると、コンテキストウィンドウを超過し、処理が正常に行われない可能性があります。ドキュメントを分割することで、各チャンクがLLMのコンテキストウィンドウ内に収まるように調整します。

LangChainでは、様々なテキスト分割方法 (Text Splitter) が提供されています。記事では、RecursiveCharacterTextSplitter を使用します。これは、改行文字や句読点などを区切り文字として、意味的にまとまりのある単位でテキストを再帰的に分割するText Splitter です。分割の粒度を調整するために、chunk_size (チャンクの最大文字数) や chunk_overlap (チャンク間の重複文字数) などのパラメータを設定できます。

他にも、指定された区切り文字で分割するCharacterTextSplitterや、Markdown形式のテキストを、Markdownの構造(見出し、リスト、コードブロックなど) を考慮して分割するMarkdownTextSplitterなどもあるため、目的に応じて使い分けることで回答性能向上も期待できます。

3. 埋め込み生成(Embedding)

ドキュメント分割で得られたテキストチャンクに対し、埋め込み生成 (Embedding) を行います。埋め込み生成とは、テキストの意味内容を捉えた数値ベクトルへと変換する処理です。

埋め込み生成の目的は、テキスト同士の意味的な類似性を数値で比較可能にすることです。テキストをベクトル化することで、意味が近い文章はベクトル空間上で近い位置に配置され、類似度を計算できるようになります。

記事では、高性能なOpenAI Embeddingsを埋め込みモデルとして採用しています。OpenAI Embeddingsは、OpenAI社が提供するAPIを通じて利用でき、テキストの意味を高精度に捉え、多言語 にも対応している点が特徴です。LangChain の OpenAIEmbeddingsクラスで、OpenAI Embeddings APIを容易に扱えます。
OpenAI Embeddingsには、主にtext-embedding-ada-002text-embedding-3(small/large) の種類があり、それぞれ性能と価格が異なります。text-embedding-ada-002は、高性能かつ低価格 でバランスが良く、text-embedding-3は、さらに高性能を追求したい場合に適しています。

また、埋め込みモデルは OpenAI Embeddings以外にも、Hugging Face Transformers の Sentence-BERTなど、オープンソースで高性能なモデルが多数存在します。Sentence-BERTは、特に文章の類似度判定に優れており、RAGの検索精度向上に貢献する可能性があります。

より専門的な内容に関しては、汎用モデルではなく、ドメイン固有のデータで事前学習やファインチューニングした埋め込みモデルを利用することで、専門用語や業界特有の言い回しを正確に理解できるようになり、さらなる精度向上が期待できます。

4. ベクトルデータベースへの登録 (Vector Store)

埋め込み生成ステップで得られた埋め込みベクトルを、ベクトルデータベース (Vector Store) に登録します。ベクトルデータベースは、大量のベクトルデータを効率的に管理し、類似検索を高速に行うことに特化したデータベースです。

RAGシステムにおいて、ベクトルデータベースは以下の役割を担います

  • 大量ベクトルデータの効率的な管理:
    PDFドキュメント全体をベクトル化すると、大量の埋め込みベクトルが生成されます。ベクトルデータベースは、これらのベクトルデータを効率的に保存、管理、および検索するための基盤となります。
  • 高速な類似ベクトル検索:
    質問文をベクトル化し、ベクトルデータベースに対して類似検索を行うことで、質問文と意味的に類似するドキュメントやテキストチャンクを高速に見つけ出すことができます。

記事では、学習用途に最適なChromaDBをベクトルデータベースとして採用します。
ChromaDBは、Python 環境で非常に扱いやすいオープンソースのベクトルデータベースです。インストールが簡単で、特別な設定なしにローカル環境ですぐに使い始められます。データはインメモリ(オプションで永続化も可能) で高速に処理され、LangChainとの連携もスムーズに行えるため、RAG構築の学習、プロトタイプ開発、そして個人利用に非常に適しています。

また、ベクトルデータベースはほかにもFAISSPineconeなど規模や目的に応じたものが存在します。FAISSはMeta社開発の高速類似検索ライブラリで、ローカル環境でも驚異的な速度でベクトル検索を実行でき、より高速な検索性能を求める場合や、大規模なデータを扱いたい場合に適しています。Pineconeは、高いスケーラビリティと可用性を備え、安定した運用と大規模データ処理を実現でき、商用利用や 大規模なRAGシステムを構築する場合に適しています。

<実装>

以上の 2.ドキュメント分割、3.埋め込み生成(Embedding)、.4. ベクトルデータベースへの登録 (Vector Store)をまとめて行うコードを以下に示します。

create_db.py
from langchain_openai.embeddings import AzureOpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
from dotenv import load_dotenv
from tqdm import tqdm

# 環境変数の読み込み
load_dotenv()

# Azure OpenAI の設定
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
API_VERSION = os.getenv("API_VERSION")
DEPLOYMENT_ID_FOR_EMBEDDING = os.getenv("DEPLOYMENT_ID_FOR_EMBEDDING")

# 各チャンクの最大サイズ
chunk_size = 300
# チャンクサイズに対するオーバーラップの割合
overlap_ratio = 0.25

# LangChain の埋め込みクラスを初期化
embedding_model = AzureOpenAIEmbeddings(
    deployment=DEPLOYMENT_ID_FOR_EMBEDDING,
    model="text-embedding-ada-002",
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
    chunk_size=chunk_size
)

# Chroma データベースの初期化
output_db_folder = "./chroma_db"
db = Chroma(persist_directory=output_db_folder, embedding_function=embedding_model)

# Markdownファイルが保存されているフォルダ
input_folder = "./data_sources/"
# チャンク化用の設定
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size,  # 各チャンクの最大サイズ
    chunk_overlap= int(chunk_size * overlap_ratio)  # チャンク間のオーバーラップ
)

# Markdownファイルを処理
md_files = [f for f in os.listdir(input_folder) if f.endswith(".md")]
print(f"処理対象のMarkdownファイル: {len(md_files)}")

for md_file in tqdm(md_files, desc="Processing Markdown Files", unit="file"):
    try:
        md_path = os.path.join(input_folder, md_file)

        # Markdownファイルを読み込み
        with open(md_path, "r", encoding="utf-8") as f:
            md_text = f.read()

        # チャンク化
        chunks = text_splitter.split_text(md_text)

        # 各チャンクをDocumentオブジェクトとして作成
        documents = [
            Document(page_content=chunk, metadata={"source": md_file})
            for chunk in chunks
        ]

        # テキストをベクトル化してChromaに保存
        db.add_documents(documents)

    except Exception as e:
        print(f"エラーが発生しました: {md_file} - {e}")

print(f"すべてのMarkdownファイルの処理が完了し、データベースが作成されました。出力先: {output_db_folder}")

チャンクサイズはタスク、ドキュメントの種類、使用するモデル、そして目的によって大きく異なります。 例えば、技術書やFAQなど、特定の情報や手順をピンポイントで検索したケースでは比較的小さなチャンクサイズを設定しますが、小説や物語などの文脈の流れやキャラクターの関係性を理解することが重要な場合は大きなチャンクサイズを設定する必要があります。ただ、「これが正解」というサイズはなく、実験的に最適なサイズを見つけることが重要らしいです。
また、チャンクオーバーラップも同様に明確な正解はありませんが、チャンクサイズの25%程度の値に設定することが多いらしいです。
これらの値はRAGの精度に直結してくるものですので、慎重に調整を行う必要があります。


以上で外部データの登録処理は完了です。次に、登録した外部データを活用し、RAGを用いた質問応答を実現するための検索および生成処理に進みます。

4.3 検索と応答生成 (Retrieval & Generation)

外部データの登録が完了したら、登録したデータを使って質問応答を行うフェーズです。このフェーズは大きく分けて 検索 (Retrieval) と 応答生成 (Generation) の2つのステップから構成されます。 ユーザーからの質問に対して、RAGシステムがどのように答えを導き出すのか、解説していきます。


1. 質問(クエリ)の入力・前処理

最初のステップでは、ユーザーからの質問(クエリ)を受け取り、RAGシステムが処理しやすいように前処理を行います。

まず、ユーザーはRAGシステムに対して、自然言語で質問を入力します。例えば、「〇〇について教えてください」「〇〇のやり方は?」といった具体的な質問です。

入力された質問は、そのままでは検索処理に利用できない場合があります。そこで、以下のような前処理を行います。

  • トークン化:
    質問文を単語や記号などの単位(トークン)に分割します。
  • 正規化:
    表記ゆれの修正、小文字化、不要な記号の除去などを行い、質問文を統一的な形式に整えます。
  • 埋め込み (ベクトル化):
    前処理された質問文を、外部データ登録の際と同様に埋め込みモデルを用いてベクトルに変換します。ベクトル化することで、質問の意味内容を数値データとして表現し、後続の類似度検索を効率的に行う準備をします。

また、より精度を向上させるために、ユーザーの質問をLLMに再定義させることで、曖昧な表現を明確化し、より具体的なクエリに変換する場合もあります。詳しい内容については、 第5章 精度向上のためのテクニック で解説します。


2. 類似度検索(Retrieval)

前処理でベクトル化された質問(クエリベクトル)を使い、登録された外部データの中から質問内容と関連性の高いチャンクを検索します。以下に主要な手順をまとめます。

1. ベクトルデータベースとの照合

  • 事前にベクトル化して登録しておいた外部データのチャンク(ドキュメントベクトル)と質問のベクトルをベクトルデータベース上で比較します。

2. 類似度計算

  • ベクトル同士の類似度を計算します。類似度が高いほど、質問とチャンクの内容が近いと判断されます。
  • コサイン類似度などがよく使用されます。

3. チャンクのランキング

  • 類似度が高い順にチャンクをランキングします。
  • 上位のチャンクほど、質問への回答に役立つ可能性が高いと考えられます。

4. 上位K件の取得

  • ランキング上位から K件(事前に設定した数)のチャンクを取得します。
  • 取得されたチャンクは、次のステップの応答生成で利用されます。

なお、取得するチャンクの数や類似度計算のアルゴリズムは、データの特性やタスクに応じて調整が必要です。試行錯誤を重ねて最適な設定を見つけることが重要です。


3. LLMへのプロンプト構築

検索ステップで取得した関連性の高いチャンクを、LLM(大規模言語モデル)に入力するためのプロンプトを構築します。

質問文に加えて、検索されたチャンクを文脈情報として 指示文(プロンプト) に組み込みます。これにより、LLMは外部データの内容を踏まえて回答を生成することができます。

プロンプト例
<質問> 
{ユーザーからの質問}

<参考情報>
{検索されたチャンク1}
{検索されたチャンク2}
...
{検索されたチャンクK}

<指示>
1. 参考情報のみから判断してください。
2. 参考情報から判断できない場合、「分かりません」と答えてください。
3. 根拠となった参考情報を提示してください

上記の参考情報と指示に基づいて、質問に答えてください。

より良い回答を得るためには、プロンプトの表現や構成を工夫するプロンプトエンジニアリングが重要です。
プロンプトでは、以下のポイントを意識すると効果的です:

  • 必要な情報を簡潔に記載する。
  • 指示文を明確にし、期待する回答形式を伝える。
  • 文脈に応じて具体的な例やフォーマットを提示する。

上の例では、指示1によりLLMは学習された知識ではなく、参考情報のみを用いて判断します。また、指示2により、参考情報に該当がない場合は「分かりません」と明確に回答するため、誤回答(ハルシネーション)が軽減できます。さらに、指示3によって、回答に根拠を含めることで、応答の透明性が向上します。

プロンプト設計はLLMの解答精度に直結するため、適切な文脈付与と試行錯誤による調整が重要な工程となります。


4. 応答生成(Generation)

構築されたプロンプトをLLMに入力し、質問に対する回答を生成させます。LLMは、プロンプトに含まれる質問と文脈情報(検索されたチャンク)に基づいて、自然言語で回答を生成します。本記事では、同様にGPT 4o-miniのチャットモデルを使用していますが、使用するモデルはタスクの性質や応答の要件に応じて選択することが重要です。

例えば、より高度な推論や詳細な回答が必要な場合には、GPT-4やLlama70Bのような大規模モデルを活用し、逆に簡潔な回答やリアルタイム性が重視されるタスクでは、Llama 2 7Bなどの軽量モデルを利用することが推奨されます。


5. 応答の後処理・出力

最後に、LLMによって生成された回答を、必要に応じて後処理し、ユーザーに出力するステップです。

生成された回答に対して、以下のような後処理を行います:

  • 言い換え
    回答文をより自然で分かりやすい表現に言い換えることで、ユーザーにとって理解しやすくします。

  • 不要な情報の削除
    回答に含まれるノイズや冗長な部分を取り除き、必要な情報のみを残します。

  • フォーマット調整
    回答をユーザーインターフェースに適した形式に整形します。
    例: マークダウン形式、箇条書き、表形式など。

その後、後処理された回答をユーザーに提示します。適切な形式で出力することで、ユーザーは質問に対する回答を得ることができます。


<実装>

4.3 検索と応答生成 (Retrieval & Generation)を実行するためのコードを以下に示します。LangChainを用いることで、簡単に実装できます。

query.py
from langchain_openai.embeddings import AzureOpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain_openai.chat_models import AzureChatOpenAI
from langchain.schema import HumanMessage
import os
from dotenv import load_dotenv

load_dotenv()

# Azure OpenAI の設定
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
API_VERSION = os.getenv("API_VERSION")
DEPLOYMENT_ID_FOR_EMBEDDING = os.getenv("DEPLOYMENT_ID_FOR_EMBEDDING")
DEPLOYMENT_ID_FOR_CHAT_COMPLETION = os.getenv("DEPLOYMENT_ID_FOR_CHAT_COMPLETION")  # Chat用のデプロイ名

# LangChain の埋め込みクラスを初期化
embedding_model = AzureOpenAIEmbeddings(
    deployment=DEPLOYMENT_ID_FOR_EMBEDDING,  # デプロイ名
    model="text-embedding-ada-002",  # 埋め込みモデル名
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
    chunk_size=300
)

# Azure ChatOpenAI を初期化
chat = AzureChatOpenAI(
    deployment_name=DEPLOYMENT_ID_FOR_CHAT_COMPLETION,  # Chat用のデプロイ名
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
)

# Chroma データベースの初期化
output_db_folder = "./chroma_db"
db = Chroma(persist_directory=output_db_folder, embedding_function=embedding_model)

# 質問の定義
query = "令和6年分の年末調整は前年と異なる部分がありますか?"

# データベースから類似度の高いドキュメントを取得
documents = db.similarity_search(query, k=5)

# ドキュメントの内容を結合
documents_string = "\n".join([f"---------------------------\n{doc.page_content}" for doc in documents])

# プロンプトテンプレートを初期化(1段階に統合)
combined_prompt = PromptTemplate(
    template="""以下の文章と質問を基にして、質問に対する答えを日本語で出力してください。

1. 文章のみから判断してください。
2. 文章から全く判断できない場合、「分かりません」と答えてください。
3. ソースとなった文章を提示してください

文章:
{document}

質問: {query}""",
    input_variables=["document", "query"]
)

# チャットモデルに問い合わせ
response = chat.invoke([
    HumanMessage(content=combined_prompt.format(document=documents_string, query=query))
])

answer = response.content.strip()

# 結果を出力
print(f"質問: {query}")
print(f"回答: {answer}")

上のコードを実行すると、以下のような出力結果が得られました。

質問回答結果
質問: 令和6年分の年末調整は前年と異なる部分がありますか?
回答: はい、令和6年分の年末調整には前年と比べて変わった点があります。具体的には、令和6年分所得税について定額による特別控除(定額減税)が実施されていることです。

ソース:
「令和6年分所得税について、定額による所得税の特別控除(以下「**定額減税**」といいます。)が実施されています。」

上のコードでは類似度の高いドキュメント上位5件を取得していますが、タスクによって取得件数を調整することが重要です。例えば、広範な情報が必要な場合は件数を増やし、逆に精度重視で特定の情報を求める場合は件数を減らすことで、応答の質を最適化できます。


以上で基本的なRAGシステム構築は完了となります。しかし、実際の運用では、タスクやデータに合わせてシステムをカスタマイズすることが求められます。次の章では、さらに応答精度を向上させるための具体的なテクニックについて解説します。

第5章 精度向上のためのテクニック

精度向上のためには、まずPDFからのデータ抽出が正確に行われていることが大前提です。そのうえで、より高性能な埋め込みモデルを使用したり、ベクトルデータベースを最新のものに置き換えたり、あるいはチャットモデルを高性能なものに切り替えたりするなど、システムの基盤的なアップグレードが挙げられます。しかし実運用においては、システムのアップグレードだけでは十分に精度が上がらない状況に直面することもあるかと思います。そのため、本章ではこれらの基本的な手法に加え、より細かな工夫や補助的な方法によって、さらなる精度の向上を図るテクニックを紹介します。

1. チャンクサイズの調整

チャンクサイズとは、文章を分割する際の一塊あたりの長さを指します。小さすぎると文脈が分断されて本来は関係の深い情報が分散され、関連性の薄い情報まで同時に取り込んでしまい、類似度算出が不正確になるデメリットがあります。

以下に、年末調整に関するPDFデータを使用し、チャンクサイズを 50、300、600 に変更した際の出力結果を示します。なお、チャンクの重なり(オーバーラップ)は、それぞれのチャンクサイズの 25% に設定しています。

チャンクサイズ=50
質問: 令和6年分の年末調整は前年と異なる部分がありますか?
回答: 分かりません。
チャンクサイズ=300
質問: 令和6年分の年末調整は前年と異なる部分がありますか?
回答: はい、令和6年分の年末調整には前年と異なる部分があり、特に定額減税の実施が新たに行われています。
チャンクサイズ=600
質問: 令和6年分の年末調整は前年と異なる部分がありますか?
回答: 令和6年分の年末調整には前年と異なる部分があります。特に、令和6年分所得税について定額減税が実施されており、その年末調整時点の定額減税の額を算出し、年間の所得税額の計算を行う必要があることが明記されています。また、合計所得金額が1,805万円を超える場合、年調減税額を控除しないなどの規定も変更されています。

この結果から、チャンクサイズの調整は、質問応答の精度に大きく影響を与えることが分かります。
チャンクサイズが小さい場合(50)、質問に対する回答を得られませんでした。これは、チャンクが短すぎるために、質問に必要な情報がチャンク内に含まれていなかった、もしくは文脈が途切れてしまい、AIが質問とチャンクの内容を適切に紐付けられなかったと考えられます。
一方、チャンクサイズを大きくするにつれて(300、600)、回答の質が向上しています。チャンクサイズ300では、令和6年分の年末調整に前年と異なる点があること、そしてその一つが定額減税であることを捉えられています。さらにチャンクサイズ600では、定額減税に関するより詳細な情報、例えば年末調整時点での減税額算出や、合計所得金額による規定の変更など、より深い情報まで回答に含めることができています。

このように、チャンクサイズの調整は、簡単でありながら質問応答システムの性能を大きく左右する、非常に重要な要素と言えます。

2. 取得するチャンク数の設定

チャンクベースで検索や処理を行う場合、検索時に取得するチャンク数が少なすぎると必要な情報を十分に得られず、逆に多すぎると不要な情報が混在してノイズとなり、有用な情報が埋もれてしまう場合があります。したがって、アプリケーションの目的や文書の内容、ユーザーのクエリの特性に応じて、取得するチャンク数を適切に設定することが重要です。

具体的には、チャンク数が少ないと文脈や細部が欠落してしまい、回答の根拠が不十分になるリスクがあります。一方で、チャンク数が多すぎると、処理コストが増大するだけでなく、ノイズが大きくなり回答が曖昧になりやすくなります。ただし一部では、ノイズの種類によっては背景知識の追加として機能し、回答の精度向上につながったケースが報告されています。こういった例外的な状況も考慮しつつ、実際のテストやチューニングを繰り返して最適なチャンク数を見極めることが重要です。

3. メタデータの活用

文章の内容だけでなく、メタデータと呼ばれる文書に付随する情報を効果的に利用することも重要です。メタデータとは、作成日時や著者名、文書の種類、部門やカテゴリーなど、テキストそのもの以外の属性情報を指します。

<メタデータが果たす役割>

  • 検索の効率化
    検索やリランキングの際にメタデータを活用すると、関連性の高いドキュメントを素早く絞り込めます。たとえば、“契約書”だけを検索対象とする、特定の年月に作成された文書に限定するなど、カテゴリや時系列に基づくフィルタリングが簡単に行えます。

  • ノイズの除去
    メタデータを使って、目的に合わないドキュメントをあらかじめ排除できます。たとえば、対象が「社内規定」に関する文書のみの場合、それ以外の業務マニュアルや顧客資料を省くことができ、回答の精度を向上させられます。

  • リランキングへの貢献
    RAGにおけるリランキング工程では、クエリと文書内容の類似度だけでなく、メタデータに基づくスコアリングを加味することで、より的確な文書を上位に表示できます。たとえば、最近更新された文書を優先する、特定の著者や部署が作成した文書を高評価するなどの仕組みが考えられます。

<メタデータ活用の実装例>

  • 部門タグを使ったフィルタリング
    文書に「経理部」「人事部」などのタグが付与されている場合、クエリが「給与計算」「雇用契約」などのテーマを含んでいるときは「経理部」「人事部」のタグがある文書を優先的に検索・リランキングの対象とする。

  • 時系列情報の活用
    「更新日」が含まれるメタデータを用いて、最新の文書を上位に表示する。税制や法律の情報は古い文書よりも最新の文書が正確な場合が多いため、回答の精度が上がる。

  • セクション情報の活用
    文書を章や節ごとに分割し、その構造情報をメタデータとして扱う。ユーザーが「第3章の細則について詳しく教えて」と尋ねた場合に、該当セクションに絞った情報を提示できる。

<メタデータ活用時の注意点>

  • メタデータの整備
    メタデータが正確に整備されていないと、誤ったフィルタリングやリランキングの原因になります。データの標準化や定期的な更新管理が必要です。

  • 過剰な制限に注意
    メタデータで厳しくフィルタリングしすぎると、有用な文書を見落とす可能性があります。必要に応じて柔軟にスコアリングを調整し、情報を狭めすぎないようにすることが重要です。

  • セキュリティとの兼ね合い
    部門やアクセス権限などのメタデータを扱う場合、誤って機密情報が検索・リランキングの対象にならないように、適切な権限管理が必要です。

メタデータの活用は、単にテキストベースの検索では拾いきれないドキュメントの属性情報を検索・リランキングに反映するための有力な手段です。正しく管理されたメタデータを活用すれば、RAGワークフロー全体の精度を高めるだけでなく、回答をより効率的かつ適切に取得できるようになります。

4. 検索アルゴリズムの選択

検索アルゴリズムを選定する際には、データの特性や検索の目的に応じて適切な手法を選ぶことが重要です。検索手法は、大きく分けて「類似度検索」と「キーワード検索」に分類できます。

<類似度検索>

類似度検索は、文章やデータ同士の「意味的な近さ」を計算し、関連性の高い情報を抽出する手法です。
代表的な類似度計算の方法としては、以下のようなものがあります。

  • コサイン類似度
    ベクトルの角度を基準に類似性を測る手法で、テキスト検索や文書分類に適しています。
  • ユークリッド距離
    データ間の直線距離を計算する方法で、数値データや位置情報の類似度を求める際に用いられます。
  • マンハッタン距離
    縦横の移動のみを考慮した距離計算方法で、高次元データの類似度評価に適している場合があります。
  • ジャカード係数
    集合の共通要素の割合を比較する手法で、タグベース(キーワードやカテゴリ情報)の類似度計算に有効です。

類似度検索では、文章全体の文脈を考慮した検索が可能ですが、専門用語や固有名詞がベクトル化される過程で本来の意味が失われるリスクもあります。

<キーワード検索>

キーワード検索は、特定の単語やフレーズを文字列として厳密に検索する方法です。

  • 法律文書や商品名、社名など、正確な一致が求められる場面に適しています。
  • 言葉の表現が異なる場合にヒットしないなど、言い換えへの対応が困難という課題があります。

<組み合わせによる精度向上>

実際の検索システムでは、以下のようにキーワード検索と類似度検索を組み合わせることで、より精度の高い情報取得が可能になります。

  1. キーワード検索で候補を絞り込む
    まずは厳密な文字列一致により、範囲を狭めます。
  2. 類似度検索を使って、関連性の高い情報を取得する
    候補の中から、文脈や意味に基づいて最適な結果を得ます。

このように、キーワード検索の正確性類似度検索の柔軟性を組み合わせることで、精度と効率を両立できます。検索の目的やデータの特性に応じて、どの手法をどのように組み合わせるかを検討しましょう。

5. リランキング

RAGのプロセスでは、まず検索段階で関連性の高い文書を取得し、それらを元に最終的な文章生成を行います。しかし、一次検索(ファーストパス)で取り出した文書が必ずしも最適とは限りません。そこで、リランキングという手法を用いて、検索結果を再評価し、最も関連性の高い文書を上位に並べ直すことが重要になります。

<RAGにおけるリランキングの流れ>

  1. 一次検索(Retrieval)

    • 大まかな手法(たとえばベクトル類似度検索)で候補文書を抽出し、その中からある程度数を絞り込みます。
  2. リランキング(Re-ranking)

    • 抽出された候補文書を、より高度な手法や追加の特徴量を用いて再スコアリングし、順位付けをやり直します。
    • 例:大規模言語モデル(LLM)の埋め込みを使ったより詳細な類似度計算など
  3. 最終的な文章生成(Augmented Generation)

    • リランキングによって厳選された文書をコンテキストとして、LLMに文章生成を行わせます。

<リランキングの必要性とメリット>

  • 検索精度の向上
    LLMは入力の先頭部分に注目しやすい特性があります。そのを活かし、関連性の高い文書を上位に配置することで、最適な回答生成を促します。

  • 追加の特徴量やアルゴリズムを反映
    ファーストパスで単純なベクトル検索を行い、リランキング段階で高度なテキスト埋め込みやモデル推論結果を取り入れることで、計算コストを抑えながらも精度を向上させられます。

  • 柔軟なフィルタリング
    不要な情報やドメインが違う文書を後から除外するなど、リランキングで再度スコアリングすることでノイズ除去がしやすくなります。

<リランキングの実装例>

  1. BM25 + LLM埋め込み再評価

    • ファーストパス:BM25などの伝統的な検索手法で候補を絞り込み
    • リランキング:LLMを使って埋め込みベクトルを生成し、コサイン類似度などで再スコアリング
    • 生成:厳選した上位文書をプロンプトに組み込み、回答を生成
  2. キーワード検索 + 類似度計算モデルの組み合わせ

    • ファーストパス:キーワード検索で高速に候補抽出
    • リランキング:BERT系モデルなどの文章埋め込みを使い、文脈に応じた類似度を計算して順位を付け直す
    • 生成:リランキング後の文書を追加コンテキストにして生成モデルを動作させる

<注意点>

  • 計算コスト
    リランキングに高負荷なモデルを使うと、その分計算コストが上がります。適度に絞り込んだ上でリランキングを行う戦略が重要です。

  • 過剰なフィルタリング
    絞り込みすぎると、本来意味がある文書を逃してしまう可能性があります。設定するスコアの閾値や候補数をチューニングすることが大切です。

RAGワークフローで高品質な回答を得るには、ファーストパス検索で幅広く候補文書を取得しつつ、リランキングでより高度に関連度を評価して最終的に厳選した文書をLLMに渡すことで、ノイズを抑えた、より正確な生成結果が期待できます。

6. プロンプトエンジニアリング

RAGの精度を向上させるためには、検索結果の適切な利用と、LLMへの指示の最適化が不可欠です。
適切なプロンプトエンジニアリングを行うことで、データに基づいた正確な回答を得ることが可能になります。以下に例を示します。

<プロンプトエンジニアリングの例>

1. 質問と参考情報の構造を明確にする
検索結果をそのまま並べるのではなく、質問部分と関連文書(参考情報)を区別して提示することで、モデルがどの情報をもとに回答すべきかを理解しやすくします。

例:構造化プロンプト

【質問】  
2024年の所得税控除の変更点は?  

【参考情報】  
- 所得税控除は2024年に5万円増額されました。  
- 医療費控除には小規模な変更がありました。

なぜ有効か?

  • モデルが情報の役割(質問・情報源)を明確に把握できる
  • 検索結果に基づく回答を促し、余計な推論を抑制できる

2. 「分からない」と答えることを許可する
LLMは情報が不足していても、推測を交えて回答を生成しようとする傾向があります。そこで、明確な情報がない場合は「分かりません」と答えるように促すと、誤った回答を減らすことができます。

例:不確実な回答を避けるプロンプト

以下の情報のみを基に質問に回答してください。
もし、明確な情報が見つからない場合は、「分かりません」と回答してください。

【質問】  
 ~~~~~
【参考情報】
 ~~~~~

なぜ有効か?

  • 不確実な情報を無理に生成するリスクを低減
  • データが不足している際に誤答が生じる可能性を抑制

3. データベースの情報のみを基に判断させる
LLMは、事前学習された知識をもとに推論しがちです。そこで、検索結果のみに基づいて回答するよう、あらかじめ指示することで、RAGの目的に沿った回答を得やすくなります。

例:推測を抑制するプロンプト

あなたはデータベースから取得した情報のみを基に回答するアシスタントです。
事前知識や推測を加えず、以下の参考情報のみをもとに質問に回答してください。

【質問】  
 ~~~~~
【参考情報】 
 ~~~~~

なぜ有効か?

  • 事前学習された知識による推測を抑え、検索結果ベースの回答を生成できる。

4. 回答のフォーマットを指定する
モデルが不要な情報を追加しないよう、回答形式(箇条書き・表形式・最大文字数など)を指定すると、情報が整理され、誤答や冗長な回答が減る傾向があります

例:箇条書きでの回答を指示

次の情報を参考に、質問に対して 「簡潔な箇条書き」 で回答してください。

【質問】  
 ~~~~~
【参考情報】 
 ~~~~~

なぜ有効か?

  • 冗長な回答や背景知識の羅列を防ぎ、要点を簡潔にまとめさせやすい

5. 生成のステップを分割し、段階的に回答させる
一度に結論を出させるのではなく、「関連情報の要約」→「最終回答」 のように複数ステップに分けると、モデルが文脈を整理しやすくなり、回答の精度が向上します。
例:ステップを分割する

① まず、以下の参考情報を要約してください。
② その要約をもとに、質問に回答してください。
【質問】  
 ~~~~~
【参考情報】 
 ~~~~~

なぜ有効か?

  • 長い文章や複雑な情報を段階的に処理し、より正確な回答を導きやすい。

上記のようなプロンプトエンジニアリングを行うことで、RAGの検索結果を最大限に活用し、モデルの不必要な推測を抑えながら正確な回答を引き出せます。
これらはあくまで一例であり、実際のユースケースに応じてさらに細かい指示や別の工夫を取り入れることで、RAGの精度と信頼性を一層高めることが可能です。

7. クエリ拡張

RAGの精度を向上させるためには、検索クエリを適切に拡張し、より関連性の高い情報を取得することが重要です。クエリ拡張(Query Expansion) は、元のクエリをより詳細にリフレーズしたり、関連する表現を追加したりすることで、検索結果の精度を向上させる手法です。

例えば、以下のような状況が考えられます。

  • ユーザーの入力が曖昧な場合(例:「控除の申請はどうするの?」 → 「年末調整における扶養控除の申請方法を教えてください。」)
  • 同義語や類義語が考えられる場合(例:「マイナンバー」 → 「個人番号」)
  • クエリが短すぎて検索対象を十分にカバーできない場合(例:「控除の方法」 → 「2024年の所得税における控除の申請方法」)

クエリ拡張についての詳細な情報は、こちらのサイトの資料で非常にわかりやすくまとめられており、参考になりました。


<実装例>

以下のコードでは、ユーザーが入力したクエリを、LLM(大規模言語モデル)を用いてリフレーズし、検索に適した形に変換する方法を示しています。

コード例:クエリのリフレーズ

query_expansion_1.py
from langchain_openai.embeddings import AzureOpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain_openai.chat_models import AzureChatOpenAI
from langchain.schema import HumanMessage
import os
from dotenv import load_dotenv

load_dotenv()

# Azure OpenAI の設定
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
API_VERSION = os.getenv("API_VERSION")
DEPLOYMENT_ID_FOR_EMBEDDING = os.getenv("DEPLOYMENT_ID_FOR_EMBEDDING")
DEPLOYMENT_ID_FOR_CHAT_COMPLETION = os.getenv("DEPLOYMENT_ID_FOR_CHAT_COMPLETION")

# LangChain の埋め込みクラスを初期化
embedding_model = AzureOpenAIEmbeddings(
    deployment=DEPLOYMENT_ID_FOR_EMBEDDING,
    model="text-embedding-ada-002",
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
    chunk_size=300
)

# Azure ChatOpenAI を初期化
chat = AzureChatOpenAI(
    deployment_name=DEPLOYMENT_ID_FOR_CHAT_COMPLETION,
    openai_api_key=AZURE_OPENAI_API_KEY,
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    openai_api_version=API_VERSION,
)

# Chroma データベースの初期化
output_db_folder = "./chroma_db"
db = Chroma(persist_directory=output_db_folder, embedding_function=embedding_model)

# 質問の定義
original_query = "令和6年分の年末調整は前年と異なる部分がありますか?"

# 質問をリフレーズするプロンプト
rephrase_prompt = f"""
以下の質問をリフレーズしてください。できるだけ詳細で検索に適した形にしてください。
質問: {original_query}
"""
# 質問をリフレーズ
rephrased_query = chat.invoke([HumanMessage(content=rephrase_prompt)]).content.strip()

# リフレーズされた質問を拡張クエリとして使用し、データベースから類似度の高いドキュメントを取得
documents = db.similarity_search(rephrased_query, k=5)

# ドキュメントの内容を結合
documents_string = "\n".join([f"---------------------------\n{doc.page_content}" for doc in documents])

# プロンプトテンプレートを初期化
combined_prompt = PromptTemplate(
    template="""以下の文章と質問を基にして、質問に対する答えを日本語で出力してください。

1. 文章のみから判断してください。
2. 文章から全く判断できない場合、「分かりません」と答えてください。
3. ソースとなった文章を提示してください。

文章:
{document}

質問: {query}""",
    input_variables=["document", "query"]
)

# チャットモデルに問い合わせ
response = chat.invoke([
    HumanMessage(content=combined_prompt.format(document=documents_string, query=original_query))
])

answer = response.content.strip()

# 結果を出力
print(f"元の質問: {original_query}")
print(f"リフレーズされた質問: {rephrased_query}")
print(f"回答: {answer}")

リフレーズによる出力結果

元の質問: 令和6年分の年末調整は前年と異なる部分がありますか?
リフレーズされた質問: 令和6年分の年末調整において、前年とは異なる点があるかどうか、具体的な変更内容や新たな制度、手続きの違いについて詳しく教えてください。
回答: はい、令和6年分の年末調整には前年と異なる部分があります。具体的には、定額減税の実施があり、年末調整時点での定額減税の額を算出する必要があります。

<クエリ拡張のメリット>
1. 検索精度の向上
LLMを活用してクエリ拡張することで、より具体的で詳細なクエリが生成され、検索エンジンやベクトルデータベースが適切な結果を返しやすくなります。

2. ユーザーの意図を明確にできる
ユーザーが入力するクエリは、主観的で曖昧な表現を含むことが多く、検索対象に適さない場合があります。クエリ拡張を行うことで、意図を明確にしたクエリを生成できます。

3. 検索対象の幅を広げる
クエリを拡張することで、単一の表現だけでは得られない多面的な情報を取得しやすくなります。単純なキーワードに依存している場合、特定のソースからの情報に偏りがちですが、複数の言い回しや関連用語を加えることで、検索結果の網羅性が向上します。

<応用例>

以下では、リフレーズ手法をさらに発展させる形で、さまざまなクエリ拡張の方法を紹介します。各手法を実行するための Python ファイルは、Git リポジトリに公開されていますので、興味のある方はぜひ試してみてください。

1.複数のクエリを作成し検索する方法(query_expansion_2.py

  • 概要: 元のクエリを複数のバリエーションにリフレーズし、それぞれのクエリで検索を行います。その後、複数の検索結果を統合(たとえば RRF:Reciprocal Rank Fusion)することで、検索精度を高める手法です。
  • メリット: 一つのクエリだけでは見落としてしまう情報を、複数のクエリで補い合う形で検索できるため、情報の網羅性が向上します。
クエリ改善例
元の質問: 令和6年分の年末調整は前年と異なる部分がありますか?
 リフレーズされた質問: ['1. 令和6年の年末調整には、前年と比べて変更点がありますか?', '2. 令和6年の年末調整で、昨年とは異なる点がございますか?', '3. 令和6年分の年末調整には、前年とは異なる要素が含まれていますか?', '4. 令和6年度の年末調整は、前年度と違う部分がありますか?', '5. 令和6年の年末調整に関して、前年と異なる部分はありますか?']

2. 検索結果に基づいてクエリを動的に改善する手法(query_expansion_3.py

  • 概要: まずは元のクエリで検索を行い、結果が不十分と判断された場合に、LLM(大規模言語モデル)を用いてクエリそのものを再構築(リフレーズや補足)し、再検索を行います。
  • メリット: 検索結果をフィードバックとして活用することで、段階的にクエリを最適化し、適切な情報にたどり着くまでクエリを改善できます。特に、不明点や不足が明確になった場合に有効です。
クエリ改善例
元の質問: 令和6年分の年末調整は前年と異なる部分がありますか?
改良された質問: 令和6年分の年末調整の方法と、前年との違いについて詳しく解説している情報を探しているのですが、特に定額減税に関する変更点や手続きの特徴、注意すべきポイントについて具体的な情報が知りたいです。

3. 元の質問を抽象化して検索を行い、その後具体的な回答を導出する手法(query_expansion_4.py

  • 概要: 質問が具体的すぎる場合、最初に質問を抽象化して検索することで、より汎用的な文書を取得します。その後、取得した情報をもとに、再度質問に合わせた具体的な回答を生成します。
  • メリット: あまりにも細分化されたクエリでは十分な検索結果が得られない場合がありますが、抽象化することで必要な背景知識や関連する一般的な情報を得やすくなります。最終的には元の質問に合った具体的な回答へ落とし込むため、柔軟性と網羅性を両立できます。
query_rephrase_4.py実行結果
元の質問: 令和6年分の年末調整は前年と異なる部分がありますか?
抽象化された質問: 質問: 年末調整における変更点や新たな要素は何ですか?
抽象的な回答: 年末調整における変更点や新たな要素として、令和6年分の所得税において**定額減税**が実施されることが挙げられます。これに伴い、年末調整の際には定額減税の額を算出し、それを基に年間の所得税額を計算する必要があります。この新しい要素により、年末調整に関連する事務が従来とは異なり 、特別控除の考慮が求められるようになります。詳細や手続きについては、国税庁のホームページでも案内されているため、参照することが推奨されます。   
具体的な回答: 令和6年分の年末調整には、前年と異なる部分として定額減税が実施されます。これにより、年末調整の際に定額減税の額を算出し、それに基づいて年間の所得税額を計算する必要があります。この新しい要素により、年末調整に関連する事務が従来とは異なり、特別控除の考慮が求められるようになり ます。詳細や手続きについては、国税庁のホームページを参照することが推奨されます。

これらの手法を組み合わせることで、クエリ拡張の効果を最大化し、より正確かつ豊富な検索結果を得ることが可能になります。特に、検索結果の活用や抽象化・具体化といったアプローチは、RAG(Retrieval-Augmented Generation)の精度向上にも大いに役立ちます。ぜひ、各スクリプトを実行して試してみてください。


クエリ拡張は、RAGの検索プロセスにおいて精度を向上させる重要な手法の一つです。
LLMを用いたリフレーズを活用することで、より具体的で意味の明確なクエリを生成し、適切な検索結果を取得できるようになります。また、類義語の追加や補足情報の挿入など、他の手法と組み合わせることで、さらなる精度向上が期待できます。

第6章 まとめ

本記事では、RAG(Retrieval-Augmented Generation)の基本的な仕組みから、LangChainを活用した実装方法、検索精度を向上させる工夫までを紹介しました。検索の精度を高めるには、チャンクサイズの調整やメタデータの活用、リランキング、クエリ拡張など、さまざまなアプローチがあります。特に、クエリのリフレーズや動的な改善、抽象化を取り入れることで、より的確な検索結果を得ることができることが分かりました。

RAGは、大規模言語モデルをより効果的に活用し、最新の情報や専門知識を柔軟に組み込める点で非常に有用な技術です。本記事の内容が、RAGを活用したシステムの構築や検索精度向上の参考になれば幸いです。
今回利用した環境やコードはこちらにまとめております。

第7章 参考資料

<書籍>

<Webサイト>

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?