RAG (Retrieval-Augmented Generation) の実践とハマりポイントについて

# はじめに

近年、大規模言語モデル（LLM）の活用が広がる中、**LLMの知識がトレーニング時点で固定される** という課題が浮き彫りになっています。  
この問題を解決する技術の一つが **RAG（Retrieval-Augmented Generation）** です。  

RAGは、外部データソースから関連情報を検索（Retrieval）し、それをLLMに提供することで、**最新かつ高精度な回答を生成** できるようにします。  
本記事では、RAGの基本概念とともに、ローカル環境での実装方法やハマりポイントを紹介します。

---
# RAGとは？

## RAGの基本概念と仕組み

RAG（Retrieval-Augmented Generation）は、事前に蓄積した情報を検索（Retrieval）し、その情報をLLMに提供して、より正確かつ最新の回答を生成する技術です。

## なぜ今RAGが注目されているのか？

従来のLLMは学習済みの情報範囲内でしか回答できません。しかしRAGを使えば、最新のデータや特定分野の専門情報をリアルタイムに反映できます。

## RAGが有効なケースとは？

- 最新情報や専門的知識が求められる場面
- ファクトチェックや情報の正確性が重要な場面
- 大量の情報を素早く正確に検索する必要があるケース

---

# ローカル環境で実装するには？ChromaDBを選んだ理由

## RAGをローカル環境で構築するための要件

- ベクトルDBの選定
- 埋め込みモデル（Embeddingモデル）の選定
- Docker環境の構築

## ベクトルDBの比較と選定理由

| ベクトルDB  | 特徴                  | メリット                | デメリット            |
|------------|----------------------|------------------------|----------------------|
| **ChromaDB** | 軽量・ローカル利用向け | 無料・高速・簡単導入    | 大規模運用には不向き |
| Weaviate   | スケーラブルで多機能   | 大規模向け・機能豊富    | 複雑・運用コスト高   |
| Pinecone   | クラウドマネージド     | 運用不要・高性能        | 無料枠が少なくコスト高 |

今回は**ローカル環境で手軽に運用でき、小規模案件に適している**という理由で、**ChromaDB**を選定しました。

## 埋め込みモデルの選定理由

埋め込みモデルとは、テキストを数値ベクトルに変換して類似度を測定するためのものです。  
今回は、精度が高く無料利用枠が豊富な **Gemini API** を採用しました。

---

# 実際に検証したこと・ハマったポイント

## 1. PDFデータ抽出（pdfplumberを利用）

```python
import pdfplumber

def extract_pdf_text(pdf_path):
    with pdfplumber.open(pdf_path) as pdf:
        return "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
```

## 2. テキストのクリーンアップ（正規表現）

```python
import re

def clean_pdf_text(text):
    text = re.sub(r"\n\s*\n", "\n", text)  # 空行削除
    text = re.sub(r"[ \t]+", " ", text)  # 空白を統一
    exclusion_patterns = [
        r"本資料に関して.*$",
        r"出所）.*$",
        r"本資料は.*?保証するものではありません。"
    ]
    for pattern in exclusion_patterns:
        text = re.sub(pattern, "", text, flags=re.MULTILINE)
    return text.strip()
```


## 3. Gemini APIでテキストを要約

```python
import google.generativeai as genai

def summarize_with_gemini(text):
    prompt = f"""
    以下のテキストを要約してください。

    {text}

    観点：[主要金融市場、主要国株式、マーケットの動き、注目点]
    """
    model = genai.GenerativeModel("gemini-1.5-flash")
    response = model.generate_content(prompt)
    return response.text.strip() if response.text else text
```


## 4. テキストのチャンク化（langchainを利用）

```python
from langchain.text_splitter import RecursiveCharacterTextSplitter

def split_text(text, chunk_size=300, overlap=50):
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size, chunk_overlap=overlap, length_function=len
    )
    return text_splitter.split_text(text)
```


## 5. Gemini APIで埋め込みベクトル生成

```python
def get_gemini_embedding(text):
    response = genai.embed_content(model="models/text-embedding-004", content=text)
    return response.get("embedding", None)
```


## 6. ChromaDBへ登録

```python
import chromadb
import uuid

client = chromadb.HttpClient(host="chromadb", port=8000)
collection = client.get_or_create_collection("daily_docs")

def add_to_chromadb(text_chunks, source):
    for idx, chunk in enumerate(text_chunks):
        embedding = get_gemini_embedding(chunk)
        if embedding:
            doc_id = str(uuid.uuid4())
            collection.add(
                ids=[doc_id],
                documents=[chunk],
                metadatas=[{"source": source, "chunk_index": idx}],
                embeddings=[embedding]
            )
```

## ハマったポイントとその解決策

### クエリ拡張によるノイズ問題

Gemini APIでクエリを拡張するとノイズが混じり、精度が低下する場合があります。  
**解決策**：拡張クエリを自動化せず、生成したクエリを精査する処理を追加することで解決しました。

### PDFデータの冗長性

免責事項や不要情報が多いとノイズが入り、精度低下につながりました。  
**解決策**：正規表現によるクリーニングとGemini APIによる要約処理で、精度を大幅に改善しました。

### ChromaDBのエラー対処

`ValueError: The truth value of an array is ambiguous` というエラーが発生。  
これは取得結果を `if` 文で直接評価していたためです。  
**解決策**：明確に `numpy.any()` や条件式を使用することで解決しました。

---

## 今回できていないが、今後意識すべきポイント

### RAGのパフォーマンスチューニング

検索の精度や速度向上のために、次回以降検討すべき点です。

- クエリキャッシュの実装
- インデックスの最適化
- 埋め込みベクトルの次元削減

今後の課題として引き続き取り組んでいきます！