RAGパイプラインのハルシネーションを止める:15行の修正(公開)

Dev.to / 2026/5/2

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • この記事は、RAGシステムは取得した文書を引用しているように見えても、引用された本文が実際には主張を裏付けていないためにハルシネーションが起こり得ると説明しています。
  • よくある本番での失敗パターンとして、引用下での捏造、無関係なチャンク同士の事実の融合、文書が支持しない領域への確信的な外挿を挙げています。
  • BLEU/ROUGE/BERTScoreのような評価や、同じLLMに基づく「忠実性」評価では見逃されやすいと主張しています。
  • そこで、Retrieve → Generate → Verifyのパターンを提案し、別の検証器が最終回答から主張を抽出して各主張を複数の独立した情報源で照合し、エージェントが行動する前に判定します。
  • チュートリアルでは、この検証ステップを約15行のPythonで動く実装として提供しています。

RAGパイプラインは実在するドキュメントを取得します。それでも幻覚(ハルシネーション)します。エージェントが動く前にそれを捕まえる「retrieve → generate → verify」パターンと、今すぐ実行できる動作するPythonコードを紹介します。

RAGパイプラインは実在する3つのドキュメントを取得します。LLMはそれらを読み込みます。そして、その正確な出典(ソース)を引用する応答を生成します。見た目はきれいです。

しかし、それでも約8〜15%の確率で間違えます。

すでにRAGを本番環境にデプロイしているなら、これはご存じでしょう。回答は取得したチャンクに裏付けられているように見えますが、よく読むとモデルが日付を捏造したり、名前をすり替えたり、数値を誇張したり、2つの無関係な事実を1つのもっともらしい文章に融合させたりしています。引用は実在のドキュメントを指しています。引用が裏付けているはずの文は、実際にはそれらのドキュメント内に存在しません。

この種の幻覚は、最も見つけにくい分類です。幻覚に見えません。正しい回答に見えます。

このチュートリアルでは、RAGパイプラインに約15行のPythonで検証ステップを追加する方法を示します。検証(verifier)は、あなたの取得スタックや生成モデルとは独立して動作します。最終出力を読み取り、個々の主張(claim)を抽出し、それぞれを4つの独立したソースに対してチェックし、エージェントが動く前に判定(verdict)を返します。

なぜRAGの幻覚は別物なのか

典型的なLLMの幻覚: モデルに、答えを知らない質問を投げると、モデルがそれを捏造することです。

RAGの幻覚: モデルはコンテキストウィンドウ内に正しい文脈を持っているのに、その文脈で裏付けられていない内容の主張を生成してしまいます。本番環境で私が見ている主な3つの失敗パターン:

  1. 引用のもとでの捏造。応答はソース[2]を引用しますが、その引用がソース[2]に帰属させている主張は実際にはそこにありません。引用は存在します。根拠(grounding)はありません。
  2. 事実の融合。取得した2つの異なるチャンクにある無関係な2つの事実が、1つの文に結合されます。前半と後半はそれぞれ正しいのに、結合された文は誤りです。
  3. 自信に満ちた外挿。モデルは、ドキュメントに書かれている内容から、ドキュメントが支えていない関連する主張を外挿し、それを検証済みの部分と同じ信頼度で提示します。

これら3つは、取得品質(retrieval-quality)の指標をすべてすり抜けます。BLEU、ROUGE、BERTScoreでも生き残ります。同じLLMで「faithfulness(忠実性)」の評価が実行される場合も、それをすり抜けます。

唯一信頼できる捕まえ方は、2回目の独立した検証パス(別モデル、別の証拠ソース、別のプロンプト)を行うことです。最終出力を読み取り、各主張をオープンウェブに対してスコア付けします。

retrieve → generate → verify のパターン

標準的なRAGは2つの段階です:

query → retrieve → generate → return

そこに1つの段階を追加します:

query → retrieve → generate → verify → return

検証(verify)段階では、生成された応答を個々の原子的な主張(atomic claims)に分解し、それぞれをチェックして、主張ごとの判定に加えて、全体として act / verify / reject の推奨を返します。あなたのアプリケーションは、rejectに対して何をするかを決めます。たとえば、悪い主張をユーザーに提示する、より厳しい制約で再生成する、安全な応答にフォールバックする、または処理を中止する、といった対応です。

インストール

単純なプログラム的なケース(このチュートリアルの大部分)では、依存関係は requests のみです:

pip install requests

LangChainツール統合をすべて行う場合:

pip install langchain-agentoracle

APIキーは不要です。設定も不要です。無料の /preview エンドポイントでは、テスト用に1時間あたり10回の検証が可能です。本番の /evaluate エンドポイントは、Base上でx402経由の1呼び出しあたり$0.01です。

最小のRAGパイプライン(幻覚するもの)

まず、意図的に脆弱なRAGパイプラインを作りましょう。OpenAIに関する小さなインメモリのコーパスを使うので、幻覚は見つけやすいです:

from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage

# 実在する3つのドキュメント — これが「取得されたコンテキスト」です
corpus = {
    "doc_1": """OpenAIは、非営利の
    研究組織として2015年12月に設立されました。共同創業者には、Sam Altman、Elon Musk、
    Ilya Sutskever、そしてGreg Brockmanなどが含まれています.""",
    "doc_2": """ChatGPTはOpenAIによって2022年11月30日にリリースされました。
    2023年1月までに月間アクティブユーザー数1億人に到達し、
    当時の歴史上最速で成長した消費者向けアプリケーションとなりました.""",
    "doc_3": """OpenAIはMicrosoftから大規模な投資を受けており、
    2023年1月に発表された、複数年にわたる数十億ドル規模のコミットメントを含みます."""
}

def retrieve(query):
    # おもちゃのリトリーバ — 本番ではベクタDBを使います
    return [corpus["doc_1"], corpus["doc_2"], corpus["doc_3"]]

返却形式: {"translated": "翻訳されたHTML"}def generate(query, docs):
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
    context = "

".join(docs)
    response = llm.invoke([
        SystemMessage(content=f"このコンテキストからの回答:
{context}"),
        HumanMessage(content=query)
    ])
    return response.content

answer = generate(
    "OpenAIを創設したのは誰で、ChatGPTはいつリリースされ、そしてどれくらいの速さで成長したのですか?",
    retrieve("OpenAIの創設とChatGPTの成長")
)
print(answer)

これを何回か実行してください。ある実行では、きれいな回答が得られます。別の実行では、ドキュメントに載っていない共同創設者を捏造した回答が返ってきたり、ChatGPTが2か月で10億ユーザーに到達したと主張したり、Microsoftへの投資額の誤った数値を割り当てたりすることがあります。同じ検索(retrieval)で、同じプロンプトでも、実行ごとにハルシネーション(作り話)の傾向が異なります。

これは、検証レイヤーがまさに想定して作られている状況です。

15行で検証を追加

import requests

def verify(text):
    r = requests.post(
        "https://agentoracle.co/evaluate",
        json={"content": text},
        timeout=30,
    )
    return r.json()["evaluation"]

def retrieve_generate_verify(query):
    docs = retrieve(query)
    draft = generate(query, docs)

    verdict = verify(draft)

    refuted = [c["claim"] for c in verdict["claims"] if c["verdict"] = "refuted"]
    unverifiable = [c["claim"] for c in verdict["claims"] if c["verdict"] = "unverifiable"]

    if refuted:
        return {"answer": None, "reason": "refuted", "claims": refuted}
    if verdict["recommendation"] = "reject" and unverifiable:
        return {"answer": None, "reason": "unverifiable", "claims": unverifiable}
    return {

返却形式: {"translated": "翻訳されたHTML"}"answer": draft, "confidence": verdict["overall_confidence"]}

result = retrieve_generate_verify(
    "Who founded OpenAI and how fast did ChatGPT grow?"
)
print(result)

以上で統合は完了です。あなたのエージェントが draft に基づいて行動する前に、verify(draft) は原子的な主張(atomic claims)を抽出し、4つの独立した検証ソースのそれぞれに対して確認し、構造化された判定(verdict)を返します。

LangChain ユーザー向け: 関数呼び出しではなく、エージェントのループから呼び出せるツールとして検証子(verifier)を使いたい場合は、from langchain_agentoracle import AgentOracleEvaluateTool を使ってください。これは、LLM が消費しやすい形式で整形されたテキストを返します。上記の素の HTTP 呼び出しは、アプリケーションロジック(ゲーティング、分岐、修復)に JSON が必要なときに使うべきものです。

実際の検証実行がどのようなものか

ここでは、意図的にハルシネーションを含む RAG 応答を AgentOracle に投入したときの実際の出力を示します。入力テキストは次のとおりです:

"OpenAI was founded in 2015 by Sam Altman, Elon Musk, and Mark Zuckerberg. The company released ChatGPT in 2022, which reached 1 billion users within 2 months."

そのうち4つの事実は正しいです。2つはハルシネーションです。Mark Zuckerberg は OpenAI の共同創業者ではなく、また ChatGPT は2か月で 10億ユーザーに到達したのではなく、1億ユーザーです。

検証子の応答(読みやすさのために一部省略):

{
  "recommendation": "reject",
  "overall_confidence": 0.47,
  "total_claims": 6,
  "verified_claims": 4,
  "refuted_claims": 2,
  "claims": [
    {
      "claim": "OpenAI was founded in 2015",
      "verdict": "supported",
      "confidence": 0.83
    },
    {
      "claim": "OpenAI was founded by Sam Altman",
      "verdict": "supported",
      "confidence": 1.0
    },
    {
      "claim": "OpenAI was founded by Elon Musk",
      "verdict": "supported",
      "confidence": 1.0
    },
    {
      "claim": "OpenAI was founded by Mark Zuckerberg",
      "verdict": "refuted",
      "confidence": 0.75,
      "evidence": "No search results mention Mark Zuckerberg as a founder; founders listed include Sam Altman, Elon Musk, Ilya Sutskever, Greg Brockman."
    },
    {
      "claim": "OpenAI released ChatGPT in 2022",
      "verdict": "supported",
      "confidence": 0.95
    },
    {
      "claim": "ChatGPT reached 1 billion users within 2 months",
      "verdict": "refuted",
      "confidence": 0.48,
      "evidence": "ChatGPT reached 100 million users in 2 months (Jan 2023), not 1 billion. 1 billion milestone was later."
    }
  ]
}

2つのハルシネーションを検知しました。4つの真の主張は確認されました。下流のエージェントのアクションをショートサーキットする reject の提案が1件あります。

返却形式: {"translated": "翻訳されたHTML"}

検証者がやらないことに注目してください。つまり、取得したドキュメントに対して回答を採点しません。そうしたRAG固有の評価でそれを行うものは、毎回「引用のもとでの捏造(fabrication-under-citation)」や「事実の融合(fact-fusion)」を見逃します。代わりに、検証者は生成された主張を独立したステートメントとして扱い、4つの独立した情報源を通じて公開Webと照合します。取得されたドキュメントは、パイプラインの次のステップ次第でしか良くなりません。そして次のステップはLLMであり、それらをすでに持っているのに、それでもハルシネーションしています。

When To Use Each Recommendation

検証者は、3つのトップレベル推奨のいずれかを返し、さらに各主張ごとの判定を、より豊かな4方向の空間から返します。

トップレベルの推奨:

推奨 おおよその確信度レンジ あなたのエージェントは何をすべきか
act ≥ 0.80 進める。主張は情報源をまたいで十分に裏付けられています。
verify 0.50 – 0.80 ソフトパス。確信度を下げた主張をログに残す。重要度の高いアクションについてはhuman-in-the-loopを検討してください。
reject < 0.50、または否定された(refuted)任意の主張 そのままの応答を実行しないでください。

主張ごとの判定:

判定 意味 推奨アクション
supported 複数の情報源がその主張を裏付けています。 信頼する。
refuted 証拠がその主張を直接的に否定しています。 常にブロックする — これはハルシネーションです。
unverifiable 裏付ける、または否定する証拠が見つかりませんでした。 ソフトフラグとして扱い、ハード失敗(hard fail)にはしない。多くの場合、その主張が公開Webに対して「具体的すぎる」「時期が新しすぎる」「不明瞭すぎる」ことを意味します。「false」と同じではありません。

よくある本番のミスは、unverifiablerefutedと同じ扱いにすることです。違います。下書きが複数のunverifiableの主張による低い全体確信度だけで、純粋にrejectの推奨を受け取ることがあります。実際には何も間違っていないのにです。実行を決める前に、verdict["refuted_claims"]を別途確認してください — 上のコードはそれを行っています。

Handling The Three RAG Failure Modes

この投稿の冒頭で挙げた3つの失敗モード――「引用のもとでの捏造(fabrication-under-citation)」「事実の融合(fact-fusion)」「自信満々の推測(confident-extrapolation)」はいずれも、同じパターンで検出されます。理由は次の通りです:

引用のもとでの捏造。検証者は応答を原子的な主張に分解し、それぞれを公開Webに対して確認します。引用されている情報源自体は検証者にとって無関係です。重要なのは、その主張そのものが裏付けられているかどうかです。応答が「情報源[2]は47%の売上成長を報告している」と言っていて、情報源[2]が実際には4.7%を報告しているなら、47%の主張は引用とは無関係に独立してrefutedになります。

事実の融合。各原子的な主張は独立して検証されます。応答が「AppleのQ4の売上は1200億ドルだった」(真)と「3月3日に発表された」(別の商品に対して真)を「Appleの1200億ドルのQ4売上は3月3日に発表された」(偽)に融合してしまった場合、融合された主張はそのまま検証され、refutedになります。

自信満々の推測。検証者は生成モデルがどれだけ自信ありげに見えたかは気にしません。関心があるのは公開Webが何と言っているかです。文脈の中では権威あるように見えても、独立したいずれの情報源にも裏付けられていない推測は、unverifiableまたはrefutedになります。

Upgrading: Per-Claim Regeneration

verdict["claims"]を得たら、応答全体を拒否する以上のことができます。失敗した主張だけを、ピンポイントに再生成できます:

def verify_and_repair(query):
    docs = retrieve(query)
    draft = generate(query, docs)
    verdict = verify(draft)

    refuted = [c["claim"] for c in verdict["claims"] if c["verdict"] == "refuted"]
    if not refuted:
        return draft

    # 明示的な「do not include(含めない)」リストで再生成する
    repair_prompt = (
        f"Answer the following using ONLY the retrieved context. "
        f"Do not include these claims that were refuted: {refuted}

"
        f"Original query: {query}"
    )
    repaired = generate(repair_prompt, docs)
    return repaired

これは本番のRAGパイプラインで私が最もよく見かけるパターンです。ソフトリジェクト → 指名された失敗リスト → 対象を絞った再生成。検証による安全性と、自動生成によるスピードの恩恵を得られ、ユーザーはハルシネーションされたバージョンを見ることがありません。

Production Notes

これを実際のパイプラインで動かして学んだことがいくつかあります:

  • レイテンシ。 /evaluate は、3〜6件の主張を含む短い段落であれば通常3〜6秒で返ってきます。RAGパイプラインが常に高負荷で動いていてそれだと遅い場合は、すべてのチャットターンに対してではなく、高リスクのエージェントアクション(書き込み、トランザクション、外部メッセージ)にのみ検証を追加してください。
  • コスト。 無料枠(10/時)は開発には十分です。本番では、/evaluateはBase上のx402に対する、クエリごとの従量課金です。$0.01 per call。1時間あたり100回の検証を行うエージェントは、約$1/時のコストになります。これは、検証している応答を生成したLLM呼び出しよりも、通常は安価です。
  • 返却形式: {"translated": "翻訳されたHTML"}
  • しきい値。 act のデフォルトは 0.80 です。規制されたワークフロー(医療、法律、金融)では 0.90 に引き上げてください。これは、「真の主張に対する 10% の偽陽性」が、「幻覚に対する 1% の偽陰性」よりもコストが安い場合です。
  • 失敗モード。 まれに /evaluatesupported / refuted の代わりに unverifiable を返すことがあります。これは通常、その主張が公開ウェブに対して「具体的すぎる」「新しすぎる」「または曖昧すぎる」ことを意味します。unverifiableverify と同様に扱ってください――ソフトフラグ(軽微な扱い)にし、ハードに失敗(エラー停止)しないことです。このチュートリアルのコードでは、意図的に refutedunverifiable を分けています。

完全な最小例

簡単にコピペできるように、完全に動く例を 1 つのブロックにまとめます:

import requests
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage

corpus = [
    "OpenAI was founded in December 2015 as a non-profit research organization.",
    "ChatGPT was released by OpenAI on November 30, 2022 and reached 100 million users by January 2023.",
]

def verify(text):
    return requests.post(
        "https://agentoracle.co/evaluate",
        json={"content": text},
        timeout=30,
    ).json()["evaluation"]

def rag_with_verification(query):
    # Retrieve
    docs = corpus

    # Generate
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
    draft = llm.invoke([
        SystemMessage(content=f"Answer only from this context:
{chr(10).join(docs)}"),
        HumanMessage(content=query),
    ]).content

    # Verify
    verdict = verify(draft)
    refuted = [c["claim"] for c in verdict["claims"] if c["verdict"] == "refuted"]
    if refuted:
        return f"REJECTED — hallucinated claims: {refuted}"
    return draft

print(rag_with_verification("When was OpenAI founded and how fast did ChatGPT grow?"))

実行してください。温度(temperature)を緩める、あるいはコーパス(corpus)を狭めることで、わざと壊してみてください。そして、検証(verifier)が何を捕まえるか確認しましょう。

はじめに

プレイグラウンド(セットアップ不要): agentoracle.co

パッケージ:

  • pip install langchain-agentoraclePyPI
  • pip install crewai-agentoraclePyPI
  • npx agentoracle-mcpnpm(Claude Desktop、Cursor、Windsurf)

出典: GitHub

検証可能なレシート(receipts)仕様: github.com/TKCollective/agentoracle-receipt-spec — すべての /evaluate 応答は、公開 JWKS に対してオフライン検証できる JWS 署名レシート形式にコミットします。Node と Python での検証例は、/examples ディレクトリを参照してください。

この連載の前:

RAGは幻覚(ハルシネーション)を解決するはずでした。ある程度は解決しましたが、次により難しいクラスを導入しました。修正はいつも同じです。つまり、それを生成したどんなパイプラインであっても独立して、出力に対して実行する検証ステップです。

Pythonは15行。試せる無料枠。上のコードはそのまま動作します。