この記事の対象
- RAGアプリを開発していて、本番環境での精度劣化に悩んでいる人
- 「検索は当たっているのに回答がおかしい」問題を経験したことがある人
- RAGの品質改善を感覚ではなくデータで回したい人
RAGの精度劣化は技術的な課題であると同時に、事業リスクでもある。社内ナレッジ検索で誤った情報を返せば業務判断を誤り、カスタマーサポートで事実と異なる回答をすれば顧客の信頼を失う。「なんとなく精度が悪い」を放置するコストは、思っているより大きい。
RAGの厄介さ
RAG(Retrieval-Augmented Generation)は、外部データをLLMに食わせることで回答精度を上げる手法だ。社内ドキュメント検索、カスタマーサポート、ナレッジベースQAなど、用途は広い。
仕組みとしてはシンプルに見える。
ユーザーの質問
→ ベクトル検索(関連ドキュメントを取得)
→ LLMに質問 + 検索結果を渡す
→ 回答を生成
開発時にテストケースを作って評価すると、精度90%とか出る。よし、いける。本番デプロイ。
1ヶ月後。
「最近なんか回答がおかしいんですけど」
開発時のテストケースでは通るのに、本番のリアルなクエリでは精度が落ちている。この現象は珍しくない。
そしてこの問題を「開発者の感覚」で対処しようとすると泥沼にはまる。本番のリアルなクエリを体系的に分析する仕組みがなければ、改善は運任せになる。
なぜ本番で精度が劣化するのか
原因はいくつかあるが、主なものを3つ挙げる。
1. 検索が外れている(Retrieval failure)
ユーザーの質問と、ベクトルDBに入っているドキュメントのチャンクの距離がうまく近くならないケース。
開発時は「この質問にはこのドキュメントが引っかかるはず」という前提でテストを書く。しかし本番では、想定していなかった言い回しや省略表現が飛んでくる。
想定: 「返品ポリシーについて教えてください」
実際: 「買ったやつ返せる?」
この2つは意味的には同じだが、embeddingの距離は意外と離れることがある。特に日本語はカジュアルな表現と丁寧な表現の振れ幅が大きい。
2. 検索は当たっているのに、LLMが無視する(Generation failure)
検索で正しいドキュメントを取得できているのに、LLMがそれを踏まえずに回答するパターン。これが一番タチが悪い。
原因としては:
- コンテキストウィンドウに入れたチャンクが多すぎて、重要な情報が埋もれている
- システムプロンプトの指示が弱くて、LLMが検索結果より自身の学習データを優先している
- 検索結果の順序が不適切で、関連度の低いチャンクが先頭に来ている
ログにHTTPステータスとレスポンスタイムだけ記録していても、この問題は見えない。検索結果として何が返ってきて、それに対してLLMが何を生成したのか、両方を記録して突き合わせる必要がある。
3. ドキュメントが更新されている(Data drift)
RAGが参照するドキュメントは静的ではない。社内Wikiが更新される、PDFが差し替わる、FAQが追加される。
ところがembeddingの再生成が追いついていないと、古いチャンクを引っ張ってきて古い情報で回答する。あるいは新しいドキュメントのチャンク分割が不適切で、そもそも検索に引っかからない。
トレースで何を記録すべきか
RAGの品質問題を追い込むには、パイプライン全体をトレースとして記録する必要がある。最低限必要な情報を整理する。
Retrievalフェーズ
{
"query": "返品できますか",
"query_embedding_model": "text-embedding-3-small",
"results": [
{
"chunk_id": "doc-123-chunk-7",
"source": "返品ポリシー.pdf",
"score": 0.87,
"content": "商品到着後14日以内であれば返品を受け付けます..."
},
{
"chunk_id": "doc-456-chunk-2",
"source": "FAQ.md",
"score": 0.72,
"content": "返品送料はお客様負担となります..."
}
],
"top_k": 5,
"latency_ms": 45
}
ポイントは類似度スコアを必ず記録すること。スコアが低い検索結果を掴んでいるケースを後から分析できる。
Generationフェーズ
{
"model": "gpt-4o",
"system_prompt_length": 1200,
"context_chunks": 3,
"context_total_tokens": 2800,
"prompt_tokens": 3100,
"completion_tokens": 150,
"latency_ms": 1200,
"cost_usd": 0.0095,
"response": "商品到着後14日以内であれば返品が可能です。返品送料は..."
}
評価フェーズ
ここが肝だ。検索結果と生成結果を突き合わせて、品質を定量化する。
| 評価軸 | 何を測るか | 具体例 |
|---|---|---|
| Faithfulness | 回答がコンテキストに基づいているか | コンテキストに「14日以内」とあるのに「30日以内」と回答していたら0点 |
| Relevance | 質問に対して的確に答えているか | 「返品できますか」に対して返品手順を長々説明して肝心のYes/Noがなければ低スコア |
| Hallucination | コンテキストにない情報を捏造していないか | 「全額返金保証」と回答しているがコンテキストにそんな記述がない |
これを人手で毎回やるのは非現実的なので、LLM-as-Judgeを使う。別のLLMに評価用プロンプトを投げて、0.0〜1.0のスコアをつけさせる。
あなたは回答品質の評価者です。
以下のコンテキストと回答を比較し、Faithfulness(忠実度)を0.0〜1.0で評価してください。
【コンテキスト】
{retrieved_chunks}
【ユーザーの質問】
{user_query}
【生成された回答】
{llm_response}
全リクエストに対してLLM-as-Judgeを走らせるとコストが倍になるので、サンプリングで対応する。10%のリクエストをランダムに抽出して評価するだけでも、品質の傾向は十分に見える。
トレースデータを使った品質改善サイクル
トレースを記録するだけでは意味がない。改善サイクルを回して初めて価値がある。
このサイクルをデータドリブンで回せるチームは、クライアントに対して「品質は定量的に管理しています」と胸を張って言える。受託開発では特に、この説明力が継続契約の決め手になる。
Step 1: 低スコアのトレースを抽出する
Faithfulnessが0.5以下、Hallucination Rateが0.3以上のトレースを定期的にピックアップする。これが改善対象になる。
Step 2: Retrieval failureかGeneration failureかを切り分ける
低スコアのトレースを開いて、検索結果を確認する。
- 検索結果に正解が含まれていない → Retrieval failure
- 検索結果に正解が含まれているのに回答が間違っている → Generation failure
この切り分けができるのは、トレースに検索結果と生成結果の両方を記録しているからだ。どちらか片方だけでは原因特定ができない。
Step 3: 原因に応じた対策を打つ
Retrieval failureの場合:
- チャンク分割の粒度を調整する(大きすぎると関係ない情報が混ざる、小さすぎると文脈が失われる)
- embeddingモデルを変える(日本語ならmultilingual-e5-largeなどを試す)
- HyDEやQuery Rewritingで検索クエリを補強する
- リランキングを導入する(BGE-reranker等)
Generation failureの場合:
- システムプロンプトを強化する(「検索結果に記載がない情報は回答しないでください」等)
- コンテキストに入れるチャンク数を減らす(多すぎると逆効果)
- チャンクの挿入順序を変える(関連度の高いものを先頭に)
Step 4: 改善効果を定量的に確認する
対策を入れたら、同じ評価軸でスコアの推移を見る。Faithfulnessの週次平均が0.65から0.80に上がったなら、改善が効いている。
この改善サイクルをデータドリブンで回せるかどうかは、トレース基盤があるかどうかにかかっている。
PII検出: 日本語RAGで見落としがちなリスク
RAGアプリは社内ドキュメントを扱うことが多い。ということは、プロンプトに個人情報が混入するリスクがある。
従業員名簿を検索対象に入れていると、「山田太郎さんの連絡先は?」という質問に対して、電話番号やマイナンバーがLLMに送信される可能性がある。
英語圏のPII検出ツールはSSNやクレジットカード番号の検出には対応しているが、日本語固有のPIIには弱い。
日本企業がRAGを本番運用する上で、日本語PIIの検出は避けて通れない要件だ。個人情報保護法の観点から、AI経由の情報漏洩が発覚すれば法的責任を問われるリスクがある。技術選定の段階で、日本語PII対応の有無を確認しておくべきだ。
運用上、最低限検出すべき日本語PIIパターン:
- マイナンバー(12桁の数字列)
- 銀行口座番号
- 運転免許証番号
- 保険証番号
- 旅券番号
- 電話番号(090/080/070から始まる携帯番号、+81形式)
- 住所(都道府県から始まるパターン)
これらをリアルタイムで検出して、BLOCKするのかWARNするのかをポリシーとして設定できる仕組みが要る。受託案件でクライアントの社内データを扱う場合、ここの対策が弱いと納品時の監査で確実に引っかかる。
実際のアーキテクチャ例
最後に、RAGアプリにオブザバビリティを組み込む場合の構成例を示す。
+------------------+
| オブザバビリティ |
| ダッシュボード |
+--------+---------+
|
ユーザー → アプリ → [トレースプロキシ] → LLM API (OpenAI/Anthropic/Gemini)
|
+-----+------+
| トレースDB |
| - リクエスト |
| - レスポンス |
| - 検索結果 |
| - 評価結果 |
| - PII検出 |
+------------+
プロキシ型であれば、アプリ側のコード変更はbaseURLの書き換えだけだ。RAGパイプラインの途中に入るわけではなく、LLM API呼び出しの手前で透過的にトレースを記録する。
検索結果はアプリからLLMへのリクエスト(systemメッセージやuserメッセージ)に含まれているので、プロキシ側でパースすれば取得できる。別途SDKを入れてRetrieval結果を送る方式もあるが、プロキシだけで完結するほうが導入は楽だ。
まとめ
RAGの品質改善を感覚でやっていると限界がくる。
- Retrieval failureとGeneration failureを切り分けるには、検索結果と生成結果の両方をトレースとして記録する必要がある
- LLM-as-Judgeによるサンプリング評価で品質を定量化し、改善サイクルを回す
- 日本語PII検出は英語圏のツールでは不十分で、マイナンバーや住所パターン等への対応が必要
これらの仕組みを整えることで、RAGの品質を「体感」ではなく「数値」で管理できるようになる。クライアントへの品質報告、社内でのAI投資判断、監査対応 — いずれの場面でも、データに基づいた説明ができるかどうかが信頼の分かれ目になる。
自分が開発している FujiTrace では、RAGトレースの自動記録、LLM-as-Judgeによる品質スコアリング、日本語PII検出(15パターン以上)をプロキシ型で提供している。baseURLを変えるだけで導入できるので、既存のRAGアプリにもすぐ組み込める。
技術者向けの詳細はこちら。
質問やフィードバックがあればコメントか X(@FujiTrace) で気軽にどうぞ。