核心摘要
**RAG 幻觉治理本质上是证据控制问题,不是 Prompt 技巧。**生产系统必须保留经过授权的来源版本,检索充分而不只是相似的证据,显式暴露来源冲突,把每个重要声明绑定到支持片段,再由经过校准的策略决定完整发布、缩减回答、拒答或升级人工。模型只提出回答;确定性的应用代码决定回答能否展示。
目录
- 什么是 RAG 幻觉
- 先定位哪一层失败
- 控制一:治理证据语料
- 控制二:用证据充分性约束检索
- 控制三:把上下文当作不可信证据
- 控制四:校验声明与引用
- 控制五:用确定性策略发布与拒答
- 实现最小证据门禁
- 验证完整控制闭环
- 常见问题
核心要点
- 检索相关性不等于证据充分性,高相似度分数也不是事实证明。
- 如果来源过期或错误,忠于上下文的回答仍可能不符合现实。
- Prompt 约束、低温度、引用和第二个模型都是信号,不是强制边界。
- 权限必须在检索前执行;生成后再过滤泄露内容已经太晚。
release、partial、abstain与review应是经过损失权衡测试的明确策略结果。
什么是 RAG 幻觉
RAG 幻觉是指回答中的重要声明与本次运行提供的证据矛盾,或无法从这些证据中推出。这个工程定义聚焦答案忠实度,不会把所有质量问题都混成一个“幻觉率”。
以下四个属性必须分开:
| 属性 | 要回答的问题 | 失败示例 |
|---|---|---|
| 检索覆盖 | 必要证据是否进入候选集 | 退款例外条款从未被召回 |
| 证据质量 | 来源是否权威、当前有效且适用 | 检索到已经失效的政策 |
| 答案忠实度 | 每个声明是否能从证据推出 | 回答补充了无依据的期限 |
| 外部正确性 | 声明是否符合外部参考或现实 | 来源本身含有错误 |
回答可能忠实复述过期文档,也可能依靠模型记忆说出正确事实,却没有得到本次授权证据快照支持。前者是来源治理失败;后者即使碰巧正确,也违反封闭证据任务的边界。
若要建立跨系统的检测契约,可阅读LLM 幻觉检测。本文只回答更具体的实现问题:RAG 应用怎样阻止无依据回答到达用户?
先定位哪一层失败
治理的第一步是判断证据链在哪一层断裂。更换生成模型无法找回检索阶段遗漏的片段;增加 top_k 也不能修复生成器忽略有效上下文的问题。
故障记录必须保存准确的运行快照:
{
"run_id": "rag-202",
"query": "外包人员的国际差旅能否报销?",
"principal": "user-42",
"tenant": "acme",
"corpus_revision": "policy-index@sha256:7ad1",
"retriever_version": "hybrid-v4",
"reranker_version": "rerank-v3",
"generator_version": "answer-v8",
"policy_version": "evidence-policy-v5",
"evidence_ids": ["travel-2026#p18", "contractors-2026#p4"],
"decision": "review"
}
缺少这份快照时,后续重放可能使用不同文档、排序、Prompt 或策略,无法复现事故。
控制一 治理证据语料
第一层控制是把每个检索片段变成版本化 Evidence Object。纯文本加文件名不足以判断来源是否适用于当前用户和问题。
证据记录至少应包含:
{
"evidence_id": "travel-2026#p18",
"document_id": "travel-policy",
"revision": "sha256:3c81",
"locator": {"page": 18, "section": "International travel"},
"authority": "finance-policy-owner",
"valid_from": "2026-01-01",
"valid_to": null,
"tenant": "acme",
"allowed_roles": ["employee", "contractor-manager"],
"status": "active",
"content_hash": "sha256:915f"
}
这份契约支持 Embedding 无法提供的控制:
- **权威性:**区分已批准政策、草稿、评论、转载页面和模型摘要。
- **时效性:**保存生效时间和替代关系,而不是按上传时间猜测新旧。
- **适用性:**保留司法辖区、产品、套餐、租户、角色等范围限定。
- **可追溯性:**保存不可变 Revision 与 Locator,让引用以后仍解析到同一证据。
- **可删除性:**把撤销和删除传播到全部派生 Chunk 与 Index。
访问控制必须在检索和排序之前执行。先检索其他租户的片段,再在生成后隐藏,仍然会把内容暴露给模型和日志。事实锚定(Grounding)词条进一步解释了为什么 Provenance、Authorization 和 Validity 是证据契约的一部分,而不是可选 Metadata。
控制二 用证据充分性约束检索
检索阶段应回答两个不同问题:**哪些片段是候选,以及这些候选是否足以回答问题?**相似度、Reranker Score 或非空结果列表都不能通用地回答这两个问题。
将检索拆成两步:
- 根据查询分布选择词法、稠密、结构化、图或 API 检索,召回候选证据。
- 重排候选,并判断入选证据的相关性、覆盖度、适用性和冲突。
混合检索可以改善标识符精确匹配和同义表达召回,但必须与标注基线比较。例如 Anthropic 的 Contextual Retrieval 收益来自其特定语料、Embedding 配置和 Recall@20 实验;它证明该方法值得测试,不代表迁移到任意生产语料后都有固定增益。
查询改写也遵循同一原则。Rewrite 是搜索提案,不是证据。系统应保留原始查询,记录改写器版本,限制变体数量,并测试意图漂移。如果扩展查询虚构了产品名或政策前提,它可能召回一个很有说服力、但回答了错误问题的结果。
按问题要求定义充分性
每个评测问题都应标注证据要求:
query_id: contractor-international-travel
required_facts:
- contractor eligibility
- international travel rule
- approval authority
required_qualifiers:
- employment type
- effective date
unanswerable_when:
- contractor policy is absent
- applicable revisions conflict without precedence
检索门禁才能区分:
- **sufficient:**所有必要事实与限定条件都有适用证据;
- **partial:**部分信息有用且受支持,但不足以完整回答;
- **conflicting:**适用来源相互冲突,且无法按优先级解决;
- **insufficient:**缺少必要证据;
- **unauthorized:**证据可能存在,但当前主体无权访问。
任何模型分数或阈值都应使用代表性的可回答、不可回答、冲突、过期来源和跨租户切片校准。从其他语料复制来的阈值,在当前系统中没有经过证明的含义。
控制三 把上下文当作不可信证据
被检索内容是数据,不是指令。文档可能过期、相互矛盾、格式损坏,也可能故意携带提示注入文本。
Context Assembler 应做到:
- 把 System Instruction 放在 Evidence Delimiter 外;
- 为每个片段标注不透明 Evidence ID、Revision、Locator 和适用范围;
- 在模型调用前排除越权或非 Active 证据;
- 保留重要限定条件,不因截断而丢失例外;
- 对冲突声明分组或打标,不让模型静默选择;
- 对近重复片段去重,避免重复文本看起来像独立佐证;
- 控制 Token Budget,同时保留引用背后的原始 Span。
OWASP Prompt Injection 指南明确将 RAG 内容视为间接提示注入路径。Delimiter 与提示约束可以降低风险,但不能形成安全边界。模型不能因为检索片段中的文字而获得权限、执行 Tool 或选择受保护来源。
Context 应显式暴露冲突:
<evidence id="travel-2026#p18" status="active" valid_from="2026-01-01">
外包人员国际差旅必须由项目所属总监审批。
</evidence>
<evidence id="travel-2024#p11" status="superseded" valid_to="2025-12-31">
外包人员国际差旅必须由部门经理审批。
</evidence>
如果优先级确定,应用代码可以删除被替代片段。如果有效性或权威性存在歧义,应保留冲突并升级复核,而不是让生成器猜测政策。
控制四 校验声明与引用
只有系统检查引用能否解析、是否蕴含声明以及是否完整覆盖,Citation 才有工程价值。模型生成的 [来源: policy.pdf] 只能证明它输出了一个看似合理的标识符。
采用 Claim-First 响应契约:
{
"claims": [
{
"claim_id": "c1",
"text": "外包人员国际差旅必须由项目所属总监审批。",
"evidence_ids": ["travel-2026#p18"]
}
],
"answer": "外包人员国际差旅必须由项目所属总监审批 [c1]。"
}
按以下顺序验证:
- 响应符合 Schema。
- 每个 Evidence ID 都来自本次 Run 的授权证据快照。
- 每个重要回答句都映射到一个或多个原子声明。
- 每个引用片段对对应 Claim 给出支持、矛盾或证据不足判定。
- 所有发布声明都具有充分的引用覆盖。
ALCE Benchmark 把答案正确性与引用质量分开,因为引用可能存在,却不完整或不支持声明。RAGAS 同样分开 Context Relevance、Answer Relevance 与 Faithfulness。生产系统应保留这些差异,不能把它们压缩成一个 is_hallucinated Boolean。
LLM 或 NLI 模型可以提出 Claim Support Verdict,但它仍是会出错的评估器。应版本化其 Prompt 和模型,保存 Evidence Pair,并用人工标签校准矛盾与证据不足的 Precision 和 Recall。RAGTruth 同时包含与来源矛盾的声明和无依据新增内容,可用于测试 Verifier 是否能区分不同故障。
控制五 用确定性策略发布与拒答
最终发布决定应由应用策略执行,而不是再让模型自由回答一次。策略消费 Evidence 和 Verifier 结果,再选择有边界的结果:
| 结果 | 必要条件 | 用户看到的行为 |
|---|---|---|
release |
所有重要声明均受支持 | 返回完整回答与可解析引用 |
partial |
有用子集得到支持 | 只返回受支持声明,并说明缺失范围 |
abstain |
证据不存在或不足 | 明确说明无法确定的内容 |
review |
来源冲突或风险高 | 暂停回答并交给授权审核人 |
deny |
请求或证据越权 | 不返回任何受保护证据 |
OpenAI 关于幻觉的研究解释了为什么只看 Accuracy 会奖励猜测。RAG 策略应让自信错误的代价高于适当拒答,同时度量错误拒答。永远拒答的系统虽然幻觉少,却没有业务效用。
不要在每次校验失败后自动重新生成。只有重试改变了可测试条件,例如修正查询、获取另一条授权来源、缩小 Claim 或更换生成配置时,才适合做有界重试。使用相同证据和 Prompt 重试,可能重复同一错误并增加延迟与成本。
实现最小证据门禁
以下 Python 3.9+ 程序实现契约中的确定性部分。Retriever 和 Verifier 可以使用机器学习模型,但只有这段 Policy 决定哪些内容可以发布。
from dataclasses import dataclass
from enum import Enum
from typing import Dict, FrozenSet, List, Optional
class Verdict(str, Enum):
SUPPORTED = "supported"
CONTRADICTED = "contradicted"
INSUFFICIENT = "insufficient"
class Outcome(str, Enum):
RELEASE = "release"
PARTIAL = "partial"
ABSTAIN = "abstain"
REVIEW = "review"
DENY = "deny"
@dataclass(frozen=True)
class Evidence:
evidence_id: str
tenant: str
allowed_roles: FrozenSet[str]
status: str
valid_from: int
valid_to: Optional[int]
@dataclass(frozen=True)
class Claim:
claim_id: str
text: str
evidence_ids: tuple
verdict: Verdict
is_material: bool = True
@dataclass(frozen=True)
class Decision:
outcome: Outcome
released_claim_ids: tuple
reasons: tuple
def is_authorized(
evidence: Evidence,
*,
tenant: str,
roles: FrozenSet[str],
now: int,
) -> bool:
return (
evidence.tenant == tenant
and bool(evidence.allowed_roles & roles)
and evidence.status == "active"
and evidence.valid_from <= now
and (evidence.valid_to is None or now <= evidence.valid_to)
)
def decide(
claims: List[Claim],
evidence_by_id: Dict[str, Evidence],
*,
tenant: str,
roles: FrozenSet[str],
now: int,
high_risk: bool,
) -> Decision:
reasons = []
supported = []
for claim in claims:
evidence = [evidence_by_id.get(item) for item in claim.evidence_ids]
if not evidence or any(item is None for item in evidence):
reasons.append("%s:unknown_evidence" % claim.claim_id)
continue
if not all(
is_authorized(item, tenant=tenant, roles=roles, now=now)
for item in evidence
):
return Decision(Outcome.DENY, (), ("%s:unauthorized" % claim.claim_id,))
if claim.verdict == Verdict.CONTRADICTED:
return Decision(Outcome.REVIEW, (), ("%s:contradicted" % claim.claim_id,))
if claim.verdict == Verdict.SUPPORTED:
supported.append(claim.claim_id)
elif claim.is_material:
reasons.append("%s:insufficient" % claim.claim_id)
material = [claim for claim in claims if claim.is_material]
if not material or not supported:
return Decision(Outcome.ABSTAIN, (), tuple(reasons or ["no_material_claims"]))
if high_risk and reasons:
return Decision(Outcome.REVIEW, (), tuple(reasons))
if reasons:
return Decision(Outcome.PARTIAL, tuple(supported), tuple(reasons))
return Decision(Outcome.RELEASE, tuple(supported), ())
evidence = {
"travel-2026#p18": Evidence(
evidence_id="travel-2026#p18",
tenant="acme",
allowed_roles=frozenset({"employee", "contractor-manager"}),
status="active",
valid_from=1_767_225_600,
valid_to=None,
)
}
supported_claim = Claim(
claim_id="c1",
text="Contractors need sponsoring-director approval.",
evidence_ids=("travel-2026#p18",),
verdict=Verdict.SUPPORTED,
)
missing_claim = Claim(
claim_id="c2",
text="The company always reimburses business-class fares.",
evidence_ids=("travel-2026#p18",),
verdict=Verdict.INSUFFICIENT,
)
release = decide(
[supported_claim],
evidence,
tenant="acme",
roles=frozenset({"contractor-manager"}),
now=1_800_000_000,
high_risk=False,
)
partial = decide(
[supported_claim, missing_claim],
evidence,
tenant="acme",
roles=frozenset({"contractor-manager"}),
now=1_800_000_000,
high_risk=False,
)
denied = decide(
[supported_claim],
evidence,
tenant="other",
roles=frozenset({"contractor-manager"}),
now=1_800_000_000,
high_risk=False,
)
assert release.outcome == Outcome.RELEASE
assert partial.outcome == Outcome.PARTIAL
assert partial.released_claim_ids == ("c1",)
assert denied.outcome == Outcome.DENY
print(release.outcome.value, partial.outcome.value, denied.outcome.value)
预期输出:
release partial deny
程序刻意不自行推断语义支持关系。它消费版本化的 Verifier Verdict,并独立证明 Policy 行为。生产系统应把 Verifier Version、Evidence Digest、Claim Digest、Policy Version 与 Decision 一起存储。
验证完整控制闭环
测试必须扰动每个边界,不能只观察几个正常回答是否“看起来不错”。
| 测试切片 | 注入条件 | 预期行为 |
|---|---|---|
| 缺少证据 | 必要片段不在候选集 | abstain,或在预算内再次检索 |
| 来源过期 | 已被替代的政策排在首位 | 在生成前过滤 |
| 跨租户来源 | 相关片段属于其他租户 | deny,且不把内容发送给模型 |
| 来源冲突 | 两个适用 Revision 相互矛盾 | review 或显式冲突回答 |
| 限定条件丢失 | Chunk 缺少例外或日期 | 不通过充分性门禁 |
| 间接注入 | 文档要求模型忽略 Policy | 只作为数据处理,不改变权限 |
| 引用伪造 | Claim 引用未见过的 Evidence ID | 发布前拒绝 |
| 无依据补充 | 回答增加未引用的重要声明 | partial、abstain 或 review |
| 错误拒答 | 证据充分但系统拒答 | 计入拒答召回与效用损失 |
| Verifier 漂移 | Judge 版本改变判定分布 | 重新校准前阻断发布 |
具体指标与发布测试集设计可参考生产级 RAG 评估指南。至少报告:
- Evidence Recall@k 与 Sufficiency Recall;
- 过期、越权和冲突证据比例;
- Claim Support 与 Contradiction 的 Precision/Recall;
- Citation Precision 与重要声明覆盖率;
- Abstention Precision、Abstention Recall、Selective Risk 与 Answer Coverage;
- 外部正确性和端到端任务成功率;
- 延迟、模型调用数、Token 消耗与人工复核负担。
这些指标必须按查询类型、来源类型、语言、租户、风险和可回答性切片。平均值可能掩盖最关键政策或客户群体上的完全失效。
上线检查清单
发布前确认:
- 每次 Corpus Build 都有不可变 Revision 与 Deletion Manifest。
- Authorization Filter 在候选检索之前执行。
- Query Rewrite 有数量上限、完整日志和意图漂移测试。
- Retrieval 测试包含可回答、不可回答、冲突、过期来源和跨租户案例。
- Context 保留限定条件、Source Locator、Validity 与 Conflict State。
- 被检索文本不能授权 Tool 或覆盖系统 Policy。
- 每个重要 Claim 都映射到可解析的 Evidence ID。
- Claim-Support Judge 已版本化,并用人工标签校准。
- 发布策略支持
partial、abstain、review与deny。 - 生产事件记录 Digest 与 Decision,不记录原始 Secret 或无必要的完整文档。
常见问题
RAG 已经检索到文档,为什么仍会产生幻觉?
检索得到的是候选证据,不是事实保证。正确片段可能缺失,限定条件可能在切分时丢失,来源可能过期或冲突,生成器也可能添加任何片段都不支持的声明。语料、检索、上下文、生成、引用与发布策略必须分别诊断。
相似度或重排阈值能防止 RAG 幻觉吗?
不能用通用阈值证明可回答性。Retriever、Corpus、语言、Query Class 和模型版本都会改变分数分布。应在已标注的可回答和不可回答案例上校准,再把分数与必要证据覆盖及冲突检测结合。
带有引用的 RAG 回答一定忠实吗?
不一定。系统要检查三个属性:引用能解析到本次授权运行的 Evidence Object;引用 Span 支持精确 Claim;引用覆盖所有重要 Claim。只有来源标签却没有 Entailment 与 Coverage,只是展示,不是验证。
RAG 回答校验失败后应该自动重新生成吗?
只有有界重试改变了可测试条件时才适合,例如检索另一条授权来源、修复发生漂移的查询或缩小待回答 Claim。其他情况应返回受支持的部分回答、拒答或交给授权人员复核。
如何衡量 RAG 幻觉治理效果?
要测量完整证据链:来源权威性与时效、检索覆盖与充分性、冲突与权限泄露、声明支持与矛盾、引用质量、拒答行为、外部正确性、任务成功、延迟、成本和复核负担。报告时保留分母和风险切片。
总结
当应用把每个回答视为 Claim-Evidence Proposal 时,RAG 幻觉治理才真正可执行。治理来源身份与范围,为充分性而检索,在组装不可信上下文时保留冲突,逐条校验重要声明与引用,再由确定性 Policy 决定完整发布、缩减、拒答、复核或拒绝访问。任何 Prompt、分数、引用标签、模型升级或 Judge 都不能替代这套控制闭环。