📚
RAGの“出典表示”をプロンプト依存から脱却する:出典検証 v2.3.0 実装メモ
前回の記事では、「RAGの検索精度を上げるチャンク分割を実装してみた!」を作りました。
まだ読んでない人はぜひ読んでみてください 🙇♂️
👉 前回の記事
はじめに
社内ドキュメント検索を想定したRAGを作っています!
その中で、特に重要だと感じているのが以下です。
- ハルシネーションで誤情報を出したくない
そこで当初は、プロンプトで
「出典を書け」
と指示していました。
ただ、実運用に近づけるほど
「プロンプトだけでは守られないパターン」 が出てきました。
RAG全体の処理フロー
まず、このRAGシステムの全体像です。
┌─────────────────────────────────────────────────────────────────┐
│ ユーザー質問 │
│ 「有給休暇は何日もらえますか?」 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. クエリ拡張 (main.py) │
│ QueryExpander で検索キーワードを追加 │
│ → 「有給休暇は何日もらえますか? 有給 勤怠 休み」 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. ベクトル検索 (main.py → ChromaDB) │
│ 拡張クエリで類似ドキュメントを検索(上位3件) │
│ → attendance.md, expense.md, ... を取得 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. LLM回答生成 (main.py → Gemini API) │
│ 検索結果をコンテキストとしてプロンプトに含め、回答を生成 │
│ → 「入社6ヶ月後に10日付与されます。(attendance.md)」 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. 出典検証 ★v2.3.0で追加 (main.py) │
│ verify_citation_presence() で回答に出典が含まれるかチェック │
│ adjust_confidence_by_citation() で信頼度を調整 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. レスポンス返却 → フロントエンド表示 (frontend/index.html) │
│ 回答 + 出典 + 検証結果(警告があれば表示) │
└─────────────────────────────────────────────────────────────────┘
今までの実装
LLMに渡す入力
ChromaDBで検索した結果(sources)と、その内容(context)をLLMに渡して回答を生成しています。
プロンプトで「出典を必ず含めてください」と指示していました。
# 検索でヒットしたファイル名のリスト
# → この後の「出典検証」で、LLMの回答がこのリストに含まれるかチェックする
sources = ["attendance.md"]
# 検索結果の内容(LLMに渡すコンテキスト)
context = """
## attendance.md - 有給休暇
有給休暇は入社6ヶ月後に10日付与されます。
"""
# LLMへのプロンプト
prompt = f"""
以下の資料のみに基づいて回答してください。
回答には必ず参照元ファイル名を含めてください。 ← ★ここでお願いしている
{context}
質問: 有給休暇はいつから何日もらえますか?
"""
プロンプト依存のリスク
この設計だと プロンプト依存 になってしまい、次のような問題が起きます。
❌ パターン1:出典を書かない
LLMの出力
有給休暇は入社6ヶ月後に10日付与されます。
問題点
- 回答は正しいが、どの資料を見ればいいか分からない
- 人間が確認できない
- 指示しても省略されることがある
❌ パターン2:存在しないファイル名を持ち出す(捏造)
company_policy.md によると、有給休暇は入社6ヶ月後に10日付与されます。
問題点
- 実際に検索したのは
attendance.md - LLMが存在しないファイル名を生成
- 最も危険なパターン
❌ パターン3:曖昧な言及
勤怠関連の資料によると、有給休暇は入社6ヶ月後に10日付与されます。
問題点
- どの資料か特定できない
- 出典のようで出典になっていない
解決策:出典検証をコードで強制する
ここからが本題です。
LLMの回答文を解析して、出典の状態をコードで判定 します。
出典品質の定義
| citation_quality | 条件 |
|---|---|
| strong |
attendance.md のような完全一致 |
| weak |
attendance のようなベース名一致 |
| none | 出典が検出できない |
さらに 捏造検出 も行います。
- 回答に
*.mdがある - しかし
sourcesに存在しない
実装コード
1. 出典検証 verify_citation_presence()
import re
from typing import List, Optional
def verify_citation_presence(answer: str, sources: List[str]) -> Optional[dict]:
"""
回答に出典(ファイル名)が含まれるかチェック
段階判定:
1. *.md の完全一致 → citation_quality="strong"
2. ベース名の一致 → citation_quality="weak"
3. どちらもない → citation_quality="none"
追加で「出典捏造検出」も実施:
- 回答中に *.md があるのに sources にない → 警告
"""
# sources が空なら検証不可(聞き返しフローなど)
if not sources:
return None
# 回答から *.md パターンを抽出
# 例: "attendance.md", "it_support.md" などをキャプチャ
mentioned_md_files = set(re.findall(r'\b[\w_-]+\.md\b', answer))
# 期待される出典(sourcesに含まれるもの)
expected_sources = set(sources)
# 捏造された出典(回答にあるがsourcesにない)
unexpected_sources = mentioned_md_files - expected_sources
# 正しく言及された出典(*.md形式で完全一致)
mentioned_correctly = mentioned_md_files & expected_sources
# ベース名での弱い一致チェック(*.mdが見つからなかった場合のみ)
weak_matches = []
if not mentioned_correctly:
for source in sources:
base_name = source.replace(".md", "")
# 単語境界で囲まれている場合のみマッチ(誤検知軽減)
if re.search(rf'\b{re.escape(base_name)}\b', answer, re.IGNORECASE):
weak_matches.append(source)
# 判定結果
has_strong_citation = len(mentioned_correctly) > 0
has_weak_citation = len(weak_matches) > 0
has_unexpected = len(unexpected_sources) > 0
# 警告メッセージの決定(優先度順)
warning = None
if has_unexpected:
warning = f"回答に検索結果にない出典が含まれています: {', '.join(sorted(unexpected_sources))}"
elif not has_strong_citation and not has_weak_citation:
warning = "回答に出典が含まれていません"
elif not has_strong_citation and has_weak_citation:
warning = "出典の表記が不完全です(*.md形式での明示を推奨)"
return {
"has_citation": has_strong_citation or has_weak_citation,
"citation_quality": "strong" if has_strong_citation else ("weak" if has_weak_citation else "none"),
"mentioned_sources": list(mentioned_correctly) + weak_matches,
"expected_sources": list(expected_sources), # デバッグ用(検索結果の出典一覧)
"mentioned_md_files": list(mentioned_md_files), # デバッグ用(回答から抽出した*.md)
"unexpected_sources": list(unexpected_sources),
"warning": warning
}
2. 信頼度調整 adjust_confidence_by_citation()
def adjust_confidence_by_citation(confidence: str, citation_result: Optional[dict]) -> str:
"""
出典検証結果に基づいてconfidenceを調整
ルール:
1. citation_result が None → 調整なし(検証不可)
2. unexpected_sources あり(捏造検出) → "low" に固定
3. has_citation=false → 1段階ダウン(lowは下げ止まり)
4. citation_quality="weak" → 調整なし(弱い警告のみ)
"""
if citation_result is None:
return confidence
# 捏造検出: 最も深刻なので "low" に固定
if citation_result.get("unexpected_sources"):
return "low"
# 出典なし: 1段階ダウン(下げ止まり)
if not citation_result.get("has_citation"):
if confidence == "high":
return "medium"
elif confidence == "medium":
return "low"
return confidence # "low" は下げ止まり
# citation_quality="weak" は警告のみ(confidence維持)
return confidence
3. APIエンドポイントでの使用例
@app.post("/ask")
def ask_question(question: Question):
# ... 検索・回答生成 ...
# 出典検証(v2.3.0で追加)
citation_result = verify_citation_presence(answer, sources)
adjusted_confidence = adjust_confidence_by_citation(confidence, citation_result)
response_data = {
"answer": answer,
"sources": sources,
"confidence": adjusted_confidence,
"version": "2.3.0"
}
# verification フィールドを追加
if citation_result is not None:
response_data["verification"] = {
"citation_presence": citation_result
}
return response_data
判定結果の具体例
strong(正常)
{
"answer": "有給休暇は入社6ヶ月後に10日付与されます。(attendance.md)",
"confidence": "high"
}
weak(曖昧な一致)
{
"answer": "attendanceの資料によると、有給休暇は10日付与されます。",
"confidence": "high"
}
none(出典なし)
{
"answer": "有給休暇は入社6ヶ月後に10日付与されます。",
"confidence": "medium"
}
捏造検出
{
"answer": "company_policy.md によると、有給休暇は10日付与されます。",
"confidence": "low"
}
confidence 調整ルール
| 状況 | confidence |
|---|---|
| strong | 変更なし |
| weak | 変更なし(警告のみ) |
| none | 1段階ダウン |
| 捏造検出 | low に固定 |
フロントエンドUI
警告表示(XSS対策)
// 安全な実装
warningText.textContent = verification.citation_presence.warning;
LLMの出力は 信頼しない前提 で扱います。
この実装で得られた学び
1. プロンプトは「お願い」
| 方法 | 強制力 |
|---|---|
| プロンプト | ❌ |
| コード | ✅ |
2. 「禁止」より「検証」
- ❌ 嘘をつくな
- ✅ 嘘かどうか検証する
3. 社内RAGは「検証可能性」が重要
まとめ
v2.3.0 の出典検証は
思想ではなく挙動として「資料にないことは答えない」 を実現しました。
- 出典品質の判定
- confidence の自動調整
- UIでの警告表示
実運用向けRAGでは、この考え方が不可欠だと実感しました。
GitHub: rag-portfolio
最後まで読んでいただきありがとうございました!
Discussion