はじめに
近年、大規模言語モデル(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を利用)
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. テキストのクリーンアップ(正規表現)
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でテキストを要約
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を利用)
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で埋め込みベクトル生成
def get_gemini_embedding(text):
response = genai.embed_content(model="models/text-embedding-004", content=text)
return response.get("embedding", None)
6. ChromaDBへ登録
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のパフォーマンスチューニング
検索の精度や速度向上のために、次回以降検討すべき点です。
- クエリキャッシュの実装
- インデックスの最適化
- 埋め込みベクトルの次元削減
今後の課題として引き続き取り組んでいきます!