# RAG幻觉抑制实战:从索引到生成,四环节锁死模型瞎编

RAG幻觉抑制实战:从索引到生成,四环节锁死模型瞎编

导读:大模型"瞎编"是 RAG 系统最大的敌人。你给了它 5 条参考资料,它却自己编了第 6 条;你让它只基于资料回答,它却在引用标注里写了不存在的来源。幻觉分两种——“知识幻觉”(没资料硬编)和"推理幻觉"(有资料但理解错)。RAG 不能彻底消灭幻觉,但可以从四个环节层层设防,把幻觉率压到最低。本文从政务政策 RAG 项目出发,完整拆解索引治理、检索控制、Prompt约束、生成后校验四环节的具体做法和代码实现。

适合读者

  • RAG 系统回答经常"跑偏"、需要抑制幻觉的开发者
  • 想理解"RAG为什么不能彻底消灭幻觉"的工程师
  • 准备 RAG 面试、需要系统回答"幻觉怎么解决"的同学
  • 需要设计"引用校验"和"拒答机制"的技术负责人

阅读收益

  • 理解知识幻觉和推理幻觉的区别和各自解法
  • 掌握四环节抑制策略:索引治理 → 检索控制 → Prompt约束 → 生成后校验
  • 学会设计"证据不足就拒答"的兜底机制
  • 掌握引用溯源校验的代码实现
  • 获得一套可直接落地的幻觉抑制检查清单

目录

  1. 什么是幻觉:知识幻觉 vs 推理幻觉
  2. 为什么RAG不能彻底消灭幻觉
  3. 第一环节:索引治理(源头防脏)
  4. 第二环节:检索控制(不相关就拒答)
  5. 第三环节:Prompt约束(锁死模型行为)
  6. 第四环节:生成后校验(引用溯源)
  7. 拒答机制:什么时候应该不回答
  8. 完整幻觉抑制链路代码
  9. 踩坑清单:幻觉抑制的8个关键问题
  10. 面试速答版
  11. 总结与延伸
  12. 文末互动

1. 什么是幻觉:知识幻觉 vs 推理幻觉

1.1 知识幻觉

模型自身参数中没有该知识,凭空编造事实、数据、引用。

场景:用户问"你们公司2024年年假几天?"

模型参数中不可能有"你们公司"的具体政策 →
模型不会说"我不知道" →
模型编造:"根据一般企业惯例,年假为5-15天..."

→ 这是知识幻觉:模型没有知识,硬编一个通用答案

RAG解法:检索公司真实政策文档,让模型基于资料回答。

1.2 推理幻觉

输入上下文中存在相关信息,但模型错误理解、篡改或乱拼接上下文内容。

场景:用户问"怎么申请公积金提取?"

检索到的资料:
  [1] "公积金提取需要身份证、公积金卡、提取申请表"
  [2] "网上提取可以通过支付宝市民中心办理"

模型回答:
  "公积金提取需要身份证、公积金卡、提取申请表,
   并且可以通过支付宝市民中心办理网上提取。"

→ 看起来对?但如果资料[1]和[2]是互斥的两种渠道,
  模型把"线下"和"线上"强行拼接成"线下+线上",就是推理幻觉

RAG解法:需要更精细的上下文组织和引用校验。

1.3 两种幻觉的对比

类型原因表现RAG解法
知识幻觉模型没这个知识编造事实、数据、来源检索补充知识
推理幻觉有资料但理解错拼接错误、篡改原文、错误推理精细约束+校验

2. 为什么RAG不能彻底消灭幻觉

2.1 RAG的局限性

RAG能解决的:
  ✓ 模型不知道的知识 → 检索补充
  ✓ 模型训练数据过时 → 检索最新资料

RAG不能彻底解决的:
  ✗ 检索结果本身错误(知识库质量差)
  ✗ 模型错误理解检索内容(推理幻觉)
  ✗ 模型自由发挥(Prompt约束不到位)
  ✗ 引用标注错误(生成后没校验)

2.2 核心认知

RAG 不是幻觉的"解药",而是"抑制器"。 它能把幻觉率从 30% 压到 5%,但不能压到 0%。要把幻觉率继续压低,需要四个环节层层把关。


3. 第一环节:索引治理(源头防脏)

3.1 知识库质量决定上限

如果知识库里的资料就是错的,检索再精准、模型再强也没用。

错误资料入库 → 检索召回错误资料 → 模型基于错误资料回答 → 回答还是错的

数据质量原则:Garbage In, Garbage Out

3.2 索引治理四步法

"""索引治理:确保入库资料质量"""

class IndexGovernance:
    def process_document(self, raw_doc: str) -> list[Chunk]:
        # 1. 清洗
        cleaned = self.clean(raw_doc)
        # 去除页眉页脚、页码、水印
        # 过滤无效字符、乱码

        # 2. 去重
        unique = self.deduplicate(cleaned)
        # 文件指纹(MD5)去重
        # 语义去重(高相似度文档合并)

        # 3. 分块保上下文
        chunks = self.split(cleaned)
        # 父子分块:子块420字用于检索,父块1200字用于提供上下文
        # 每个切片保留章节名、页码、来源

        # 4. 校验
        validated = self.validate(chunks)
        # 空内容检测
        # 格式校验(JSON/XML非法字符)
        # 敏感信息检测

        return validated

4. 第二环节:检索控制(不相关就拒答)

4.1 核心原则:检索不到相关内容,就不回答

# 错误做法:不管检索到什么,都送给LLM
results = vectorstore.search(question, k=10)
answer = llm.generate(question, results)  # 即使results全是垃圾也硬答

# 正确做法:检索不到就拒答
results = vectorstore.search(question, k=10)
if not results or max_score < threshold:
    return "抱歉,知识库中没有找到相关资料。"
answer = llm.generate(question, results)

4.2 相似度阈值控制

"""检索控制:相似度阈值 + 重排序过滤"""

class RetrievalController:
    def __init__(self, score_threshold: float = 0.3):
        self.score_threshold = score_threshold

    def retrieve(self, question: str) -> RetrievalResult:
        # 1. 向量检索
        vector_results = vector_search(question, k=10)

        # 2. BM25检索
        bm25_results = bm25_search(question, k=10)

        # 3. RRF融合
        fused = rrf_fusion(vector_results, bm25_results)

        # 4. 重排序打分
        reranked = rerank(question, fused[:15])

        # 5. 过滤低分证据(关键!)
        evidence = [
            (doc, score) for doc, score in reranked
            if score >= self.score_threshold
        ]

        # 6. 没有合格证据 → 拒答
        if not evidence:
            return RetrievalResult(
                documents=[],
                refused=True,
                reason="未找到相关资料",
            )

        return RetrievalResult(
            documents=[doc for doc, _ in evidence[:5]],
            refused=False,
        )

4.3 为什么要设阈值

不设阈值的结果:
  检索召回的Top-5文档相似度只有0.1-0.2(几乎无关)
  全部送给LLM
  LLM被迫在这些无关内容中"编"一个答案
  → 幻觉率飙升

设阈值的结果:
  只有相似度>=0.3的文档才送给LLM
  如果没有文档达标 → 直接拒答
  → 宁可不答,也不瞎答

5. 第三环节:Prompt约束(锁死模型行为)

5.1 约束Prompt设计

SYSTEM_PROMPT = """你是一个企业政策问答助手。

【核心规则】
1. 只能基于下面提供的参考资料回答用户问题
2. 如果参考资料中没有相关信息,请如实说明"知识库中没有相关资料"
3. 每条关键结论必须标注引用来源,格式为[1][2]
4. 不要引用参考资料之外的内容
5. 不要自行补充、推理或延伸资料中没有的信息
6. 如果参考资料之间有矛盾,优先采信权威等级更高的资料

【参考资料】
{context}
"""

5.2 四条核心规则解析

规则作用防止的幻觉类型
只能基于资料回答锁死知识边界知识幻觉
没有信息就拒答兜底机制知识幻觉
必须标注引用强制溯源推理幻觉(乱拼接)
不引用资料外内容禁止编造知识幻觉

5.3 上下文组织方式

def format_context(docs: list[Document]) -> str:
    """带编号的上下文组织"""
    lines = []
    for i, doc in enumerate(docs, 1):
        lines.append(
            f"[{i}] 来源:{doc.metadata['filename']} "
            f"(章节:{doc.metadata.get('section', '未知')})\n"
            f"{doc.page_content}"
        )
    return "\n\n".join(lines)

# 生成的上下文格式:
# [1] 来源:员工手册v2.4(章节:考勤制度)
# 公司所有正式员工每年享有10天带薪年假...
#
# [2] 来源:项目组wiki(章节:请假流程)
# 请假需要提前3天在OA系统提交申请...

6. 第四环节:生成后校验(引用溯源)

6.1 引用校验

import re
from dataclasses import dataclass

@dataclass
class Citation:
    index: int
    text: str

@dataclass
class ValidationResult:
    valid: bool
    invalid_citations: list[Citation]
    reason: str | None


def extract_citations(answer: str) -> list[Citation]:
    """提取回答中的引用标注 [1] [2] [3]"""
    pattern = r'\[(\d+)\]'
    matches = re.finditer(pattern, answer)
    citations = []
    for match in matches:
        index = int(match.group(1))
        # 提取引用前后的文本片段
        start = max(0, match.start() - 50)
        end = min(len(answer), match.end() + 50)
        context = answer[start:end]
        citations.append(Citation(index=index, text=context))
    return citations


def validate_citations(
    answer: str,
    docs: list[Document],
) -> ValidationResult:
    """校验回答中的引用是否合法

    检查项:
    1. 引用编号是否在有效范围内(1 ~ len(docs))
    2. 引用内容是否与对应文档语义匹配
    """
    citations = extract_citations(answer)
    invalid = []

    for citation in citations:
        # 检查1:编号越界
        if citation.index < 1 or citation.index > len(docs):
            invalid.append(citation)
            continue

        # 检查2:语义匹配(简化版:检查关键词重叠)
        doc = docs[citation.index - 1]
        doc_keywords = set(extract_keywords(doc.page_content))
        cite_keywords = set(extract_keywords(citation.text))

        overlap = len(doc_keywords & cite_keywords)
        if overlap < 2:  # 关键词重叠太少
            invalid.append(citation)

    if invalid:
        return ValidationResult(
            valid=False,
            invalid_citations=invalid,
            reason=f"发现 {len(invalid)} 处无效引用",
        )

    return ValidationResult(valid=True, invalid_citations=[], reason=None)

6.2 答案一致性校验

def validate_answer_consistency(
    answer: str,
    docs: list[Document],
) -> bool:
    """校验回答是否与参考资料一致

    简单实现:检查回答中的关键事实是否能在文档中找到
    """
    # 提取回答中的数字、日期、专有名词
    answer_facts = extract_facts(answer)

    # 在文档中搜索这些事实
    doc_text = " ".join(d.page_content for d in docs)
    for fact in answer_facts:
        if fact not in doc_text:
            return False  # 回答包含文档中没有的事实

    return True

7. 拒答机制:什么时候应该不回答

7.1 三种拒答场景

场景判断条件回复方式
检索无结果向量+BM25都召回不到“抱歉,没有找到相关资料”
证据质量低所有片段重排序分数 < 0.3“抱歉,现有资料不足以回答”
问题越界与知识库领域无关“这个问题不在我的知识范围内”

7.2 拒答不是失败,是保护

错误认知:
  "系统必须回答每一个问题,不回答就是 bug"

正确认知:
  "答错比不答更可怕——答错会传播错误信息,不答至少不害人"

设计原则:
  宁可不答,也不瞎答
  宁可说"不知道",也不编一个"看起来像对"的答案

8. 完整幻觉抑制链路代码

"""完整RAG幻觉抑制链路"""
from dataclasses import dataclass
from enum import Enum


class AnswerType(Enum):
    NORMAL = "正常回答"
    REFUSED = "拒答"
    UNCERTAIN = "不确定"


@dataclass
class SafeAnswer:
    content: str
    answer_type: AnswerType
    citations: list[str]
    confidence: float
    trace: dict


class HallucinationGuard:
    def __init__(
        self,
        retriever,
        llm,
        score_threshold: float = 0.3,
        citation_validator=None,
    ):
        self.retriever = retriever
        self.llm = llm
        self.score_threshold = score_threshold
        self.citation_validator = citation_validator

    async def safe_answer(self, question: str) -> SafeAnswer:
        trace = {}

        # ========== 环节1:检索控制 ==========
        retrieval_result = await self.retriever.retrieve(question)
        trace["retrieval"] = retrieval_result.trace

        if retrieval_result.refused:
            return SafeAnswer(
                content="抱歉,知识库中没有找到相关资料,请尝试换个问法。",
                answer_type=AnswerType.REFUSED,
                citations=[],
                confidence=0.0,
                trace=trace,
            )

        docs = retrieval_result.documents

        # ========== 环节2:Prompt约束 ==========
        context = self._format_context(docs)
        messages = [
            SystemMessage(content=SYSTEM_PROMPT.format(context=context)),
            HumanMessage(content=question),
        ]

        # ========== 环节3:生成(temperature=0降低随机性) ==========
        response = await self.llm.ainvoke(messages, temperature=0)
        answer = response.content

        # ========== 环节4:引用校验 ==========
        if self.citation_validator:
            validation = self.citation_validator.validate(answer, docs)
            trace["validation"] = {
                "valid": validation.valid,
                "reason": validation.reason,
            }

            if not validation.valid:
                # 引用校验失败,降级处理
                return SafeAnswer(
                    content=(
                        f"根据资料初步分析:{answer[:100]}...\n\n"
                        "(注:引用校验发现异常,建议人工复核)"
                    ),
                    answer_type=AnswerType.UNCERTAIN,
                    citations=[],
                    confidence=0.5,
                    trace=trace,
                )

        # 提取引用
        citations = self._extract_citations(answer)

        return SafeAnswer(
            content=answer,
            answer_type=AnswerType.NORMAL,
            citations=citations,
            confidence=retrieval_result.confidence,
            trace=trace,
        )

    def _format_context(self, docs: list[Document]) -> str:
        lines = []
        for i, doc in enumerate(docs, 1):
            lines.append(
                f"[{i}] 来源:{doc.metadata.get('filename', '未知')}\n"
                f"{doc.page_content}"
            )
        return "\n\n".join(lines)

    def _extract_citations(self, answer: str) -> list[str]:
        import re
        pattern = r'\[(\d+)\]'
        return re.findall(pattern, answer)

9. 踩坑清单:幻觉抑制的8个关键问题

序号问题现象原因解决方案
1不设相似度阈值无关内容送入LLM,模型硬编检索召回质量低设score_threshold=0.3
2Prompt约束弱模型自由发挥,引用不存在来源系统Prompt没锁死边界四条核心规则强制约束
3知识库质量差检索到的资料本身就是错的入库没做清洗校验索引治理四步法
4切片不带来源引用标注无法溯源元数据缺失强制绑定filename/section/page
5temperature太高模型随机编造temperature>0导致创造性RAG生成设temperature=0
6不做引用校验回答引用[99]但只有5条资料生成后没校验正则提取+编号范围校验
7拒答阈值太高该答的不答,用户体验差threshold=0.8过于严格设0.3,根据业务调整
8不做定期评测幻觉率缓慢上升 unnoticed缺乏监控定期跑标准问答对评估

10. 面试速答版

RAG不能彻底消灭幻觉,只能抑制。幻觉分两种:知识幻觉(模型没知识硬编)和推理幻觉(有资料但理解错)。四环节层层设防:

  1. 索引治理:清洗去重,保证入库资料质量
  2. 检索控制:设相似度阈值,不相关就拒答,宁可不答也不瞎答
  3. Prompt约束:锁死四条规则(只能基于资料、没资料拒答、必须标引用、不引用资料外内容)
  4. 生成后校验:正则提取引用编号,校验是否在有效范围,语义匹配度检查
    一句话:答错比不答更可怕,宁可拒答也不瞎编。

11. 总结与延伸

11.1 核心知识点回顾

幻觉抑制四环节:
  索引治理 → 检索控制 → Prompt约束 → 生成后校验

索引治理:
  清洗 → 去重 → 分块保上下文 → 校验

检索控制:
  混合检索 → 重排序 → 阈值过滤 → 无证据则拒答

Prompt约束(四条规则):
  1. 只能基于资料
  2. 没资料就拒答
  3. 必须标引用
  4. 不引用资料外

生成后校验:
  引用编号范围校验 → 语义匹配校验 → 事实一致性校验

11.2 延伸方向

  • 自动评估体系:定期用标准问答对跑评估,监控幻觉率变化
  • 人在回路(Human-in-the-loop):低置信度回答推人工审核
  • 对抗测试:故意输入"陷阱问题",测试模型是否会编造
  • 多模型交叉验证:同一问题用多个模型回答,不一致时标警示

12.文末互动

你的 RAG 系统有没有遇到过模型"硬编"答案的情况?你是怎么发现的——用户反馈、引用校验失败、还是定期评估?评论区聊聊你的幻觉对抗经验。

思考题:如果用户问了一个"知识库里没有"的问题,但这个问题和库里某条资料"看起来有点像",模型可能会"勉强"用这条资料回答。你的系统应该如何区分"真正相关"和"看起来相关"?欢迎在评论区讨论。


本文聚焦 RAG 幻觉抑制的四环节实战方案。如果觉得有帮助,欢迎点赞收藏,后续会更新自动评估体系和对抗测试的进阶内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值