この記事で分かること
- RAGが本番で破綻する原因の80%がチャンキングに起因する理由
- 固定長・セマンティック・親子チャンキングの使い分け
- データ品質がRAGの出力品質を決定づける数学的根拠
- RAGASによる品質監視の導入方法
- 本番運用で実際に効いたチップス5選
はじめに
「デモでは動く。ノートブックでは動く。本番で動かない。」
RAGシステムを本番投入した経験がある方なら、この痛みに心当たりがあるのではないでしょうか。2024〜2025年にかけて、Agentic RAGプロジェクトの多くが本番環境で期待通りの精度を出せませんでした。
原因は、検索アルゴリズムでもLLMの性能でもありません。ベクトルDBに投入するデータの品質——とりわけチャンキング設計に問題があるケースが大半です。
この記事では、筆者が実際に運用しているRAGシステムの知見と、業界で報告されている失敗パターンを元に、本番で破綻しないための実践的なチップスを紹介します。
前提知識
RAG(Retrieval-Augmented Generation) とは、LLMが学習していない情報(社内文書など)をベクトルDBから検索し、検索結果をコンテキストとしてLLMに渡すことで、正確な回答を生成するシステムです。
処理フローは大きく2つに分かれます。
チャンクとは、長い文書を検索に適した小さな単位に分割したものです。本の索引をイメージしてください。1冊まるごとでは「どこに何が書いてあるか」がぼやけますが、章や節に分けることで、探している情報にピンポイントでたどり着けます。
なぜチャンキングがRAGの生死を分けるのか
RAGシステムの出力品質は、以下の式で表せます。
Q_output = f(Q_retrieval, Q_generation, Q_data)
業界はQ_retrieval(検索パイプライン)とQ_generation(生成モデル)の最適化に注力していますが、Q_data(入力データの品質)はほとんど無視されています。
しかし数学的に、入力データの品質がゼロに近づけば、検索や生成がどれだけ優秀でも出力品質はゼロに近づきます。Garbage In, Garbage Outです。
そしてデータ品質の中でも、チャンキングの判断が最も影響が大きいことが分かっています。
| チャンキング手法 | Faithfulness(忠実度) |
|---|---|
| 固定サイズ(Naive) | 0.47〜0.51 |
| セマンティック | 0.79〜0.82 |
固定サイズチャンキングでは、概念の途中で文章が切断され、文脈が失われた断片がLLMに渡されます。その結果、回答の忠実度がほぼコイントス並みに落ちるのです。
3つのチャンキング手法と使い分け
1. 固定長チャンキング
指定した文字数やトークン数で機械的に分割する最もシンプルな手法です。
def fixed_chunk(text: str, chunk_size: int = 512, overlap: int = 50) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap # オーバーラップで文脈を維持
return chunks
使いどころ: 構造が均一でシンプルなテキスト(ログデータなど)
注意: チャンクサイズは最低256トークンを確保してください。128トークンでは概念が途中で切断され、ハルシネーション率が上昇します。分析用途では1024トークンが推奨です。
2. セマンティックチャンキング
文の意味的な区切り(段落・見出し・話題の転換点)で分割する手法です。
def semantic_chunk_markdown(text: str, max_size: int = 500, overlap: int = 50) -> list[dict]:
"""Markdown見出しをセマンティックな境界として利用する例"""
import re
sections = re.split(r'(?=^#{1,3}\s)', text, flags=re.MULTILINE)
chunks = []
for section in sections:
if len(section) > max_size:
# 大きすぎるセクションは段落単位でさらに分割
paragraphs = section.split('\n\n')
buffer = ""
for para in paragraphs:
if len(buffer) + len(para) > max_size:
if buffer:
chunks.append(buffer.strip())
buffer = para
else:
buffer += "\n\n" + para
if buffer.strip():
chunks.append(buffer.strip())
elif len(section.strip()) >= 50: # 短すぎるセクションは除外
chunks.append(section.strip())
return chunks
使いどころ: 技術ドキュメント、マニュアル、記事など構造化された文書
ポイント: 「1内容1チャンク」を意識すると、検索精度が大幅に向上します。
3. 親子チャンキング
小さなチャンク(子)で高精度に検索し、見つかったら親となる大きな文脈を返す手法です。
使いどころ: 文脈依存度が高い文書(法律文書、仕様書など)
データ品質管理:本番で効く5つのチップス
チップス1: メタデータを必ず付与する
チャンクにメタデータを付けることで、検索のフィルタリングや権限管理が可能になります。
{
"text": "チャンク本文...",
"metadata": {
"source_file": "docs/architecture.md",
"section": "認証フローの設計",
"chunk_index": 2,
"chunk_total": 8,
"updated_at": "2026-03-01",
"access_level": "internal" # 権限情報を剥がさない!
}
}
ある企業のHR RAGアシスタントでは、ベクトルDBに取り込む際に権限メタデータが全て剥がされ、全従業員が役員報酬を閲覧できる状態になりました。ソースの権限情報は必ずメタデータとして引き継いでください。
チップス2: 「正しいが無関係な情報」を事前に除外する
ベクトルDBを汚染するノイズの大半は「間違った情報」ではなく、**「正しいが、クエリには無関係な情報」**です。ベクトル検索は意味的な距離で結果を返すため、「正しいが無関係」と「正しくて関連する」の区別が難しくなります。
対策として、投入前にドキュメントの関連性スコアリングを行い、対象ドメイン外の情報を除外しましょう。
チップス3: 重複・矛盾・古いバージョンを排除する
ドキュメントの品質は時間とともに腐敗します。同じ情報の複数バージョンがベクトルDBに共存すると、検索結果に矛盾したチャンクが混在し、LLMが誤った合成を行います。
def deduplicate_chunks(chunks: list[dict], similarity_threshold: float = 0.95) -> list[dict]:
"""コサイン類似度が閾値以上のチャンクを重複とみなして除外"""
unique = []
for chunk in chunks:
is_duplicate = False
for existing in unique:
sim = cosine_similarity(chunk["embedding"], existing["embedding"])
if sim >= similarity_threshold:
# 新しい方を残す
if chunk["metadata"]["updated_at"] > existing["metadata"]["updated_at"]:
unique.remove(existing)
unique.append(chunk)
is_duplicate = True
break
if not is_duplicate:
unique.append(chunk)
return unique
チップス4: 検索品質を定量的に監視する
本番環境では、レイテンシーだけでなく検索品質そのものを監視してください。データ量が増えるとANN(近似最近傍探索)のリコールが静かに低下します。あるシステムでは、1万→1億ドキュメントへのスケール時にリコールが0.95→0.71に落ちましたが、レスポンスは速いままだったため誰も気づきませんでした。
RAGASは、RAGパイプラインの出力を定量的に評価するためのライブラリです。
| メトリクス | 概要 |
|---|---|
| Faithfulness | 回答がコンテキストに忠実かどうか |
| Context Recall | 正解に必要な情報がコンテキストに含まれていたか |
| Factual Correctness | 回答と正解の事実一致度 |
from ragas.metrics import Faithfulness, LLMContextRecall, FactualCorrectness
from ragas import evaluate
metrics = [Faithfulness(), LLMContextRecall(), FactualCorrectness()]
result = evaluate(
dataset=eval_dataset,
metrics=metrics,
llm=evaluator_llm,
)
# result.scores で各メトリクスのスコアを確認
定期的な評価パイプラインを構築し、スコアが閾値を下回ったらアラートを出す仕組みにしておきましょう。
チップス5: 不要なクエリをRAGパイプラインに流さない
ある住宅ローンアシスタントでは、月間コスト$45,000のうち70%が検索不要な単純な事実確認クエリに消費されていました。クエリの分類レイヤーを手前に設け、RAGが不要な質問はLLM単体で回答させることで、コストと精度の両方を改善できます。
よくある落とし穴
| 落とし穴 | 症状 | 対策 |
|---|---|---|
| チャンクが小さすぎる | 文脈が失われ、断片的な回答になる | 最低256トークン。分析用途は1024トークン |
| メタデータの欠落 | 権限バイパス、フィルタリング不能 | ソースの権限・日時・出典を必ず引き継ぐ |
| 重複ドキュメントの放置 | 矛盾した検索結果 → ハルシネーション | 定期的な重複排除パイプラインを構築 |
| レイテンシーだけ監視 | 「速く間違った答え」に気づかない | FaithfulnessやRecallのスコアを継続監視 |
| 全クエリにRAGを適用 | コスト爆発、不要な検索ノイズ | クエリ分類で検索不要なものを振り分け |
まとめ
RAGの本番品質は、LLMの選定やプロンプトの工夫よりも前段階のデータ品質で決まります。
- チャンキング設計を最優先に考える — セマンティックな境界で分割し、最低256トークンを確保する
- メタデータを付与する — 権限・出典・日時は必ず引き継ぐ
- データ品質を継続的に管理する — 重複排除、陳腐化チェック、ノイズ除去のパイプラインを構築する
- 検索品質を定量的に監視する — RAGASなどで定期的に評価し、劣化を早期に検知する
- クエリを賢く分類する — 全てのクエリにRAGを適用しない
「デモでは動く」を「本番でも動く」に変えるのは、派手な技術革新ではなく、地道なデータ品質管理です。