第 70 章

Prompt Injection 防御:攻击向量 / 输入净化 / 内置分类器与防护架构设计

第七十章:幻觉检测与事实核查:让 Claude 的输出更可信

70.1 幻觉:LLM 的本质性挑战

在所有 AI 应用的可靠性问题中,**幻觉(Hallucination)**是工程师最头痛的一个。幻觉指的是 LLM 以高度自信的语气生成听起来合理但实际上错误的内容——可能是捏造的引用、不存在的 API 函数、错误的历史日期、或在统计数据上的信口开河。

幻觉问题的特殊之处在于它的欺骗性:与语法错误或明显的逻辑矛盾不同,幻觉内容往往在措辞、格式和表达风格上与正确信息无异。一个不知情的读者,甚至是有一定领域知识的读者,都可能被高质量的幻觉内容所误导。

幻觉的根本原因

从技术层面理解幻觉的成因,有助于设计更有针对性的防御措施:

训练数据偏见:LLM 被训练来生成"在统计上合理的"文本。如果训练数据中某个错误信息出现频率较高,模型就可能将其学习为"正确"答案。

自回归生成的累积误差:语言模型逐 token 生成文本,每一步的生成都基于之前已生成的内容。一旦某一步产生了轻微偏差,后续的生成会在这个偏差基础上继续展开,导致误差累积。

知识截止问题:Claude 的训练数据有知识截止日期,对截止日期后发生的事件缺乏信息,可能导致"推测填空"式的幻觉。

过度推广:模型可能将学到的模式应用到不适用的情况——如将知名作者的写作风格"推广"到该作者从未写过的作品上。

置信度校准问题:LLM 有时缺乏准确的"我不知道"能力,倾向于生成看似合理的内容而不是承认不确定性。

70.2 幻觉的分类体系

内在幻觉 vs 外在幻觉

内在幻觉(Intrinsic Hallucination) 与提供的源文档相矛盾的幻觉。在 RAG 系统中尤为常见:

# 示例:内在幻觉
# 提供的文档内容:
source_doc = "2024年第三季度,公司营收达到 1.2 亿元,同比增长 15%。"

# Claude 的幻觉输出(与源文档矛盾):
hallucinated_output = "根据文档,公司 2024 年第三季度营收为 1.8 亿元,增长率 20%。"
# 错误:金额和增长率均与源文档不符

外在幻觉(Extrinsic Hallucination) 无法从提供的源文档中验证的声明——信息可能是正确的,但无法从给定上下文中核实:

# 示例:外在幻觉
# 提供的文档只描述了公司营收数据
# Claude 的外在幻觉:
hallucinated_output = "该公司成立于 2008 年,总部位于上海,..." 
# 这些信息没有出现在源文档中,Claude 是在"填补"信息

按风险等级分类

在实际应用中,更实用的是按照幻觉的潜在危害程度分类:

高风险幻觉:
├── 医疗信息:错误的药物剂量、诊断、治疗建议
├── 法律信息:不存在的法规、错误的案例引用
├── 财务数据:捏造的统计数字、价格、财报数据
└── 引用伪造:不存在的论文、书籍、研究报告

中风险幻觉:
├── 历史事实:错误的日期、人物、事件顺序
├── 技术规格:不存在的 API、函数签名
└── 产品信息:不准确的功能描述、价格

低风险幻觉:
├── 创作性扩展:在已知信息基础上的合理推断
└── 风格性填充:措辞变化,但实质含义正确

70.3 检测幻觉的技术方法

方法一:自洽性检查(Self-Consistency Check)

基于同一问题多次采样,通过投票机制检测不一致性:

import anthropic
from collections import Counter
from typing import Optional

client = anthropic.Anthropic()

def consistency_check(
    question: str,
    n_samples: int = 5,
    temperature: float = 0.8
) -> dict:
    """
    通过多次采样检测 Claude 回答的自洽性
    高不一致性通常暗示存在幻觉风险
    """
    responses = []
    
    for _ in range(n_samples):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=512,
            temperature=temperature,
            messages=[{
                "role": "user",
                "content": question
            }]
        )
        responses.append(response.content[0].text)
    
    # 对于结构化问题,提取关键答案进行对比
    # 这里以数值提取为例
    import re
    
    numbers_found = []
    for resp in responses:
        # 提取数字
        nums = re.findall(r'\b\d+(?:\.\d+)?%?\b', resp)
        numbers_found.append(set(nums))
    
    # 计算不一致率
    if numbers_found:
        all_nums = [num for s in numbers_found for num in s]
        num_counter = Counter(all_nums)
        consistency_score = max(num_counter.values()) / n_samples
    else:
        consistency_score = 1.0  # 无数值型声明,无法判断
    
    return {
        "consistency_score": consistency_score,
        "responses": responses,
        "is_reliable": consistency_score >= 0.8,
        "warning": "Low consistency detected" if consistency_score < 0.6 else None
    }

方法二:引用验证(Citation Verification)

对于 RAG 系统,验证 Claude 的输出是否真实地引用了提供的文档:

def verify_citations(
    model_output: str,
    source_documents: list,
    similarity_threshold: float = 0.75
) -> dict:
    """
    验证模型输出中的声明是否有源文档支撑
    """
    from sentence_transformers import SentenceTransformer, util
    
    encoder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
    
    # 将输出按句子拆分
    import nltk
    sentences = nltk.sent_tokenize(model_output)
    
    verification_results = []
    
    for sentence in sentences:
        # 跳过非陈述性句子
        if len(sentence) < 20 or sentence.endswith('?'):
            continue
        
        sentence_embedding = encoder.encode(sentence)
        
        best_match = None
        best_score = 0
        
        for doc in source_documents:
            doc_sentences = nltk.sent_tokenize(doc["content"])
            
            for doc_sentence in doc_sentences:
                doc_embedding = encoder.encode(doc_sentence)
                score = float(util.cos_sim(sentence_embedding, doc_embedding))
                
                if score > best_score:
                    best_score = score
                    best_match = {
                        "source": doc["source"],
                        "matching_text": doc_sentence,
                        "similarity": score
                    }
        
        verification_results.append({
            "claim": sentence,
            "supported": best_score >= similarity_threshold,
            "confidence": best_score,
            "best_match": best_match
        })
    
    unsupported_claims = [r for r in verification_results if not r["supported"]]
    
    return {
        "total_claims": len(verification_results),
        "unsupported_claims": len(unsupported_claims),
        "reliability_score": 1 - (len(unsupported_claims) / max(len(verification_results), 1)),
        "details": verification_results
    }

方法三:使用 Claude 自我评估

Claude 可以被用来评估自己或另一个模型输出的可靠性:

def hallucination_self_eval(
    original_question: str,
    model_response: str,
    source_context: Optional[str] = None
) -> dict:
    """
    使用 Claude 评估输出中潜在的幻觉
    """
    
    context_section = ""
    if source_context:
        context_section = f"""
参考来源(应作为事实依据):
{source_context}

---
"""
    
    eval_prompt = f"""你是一个专业的事实核查助手。请评估以下 AI 回答的可靠性。

{context_section}
问题:{original_question}

AI 回答:{model_response}

请完成以下任务:
1. 识别回答中所有具体的事实性声明(数字、日期、人名、引用等)
2. 对每个声明评估:
   - 是否可以从参考来源中验证(如果有)
   - 是否是明显的常识性知识(高可信度)
   - 是否是具体的、不易验证的声明(高风险)
3. 给出整体可信度评分(0-10)

以 JSON 格式输出:
{{
  "factual_claims": [
    {{
      "claim": "具体声明文本",
      "verification_status": "verified|unverified|likely_correct|suspicious",
      "risk_level": "high|medium|low",
      "reason": "判断依据"
    }}
  ],
  "overall_credibility_score": 0-10,
  "main_concerns": ["主要风险点"],
  "recommendation": "safe_to_use|use_with_caution|needs_verification|do_not_use"
}}"""
    
    eval_response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=2048,
        temperature=0.1,
        messages=[{"role": "user", "content": eval_prompt}]
    )
    
    import json
    try:
        return json.loads(eval_response.content[0].text)
    except json.JSONDecodeError:
        return {"error": "Failed to parse evaluation response"}

70.4 Grounding 技术:减少幻觉的 Prompt 工程

显式的不确定性表达

通过 prompt 指令,要求 Claude 明确表达不确定性:

def create_grounded_prompt(question: str, sources: list) -> str:
    """
    构建能减少幻觉的 grounded prompt
    """
    sources_text = "\n\n".join([
        f"[来源 {i+1}: {s['title']}]\n{s['content']}"
        for i, s in enumerate(sources)
    ])
    
    return f"""请严格根据以下提供的来源资料回答问题。

来源资料:
{sources_text}

问题:{question}

回答要求:
1. 只陈述能从来源资料中直接支持的内容
2. 对每个重要声明,使用"根据来源 X"或"来源 X 指出"等归因表达
3. 如果问题的某部分无法从来源资料中找到答案,明确说明"来源资料中未提及此信息"
4. 不要添加来源中没有的信息,即使你认为这些信息是正确的
5. 如果对某个内容不确定,使用"根据来源资料,似乎..."或"来源资料暗示..."等措辞

请开始回答:"""

结构化输出要求

将输出结构化可以让幻觉更容易被检测:

def structured_factual_query(
    question: str,
    require_citations: bool = True
) -> str:
    citation_instruction = """
每个事实性声明后必须附上引用标注:[需要核实] 或 [常识/公开知识] 或 [来源: 具体来源名称]
""" if require_citations else ""
    
    return f"""请以结构化方式回答以下问题:

问题:{question}

输出格式:
{{
  "direct_answer": "一句话直接回答",
  "supporting_facts": [
    {{
      "fact": "支持性事实陈述",
      "confidence": "high|medium|low",
      "source_type": "verified_source|common_knowledge|inference|uncertain"
    }}
  ],
  "caveats": ["重要注意事项或不确定性说明"],
  "information_gaps": ["无法确认或回答的方面"]
}}

{citation_instruction}

请保持保守:对于任何你不完全确定的信息,优先选择 low confidence 而不是 medium 或 high。"""

不知道就说不知道

一个常被忽视但非常有效的技巧是明确允许和鼓励 Claude 表达不知道

CALIBRATION_SYSTEM_PROMPT = """你是一个高度重视准确性的助手。

关于你的不确定性:
- 当你不确定某个事实时,明确说"我不确定"或"我的信息可能不是最新的"
- 当你的知识截止日期可能影响回答时,主动提醒用户
- 不要为了显得知识渊博而猜测具体的数字、日期或名称
- 对于专业领域(医疗、法律、金融),即使你有相关知识,也建议用户寻求专业人士意见

当你不知道时:
- 直接说"我没有关于这个的可靠信息"
- 或者说"这个问题我无法给出确定的答案,建议查阅[具体资源]"
- 而不是编造听起来合理的答案

你宁可让用户觉得你知识有限,也不要让用户因为你的错误信息而做出错误决定。"""

70.5 引用要求系统

强制引用格式

对于需要高可信度输出的场景,建立强制引用机制:

class CitationRequirementSystem:
    """
    强制 Claude 为每个重要声明提供引用或可信度标注
    """
    
    CITATION_SYSTEM_PROMPT = """你必须遵循以下引用规则:

1. 对于每个事实性声明,在句末添加引用标注:
   - [已验证: {来源}] — 信息来自提供的文档
   - [公开知识] — 广为人知的事实
   - [推断] — 基于逻辑推断
   - [不确定] — 无法确认准确性

2. 如果某项信息无法从提供的上下文中找到,必须说明:
   "此信息不在提供的资料中,以下是我的知识范围内的内容,请独立核实:..."

3. 绝对不允许省略引用标注来让回答看起来更流畅。"""
    
    def process_with_citations(
        self,
        query: str,
        context_docs: list
    ) -> dict:
        """处理查询并验证引用完整性"""
        
        # 构建带引用要求的提示
        docs_text = self._format_docs(context_docs)
        
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=2048,
            system=self.CITATION_SYSTEM_PROMPT,
            messages=[{
                "role": "user",
                "content": f"参考资料:\n{docs_text}\n\n问题:{query}"
            }]
        )
        
        raw_output = response.content[0].text
        
        # 解析引用标注
        citation_analysis = self._analyze_citations(raw_output)
        
        return {
            "output": raw_output,
            "citation_coverage": citation_analysis["coverage"],
            "uncited_sentences": citation_analysis["uncited"],
            "reliability_assessment": self._assess_reliability(citation_analysis)
        }
    
    def _analyze_citations(self, text: str) -> dict:
        """分析文本中的引用覆盖率"""
        import re
        
        citation_pattern = r'\[(已验证|公开知识|推断|不确定)[^\]]*\]'
        sentences = [s.strip() for s in text.split('.') if len(s.strip()) > 20]
        
        cited = [s for s in sentences if re.search(citation_pattern, s)]
        uncited = [s for s in sentences if not re.search(citation_pattern, s)]
        
        return {
            "coverage": len(cited) / max(len(sentences), 1),
            "cited": cited,
            "uncited": uncited
        }

70.6 生产系统中的幻觉管控

高风险场景的双重验证

class HighStakesResponsePipeline:
    """
    高风险场景(医疗、法律、财务)的双重验证流水线
    """
    
    async def generate_verified_response(
        self,
        query: str,
        domain: str,
        sources: list
    ) -> dict:
        """
        生成 + 验证双重流水线
        """
        
        # 步骤 1:生成初始回答(较高温度,强制引用)
        initial_response = await self.generate_with_citations(query, sources)
        
        # 步骤 2:自我批评(不同温度,专注于发现问题)
        critique = await self.self_critique(
            question=query,
            response=initial_response["output"],
            sources=sources
        )
        
        # 步骤 3:基于批评修正
        if critique["issues_found"]:
            revised_response = await self.revise_response(
                original_response=initial_response["output"],
                critique=critique["critique"],
                sources=sources
            )
        else:
            revised_response = initial_response["output"]
        
        # 步骤 4:最终风险评估
        risk_score = self.calculate_risk_score(
            domain=domain,
            response=revised_response,
            citation_coverage=initial_response["citation_coverage"]
        )
        
        return {
            "response": revised_response,
            "risk_score": risk_score,
            "requires_human_review": risk_score > 0.7,
            "verification_steps_completed": 4
        }
    
    def calculate_risk_score(
        self,
        domain: str,
        response: str,
        citation_coverage: float
    ) -> float:
        """计算输出的风险分数"""
        
        base_risk = {
            "medical": 0.8,
            "legal": 0.7,
            "financial": 0.6,
            "general": 0.3
        }.get(domain, 0.5)
        
        # 引用覆盖率越高,风险越低
        citation_factor = 1 - (citation_coverage * 0.4)
        
        # 检查是否有数字声明(数字更容易被幻觉化)
        import re
        num_count = len(re.findall(r'\b\d+(?:\.\d+)?%?\b', response))
        numeric_factor = 1 + (num_count * 0.02)  # 每个数字增加 2% 风险
        
        return min(base_risk * citation_factor * numeric_factor, 1.0)

监控与告警

# 在生产系统中记录幻觉检测结果,建立质量监控

def log_hallucination_metrics(
    request_id: str,
    domain: str,
    citation_coverage: float,
    consistency_score: float,
    risk_score: float
):
    metrics = {
        "request_id": request_id,
        "domain": domain,
        "citation_coverage": citation_coverage,
        "consistency_score": consistency_score,
        "risk_score": risk_score,
        "timestamp": datetime.utcnow().isoformat()
    }
    
    # 记录到监控系统
    monitoring_client.record(metrics)
    
    # 高风险告警
    if risk_score > 0.8:
        alert_system.send_alert(
            severity="HIGH",
            message=f"High hallucination risk detected in {domain} response",
            details=metrics
        )

小结

幻觉是 LLM 应用中最需要系统性应对的可靠性问题。理解幻觉的根本成因(统计生成的本质、知识截止、置信度校准不足),是设计有效防御的前提。

检测层面,自洽性检查、引用验证和自我评估是三种互补的方法。预防层面,Grounding 技术、结构化输出和强制引用要求能显著降低幻觉率。在高风险场景(医疗、法律、财务),应建立生成-批评-修正的多步验证流水线,并结合人工审核兜底。

最终,消除幻觉不是目标——完全无幻觉的 LLM 目前并不存在。目标是将幻觉的发生率和风险控制在可接受的范围内,并为每个输出提供可测量的可信度指标。

本章评分
4.5  / 5  (3 评分)

💬 留言讨论