🛡️

実務RAGのリスクと信頼性を整理してみる — TrustworthyなRAGを設計するには

に公開

はじめに

ルミナイR&Dチームの栗原です。
ここまでの記事では、

  • Self-RAG / CRAG / RAPTOR / FLARE などの「精度やハルシネーションを改善する」手法
  • RGB / RAGTruth を使った RAG 評価・ハルシネーション検出 の話

https://zenn.dev/lluminai_tech/articles/f408b63083e09f

https://zenn.dev/lluminai_tech/articles/26504e8bfcbdc6

を見てきました。

ここまでの議論は、主に

どれだけ正しく答えられるか
どれだけハルシネーションを減らせるか

という軸に寄っていました。

しかし、実際に RAG をプロダクションに載せようとすると、
もう少し広い意味での「信頼できるか?」が問題になってきます。

  • ユーザーの入力や社内情報は、どこに保存され、誰が見られるのか
  • 敵対的なプロンプトやデータ汚染で、誤誘導されないか
  • 特定の属性に対して、露骨に不利な出力をしていないか
  • なにか事故が起きたとき、「どのコンポーネントがやらかしたか」を追跡できるか

こういった論点を 「Trustworthy RAG(信頼できる RAG)」の枠組みで整理したサーベイ が、
Ni et al. による “Towards Trustworthy Retrieval Augmented Generation for Large Language Models: A Survey” です。

https://arxiv.org/abs/2502.06872

この論文は、RAG の信頼性を

  • Reliability(信頼性)
  • Privacy(プライバシ)
  • Safety(安全性)
  • Fairness(公平性)
  • Explainability(説明可能性)
  • Accountability(説明責任)

という 6 つの観点で整理し、それぞれに対して

  • どんなリスクがあるか
  • どんな研究・対策が出てきているか
  • 何を評価すればよいか

を俯瞰しています。

本記事では、このサーベイを手がかりに

  • Trustworthy RAG の 6 つの観点の概要
  • 現場で特に優先度が高い Reliability / Privacy / Safety の論点
  • それらを意識した「ミニマム運用」をイメージできるような
    メタデータ付き RAG 実装例(Python)

をコンパクトに整理します。

この記事で学べること

  • Ni et al. (2025) が整理する Trustworthy RAG の 6 観点 の全体像
  • 特に現場で効きやすい Reliability / Privacy / Safety の代表的なリスクと対策イメージ
  • 評価・監査を想定した ログ&メタデータ付き RAG 実装のサンプル

1. なぜ「Trustworthy RAG」を考える必要があるのか

Ni らは、RAG が ハルシネーション・知識の陳腐化・説明可能性の不足 といった LLM の課題を緩和する一方で、
RAG 特有の新たなリスク も生み出すと指摘します。

代表的なものだけ挙げると、

  • Robustness / Reliability の問題
    • 検索結果がノイズだらけ・古い・対立している場合に、変な結論を出す
    • データ汚染(poisoned passages)で、意図的に誤誘導される
  • Privacy の問題
    • 検索クエリ・ログ・ベクタ DB に、個人情報や機密情報がそのまま載る
    • 外部の LLM API に、検査されていない情報をそのまま投げてしまう
  • Safety の問題
    • retrieval を通じて有害コンテンツ(ヘイト・自傷・違法情報など)が流れ込む
    • 「RAG を挟んでいるから安全そう」と誤信して、フィルタを弱めてしまう
  • Fairness / Bias の問題
    • retriever のランキングが、特定の属性に関する情報だけ偏って拾う
    • generation 側で、retrieval した情報を増幅して偏った回答を出す
  • Explainability / Accountability の問題
    • 間違ったときに、「retriever の問題か」「reranker の問題か」「LLM の推論の問題か」が分からない
    • どの文書に基づいて判断したかを後から追跡できない

つまり、RAG を「精度と UX を上げるためのギミック」としてだけ見るのでは足りず、

“どのような前提で・どこまで信用して・どのように運用するか”

まで含めて設計しよう、というのが Trustworthy RAG の発想です。

2. Trustworthy RAG の 6 つの観点

Ni らは、先行研究を整理したうえで、
Trustworthy RAG を以下の 6 観点で構造化しています。

  1. Reliability(信頼性)
  2. Privacy(プライバシ)
  3. Safety(安全性)
  4. Fairness(公平性)
  5. Explainability(説明可能性)
  6. Accountability(説明責任)

それぞれ、RAG システムの

  • どの部分 に関わる信頼性なのか
  • どんな失敗モード があるのか
  • どんな対策・評価軸 が提案されているのか

を整理しています。

本記事では、特に現場で優先度が高いと思われる

  • Reliability
  • Privacy
  • Safety

の 3 つに絞って見ていきます。

3. Reliability:信頼性とロバストネス

3.1 何に対する「信頼性」か

RAG の Reliability は主に次の軸で整理できます。

  • Uncertainty(不確実性)
    • 検索結果が本当にクエリと関係しているか
    • 取得した文書が矛盾していないか・情報が足りているか
  • Robust Generalization(ロバストな汎化)
    • クエリノイズ(typo・言い換え)やドメインシフトに対して、性能がどれくらい落ちるか
    • 敵対的な攻撃(retrieval poisoning など)への耐性

たとえば、

  • retriever が「似ているけれど別物」の文書を引いてきた
  • 古い仕様書と新しい仕様書が混ざっていて、LLM が古い方を採用した
  • 敵対的に埋め込まれたパッセージに引きずられて、悪意のある回答を出した

といったケースは、Reliability の範囲に入ります。

3.2 関連研究

サーベイでは、Reliability の文脈で次のような方向性の研究が紹介されています。

  • 不確実性推定
    • retriever や LLM の出力に対して、信頼度スコア(confidence / calibration)を付与する
    • 信頼度が低いときは「分からない」や追加の検索を選択する
  • ロバスト訓練
    • ノイズ・typo・敵対的パッセージを混ぜたコーパスで retriever を訓練する
    • RAG 全体を「ノイズ入りデータに対しても壊れにくいように」訓練する
  • 攻撃・防御研究
    • retrieval poisoning / prompt injection / jailbreak などに対する攻撃手法と防御手法の整理

7 本目で扱った RGB(4 能力ベンチマーク)や
RAGTruth(ハルシネーションコーパス)は、主に Reliability を評価するための道具と見ることができます。

4. Privacy:プライバシとデータ漏洩

4.1 RAG のどこで情報が漏れるのか

RAG 特有のプライバシリスクとして、主に次の 2 つが挙げられます。

  1. 外部 retrieval データベースからの漏洩
    • ベクタ DB や検索インデックスに、個人情報・機密情報が含まれる
    • membership inference attack などにより、「この人のデータが含まれているか」を推定される
  2. LLM の訓練データからの漏洩
    • retrieval した文書を使って LLM を fine-tune した結果、
    • 元の文書をほぼそのまま復元できてしまう(model extraction / training data leakage)

さらに、実務では次のような経路も問題になります。

  • ユーザーのクエリや回答が、ログとして長期保存される
  • 社外の LLM API に、生データをそのまま送ってしまう

4.2 実務で考えたい対策

詳しいプライバシ技術(DP, HE, MPC など)に踏み込む前に、
まずは設計レベルで押さえておきたいポイントを挙げると:

  • データの分離
    • 匿名化したコーパスと、個人識別子を含むデータベースを分離する
    • 個別ユーザーのデータは tenant ごとのインデックスに閉じ込める
  • クエリ/コンテキストのマスキング
    • 電話番号・メール・住所など典型的なパターンは、自動マスキングしてログする
    • LLM に送る前に、不要な識別情報を削るフィルタを挟む
  • ログポリシー
    • どのレイヤーで何をログするかを設計して、保存期間とアクセス権限を決めておく
    • 「後で評価したいから全部残す」ではなく、
      評価や監査に必要な最小限の情報+匿名化をセットで考える

サーベイ本体では、これに加えてプライバシ攻撃・防御の詳細な整理がありますが、
実務導入の第一歩としては、こうした「システム構成としての防御」を固めるのが現実的なスタート地点です。

5. Safety:有害出力と攻撃耐性

5.1 RAG だからこそ起きる安全性の問題

Safety の観点では、主に次のような問題が挙げられます。

  • retrieval 経由での有害コンテンツ流入
    • ヘイトスピーチ・差別的表現・違法情報などがコーパスに含まれている場合、
      retrieval がそれを拾って LLM が拡張・増幅してしまう
  • RAG 固有の攻撃
    • Retrieval corpus poisoning:
      コーパスに悪意あるパッセージを混ぜ、特定のクエリでそれが引かれるようにする
    • LangChain / RAG 構成を利用した jailbreak:
      「retrieval レイヤー側に仕込まれたプロンプト注入」で guardrail を回避する

単純に「LLM に safety チェックをさせる」だけでなく、

  • retrieval レイヤー・コーパス側で何を許容するか
  • どの段階でフィルタリング・モニタリングを行うか

をシステムとして設計する必要があります。

5.2 実務でできる安全策の例

  • retrieval 前のクエリフィルタ
    • 明らかに NG なクエリ(自傷・犯罪の具体的な手順など)には、
      retrieval を行わず safety 応答だけを返す
  • retrieval 後のコンテンツフィルタ
    • 取得した文書に対して、LLM ベースの moderation をかける
    • 有害スコアが高い文書はコンテキストに入れない
  • 回答前の最終 moderation
    • 最終回答にも safety チェックをかける(RAG でも通常の Chat と同様に)

ここでは「retrieval / generation / logging の各レイヤーで safety を考える」という構図だけ押さえておきます。

6. ミニ実装:メタデータ付き RAG & ロギングの例

最後に、Trustworthy RAG を意識した 「最低限のログ&メタデータ付き RAG」 の例を示します。

ここでは簡単に、

  • OpenAI API を使ったシンプルな RAG
  • retriever はインメモリのベクタ検索(埋め込みは OpenAI)
  • 各リクエストに対して
    • trace_id
    • マスク済みのユーザークエリ
    • 取得した文書の ID / スコア
    • 最終回答
  • を JSON ログとして書き出す

…という構成にします。

6.1 前提となるサンプルコーパス

DOCS = [
    {
        "id": "policies_001",
        "title": "サービスX 無料プランの制限",
        "text": "サービスXの無料プランでは、ユーザー数は3名まで、ストレージは5GBまで利用できます。"
    },
    {
        "id": "policies_002",
        "title": "サービスX 有料プランの支払い方法",
        "text": "有料プランは月額課金のみ対応しており、現在は年払いオプションを提供していません。"
    },
    {
        "id": "safety_001",
        "title": "有害な利用への対応ポリシー",
        "text": "本サービスは、差別的発言や違法行為を助長する利用を禁止しています。"
    },
]

6.2 ログとマスキングを意識した RAG 最小実装

import json
import os
import re
import uuid
from pathlib import Path
from typing import List, Dict, Any

from openai import OpenAI
import numpy as np


# ===== 0) OpenAI クライアント =====
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

EMBED_MODEL = "text-embedding-3-small"
GEN_MODEL = "gpt-4.1-mini"  # 例:適宜変更


# ===== 1) サンプル文書 =====
DOCS = [
    {
        "id": "policies_001",
        "title": "サービスX 無料プランの制限",
        "text": "サービスXの無料プランでは、ユーザー数は3名まで、ストレージは5GBまで利用できます。"
    },
    {
        "id": "policies_002",
        "title": "サービスX 有料プランの支払い方法",
        "text": "有料プランは月額課金のみ対応しており、現在は年払いオプションを提供していません。"
    },
    {
        "id": "safety_001",
        "title": "有害な利用への対応ポリシー",
        "text": "本サービスは、差別的発言や違法行為を助長する利用を禁止しています。"
    },
]


# ===== 2) 簡易マスキング(電話・メールなど) =====
MASK_PATTERNS = [
    (re.compile(r"\b\d{2,4}-\d{2,4}-\d{3,4}\b"), "<PHONE>"),
    (re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "<EMAIL>"),
]


def mask_text(s: str) -> str:
    masked = s
    for pat, repl in MASK_PATTERNS:
        masked = pat.sub(repl, masked)
    return masked


# ===== 3) 埋め込みとインデックス構築 =====
def embed_texts(texts: List[str]) -> np.ndarray:
    resp = client.embeddings.create(model=EMBED_MODEL, input=texts)
    vecs = [d.embedding for d in resp.data]
    return np.asarray(vecs, dtype="float32")


DOC_EMBS = embed_texts([d["title"] + " " + d["text"] for d in DOCS])
DOC_EMBS_NORM = DOC_EMBS / np.linalg.norm(DOC_EMBS, axis=1, keepdims=True)


def search_docs(query: str, top_k: int = 3) -> List[Dict[str, Any]]:
    q_emb = embed_texts([query])[0]
    q_emb = q_emb / np.linalg.norm(q_emb)

    scores = DOC_EMBS_NORM @ q_emb
    idx = np.argsort(scores)[::-1][:top_k]

    results = []
    for i in idx:
        results.append({
            "id": DOCS[i]["id"],
            "title": DOCS[i]["title"],
            "text": DOCS[i]["text"],
            "score": float(scores[i]),
        })
    return results


# ===== 4) LLM へのプロンプト生成 =====
def build_prompt(user_query: str, retrieved: List[Dict[str, Any]]) -> str:
    context_lines = []
    for r in retrieved:
        context_lines.append(
            f"[{r['id']}] {r['title']}\n{r['text']}"
        )
    context_block = "\n\n".join(context_lines)

    prompt = f"""あなたは社内ナレッジベースに基づいて回答するアシスタントです。

# 質問
{user_query}

# 参照ドキュメント
{context_block}

# 回答方針
- ドキュメントに書かれていないことは推測で書かず、「分かりません」と答えてください。
- 回答中に、どのドキュメントに基づいているかを [policies_001] のようなIDで明示してください。
- 結論を最初に簡潔に述べ、そのあとに根拠を補足してください。
"""
    return prompt


# ===== 5) LLM 呼び出し =====
def generate_answer(prompt: str) -> str:
    resp = client.responses.create(
        model=GEN_MODEL,
        input=[
            {"role": "system", "content": "あなたは慎重で根拠重視のアシスタントです。"},
            {"role": "user", "content": prompt},
        ],
        max_output_tokens=600,
        temperature=0.2,
    )
    return resp.output_text


# ===== 6) ログ保存 =====
LOG_PATH = Path("rag_trustworthy_logs.jsonl")


def log_request(record: Dict[str, Any]) -> None:
    line = json.dumps(record, ensure_ascii=False)
    with LOG_PATH.open("a", encoding="utf-8") as f:
        f.write(line + "\n")


# ===== 7) 1 リクエスト分のフロー =====
def answer_with_trustworthy_rag(user_query: str) -> str:
    trace_id = str(uuid.uuid4())

    # ログ用にマスクしたクエリを作成
    masked_query_for_log = mask_text(user_query)

    # 1) 検索
    retrieved = search_docs(user_query, top_k=3)

    # 2) プロンプト生成
    prompt = build_prompt(user_query, retrieved)

    # 3) LLM で回答生成
    answer = generate_answer(prompt)

    # 4) ログレコード作成
    log_record = {
        "trace_id": trace_id,
        "user_query_masked": masked_query_for_log,
        "retrieved": [
            {
                "id": r["id"],
                "score": r["score"],
                "title": r["title"],
            }
            for r in retrieved
        ],
        "answer": answer,
    }
    log_request(log_record)

    return answer


if __name__ == "__main__":
    q = "サービスXの無料プランの制限と、有料プランで年払いができるかを教えてください。"
    ans = answer_with_trustworthy_rag(q)
    print("=== Answer ===")
    print(ans)

6.3 実行結果のイメージ

上のコードを実行すると、コンソールとログファイルにそれぞれ次のような情報が残ります。

ターミナル出力(回答例)

=== Answer ===
無料プランではユーザー数とストレージ容量に制限があり、有料プランでは現在年払いは利用できません。[policies_001][policies_002]

まず、無料プランについては、ユーザー数は3名まで、ストレージは5GBまで利用できると定められています。[policies_001]
一方、有料プランの支払い方法については、月額課金のみ対応しており、年払いオプションは提供されていないと明記されています。[policies_002]

したがって、無料プランにはユーザー数とストレージの制限があり、有料プランでの年払いは現時点では用意されていないと結論づけられます。[policies_001][policies_002]

ログファイル rag_trustworthy_logs.jsonl の例

{
  "trace_id": "c9b8f4de-7c43-4e23-9a3a-0a7b7d5f4e12",
  "user_query_masked": "サービスXの無料プランの制限と、有料プランで年払いができるかを教えてください。",
  "retrieved": [
    {
      "id": "policies_001",
      "score": 0.8123458624,
      "title": "サービスX 無料プランの制限"
    },
    {
      "id": "policies_002",
      "score": 0.7932101488,
      "title": "サービスX 有料プランの支払い方法"
    },
    {
      "id": "safety_001",
      "score": 0.4123455286,
      "title": "有害な利用への対応ポリシー"
    }
  ],
  "answer": "無料プランではユーザー数とストレージ容量に制限があり、有料プランでは現在年払いは利用できません。[policies_001][policies_002]\\n\\nまず、無料プランについては、ユーザー数は3名まで、ストレージは5GBまで利用できると定められています。[policies_001]\\n一方、有料プランの支払い方法については、月額課金のみ対応しており、年払いオプションは提供されていないと明記されています。[policies_002]\\n\\nしたがって、無料プランにはユーザー数とストレージの制限があり、有料プランでの年払いは現時点では用意されていないと結論づけられます。[policies_001][policies_002]"
}

この程度でも、

  • どの質問に対して
  • どの文書が、どのスコアで使われたか
  • 回答がどのドキュメント ID を引用しているか

が一箇所にまとまるので、

  • 後から「この回答はどこから来たのか」を検証する
  • RGB / RAGTruth 風の評価用に「入力・コンテキスト・出力」を再利用する
  • 事故発生時に、どのリクエスト・どの retriever の振る舞いが原因かをたどる

といった用途にすぐ回せます。

7. まとめ

本記事では、Ni et al. の Trustworthy RAG サーベイ をベースに、

  • Trustworthy RAG の 6 観点(Reliability / Privacy / Safety / Fairness / Explainability / Accountability)
  • 特に現場で意識したい Reliability / Privacy / Safety の代表的なリスクと対策イメージ
  • ログ&メタデータ付きの ミニマル RAG 実装例

をざっと見ました。

ここまでのシリーズをざっくり整理すると:

  • Gao et al. の RAG サーベイ
    → 「RAG の全体像と設計パターン」
  • Self-RAG / CRAG / RAPTOR / FLARE など
    → 「どう賢くするか・ハルシネーションを抑えるか」
  • RGB / RAGTruth
    → 「どこがどれくらい改善されたかをどう測るか」
  • Ni et al. の Trustworthy RAG
    → 「そもそも、どんな意味で“信頼できる”状態を目指すのか」

という対応関係になります。


【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

参考文献

  • Bo Ni, Zheyuan Liu, Leyao Wang, et al.
    “Towards Trustworthy Retrieval Augmented Generation for Large Language Models: A Survey.”
    arXiv:2502.06872, 2025.
    https://arxiv.org/abs/2502.06872

  • Yunfan Gao, Yun Xiong, Xinyu Gao, et al.
    “Retrieval-Augmented Generation for Large Language Models: A Survey.”
    arXiv:2312.10997, 2023.
    https://arxiv.org/abs/2312.10997

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion