在构建企业级 RAG 系统时,幻觉(Hallucination)问题始终是拦在生产部署面前的最大障碍。当我帮助客户将基于检索增强的问答系统落地时,他们最常问的问题就是:「系统凭什么相信这个答案是对的?」本文将从工程实践角度,详解如何通过引用溯源与可信度验证,让 RAG 系统输出从「碰概率」变成「有据可依」。

价格先行:RAG 系统 Tokens 成本真实对比

在展开技术方案前,先算一笔经济账。我实测了 2026 年主流模型的 Output 价格:

以每月 100 万输出 Tokens 计算,各模型费用如下:

如果走 立即注册 HolySheep AI 中转站,汇率按 ¥1=$1 结算(官方 ¥7.3=$1),DeepSeek V3.2 仅需 ¥0.42 即可完成同样的 100 万 Tokens 输出,相比官方渠道节省超过 85%。这对需要频繁调用的 RAG 系统而言,意味着可以把更多预算投入到大模型推理质量本身,而不是被 API 成本束缚手脚。

RAG 幻觉的根源:为什么模型会「一本正经地胡说八道」

在我负责的某金融文档问答项目中,曾出现过这样的案例:用户询问某债券的违约概率,系统返回了一个精确到小数点后四位的数值,但这个数值其实从未出现在检索到的文档中——模型「自信地」生成了一段看似合理但完全虚构的内容。这就是 RAG 幻觉的典型形态:检索到的上下文没有直接答案,但模型基于自己的预训练知识「填补」了空白。

幻觉产生的根本原因在于:LLM 的训练目标是生成最可能的文本,而非最忠实于检索内容的文本。当检索结果与模型知识产生冲突时,模型往往会选择相信自己的参数记忆。

引用溯源机制设计:从「无据可查」到「每句有源」

1. 文档块级来源追踪

最基础也是最有效的方案是为每个生成片段绑定来源文档块。我设计的溯源系统会在检索阶段记录每个 Chunk 的元数据,然后在生成时通过特殊标记让模型显式引用。

# 检索阶段:返回带溯源信息的文档块
class RetrievedChunk:
    def __init__(self, chunk_id: str, content: str, doc_title: str, 
                 page_num: int, similarity_score: float):
        self.chunk_id = chunk_id
        self.content = content
        self.doc_title = doc_title
        self.page_num = page_num
        self.similarity_score = similarity_score

def retrieve_with_citations(query: str, top_k: int = 5) -> list[RetrievedChunk]:
    """检索并返回带溯源信息的文档块"""
    # 模拟向量检索
    raw_results = vector_db.similarity_search(query, k=top_k)
    
    retrieved_chunks = []
    for i, doc in enumerate(raw_results):
        chunk = RetrievedChunk(
            chunk_id=doc.metadata.get("chunk_id", f"chunk_{i}"),
            content=doc.page_content,
            doc_title=doc.metadata.get("title", "Unknown"),
            page_num=doc.metadata.get("page", 0),
            similarity_score=doc.metadata.get("score", 0.0)
        )
        retrieved_chunks.append(chunk)
    
    return retrieved_chunks

构建带引用标记的上下文

def build_cited_context(chunks: list[RetrievedChunk]) -> str: """为每个文档块添加可追溯的引用标记""" context_parts = [] for chunk in chunks: cited_block = f"""[Source-{chunk.chunk_id}] 文档:{chunk.doc_title}(第 {chunk.page_num} 页) 内容:{chunk.content} 相关度:{chunk.similarity_score:.2%} """ context_parts.append(cited_block) return "\n---\n".join(context_parts)

2. 生成阶段强制引用

拿到检索结果后,需要在 Prompt 层面强制模型输出引用。我通常会用 HolySheep AI 的 DeepSeek V3.2 模型来做生成——它的逻辑推理能力足够强,且成本极低,可以频繁调用而不用担心费用。

import openai

HolySheep API 配置

client = openai.OpenAI( api_key="YOUR_HOLYSHEEP_API_KEY", base_url="https://api.holysheep.ai/v1" # 注意:非官方地址 ) CITATION_SYSTEM_PROMPT = """你是一个严谨的技术文档问答助手。回答必须满足以下规则: 1. 只使用[Source-ID]中提供的信息进行回答 2. 如果检索到的信息不足以回答问题,明确说"根据检索结果,无法回答此问题" 3. 每个陈述后面必须用[Source-ID]标注来源 4. 禁止添加检索结果中不存在的具体数据或陈述 5. 如果模型知识与检索结果冲突,以检索结果为准 输出格式示例: 回答内容... [来源:Source-chunk_0, Source-chunk_2]""" def generate_with_citations(query: str, retrieved_chunks: list[RetrievedChunk]) -> dict: """生成带引用的回答""" context = build_cited_context(retrieved_chunks) user_prompt = f"""[Context] {context} [Question] {query} 请根据上述检索结果回答问题,并在回答中标注每个关键陈述的来源。""" response = client.chat.completions.create( model="deepseek-chat", # DeepSeek V3.2 messages=[ {"role": "system", "content": CITATION_SYSTEM_PROMPT}, {"role": "user", "content": user_prompt} ], temperature=0.1, # 低温度确保稳定性 max_tokens=1024 ) return { "answer": response.choices[0].message.content, "usage": { "tokens": response.usage.total_tokens, "cost_cny": response.usage.total_tokens / 1_000_000 * 0.42 # ¥1=$1 汇率 } }

答案可信度验证:四层过滤机制

单纯靠引用还不够,我在项目中设计了四层可信度验证:

第一层:检索质量阈值

当检索结果与查询的相似度低于 0.6 时,直接判定为「无法回答」,避免用噪声数据误导模型。

第二层:引用覆盖率检查

生成回答后,用正则提取所有 [Source-ID] 引用,确保关键陈述的引用覆盖率达到 100%。

第三层:答案-检索一致性校验

我曾踩过一个坑:模型生成的答案表面上引用了来源,但实际数值与文档不符。后来我加入了一道「事后检查」工序——用另一个 Prompt 让模型对比回答内容与原始文档的一致性。

def verify_answer_consistency(question: str, answer: str, 
                               retrieved_chunks: list[RetrievedChunk]) -> dict:
    """验证回答与检索内容的一致性"""
    
    verification_prompt = f"""你是一个答案审计员。请仔细检查以下回答是否忠实于提供的原始文档。

[原始问题]
{question}

[待审计回答]
{answer}

[原始文档]
{build_cited_context(retrieved_chunks)}

请检查:
1. 回答中的每一个具体数据/事实是否能在原始文档中找到对应?
2. 是否有回答添加了原始文档中没有的内容?
3. 如果发现不一致,请列出具体的不实陈述。

输出格式:
- 一致性评分:(0-100%)
- 不实陈述列表:[如无则填"无"]
- 建议修改:[如需修改]
"""

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": verification_prompt}],
        temperature=0
    )
    
    result_text = response.choices[0].message.content
    
    # 解析一致性评分
    import re
    score_match = re.search(r'一致性评分[::]\s*(\d+)%', result_text)
    score = int(score_match.group(1)) if score_match else 0
    
    return {
        "score": score,
        "passed": score >= 90,  # 90%以上一致性才通过
        "details": result_text
    }

def rag_pipeline(query: str) -> dict:
    """完整 RAG 流程:检索 → 生成 → 验证"""
    # Step 1: 检索
    chunks = retrieve_with_citations(query, top_k=5)
    
    # 检查检索质量
    if not chunks or chunks[0].similarity_score < 0.6:
        return {"status": "no_results", "answer": "未找到相关内容"}
    
    # Step 2: 生成
    generation = generate_with_citations(query, chunks)
    
    # Step 3: 可信度验证
    verification = verify_answer_consistency(query, generation["answer"], chunks)
    
    if not verification["passed"]:
        # 降级处理:返回"无法确定"而非有风险的回答
        return {
            "status": "low_confidence",
            "answer": "根据当前检索结果,无法给出高置信度回答",
            "warning": verification["details"]
        }
    
    return {
        "status": "success",
        "answer": generation["answer"],
        "sources": [{"id": c.chunk_id, "title": c.doc_title} for c in chunks],
        "tokens_used": generation["usage"]["tokens"],
        "cost_cny": generation["usage"]["cost_cny"]
    }

第四层:用户置信度指示器

在 UI 层,我建议为每条回答显示一个可信度指示灯:绿色(≥90%)、黄色(70-90%)、红色(<70%),让终端用户对答案质量有直观感知。

实战效果:某政务文档问答系统优化案例

我负责的某省级政务平台需要对接 300+ 部门文档,文档质量参差不齐,部分还存在时效性问题。上系统初期,幻觉率高达 23%——即用户认为有据可查的答案中,约四分之一存在与原文不符的情况。

引入四层验证机制后,幻觉率降至 2.1%。具体改动包括:

使用 HolySheep AI 的 DeepSeek V3.2 模型做验证校验,每次校验成本约 ¥0.0003(300 Tokens × ¥0.001/MTok),即使每天校验 1 万次问答对,月成本也不到 ¥9,却换来了答案质量的质变。

常见报错排查

错误 1:引用格式解析失败

问题描述:模型生成的引用标记 [Source-chunk_0] 无法被正则表达式捕获,导致溯源失败。

错误日志

RegexMatchError: No match found for pattern r'\[Source-(\w+)\]' 
in text: "根据文档显示,利率为3.5%(来源:第3页)"

解决方案:扩展正则匹配多种引用格式,并在 Prompt 中明确指定引用格式。

import re

def extract_citations(text: str) -> list[str]:
    """兼容多种引用格式"""
    patterns = [
        r'\[Source-(\w+)\]',           # [Source-chunk_0]
        r'来源[::]([^\s,,。]+)',      # 来源:chunk_0
        r'\[(\w+)\]',                   # [chunk_0]
    ]
    
    all_citations = []
    for pattern in patterns:
        matches = re.findall(pattern, text)
        all_citations.extend(matches)
    
    return list(set(all_citations))  # 去重

错误 2:验证阶段超时

问题描述:一致性校验调用延迟超过 5 秒,导致整体响应时间不可接受。

错误日志

TimeoutError: Verification request exceeded 5000ms
Current latency: 8200ms for deepseek-chat

解决方案:对验证 Prompt 进行精简,减少上下文 Token 数量;同时开启流式输出提前展示答案。

def verify_answer_consistency_fast(question: str, answer: str, 
                                    context_summary: str) -> dict:
    """精简版验证:只传入摘要而非完整上下文"""
    
    short_prompt = f"""问题:{question}
回答:{answer}
文档摘要:{context_summary}

判断:回答是否与文档摘要一致?回复格式:
[一致/不一致] 理由:"""
    
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": short_prompt}],
        max_tokens=100,  # 限制输出长度
        timeout=3.0      # 3秒超时
    )
    
    return {"quick_verdict": response.choices[0].message.content}

错误 3:检索结果为空时的静默失败

问题描述:向量数据库连接超时或无结果时,系统返回空列表,但模型仍尝试生成「看似合理」的答案。

错误日志

VectorDBError: Connection timeout after 3000ms
Empty retrieval results for query: "某债券的清算优先级"
Model hallucinated: "根据文档,该债券优先级为第一顺位"

解决方案:在检索前增加健康检查,检索后增加结果校验。

def safe_retrieve(query: str, top_k: int = 5) -> list[RetrievedChunk]:
    """带防护的检索"""
    try:
        chunks = retrieve_with_citations(query, top_k)
        
        # 防护:检查结果是否为空或质量过低
        if not chunks:
            raise ValueError(f"检索结果为空:{query}")
        
        if chunks[0].similarity_score < 0.5:
            raise ValueError(f"检索质量过低(最高相似度:{chunks[0].similarity_score:.2%})")
        
        return chunks
        
    except Exception as e:
        # 记录错误并返回明确状态
        logger.error(f"检索失败: {str(e)}")
        return []  # 返回空列表触发上层降级逻辑

错误 4:多轮对话中上下文混淆

问题描述:多轮对话时,早期轮次的引用出现在后期回答中,导致溯源混乱。

解决方案:在每次生成时清空历史引用的上下文,并显式传递当前轮的检索结果。

def chat_with_isolation(history: list[dict], current_query: str) -> dict:
    """隔离历史上下文,每次只使用当前检索结果"""
    
    # 关键:不传递历史对话中的检索块
    current_chunks = retrieve_with_citations(current_query)
    
    # 只将当前轮检索结果作为上下文
    current_context = build_cited_context(current_chunks)
    
    # 历史对话只保留文本摘要,不保留完整 Chunk
    history_summary = "\n".join([
        f"用户:{h['user']}\n助手:{h['assistant'][:100]}..."  # 截断历史
        for h in history[-2:]  # 只保留最近2轮
    ])
    
    # 完整 Prompt
    full_prompt = f"""[历史对话摘要]
{history_summary}

[当前问题检索结果]
{current_context}

[当前问题]
{current_query}

请基于当前检索结果回答,当前问题必须只引用上述检索结果中的信息。"""

总结:控制幻觉的核心思路

经过多个生产项目的验证,我认为控制 RAG 幻觉的核心在于三层防线:

  1. 检索侧:提高检索质量阈值,宁可「答不上来」也不要「答错了」
  2. 生成侧:强制引用且只引用,限制模型的「创作空间」
  3. 验证侧:事后审计,用额外调用换取答案可信度

其中成本最高的是验证侧,但如果用 HolySheep AI 的 DeepSeek V3.2(¥0.42/M Token,汇率 ¥1=$1),每次验证成本不到 ¥0.0003,完全可以承受。我个人的经验是:省下的 API 成本,完全可以覆盖额外的验证调用次数,最终用更低的总成本换来了更高的答案质量。

完整代码已开源至 GitHub,有兴趣的读者可以自行部署测试。

👉 免费注册 HolySheep AI,获取首月赠额度