• 微信:WANCOME
  • 扫码加微信,提供专业咨询
  • 服务热线
  • 13215191218
    13027920428

  • 微信扫码访问本页
RAG引用优化
RAG 管道返回正确答案但引用页码错误且偶发幻觉如何优化?

RAG 管道返回正确答案但引用页码错误且偶发幻觉如何优化?

问题分析

RAG(检索增强生成)系统在实际应用中常出现一类棘手问题:模型生成的答案内容大体正确,但引用的页码与实际来源不符,或者模型"编造"了不存在的内容。这种现象被称为"幻觉"(hallucination),在 RAG 场景中尤其隐蔽。

引用页码错误的根源在于检索与生成的衔接层。向量相似度检索返回的是语义相关的文档片段,但这些片段可能分散在原文的不同位置。当 LLM 试图合成答案并标注来源时,可能出现位置混淆。例如,检索返回了第 5 页和第 15 页的内容,但模型错误地将第 5 页的内容标注为来自第 15 页。

幻觉问题在 RAG 中有特殊成因。当检索到的上下文不足以支撑完整答案时,模型倾向于用自己的预训练知识"填补空白"。这种填补可能正确,也可能错误,且用户难以分辨哪些是检索内容、哪些是模型推断。

Cross-Encoder 重排序可能加剧问题。虽然 Cross-Encoder 能提升检索精度,但如果重排序时没有保留原始元数据(如页码、段落号),排序后的结果丢失了位置信息。

LangGraph 的状态管理也可能引入问题。在多轮对话场景中,历史上下文的累积可能导致模型对来源记忆混乱,特别是当用户询问"刚才说的第三条来自哪里"时。

解决原理

优化引用准确性和减少幻觉需要多层面干预:

策略一:强化来源绑定

在构建索引时,将元数据(页码、段落号、文档标题)作为不可分割的部分注入到文本中。检索返回后,保留完整的元数据链路。在 Prompt 中明确要求模型逐句标注来源。

具体实现方式是在文档切分时添加特殊标记,如 [Page:5]...内容...[EndPage]。模型在生成时复制这些标记,确保内容与页码的绑定关系不会丢失。

策略二:分离检索与引用

采用两阶段生成:第一阶段生成答案内容,第二阶段为答案中的每个事实性陈述匹配引用来源。这种方式可以避免生成与引用的干扰。

实现时使用两个 LLM 调用:第一个生成答案,第二个输入答案和检索上下文,输出每个句子的来源标注。

策略三:引入验证机制

使用 NLI(自然语言推断)模型验证生成内容与检索上下文的一致性。如果模型生成的某句话在上下文中无法找到支撑证据,标记为"低置信度"或要求人工审核。

开源工具如 Ragas 可以自动化评估 RAG 输出的忠实度。

策略四:抑制幻觉的 Prompt 工程

明确告诉模型"如果上下文中没有答案,请回答'信息不足'而非推测"。使用系统提示词约束模型行为。

策略五:优化检索粒度

过大的切片会导致边界模糊,过小的切片丢失上下文。建议根据文档类型调整切片大小,技术文档通常 512-1024 tokens,法律合同可能需要更完整的段落。

程序实现与说明

"""
RAG 系统引用准确性优化实现
包含来源绑定、验证机制和幻觉抑制
"""

import re
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
import numpy as np

from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter


# ================== 数据结构定义 ==================

@dataclass
class DocumentChunk:
    """带元数据的文档切片"""
    content: str
    page_number: int
    paragraph_number: int
    source_doc: str
    start_char: int
    end_char: int


@dataclass
class CitedAnswer:
    """带引用标注的答案"""
    answer_text: str
    citations: List[Dict]
    confidence_scores: List[float]


# ================== 文档处理模块 ==================

class SourceAwareSplitter:
    """
    来源感知的文档切分器
    在切片中注入页码标记,确保来源可追溯
    """
    
    def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50):
        self.base_splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " "]
        )
    
    def split_with_metadata(
        self,
        text: str,
        page_mappings: Dict[int, Tuple[int, int]]
    ) -> List[Document]:
        """
        切分文本并附加页码元数据
        
        text: 原始文本
        page_mappings: {页码: (起始字符位置, 结束字符位置)}
        """
        # 基础切分
        raw_chunks = self.base_splitter.split_text(text)
        
        # 为每个切片查找所属页码
        documents = []
        current_pos = 0
        
        for chunk in raw_chunks:
            # 查找当前切片的字符范围
            chunk_start = text.find(chunk, current_pos)
            chunk_end = chunk_start + len(chunk)
            current_pos = chunk_start + 1  # 更新搜索起点
            
            # 确定页码(取主要位置)
            page_number = self._find_page(chunk_start, chunk_end, page_mappings)
            
            # 构建带标记的文本
            # 关键:在内容中嵌入页码信息
            marked_content = f"[来源:第{page_number}页]\n{chunk}"
            
            # 创建 Document 对象
            doc = Document(
                page_content=marked_content,
                metadata={
                    "page": page_number,
                    "char_range": (chunk_start, chunk_end)
                }
            )
            documents.append(doc)
        
        return documents
    
    def _find_page(
        self,
        start: int,
        end: int,
        page_mappings: Dict[int, Tuple[int, int]]
    ) -> int:
        """根据字符位置确定页码"""
        # 找到包含该切片大部分内容的页
        chunk_mid = (start + end) // 2
        
        for page, (page_start, page_end) in page_mappings.items():
            if page_start <= chunk_mid < page_end:
                return page
        
        return 1  # 默认第一页


# ================== RAG 管道 ==================

class HallucinationResistantRAG:
    """
    抗幻觉 RAG 系统
    包含来源验证和置信度评估
    """
    
    def __init__(self, model_name: str = "gpt-4o"):
        self.llm = ChatOpenAI(model=model_name, temperature=0)
        self.embeddings = OpenAIEmbeddings()
        self.vectorstore = None
        self.splitter = SourceAwareSplitter()
    
    def index_documents(self, documents: List[Document]):
        """索引文档"""
        self.vectorstore = Chroma.from_documents(
            documents=documents,
            embedding=self.embeddings
        )
        print(f"[索引完成] {len(documents)} 个文档片段")
    
    def query_with_citations(
        self,
        question: str,
        k: int = 5
    ) -> CitedAnswer:
        """
        执行带引用标注的查询
        两阶段生成:先答案,后标注
        """
        # 第一阶段:检索相关文档
        retrieved_docs = self.vectorstore.similarity_search(question, k=k)
        
        # 提取上下文
        context_text = self._format_context(retrieved_docs)
        
        # 第二阶段:生成答案(带来源约束)
        answer_prompt = ChatPromptTemplate.from_messages([
            ("system", """你是专业的问答助手。请根据提供的上下文回答问题。

重要规则:
1. 只使用上下文中的信息,不要使用外部知识
2. 每个事实性陈述后标注来源,格式为[第X页]
3. 如果上下文不足以回答,明确说明"信息不足"
4. 不要编造或推测任何内容

上下文:
{context}"""),
            ("human", "{question}")
        ])
        
        answer_chain = answer_prompt | self.llm | StrOutputParser()
        answer_text = answer_chain.invoke({
            "context": context_text,
            "question": question
        })
        
        # 第三阶段:验证并提取引用
        citations = self._extract_citations(answer_text, retrieved_docs)
        
        # 第四阶段:置信度评估
        confidence_scores = self._evaluate_confidence(
            answer_text,
            retrieved_docs
        )
        
        return CitedAnswer(
            answer_text=answer_text,
            citations=citations,
            confidence_scores=confidence_scores
        )
    
    def _format_context(self, docs: List[Document]) -> str:
        """格式化检索结果为上下文"""
        formatted = []
        for i, doc in enumerate(docs, 1):
            # 保留原文中的页码标记
            formatted.append(f"[文档{i}]\n{doc.page_content}\n")
        return "\n".join(formatted)
    
    def _extract_citations(
        self,
        answer: str,
        docs: List[Document]
    ) -> List[Dict]:
        """从答案中提取引用信息"""
        # 正则匹配页码引用
        pattern = r'\[第(\d+)页\]'
        matches = re.findall(pattern, answer)
        
        citations = []
        for page_str in matches:
            page = int(page_str)
            # 查找对应的文档片段
            matching_docs = [
                d for d in docs 
                if d.metadata.get('page') == page
            ]
            
            if matching_docs:
                citations.append({
                    "page": page,
                    "source_count": len(matching_docs),
                    "preview": matching_docs[0].page_content[:100] + "..."
                })
        
        return citations
    
    def _evaluate_confidence(
        self,
        answer: str,
        docs: List[Document]
    ) -> List[float]:
        """
        评估答案各部分的置信度
        使用句子级比对
        """
        # 简化实现:基于引用密度的启发式评估
        sentences = re.split(r'[。!?.!?]', answer)
        scores = []
        
        for sent in sentences:
            if not sent.strip():
                continue
            
            # 检查是否有引用标记
            has_citation = bool(re.search(r'\[第\d+页\]', sent))
            
            # 检查是否声明信息不足
            is_uncertain = "信息不足" in sent or "无法" in sent
            
            if is_uncertain:
                scores.append(1.0)  # 诚实声明不确定性
            elif has_citation:
                scores.append(0.8)  # 有引用,较高置信度
            else:
                scores.append(0.5)  # 无引用,中等置信度
        
        return scores


# ================== 幻觉检测器 ==================

class HallucinationDetector:
    """
    基于 NLI 的幻觉检测
    验证生成内容与检索上下文的一致性
    """
    
    def __init__(self):
        # 使用轻量级 NLI 模型
        # 实际应用中可替换为更大模型
        pass
    
    def check_claim(
        self,
        claim: str,
        context: str
    ) -> float:
        """
        检查陈述与上下文的一致性
        返回置信度分数(0-1)
        """
        # 简化实现:基于关键词覆盖
        # 实际应用应使用 NLI 模型
        
        claim_words = set(claim.lower().split())
        context_words = set(context.lower().split())
        
        # 计算重叠率
        overlap = claim_words & context_words
        coverage = len(overlap) / max(len(claim_words), 1)
        
        # 归一化到 0-1
        return min(coverage * 2, 1.0)
    
    def verify_answer(
        self,
        answer: str,
        context: str
    ) -> Dict:
        """验证完整答案"""
        sentences = re.split(r'[。!?.!?]', answer)
        results = []
        
        for sent in sentences:
            if not sent.strip():
                continue
            
            score = self.check_claim(sent, context)
            results.append({
                "sentence": sent,
                "confidence": score,
                "status": "高置信" if score > 0.7 else "低置信"
            })
        
        return {
            "sentence_results": results,
            "overall_confidence": np.mean([r["confidence"] for r in results])
        }


# ================== 使用示例 ==================

if __name__ == "__main__":
    # 构建示例文档
    sample_text = """
    [第1页]
    人工智能是计算机科学的一个分支,旨在创建能够模拟人类智能的系统。
    机器学习是人工智能的核心技术之一。
    
    [第2页]
    深度学习是机器学习的子领域,使用多层神经网络处理数据。
    卷积神经网络(CNN)是深度学习的重要架构。
    
    [第3页]
    自然语言处理(NLP)使计算机能够理解和生成人类语言。
    Transformer 架构彻底改变了 NLP 领域。
    """
    
    # 简化的页码映射
    page_mappings = {
        1: (0, 100),
        2: (100, 200),
        3: (200, 300)
    }
    
    # 实际应用时使用真正的文档和页码映射
    # rag = HallucinationResistantRAG()
    # rag.index_documents(documents)
    # result = rag.query_with_citations("什么是深度学习?")
    # print(result)

关键代码行解析:

marked_content = f"[来源:第{page_number}页]\n{chunk}":在文档切片中注入页码标记,这是确保引用准确的核心机制。LLM 在生成时会复制这些标记,形成内容与来源的绑定。

has_citation = bool(re.search(r'\[第\d+页\]', sent)):检测句子是否包含引用标记。无引用的句子被视为低置信度,可能需要人工审核。

if "信息不足" in sent: scores.append(1.0):模型诚实承认不确定性时给予高置信度。这鼓励模型"不知为不知",避免编造。

overlap = claim_words & context_words:计算陈述与上下文的词汇重叠。完全基于上下文的陈述重叠率高,编造内容的重叠率低。

优化建议总结:

  1. 在文档切分时注入页码标记,确保来源可追溯
  2. 使用两阶段生成:先生成答案,再匹配引用
  3. 引入 NLI 模型验证内容与上下文的一致性
  4. Prompt 中明确要求模型在信息不足时声明
  5. 为用户提供置信度指标,帮助判断可信度