第十一章:RAG知识库开发之【RAG 的缺陷分析与优化:从入门到实践的完全指南】

目录

前言

一、文档加载准确性和效率

[1.1 问题描述](#1.1 问题描述)

[1.2 真实案例](#1.2 真实案例)

[1.3 优化方案](#1.3 优化方案)

方案一:选择合适的解析器

方案二:并行加载与增量更新

[1.4 最佳实践总结](#1.4 最佳实践总结)

二、文档切分的粒度

[2.1 问题描述](#2.1 问题描述)

[2.2 常见切分方法对比](#2.2 常见切分方法对比)

[2.3 高级优化:自适应切分](#2.3 高级优化:自适应切分)

[2.4 最佳实践建议](#2.4 最佳实践建议)

三、错过排名靠前的文档

[3.1 问题本质](#3.1 问题本质)

[3.2 混合检索策略](#3.2 混合检索策略)

[3.3 查询改写与扩展](#3.3 查询改写与扩展)

[3.4 多路召回融合策略](#3.4 多路召回融合策略)

[3.5 最佳实践总结](#3.5 最佳实践总结)

四、格式错误

[4.1 常见格式问题](#4.1 常见格式问题)

[4.2 格式修复与标准化](#4.2 格式修复与标准化)

[4.3 高级:基于LLM的格式修复](#4.3 高级:基于LLM的格式修复)

[4.4 最佳实践](#4.4 最佳实践)

五、答案不完整

[5.1 问题分析](#5.1 问题分析)

[5.2 多步推理与上下文扩展](#5.2 多步推理与上下文扩展)

[5.3 答案完整性验证](#5.3 答案完整性验证)

[5.4 最佳实践](#5.4 最佳实践)

六、未提取到答案

[6.1 问题场景](#6.1 问题场景)

[6.2 多文档推理与证据链](#6.2 多文档推理与证据链)

[6.3 处理"未找到"的情况](#6.3 处理"未找到"的情况)

[6.4 最佳实践](#6.4 最佳实践)

七、答案太具体或者太笼统

[7.1 问题描述](#7.1 问题描述)

[7.2 自适应答案粒度](#7.2 自适应答案粒度)

[7.3 用户意图理解与个性化](#7.3 用户意图理解与个性化)

[7.4 最佳实践](#7.4 最佳实践)

八、幻觉问题

[8.1 幻觉的类型](#8.1 幻觉的类型)

[8.2 幻觉检测与缓解](#8.2 幻觉检测与缓解)

[8.3 最佳实践总结](#8.3 最佳实践总结)

第九章:练习题及其答案

[9.1 选择题(每题10分)](#9.1 选择题(每题10分))

[9.2 填空题(每题5分)](#9.2 填空题(每题5分))

[9.3 简答题(每题15分)](#9.3 简答题(每题15分))

[9.4 实操题(30分)](#9.4 实操题(30分))

第十章:总结


前言

检索增强生成(Retrieval-Augmented Generation, RAG)已经成为当前大语言模型应用中最热门的技术架构之一。它通过为 LLM 提供外部知识库的检索结果,显著缓解了模型幻觉、知识截止日期限制等问题。然而,RAG 并非银弹------在实际生产环境中,RAG 系统面临着诸多挑战:文档加载不准确、切分粒度不当、检索遗漏、格式解析错误、答案不完整或过于泛化,乃至最令人头疼的幻觉问题。

本文面向 AI 爱好者和各层级开发人员,旨在系统梳理 RAG 系统在实际落地中常见的 8 大类缺陷,并提供可操作的优化方案。我们将从文档加载这一基础环节讲起,逐步深入到切分策略、检索排序、答案生成等核心模块。每节配有代码示例和最佳实践建议,最后一章提供配套练习题以巩固学习成果。

通过阅读本文,你将能够诊断现有 RAG 系统的性能瓶颈,并有针对性地进行优化,将检索准确率从 60% 提升至 90% 以上。无论你是前端、后端还是运维工程师,都能从中获得可直接应用到工作中的技术方案。


一、文档加载准确性和效率

1.1 问题描述

RAG 的第一步是将外部文档加载到系统中。看似简单的步骤却隐藏着大量陷阱:

  • 格式多样性:PDF、Word、HTML、Markdown、扫描件(图像型 PDF)等格式需要不同的解析器

  • 布局干扰:多列排版、页眉页脚、表格、水印会破坏文本连续性

  • 编码问题:中文乱码、特殊符号、emoji 导致解析失败

  • 效率瓶颈:单线程加载大文件耗时过长,内存占用过高

1.2 真实案例

某法律文档 RAG 系统在处理一份 500 页的判决书 PDF 时,由于 PDF 中包含表格和脚注,使用默认的 PyPDF2 解析后,表格内容完全错位,脚注插入到了正文中间,导致检索时无法定位正确的法律条款。

1.3 优化方案

方案一:选择合适的解析器
python 复制代码
# 需要安装: pip install pypdf pdfplumber unstructured[pdf] pymupdf

import pdfplumber
import fitz  # PyMuPDF
from unstructured.partition.pdf import partition_pdf
import time
from typing import List, Dict

class PDFLoader:
    """
    多策略PDF加载器,支持不同格式的PDF文件
    """
    
    def __init__(self, strategy: str = "auto"):
        """
        初始化加载器
        :param strategy: 解析策略 - "auto", "pdfplumber", "pymupdf", "unstructured"
        """
        self.strategy = strategy
    
    def load_with_pdfplumber(self, file_path: str) -> str:
        """
        使用pdfplumber解析PDF,擅长处理表格和复杂布局
        """
        text = []
        with pdfplumber.open(file_path) as pdf:
            for page_num, page in enumerate(pdf.pages, 1):
                # 提取普通文本
                page_text = page.extract_text()
                if page_text:
                    text.append(f"--- Page {page_num} ---\n{page_text}")
                
                # 提取表格(法律文档中常见)
                tables = page.extract_tables()
                for table in tables:
                    if table:
                        table_text = "\n".join([
                            " | ".join([str(cell) if cell else "" for cell in row])
                            for row in table
                        ])
                        text.append(f"[Table on page {page_num}]:\n{table_text}")
        return "\n\n".join(text)
    
    def load_with_pymupdf(self, file_path: str) -> str:
        """
        使用PyMuPDF解析,速度最快,适合纯文本PDF
        """
        doc = fitz.open(file_path)
        text = []
        for page_num, page in enumerate(doc, 1):
            page_text = page.get_text()
            if page_text.strip():
                text.append(f"--- Page {page_num} ---\n{page_text}")
        doc.close()
        return "\n\n".join(text)
    
    def load_with_unstructured(self, file_path: str) -> str:
        """
        使用unstructured库,自动识别文档结构
        """
        elements = partition_pdf(
            filename=file_path,
            strategy="hi_res",  # 高分辨率策略,处理复杂布局
            extract_images_in_pdf=False,
        )
        text = "\n\n".join([str(el) for el in elements])
        return text
    
    def load(self, file_path: str) -> Dict:
        """
        根据策略加载PDF,返回文本和元数据
        """
        start_time = time.time()
        
        try:
            if self.strategy == "pdfplumber":
                text = self.load_with_pdfplumber(file_path)
            elif self.strategy == "pymupdf":
                text = self.load_with_pymupdf(file_path)
            elif self.strategy == "unstructured":
                text = self.load_with_unstructured(file_path)
            else:  # auto模式:优先pdfplumber,失败回退pymupdf
                try:
                    text = self.load_with_pdfplumber(file_path)
                except Exception as e:
                    print(f"pdfplumber failed: {e}, falling back to pymupdf")
                    text = self.load_with_pymupdf(file_path)
        
        except Exception as e:
            raise RuntimeError(f"Failed to load PDF {file_path}: {e}")
        
        elapsed_time = time.time() - start_time
        return {
            "text": text,
            "pages": text.count("--- Page"),
            "characters": len(text),
            "load_time_seconds": elapsed_time
        }

# 使用示例
if __name__ == "__main__":
    loader = PDFLoader(strategy="auto")
    result = loader.load("sample_document.pdf")
    print(f"Loaded {result['pages']} pages, {result['characters']} chars in {result['load_time_seconds']:.2f}s")
方案二:并行加载与增量更新
python 复制代码
from concurrent.futures import ThreadPoolExecutor, as_completed
import hashlib
import os
import json
from pathlib import Path
from typing import List, Optional

class IncrementalDocumentLoader:
    """
    支持增量更新和并行加载的文档加载器
    """
    
    def __init__(self, cache_dir: str = ".cache/docs"):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.loader = PDFLoader(strategy="auto")
    
    def get_file_hash(self, file_path: str) -> str:
        """计算文件MD5,用于检测变更"""
        hasher = hashlib.md5()
        with open(file_path, 'rb') as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hasher.update(chunk)
        return hasher.hexdigest()
    
    def load_single_file(self, file_path: str, force_reload: bool = False) -> Optional[Dict]:
        """
        加载单个文件,支持缓存
        """
        file_path = Path(file_path)
        cache_file = self.cache_dir / f"{file_path.stem}_{self.get_file_hash(str(file_path))}.json"
        
        # 检查缓存
        if not force_reload and cache_file.exists():
            with open(cache_file, 'r', encoding='utf-8') as f:
                print(f"Loading from cache: {file_path.name}")
                return json.load(f)
        
        # 实际加载
        try:
            result = self.loader.load(str(file_path))
            result["file_name"] = file_path.name
            result["file_path"] = str(file_path)
            
            # 保存缓存
            with open(cache_file, 'w', encoding='utf-8') as f:
                json.dump(result, f, ensure_ascii=False, indent=2)
            
            print(f"Loaded and cached: {file_path.name}")
            return result
        except Exception as e:
            print(f"Failed to load {file_path.name}: {e}")
            return None
    
    def load_multiple_files(self, file_paths: List[str], max_workers: int = 4) -> List[Dict]:
        """
        并行加载多个文件
        """
        results = []
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            future_to_file = {
                executor.submit(self.load_single_file, file_path): file_path 
                for file_path in file_paths
            }
            
            for future in as_completed(future_to_file):
                file_path = future_to_file[future]
                try:
                    result = future.result()
                    if result:
                        results.append(result)
                except Exception as e:
                    print(f"Error loading {file_path}: {e}")
        
        return results

# 使用示例
loader = IncrementalDocumentLoader()
files = ["doc1.pdf", "doc2.pdf", "doc3.pdf"]
loaded_docs = loader.load_multiple_files(files, max_workers=3)

1.4 最佳实践总结

文档类型 推荐工具 注意事项
文本型 PDF PyMuPDF 速度快,占用内存小
表格密集型 PDF pdfplumber 表格提取准确
扫描件/图片 PDF OCR + unstructured 需要配合 Tesseract
HTML BeautifulSoup + readability 去除导航栏、广告
Markdown markdown库 保留标题层级

核心原则

  1. 始终保留文档元数据(页码、章节、来源文件)

  2. 实现缓存机制,避免重复解析

  3. 大文件(>100MB)采用流式读取

  4. 扫描型 PDF 必须先进行 OCR 识别


二、文档切分的粒度

2.1 问题描述

文档切分是 RAG 中最关键的决策点之一:

  • 切分过粗:单个 chunk 包含太多无关信息,检索时噪音大,且超出 LLM 上下文窗口

  • 切分过细:丢失上下文连贯性,答案可能只看到片段而误解原意

  • 切分边界不当:在句子中间或段落中间切断,破坏语义完整性

2.2 常见切分方法对比

python 复制代码
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    SentenceTransformersTokenTextSplitter,
    MarkdownHeaderTextSplitter,
    PythonCodeTextSplitter
)
import nltk
from typing import List, Dict
import re

class SmartTextSplitter:
    """
    智能文本切分器,支持多种策略和语义边界检测
    """
    
    def __init__(self, strategy: str = "recursive"):
        self.strategy = strategy
        
        # 下载NLTK的句子分割模型(首次运行)
        try:
            nltk.data.find('tokenizers/punkt')
        except LookupError:
            nltk.download('punkt')
    
    def recursive_split(self, text: str, chunk_size: int = 500, chunk_overlap: int = 50) -> List[str]:
        """
        递归字符切分 - 最常用的策略
        优先按段落切,再按句子切,最后按字符切
        """
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
            length_function=len,
        )
        return splitter.split_text(text)
    
    def semantic_split(self, text: str, max_chunk_size: int = 800) -> List[str]:
        """
        语义切分 - 基于句子边界和主题连贯性
        保持完整段落和句子,避免在句中切断
        """
        # 先按段落分割
        paragraphs = re.split(r'\n\s*\n', text)
        
        chunks = []
        current_chunk = []
        current_length = 0
        
        for para in paragraphs:
            # 按句子分割段落
            sentences = nltk.sent_tokenize(para)
            para_sentences = []
            para_length = 0
            
            for sent in sentences:
                sent_len = len(sent)
                
                # 如果单个句子就超过限制,强制切分
                if sent_len > max_chunk_size:
                    # 先保存当前chunk
                    if current_chunk:
                        chunks.append(' '.join(current_chunk))
                        current_chunk = []
                        current_length = 0
                    
                    # 长句子按从句切分(简单实现:按逗号)
                    sub_sentences = re.split(r'[,,]', sent)
                    sub_chunk = []
                    sub_len = 0
                    for sub in sub_sentences:
                        if sub_len + len(sub) < max_chunk_size:
                            sub_chunk.append(sub)
                            sub_len += len(sub)
                        else:
                            if sub_chunk:
                                chunks.append(','.join(sub_chunk))
                            sub_chunk = [sub]
                            sub_len = len(sub)
                    if sub_chunk:
                        chunks.append(','.join(sub_chunk))
                    continue
                
                # 正常句子累积
                if current_length + sent_len > max_chunk_size and current_chunk:
                    # 当前chunk已满,保存并开始新的
                    chunks.append(' '.join(current_chunk))
                    current_chunk = [sent]
                    current_length = sent_len
                else:
                    current_chunk.append(sent)
                    current_length += sent_len
            
            # 段落结束,强制保存(保持段落边界)
            if current_chunk:
                chunks.append(' '.join(current_chunk))
                current_chunk = []
                current_length = 0
        
        return chunks
    
    def markdown_split(self, markdown_text: str) -> List[Dict]:
        """
        Markdown结构化切分 - 保留标题层级
        返回带元数据的chunks
        """
        splitter = MarkdownHeaderTextSplitter(
            headers_to_split_on=[
                ("#", "Header1"),
                ("##", "Header2"),
                ("###", "Header3"),
            ]
        )
        splits = splitter.split_text(markdown_text)
        
        result = []
        for split in splits:
            result.append({
                "content": split.page_content,
                "metadata": split.metadata,
                "header_path": " > ".join(split.metadata.values())
            })
        
        return result
    
    def code_split(self, code: str, language: str = "python") -> List[str]:
        """
        代码切分 - 保持函数/类的完整性
        """
        if language == "python":
            splitter = PythonCodeTextSplitter(chunk_size=500, chunk_overlap=50)
        else:
            # 通用代码切分
            splitter = RecursiveCharacterTextSplitter(
                chunk_size=500,
                chunk_overlap=50,
                separators=["\nclass ", "\ndef ", "\nfunction ", "\n\n", "\n", " "],
            )
        return splitter.split_text(code)

# 使用示例
text = """机器学习是人工智能的一个子领域。它专注于开发能够从数据中学习的算法。

深度学习是机器学习的一个分支,使用多层神经网络。它在图像识别和自然语言处理方面取得了突破性进展。

迁移学习允许模型将从一个任务学到的知识应用到另一个相关任务。这大大减少了训练所需的数据量。"""

splitter = SmartTextSplitter()
chunks = splitter.semantic_split(text, max_chunk_size=100)
for i, chunk in enumerate(chunks):
    print(f"Chunk {i}: {chunk}")

2.3 高级优化:自适应切分

python 复制代码
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

class AdaptiveTextSplitter:
    """
    自适应切分器 - 根据内容复杂度动态调整chunk大小
    """
    
    def __init__(self, base_chunk_size: int = 500, similarity_threshold: float = 0.3):
        self.base_chunk_size = base_chunk_size
        self.similarity_threshold = similarity_threshold
        self.vectorizer = TfidfVectorizer(stop_words='english')
    
    def calculate_complexity(self, text: str) -> float:
        """
        计算文本复杂度:
        - 术语密度(罕见词比例)
        - 句子长度
        - 段落嵌套深度
        """
        sentences = nltk.sent_tokenize(text)
        if not sentences:
            return 0.5
        
        # 平均句子长度
        avg_sent_len = np.mean([len(s.split()) for s in sentences])
        
        # 罕见词比例(假设词频<3为罕见)
        words = text.lower().split()
        word_freq = {}
        for w in words:
            word_freq[w] = word_freq.get(w, 0) + 1
        rare_ratio = sum(1 for freq in word_freq.values() if freq < 3) / max(1, len(word_freq))
        
        # 复杂度 = 句子长度归一化 * 0.5 + 罕见词比例 * 0.5
        complexity = (min(avg_sent_len / 30, 1.0) * 0.5 + rare_ratio * 0.5)
        return complexity
    
    def find_optimal_boundaries(self, text: str) -> List[int]:
        """
        寻找最优切分边界(基于语义相似度)
        返回切分点的字符索引
        """
        sentences = nltk.sent_tokenize(text)
        if len(sentences) <= 2:
            return []
        
        # 计算句子间的语义相似度
        tfidf_matrix = self.vectorizer.fit_transform(sentences)
        similarities = []
        
        for i in range(len(sentences) - 1):
            sim = cosine_similarity(tfidf_matrix[i:i+1], tfidf_matrix[i+1:i+2])[0][0]
            similarities.append(sim)
        
        # 相似度骤降的地方作为切分点
        boundaries = []
        for i, sim in enumerate(similarities):
            if sim < self.similarity_threshold:
                # 找到对应句子的结束位置
                boundary_pos = text.find(sentences[i]) + len(sentences[i])
                boundaries.append(boundary_pos)
        
        return boundaries
    
    def split(self, text: str) -> List[str]:
        """
        自适应切分主函数
        """
        complexity = self.calculate_complexity(text)
        
        # 复杂内容用更小的chunk,简单内容用更大的chunk
        adaptive_size = int(self.base_chunk_size * (1.5 - complexity))
        adaptive_size = max(200, min(adaptive_size, 1500))  # 限制范围
        
        print(f"Complexity: {complexity:.2f}, Adaptive chunk size: {adaptive_size}")
        
        # 结合语义边界和长度限制
        semantic_boundaries = self.find_optimal_boundaries(text)
        
        chunks = []
        start = 0
        
        for boundary in semantic_boundaries:
            chunk_text = text[start:boundary].strip()
            if len(chunk_text) <= adaptive_size * 1.5:  # 允许一定弹性
                if chunk_text:
                    chunks.append(chunk_text)
                start = boundary
        
        # 处理剩余文本
        remaining = text[start:].strip()
        if remaining:
            # 如果剩余文本过长,降级到递归切分
            if len(remaining) > adaptive_size:
                recursive_splitter = RecursiveCharacterTextSplitter(
                    chunk_size=adaptive_size,
                    chunk_overlap=50
                )
                chunks.extend(recursive_splitter.split_text(remaining))
            else:
                chunks.append(remaining)
        
        return chunks

# 使用示例
adaptive_splitter = AdaptiveTextSplitter(base_chunk_size=500)
complex_text = "..."  # 包含大量专业术语的文本
chunks = adaptive_splitter.split(complex_text)
print(f"Original text length: {len(complex_text)}, Chunks: {len(chunks)}")

2.4 最佳实践建议

应用场景 推荐策略 Chunk大小 Overlap
问答系统 语义切分 300-500 tokens 50-100
摘要生成 段落切分 800-1200 tokens 100-150
代码检索 函数级切分 200-400 tokens 20-50
长文档分析 递归+语义混合 500-800 tokens 80-100

黄金法则

  1. Overlap 应设置为 chunk_size 的 10-20%,避免关键信息恰好在边界丢失

  2. 始终保留父文档引用,支持答案溯源到原始位置

  3. 对特殊格式(表格、代码、公式)使用专用切分器


三、错过排名靠前的文档

3.1 问题本质

检索阶段,最相关的文档可能不在 Top-K 结果中。原因包括:

  • 语义漂移:查询和文档使用不同术语表达同一概念(如 "AI" vs "机器学习")

  • embedding 模型缺陷:对领域特定术语的表示能力不足

  • 检索粒度错配:查询是问题,但答案分布在多个chunk中

  • 排序算法偏差:只依赖向量相似度,忽略文档质量、时效性等因素

3.2 混合检索策略

python 复制代码
from typing import List, Tuple
import numpy as np
from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import jieba  # 中文分词

class HybridRetriever:
    """
    混合检索器 - 结合向量检索和关键词检索
    """
    
    def __init__(self, embedding_model_name: str = "BAAI/bge-small-zh-v1.5"):
        """
        初始化混合检索器
        :param embedding_model_name: 嵌入模型名称
        """
        self.embedding_model = SentenceTransformer(embedding_model_name)
        self.bm25_index = None
        self.chunks = []
        self.chunk_embeddings = None
    
    def build_index(self, chunks: List[str]):
        """
        构建检索索引(向量 + BM25)
        """
        self.chunks = chunks
        
        # 1. 构建向量索引
        print("Building vector index...")
        self.chunk_embeddings = self.embedding_model.encode(
            chunks, 
            normalize_embeddings=True,  # 归一化,便于计算余弦相似度
            show_progress_bar=True
        )
        
        # 2. 构建BM25索引(中文需要分词)
        print("Building BM25 index...")
        tokenized_chunks = [list(jieba.cut(chunk)) for chunk in chunks]
        self.bm25_index = BM25Okapi(tokenized_chunks)
        
        print(f"Index built: {len(chunks)} chunks")
    
    def vector_search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
        """
        向量检索
        """
        query_embedding = self.embedding_model.encode(
            [query], 
            normalize_embeddings=True
        )[0]
        
        # 计算余弦相似度
        similarities = np.dot(self.chunk_embeddings, query_embedding)
        
        # 获取top_k索引
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        
        return [(idx, similarities[idx]) for idx in top_indices]
    
    def bm25_search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
        """
        BM25关键词检索
        """
        tokenized_query = list(jieba.cut(query))
        scores = self.bm25_index.get_scores(tokenized_query)
        
        top_indices = np.argsort(scores)[-top_k:][::-1]
        return [(idx, scores[idx]) for idx in top_indices]
    
    def hybrid_search(
        self, 
        query: str, 
        top_k: int = 5,
        vector_weight: float = 0.5,
        rerank: bool = True
    ) -> List[Tuple[int, float, Dict]]:
        """
        混合检索 - 加权融合向量和BM25结果
        
        :param query: 查询文本
        :param top_k: 返回数量
        :param vector_weight: 向量相似度权重(0-1)
        :param rerank: 是否使用重排序模型
        """
        # 1. 分别获取两种检索结果(扩大候选集)
        vector_results = self.vector_search(query, top_k=top_k * 3)
        bm25_results = self.bm25_search(query, top_k=top_k * 3)
        
        # 2. 分数归一化(Min-Max归一化)
        all_scores = {}
        
        # 向量分数
        if vector_results:
            max_vec_score = max(score for _, score in vector_results)
            min_vec_score = min(score for _, score in vector_results)
            for idx, score in vector_results:
                norm_score = (score - min_vec_score) / (max_vec_score - min_vec_score + 1e-8)
                all_scores[idx] = all_scores.get(idx, 0) + vector_weight * norm_score
        
        # BM25分数
        if bm25_results:
            max_bm25_score = max(score for _, score in bm25_results)
            min_bm25_score = min(score for _, score in bm25_results)
            for idx, score in bm25_results:
                norm_score = (score - min_bm25_score) / (max_bm25_score - min_bm25_score + 1e-8)
                all_scores[idx] = all_scores.get(idx, 0) + (1 - vector_weight) * norm_score
        
        # 3. 排序并返回top_k
        sorted_results = sorted(all_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
        
        results = []
        for idx, combined_score in sorted_results:
            results.append({
                "index": idx,
                "content": self.chunks[idx],
                "combined_score": combined_score,
                "vector_score": next((s for i, s in vector_results if i == idx), 0),
                "bm25_score": next((s for i, s in bm25_results if i == idx), 0)
            })
        
        # 4. 可选:使用交叉编码器重排序
        if rerank and len(results) > 0:
            results = self.rerank_results(query, results)
        
        return results
    
    def rerank_results(self, query: str, candidates: List[Dict]) -> List[Dict]:
        """
        使用重排序模型对候选结果重新排序
        需要安装: pip install sentence-transformers
        """
        from sentence_transformers import CrossEncoder
        
        # 加载交叉编码器(专门用于相关性判断)
        reranker = CrossEncoder('BAAI/bge-reranker-base')
        
        # 准备输入对
        pairs = [(query, cand["content"]) for cand in candidates]
        
        # 计算相关性分数
        relevance_scores = reranker.predict(pairs)
        
        # 重新排序
        for i, cand in enumerate(candidates):
            cand["rerank_score"] = float(relevance_scores[i])
        
        candidates.sort(key=lambda x: x["rerank_score"], reverse=True)
        
        return candidates

# 使用示例
if __name__ == "__main__":
    # 准备文档chunks
    docs = [
        "Python是一种解释型、面向对象的高级编程语言。",
        "机器学习是人工智能的一个子领域,专注于从数据中学习。",
        "深度学习使用多层神经网络进行特征学习。",
        "Python在数据科学领域应用广泛,尤其是机器学习方向。",
        "自然语言处理是AI领域的重要分支。"
    ]
    
    retriever = HybridRetriever()
    retriever.build_index(docs)
    
    query = "AI领域中的Python应用"
    results = retriever.hybrid_search(query, top_k=3, vector_weight=0.6)
    
    print(f"Query: {query}\n")
    for i, result in enumerate(results, 1):
        print(f"{i}. Score: {result['combined_score']:.3f}")
        print(f"   Content: {result['content']}")
        print(f"   Vector: {result['vector_score']:.3f}, BM25: {result['bm25_score']:.3f}\n")

3.3 查询改写与扩展

python 复制代码
from typing import List
import openai  # 假设已配置API密钥

class QueryEnhancer:
    """
    查询增强器 - 通过改写和扩展提升检索召回率
    """
    
    def __init__(self, llm_model: str = "gpt-3.5-turbo"):
        self.llm_model = llm_model
    
    def query_rewriting(self, original_query: str, num_variants: int = 3) -> List[str]:
        """
        使用LLM生成查询的多个变体
        """
        prompt = f"""请将以下问题改写为{num_variants}个不同表达方式的版本,保持原意但使用不同词汇和句式。
        原问题:{original_query}
        
        请直接输出{num_variants}个改写版本,每行一个,不要添加额外说明:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7
        )
        
        variants = response.choices[0].message.content.strip().split('\n')
        return [original_query] + variants[:num_variants]
    
    def query_expansion(self, query: str, domain_keywords: List[str] = None) -> str:
        """
        查询扩展 - 添加同义词和相关词
        """
        # 简单实现:基于同义词词典
        synonym_dict = {
            "AI": ["人工智能", "机器学习", "深度学习"],
            "Python": ["Python语言", "Python编程", "Py"],
            "数据": ["数据集", "数据样本", "特征数据"]
        }
        
        expanded_terms = [query]
        for word, synonyms in synonym_dict.items():
            if word in query:
                expanded_terms.extend(synonyms)
        
        # 添加领域特定关键词
        if domain_keywords:
            expanded_terms.extend(domain_keywords)
        
        # 去重并组合
        unique_terms = list(dict.fromkeys(expanded_terms))
        expanded_query = " OR ".join(unique_terms)  # 适用于BM25等关键词检索
        
        return expanded_query
    
    def hyde(self, query: str, num_documents: int = 3) -> List[str]:
        """
        HyDE (Hypothetical Document Embeddings) 技术
        先生成假设性答案,再用答案去检索
        """
        prompt = f"""请根据以下问题,撰写一段{num_documents}句的假设性答案文档。
        答案应该详尽、专业,并包含相关细节。
        
        问题:{query}
        
        假设性答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.5
        )
        
        hypothetical_doc = response.choices[0].message.content
        return [hypothetical_doc]

# 集成到检索器中
class EnhancedRetriever(HybridRetriever):
    """
    增强版检索器 - 包含查询改写和HyDE
    """
    
    def __init__(self, use_hyde: bool = False, **kwargs):
        super().__init__(**kwargs)
        self.use_hyde = use_hyde
        self.query_enhancer = QueryEnhancer()
    
    def search_with_enhancement(self, query: str, top_k: int = 5) -> List[Dict]:
        """
        使用增强查询进行检索
        """
        if self.use_hyde:
            # HyDE: 先生成假设文档
            hypothetical_docs = self.query_enhancer.hyde(query)
            # 用假设文档作为查询
            enhanced_queries = hypothetical_docs
        else:
            # 查询改写
            query_variants = self.query_enhancer.query_rewriting(query)
            enhanced_queries = query_variants
        
        # 对每个变体进行检索
        all_results = []
        for q in enhanced_queries:
            results = self.hybrid_search(q, top_k=top_k * 2, rerank=False)
            all_results.extend(results)
        
        # 去重并重新排序
        unique_results = {}
        for result in all_results:
            idx = result["index"]
            if idx not in unique_results or result["combined_score"] > unique_results[idx]["combined_score"]:
                unique_results[idx] = result
        
        # 重排序
        final_results = list(unique_results.values())
        final_results.sort(key=lambda x: x["combined_score"], reverse=True)
        
        # 可选:再次使用重排序模型
        final_results = self.rerank_results(query, final_results[:top_k])
        
        return final_results[:top_k]

3.4 多路召回融合策略

python 复制代码
class MultiPathRetriever:
    """
    多路召回 - 融合多种检索路径的结果
    """
    
    def __init__(self, retrievers: dict):
        """
        retrievers: {
            "vector": vector_retriever,
            "bm25": bm25_retriever,
            "code": code_retriever,  # 代码专用检索
            "knowledge_graph": kg_retriever  # 知识图谱检索
        }
        """
        self.retrievers = retrievers
    
    def reciprocal_rank_fusion(self, results_dict: dict, k: int = 60) -> List[Tuple[int, float]]:
        """
        RRF (Reciprocal Rank Fusion) 算法
        公式: score = sum(1 / (k + rank))
        """
        fusion_scores = {}
        
        for retriever_name, results in results_dict.items():
            for rank, (doc_id, score) in enumerate(results, 1):
                rrf_score = 1.0 / (k + rank)
                fusion_scores[doc_id] = fusion_scores.get(doc_id, 0) + rrf_score
        
        sorted_docs = sorted(fusion_scores.items(), key=lambda x: x[1], reverse=True)
        return sorted_docs
    
    def search(self, query: str, top_k: int = 5) -> List[Dict]:
        """
        多路召回 + RRF融合
        """
        all_results = {}
        
        # 执行各路检索
        for name, retriever in self.retrievers.items():
            if hasattr(retriever, 'search'):
                results = retriever.search(query, top_k=top_k * 2)
            else:
                results = retriever.hybrid_search(query, top_k=top_k * 2)
            
            # 转换为统一格式 (doc_id, score)
            formatted_results = []
            for i, res in enumerate(results):
                doc_id = res.get("index") if isinstance(res, dict) else res[0]
                score = res.get("combined_score") if isinstance(res, dict) else res[1]
                formatted_results.append((doc_id, score))
            
            all_results[name] = formatted_results
        
        # RRF融合
        fused_results = self.reciprocal_rank_fusion(all_results, k=60)
        
        # 返回最终结果
        final_results = []
        for doc_id, score in fused_results[:top_k]:
            final_results.append({
                "index": doc_id,
                "content": self.retrievers["vector"].chunks[doc_id],
                "fusion_score": score
            })
        
        return final_results

3.5 最佳实践总结

场景 推荐策略 参数建议
通用问答 向量+BM25混合 weight=0.6, top_k=5
代码检索 向量+代码结构检索 weight=0.4, 增加函数名索引
低资源场景 仅BM25 分词优化, 停用词过滤
高精度要求 混合检索+重排序 候选集扩大3倍, CrossEncoder

关键优化点

  1. 检索不是越多越好:top_k 通常设为 3-10,过大会引入噪音

  2. 使用 RRF 替代加权求和,避免分数尺度不一致问题

  3. 对特定领域(医疗、法律)微调 embedding 模型

  4. 实现检索缓存,相同查询直接返回结果


四、格式错误

4.1 常见格式问题

RAG 中的格式错误包括:

  • Markdown 渲染错误:表格、代码块、列表解析失败

  • 特殊字符乱码:emoji、数学符号、全角/半角混用

  • 嵌套结构丢失:HTML/XML 标签被剥离,层级信息丢失

  • 跨页内容断裂:表格或代码块在页面边界被切断

4.2 格式修复与标准化

python 复制代码
import re
import html
from typing import List, Dict
import markdown
from bs4 import BeautifulSoup

class FormatNormalizer:
    """
    格式标准化器 - 修复和统一各种格式问题
    """
    
    def fix_unicode(self, text: str) -> str:
        """
        修复Unicode编码问题
        """
        # 修复常见乱码
        replacements = {
            '“': '"', 'â€\x9d': '"', '‘': "'", '’': "'",
            'â€"': '---', '•': '•', 'é': 'é', 'ä': 'ä',
            '·': '·', 'â€"': '---', 'â€"': '--'
        }
        
        for wrong, correct in replacements.items():
            text = text.replace(wrong, correct)
        
        # 移除控制字符(保留换行和制表符)
        text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
        
        return text
    
    def normalize_tables(self, text: str) -> str:
        """
        规范化表格格式
        将各种表格表示转换为统一的Markdown表格
        """
        lines = text.split('\n')
        normalized_lines = []
        in_table = False
        table_rows = []
        
        for line in lines:
            # 检测表格行(包含多个竖线)
            if line.count('|') >= 2:
                if not in_table:
                    in_table = True
                    table_rows = []
                
                # 清理表格行
                cells = [cell.strip() for cell in line.split('|')]
                # 移除空的首尾单元格
                if cells and not cells[0]:
                    cells = cells[1:]
                if cells and not cells[-1]:
                    cells = cells[:-1]
                
                table_rows.append('| ' + ' | '.join(cells) + ' |')
                
            else:
                if in_table and table_rows:
                    # 表格结束,添加分隔行
                    if len(table_rows) >= 2:
                        # 计算列数
                        num_cols = table_rows[0].count('|') - 1
                        separator = '| ' + ' | '.join(['---'] * num_cols) + ' |'
                        normalized_lines.append(table_rows[0])
                        normalized_lines.append(separator)
                        normalized_lines.extend(table_rows[1:])
                    else:
                        normalized_lines.extend(table_rows)
                    table_rows = []
                    in_table = False
                
                normalized_lines.append(line)
        
        # 处理末尾表格
        if in_table and table_rows:
            if len(table_rows) >= 2:
                num_cols = table_rows[0].count('|') - 1
                separator = '| ' + ' | '.join(['---'] * num_cols) + ' |'
                normalized_lines.append(table_rows[0])
                normalized_lines.append(separator)
                normalized_lines.extend(table_rows[1:])
            else:
                normalized_lines.extend(table_rows)
        
        return '\n'.join(normalized_lines)
    
    def extract_and_normalize_code_blocks(self, text: str) -> str:
        """
        提取并规范化代码块
        """
        # 检测没有标记的代码块(连续缩进行)
        lines = text.split('\n')
        normalized_lines = []
        in_code_block = False
        code_lines = []
        
        for line in lines:
            # 检测代码块开始(4空格或tab缩进,且不是空行)
            if line.startswith(('    ', '\t')) and line.strip():
                if not in_code_block:
                    in_code_block = True
                    code_lines = []
                    normalized_lines.append('```python')  # 默认python
                code_lines.append(line.lstrip(' \t'))
            else:
                if in_code_block and code_lines:
                    normalized_lines.append('\n'.join(code_lines))
                    normalized_lines.append('```')
                    code_lines = []
                    in_code_block = False
                normalized_lines.append(line)
        
        # 处理末尾代码块
        if in_code_block and code_lines:
            normalized_lines.append('\n'.join(code_lines))
            normalized_lines.append('```')
        
        return '\n'.join(normalized_lines)
    
    def fix_list_indentation(self, text: str) -> str:
        """
        修复列表缩进问题
        """
        lines = text.split('\n')
        fixed_lines = []
        list_level = 0
        
        for i, line in enumerate(lines):
            # 检测列表项
            list_match = re.match(r'^(\s*)([-*+]|\d+\.)\s+(.*)', line)
            if list_match:
                indent = len(list_match.group(1))
                marker = list_match.group(2)
                content = list_match.group(3)
                
                # 计算层级(每2空格一级)
                level = indent // 2
                
                # 修复嵌套列表
                if level > list_level + 1:
                    level = list_level + 1
                elif level < list_level:
                    level = max(0, level)
                
                indent_str = '  ' * level
                fixed_line = f"{indent_str}{marker} {content}"
                fixed_lines.append(fixed_line)
                list_level = level
            else:
                fixed_lines.append(line)
                # 空行重置列表层级
                if not line.strip():
                    list_level = 0
        
        return '\n'.join(fixed_lines)
    
    def reconstruct_headers(self, text: str) -> str:
        """
        重构标题层级(基于字体大小、加粗等特征)
        适用于从PDF/HTML提取的文本
        """
        lines = text.split('\n')
        reconstructed = []
        
        for line in lines:
            stripped = line.strip()
            if not stripped:
                reconstructed.append(line)
                continue
            
            # 检测可能的标题(短行、以数字/关键词开头)
            if len(stripped) < 50 and not stripped.endswith(('.', '。', '?', '!')):
                # 判断标题层级
                if stripped.startswith(('#', '##', '###')):
                    # 已是Markdown标题
                    reconstructed.append(line)
                elif re.match(r'^\d+\.\s+', stripped):
                    # 数字编号,作为H2
                    reconstructed.append(f"## {stripped}")
                elif len(stripped) < 20:
                    # 短行作为H3
                    reconstructed.append(f"### {stripped}")
                else:
                    reconstructed.append(line)
            else:
                reconstructed.append(line)
        
        return '\n'.join(reconstructed)
    
    def normalize(self, text: str, apply_all: bool = True) -> Dict:
        """
        完整的格式标准化流程
        """
        original_length = len(text)
        
        # 按顺序应用修复
        text = self.fix_unicode(text)
        text = html.unescape(text)  # 解码HTML实体
        
        if apply_all:
            text = self.normalize_tables(text)
            text = self.extract_and_normalize_code_blocks(text)
            text = self.fix_list_indentation(text)
            text = self.reconstruct_headers(text)
        
        # 合并多余空行
        text = re.sub(r'\n{3,}', '\n\n', text)
        
        return {
            "original_text": text,  # 注意:这里实际应该是传入的原始文本,为简洁省略
            "normalized_text": text,
            "original_length": original_length,
            "normalized_length": len(text),
            "compression_ratio": len(text) / original_length
        }

# 使用示例
normalizer = FormatNormalizer()

dirty_text = """
这个表格有问题:
| Name | Age
| John | 25
| Jane | 30

代码块没有标记:
    def hello():
        print("world")

• 列表项1
  • 嵌套项(缩进错误)
"""

cleaned = normalizer.normalize(dirty_text)
print(cleaned["normalized_text"])

4.3 高级:基于LLM的格式修复

python 复制代码
class LLMFormatRepairer:
    """
    使用LLM修复复杂格式错误
    """
    
    def __init__(self, model: str = "gpt-3.5-turbo"):
        self.model = model
    
    def repair_with_llm(self, corrupted_text: str, context: str = "") -> str:
        """
        使用LLM修复格式错误
        """
        prompt = f"""请修复以下文本中的格式错误,包括:
        1. 修复表格对齐问题
        2. 规范代码块格式(添加语言标识)
        3. 修复列表缩进
        4. 保留所有原始内容,只调整格式
        
        {f'上下文信息:{context}' if context else ''}
        
        待修复文本:
        {corrupted_text}
        
        请直接输出修复后的文本,不要添加解释:"""
        
        response = openai.ChatCompletion.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        
        return response.choices[0].message.content

# 混合策略:规则修复 + LLM兜底
class HybridFormatRepairer:
    def __init__(self, use_llm_threshold: float = 0.3):
        self.rule_repairer = FormatNormalizer()
        self.llm_repairer = LLMFormatRepairer()
        self.threshold = use_llm_threshold
    
    def calculate_corruption_score(self, text: str) -> float:
        """
        计算文本损坏程度(0-1)
        """
        score = 0.0
        
        # 检测常见问题
        if re.search(r'[^\x00-\x7F\u4e00-\u9fff]', text):
            # 非ASCII且非中文的乱码
            score += 0.3
        
        if text.count('|') > 0 and '---' not in text:
            # 有表格但缺少分隔符
            score += 0.2
        
        if re.search(r'\n {4,}\S', text) and '```' not in text:
            # 有缩进但无代码块标记
            score += 0.2
        
        # 检测过多控制字符
        control_chars = sum(1 for c in text if ord(c) < 32 and c not in '\n\r\t')
        if control_chars > len(text) * 0.01:
            score += 0.3
        
        return min(score, 1.0)
    
    def repair(self, text: str) -> str:
        """
        智能修复:轻度问题用规则,重度问题用LLM
        """
        corruption_score = self.calculate_corruption_score(text)
        
        print(f"Corruption score: {corruption_score:.2f}")
        
        if corruption_score > self.threshold:
            print("Using LLM repair (high corruption)")
            return self.llm_repairer.repair_with_llm(text)
        else:
            print("Using rule-based repair (low corruption)")
            result = self.rule_repairer.normalize(text)
            return result["normalized_text"]

4.4 最佳实践

格式类型 处理策略 优先级
纯文本 直接使用,仅处理编码 P0
Markdown 正则修复 + 解析验证 P0
表格 提取为结构化数据 P1
代码 检测缩进,添加标记 P1
数学公式 保留LaTeX格式 P2

核心建议

  1. 在加载阶段就进行格式标准化,而非检索后

  2. 保留原始文本的备份,便于问题追溯

  3. 对关键文档(如API文档)使用专用解析器

  4. 定期验证格式修复效果,更新规则库


五、答案不完整

5.1 问题分析

答案不完整的典型表现:

  • 只回答了问题的前半部分

  • 缺少必要的上下文或示例

  • 多步推理问题只完成了第一步

  • 列表或枚举只给出了部分项目

5.2 多步推理与上下文扩展

python 复制代码
from typing import List, Dict, Any
import asyncio

class CompleteAnswerGenerator:
    """
    完整答案生成器 - 确保答案覆盖问题的所有方面
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def decompose_question(self, question: str) -> List[str]:
        """
        将复杂问题分解为子问题
        """
        prompt = f"""请将以下复杂问题分解为3-5个简单的子问题,每个子问题应该能够独立回答。
        使用序号列表输出,每行一个子问题。
        
        原问题:{question}
        
        子问题:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        sub_questions = response.choices[0].message.content.strip().split('\n')
        # 清理序号
        sub_questions = [re.sub(r'^\d+\.\s*', '', q) for q in sub_questions]
        
        return sub_questions
    
    def iterative_retrieval(self, query: str, retriever, max_iterations: int = 3) -> List[str]:
        """
        迭代检索 - 基于已获取信息发现新的检索词
        """
        retrieved_docs = []
        current_query = query
        
        for i in range(max_iterations):
            # 检索当前查询
            docs = retriever.search(current_query, top_k=3)
            retrieved_docs.extend(docs)
            
            # 分析已检索文档,提取关键词
            combined_content = " ".join([d["content"] for d in docs])
            keyword_prompt = f"从以下文本中提取3个最关键的新关键词(不要重复已有):\n{combined_content}"
            
            # 简化:假设有函数提取关键词
            new_keywords = self.extract_keywords(combined_content)
            
            if not new_keywords:
                break
            
            current_query = " ".join(new_keywords)
        
        return retrieved_docs
    
    def expand_context(self, answer: str, context: str, question: str) -> str:
        """
        扩展上下文 - 为答案补充缺失的背景信息
        """
        prompt = f"""基于以下问题和已有答案,判断答案是否完整。如果不完整,请补充缺失的部分。
        
        问题:{question}
        
        已有答案:{answer}
        
        参考上下文:{context[:2000]}
        
        请输出完整答案(如果已有答案已完整,直接输出原答案):"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        
        return response.choices[0].message.content
    
    def generate_complete_answer(
        self, 
        question: str, 
        retrieved_docs: List[Dict],
        max_length: int = 1000
    ) -> Dict:
        """
        生成完整答案的主函数
        """
        # 1. 问题分解
        sub_questions = self.decompose_question(question)
        print(f"Decomposed into {len(sub_questions)} sub-questions")
        
        # 2. 为每个子问题生成答案
        sub_answers = []
        for sub_q in sub_questions:
            # 从检索文档中找到相关片段
            relevant_context = self.extract_relevant_context(sub_q, retrieved_docs)
            
            prompt = f"""请基于以下上下文回答问题。
            
            子问题:{sub_q}
            
            上下文:{relevant_context}
            
            答案:"""
            
            response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.1,
                max_tokens=max_length // len(sub_questions)
            )
            
            sub_answers.append(response.choices[0].message.content)
        
        # 3. 合并子答案
        combined_answer = "\n\n".join([
            f"**{sub_q}**\n{sub_ans}" 
            for sub_q, sub_ans in zip(sub_questions, sub_answers)
        ])
        
        # 4. 最终整合和润色
        final_prompt = f"""请将以下针对子问题的答案整合成一个连贯、完整的答案,回答原始问题。
        
        原始问题:{question}
        
        子问题及答案:
        {combined_answer}
        
        请输出最终答案:"""
        
        final_response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": final_prompt}],
            temperature=0.2
        )
        
        final_answer = final_response.choices[0].message.content
        
        return {
            "question": question,
            "sub_questions": sub_questions,
            "sub_answers": sub_answers,
            "final_answer": final_answer,
            "num_sub_questions": len(sub_questions)
        }
    
    def extract_relevant_context(self, query: str, docs: List[Dict]) -> str:
        """提取与查询最相关的上下文片段"""
        # 简化实现:拼接所有文档的前500字符
        contexts = []
        for doc in docs[:3]:
            content = doc.get("content", "")
            # 找到包含查询关键词的片段
            if query.lower() in content.lower():
                # 扩展上下文窗口
                idx = content.lower().find(query.lower())
                start = max(0, idx - 200)
                end = min(len(content), idx + len(query) + 500)
                contexts.append(content[start:end])
            else:
                contexts.append(content[:500])
        
        return "\n\n".join(contexts)

# 带引用的完整答案生成器
class AnswerWithCitations(CompleteAnswerGenerator):
    """
    带引用的答案生成器 - 确保答案可溯源
    """
    
    def generate_with_citations(self, question: str, retrieved_docs: List[Dict]) -> Dict:
        """
        生成带有源文档引用的答案
        """
        # 构建带编号的文档列表
        doc_refs = []
        for i, doc in enumerate(retrieved_docs, 1):
            doc_refs.append(f"[{i}] {doc.get('source', 'Unknown')}: {doc['content'][:100]}...")
        
        prompt = f"""请基于以下参考文档回答问题。在答案中,使用上标引用标注信息来源,如[1]、[2]。
        
        问题:{question}
        
        参考文档:
        {chr(10).join(doc_refs)}
        
        要求:
        1. 答案必须基于参考文档,不要编造信息
        2. 每个事实都要标注引用
        3. 如果信息在多个文档中出现,引用所有相关文档
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        answer = response.choices[0].message.content
        
        # 提取实际使用的引用
        used_citations = re.findall(r'\[(\d+)\]', answer)
        used_citations = list(set(map(int, used_citations)))
        
        # 生成引用列表
        citations = [doc_refs[i-1] for i in used_citations if i <= len(doc_refs)]
        
        return {
            "answer": answer,
            "citations": citations,
            "num_citations": len(citations)
        }

5.3 答案完整性验证

python 复制代码
class CompletenessValidator:
    """
    答案完整性验证器 - 自动检测缺失部分
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def check_completeness(self, question: str, answer: str) -> Dict:
        """
        检查答案完整性
        """
        prompt = f"""请评估以下答案是否完整地回答了问题。
        
        问题:{question}
        
        答案:{answer}
        
        请以JSON格式输出:
        {{
            "is_complete": true/false,
            "completeness_score": 0-100,
            "missing_aspects": ["缺失方面1", "缺失方面2"],
            "suggestions": ["改进建议1", "改进建议2"]
        }}
        
        只输出JSON,不要有其他内容:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        import json
        evaluation = json.loads(response.choices[0].message.content)
        
        return evaluation
    
    def iterative_completion(self, question: str, base_answer: str, max_iterations: int = 3) -> str:
        """
        迭代完善答案直到完整
        """
        current_answer = base_answer
        
        for i in range(max_iterations):
            eval_result = self.check_completeness(question, current_answer)
            
            if eval_result.get("is_complete", False):
                print(f"Answer is complete after {i+1} iterations")
                break
            
            print(f"Iteration {i+1}: Completeness score {eval_result.get('completeness_score', 0)}")
            print(f"Missing: {eval_result.get('missing_aspects', [])}")
            
            # 请求补充缺失部分
            supplement_prompt = f"""请补充以下答案中缺失的部分。
            
            问题:{question}
            
            当前答案:{current_answer}
            
            缺失的方面:{', '.join(eval_result.get('missing_aspects', []))}
            
            请只输出补充的内容(不要重复已有部分):"""
            
            response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": supplement_prompt}],
                temperature=0.2
            )
            
            supplement = response.choices[0].message.content
            current_answer += f"\n\n[补充内容]\n{supplement}"
        
        return current_answer

5.4 最佳实践

答案类型 完整性策略 示例场景
事实性问题 多源验证 "Python的创始人是?"
步骤性问题 检查序号完整性 "如何部署Flask应用?"
对比性问题 检查对比维度 "比较RNN和Transformer"
开放式问题 问题分解+合并 "深度学习的未来趋势"

关键要点

  1. 使用问题分解确保覆盖所有方面

  2. 要求LLM输出结构化答案(列表、表格)

  3. 实现答案验证和迭代完善机制

  4. 保留中间检索结果,支持答案溯源


六、未提取到答案

6.1 问题场景

最糟糕的情况:检索返回了文档,但LLM无法提取出答案。原因包括:

  • 答案隐式存在于文档中(需要推理)

  • 信息分散在多个chunk中

  • 文档使用了不同的术语体系

  • 问题需要数值计算或逻辑推理

6.2 多文档推理与证据链

python 复制代码
class EvidenceChainExtractor:
    """
    证据链提取器 - 从多个文档中构建推理路径
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def extract_claims(self, documents: List[str]) -> List[Dict]:
        """
        从文档中提取所有事实性声明
        """
        all_claims = []
        
        for doc_idx, doc in enumerate(documents):
            prompt = f"""从以下文本中提取所有事实性声明。每个声明应该是一个独立的、可验证的事实。
            
            文本:{doc[:1000]}
            
            输出格式(每行一个声明,不要编号):
            声明1
            声明2
            ..."""
            
            response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.1
            )
            
            claims = response.choices[0].message.content.strip().split('\n')
            for claim in claims:
                if claim.strip():
                    all_claims.append({
                        "claim": claim,
                        "source_doc": doc_idx,
                        "text_snippet": doc[:200]  # 保存上下文
                    })
        
        return all_claims
    
    def build_reasoning_chain(self, question: str, claims: List[Dict]) -> List[str]:
        """
        构建推理链 - 连接相关声明形成逻辑路径
        """
        prompt = f"""问题:{question}
        
        可用的事实声明:
        {chr(10).join([f"- {c['claim']}" for c in claims])}
        
        请找出能够回答问题的事实声明,并按照逻辑顺序排列,形成一个推理链。
        如果无法回答问题,请说明缺少什么信息。
        
        推理链(每步一行):"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        
        chain = response.choices[0].message.content.strip().split('\n')
        return chain
    
    def extract_answer_with_chain(self, question: str, documents: List[str]) -> Dict:
        """
        通过证据链提取答案
        """
        # 1. 提取所有声明
        claims = self.extract_claims(documents)
        print(f"Extracted {len(claims)} claims")
        
        # 2. 构建推理链
        reasoning_chain = self.build_reasoning_chain(question, claims)
        
        # 3. 基于推理链生成答案
        chain_text = "\n".join(reasoning_chain)
        prompt = f"""基于以下推理链回答问题。
        
        问题:{question}
        
        推理链:
        {chain_text}
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        answer = response.choices[0].message.content
        
        return {
            "answer": answer,
            "reasoning_chain": reasoning_chain,
            "num_claims_used": len(reasoning_chain)
        }

# 表格数据提取器
class TableDataExtractor:
    """
    从表格中提取答案(处理结构化数据)
    """
    
    def __init__(self):
        pass
    
    def parse_table(self, table_text: str) -> Dict:
        """
        解析表格文本为结构化数据
        """
        lines = table_text.strip().split('\n')
        if len(lines) < 2:
            return {}
        
        # 检测分隔行
        header_line = lines[0]
        separator_line = lines[1] if '---' in lines[1] else None
        
        # 解析表头
        headers = [h.strip() for h in header_line.split('|') if h.strip()]
        
        # 解析数据行
        data_rows = []
        start_idx = 2 if separator_line else 1
        for line in lines[start_idx:]:
            if '|' in line:
                cells = [c.strip() for c in line.split('|') if c.strip()]
                if len(cells) == len(headers):
                    data_rows.append(dict(zip(headers, cells)))
        
        return {
            "headers": headers,
            "rows": data_rows,
            "num_rows": len(data_rows)
        }
    
    def query_table(self, table_data: Dict, question: str) -> str:
        """
        对表格数据执行查询
        """
        # 简单实现:转换为自然语言后查询
        table_description = f"表格包含列:{', '.join(table_data['headers'])}\n"
        for row in table_data['rows'][:10]:  # 限制行数
            table_description += f"行数据:{row}\n"
        
        prompt = f"""基于以下表格数据回答问题。
        
        问题:{question}
        
        表格数据:
        {table_description}
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        return response.choices[0].message.content

# 数值计算提取器
class NumericalReasoner:
    """
    处理需要计算的答案提取
    """
    
    def extract_numbers(self, text: str) -> List[float]:
        """提取文本中的数字"""
        numbers = re.findall(r'\d+(?:\.\d+)?', text)
        return [float(n) for n in numbers]
    
    def calculate_answer(self, question: str, documents: List[str]) -> Dict:
        """
        执行数值计算
        """
        # 提取所有数字和关系
        all_numbers = []
        all_context = " ".join(documents)
        
        prompt = f"""从以下文本中提取回答问题所需的数值和计算公式。
        
        问题:{question}
        
        文本:{all_context[:2000]}
        
        输出JSON格式:
        {{
            "numbers": [数值列表],
            "operations": ["加法", "平均值", ...],
            "formula": "计算公式(如:sum(x)/len(x))"
        }}"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        import json
        calc_info = json.loads(response.choices[0].message.content)
        
        # 执行计算(简化)
        numbers = calc_info.get("numbers", [])
        if not numbers:
            return {"answer": "无法提取数值", "calculation": None}
        
        if "平均值" in calc_info.get("operations", []):
            result = sum(numbers) / len(numbers)
            answer = f"平均值为 {result:.2f}"
        elif "总和" in calc_info.get("operations", []):
            result = sum(numbers)
            answer = f"总和为 {result}"
        else:
            answer = f"数值为 {', '.join(map(str, numbers))}"
        
        return {
            "answer": answer,
            "calculation": calc_info,
            "raw_numbers": numbers
        }

6.3 处理"未找到"的情况

python 复制代码
class GracefulAnswerHandler:
    """
    优雅处理无法提取答案的情况
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def detect_unanswerable(self, question: str, documents: List[str]) -> bool:
        """
        检测是否无法从文档中回答问题
        """
        prompt = f"""判断以下问题是否能够基于提供的文档回答。
        
        问题:{question}
        
        文档摘要:{documents[0][:500] if documents else "无文档"}
        
        只输出"Yes"或"No":"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            max_tokens=5
        )
        
        result = response.choices[0].message.content.strip().lower()
        return result == "no"
    
    def generate_fallback_response(self, question: str, retrieved_docs: List[Dict]) -> str:
        """
        生成降级响应
        """
        # 提取文档主题
        topics = []
        for doc in retrieved_docs[:3]:
            topic_prompt = f"提取以下文本的主题(3-5个词):\n{doc['content'][:200]}"
            response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": topic_prompt}],
                temperature=0.1,
                max_tokens=20
            )
            topics.append(response.choices[0].message.content.strip())
        
        fallback_template = f"""抱歉,我无法从现有文档中找到关于"{question}"的准确答案。

当前文档主要涵盖:{', '.join(set(topics))}

建议:
1. 尝试用不同的关键词重新提问
2. 确认问题是否与文档主题相关
3. 提供更多相关的文档

如果您有其他问题,我很乐意帮助。"""
        
        return fallback_template
    
    def suggest_query_improvements(self, question: str) -> List[str]:
        """
        建议查询改进方案
        """
        prompt = f"""用户问题未能从文档库中找到答案。请提供3个优化版本的问题,使其更可能被检索到。
        
        原问题:{question}
        
        优化建议(每行一个):"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.5
        )
        
        suggestions = response.choices[0].message.content.strip().split('\n')
        return suggestions[:3]

6.4 最佳实践

问题类型 提取策略 示例
事实性 直接提取声明 "Python的作者是谁?"
推理型 构建证据链 "为什么Transformer比RNN好?"
数值型 提取+计算 "2020-2023年增长率是多少?"
对比型 并行提取 "比较A和B的优缺点"
未找到 优雅降级 提供建议、总结相关主题

核心原则

  1. 始终尝试多文档融合,而非单文档提取

  2. 实现"无法回答"检测,避免幻觉

  3. 提供透明的推理过程,便于调试

  4. 收集失败案例,持续优化检索策略


七、答案太具体或者太笼统

7.1 问题描述

  • 太具体:回答了过于细节的问题,忽略了用户可能需要的概括性信息

  • 太笼统:回答泛泛而谈,没有提供具体数据、步骤或例子

7.2 自适应答案粒度

python 复制代码
class AdaptiveGranularity:
    """
    自适应答案粒度控制器
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def detect_preferred_granularity(self, question: str) -> str:
        """
        从问题中检测期望的答案粒度
        """
        # 关键词检测
        granularity_keywords = {
            "detailed": ["详细", "具体", "步骤", "如何", "为什么", "解释"],
            "concise": ["简单", "概括", "总结", "什么是", "定义", "简介"],
            "balanced": ["介绍", "说明", "描述"]
        }
        
        for granularity, keywords in granularity_keywords.items():
            if any(kw in question for kw in keywords):
                return granularity
        
        # 默认平衡
        return "balanced"
    
    def generate_answer_with_granularity(
        self, 
        question: str, 
        context: str, 
        granularity: str = "balanced"
    ) -> str:
        """
        根据指定粒度生成答案
        """
        prompts = {
            "concise": f"""用1-2句话简洁回答以下问题,只给出核心信息。
            问题:{question}
            上下文:{context[:1000]}
            简洁答案:""",
            
            "balanced": f"""用3-5句话回答以下问题,包含必要的解释但不冗余。
            问题:{question}
            上下文:{context}
            平衡答案:""",
            
            "detailed": f"""详细回答以下问题,包括:
            - 背景说明
            - 具体步骤或数据
            - 示例(如适用)
            - 相关注意事项
            
            问题:{question}
            上下文:{context}
            详细答案:"""
        }
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompts[granularity]}],
            temperature=0.2
        )
        
        return response.choices[0].message.content
    
    def summarize_or_expand(self, answer: str, target_granularity: str) -> str:
        """
        调整已有答案的粒度
        """
        if target_granularity == "concise":
            prompt = f"""将以下答案浓缩为1-2句话:
            {answer}
            浓缩版:"""
        elif target_granularity == "detailed":
            prompt = f"""将以下答案展开,增加具体例子和细节:
            {answer}
            展开版:"""
        else:
            return answer
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3
        )
        
        return response.choices[0].message.content
    
    def multi_level_answer(self, question: str, context: str) -> Dict:
        """
        生成多级答案(概要+详细+示例)
        """
        # 1. 概要答案
        summary = self.generate_answer_with_granularity(question, context, "concise")
        
        # 2. 详细答案
        detailed = self.generate_answer_with_granularity(question, context, "detailed")
        
        # 3. 提取示例
        example_prompt = f"""从以下文本中提取一个与问题相关的具体示例。
        问题:{question}
        文本:{context[:1500]}
        示例:"""
        
        example_response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": example_prompt}],
            temperature=0.2
        )
        example = example_response.choices[0].message.content
        
        return {
            "summary": summary,
            "detailed": detailed,
            "example": example,
            "question": question
        }

7.3 用户意图理解与个性化

python 复制代码
class IntentAwareGranularity:
    """
    基于用户意图的粒度控制
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
        self.user_preferences = {}  # 存储用户偏好
    
    def analyze_intent(self, question: str, user_id: str = None) -> Dict:
        """
        分析问题意图和期望深度
        """
        prompt = f"""分析以下问题的用户意图。
        
        问题:{question}
        
        输出JSON格式:
        {{
            "intent_type": "learn|solve|compare|define|troubleshoot",
            "expected_depth": 1-5 (1=最浅,5=最深),
            "needs_example": true/false,
            "needs_step_by_step": true/false,
            "domain": "技术|学术|生活|其他"
        }}"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        import json
        intent = json.loads(response.choices[0].message.content)
        
        # 结合用户历史偏好
        if user_id and user_id in self.user_preferences:
            pref = self.user_preferences[user_id]
            intent["expected_depth"] = min(5, intent["expected_depth"] + pref.get("depth_bias", 0))
        
        return intent
    
    def generate_intent_aware_answer(
        self, 
        question: str, 
        context: str, 
        user_id: str = None
    ) -> Dict:
        """
        生成符合意图的答案
        """
        intent = self.analyze_intent(question, user_id)
        
        # 根据意图构建prompt
        components = []
        
        if intent["needs_step_by_step"]:
            components.append("请提供分步骤的说明")
        
        if intent["needs_example"]:
            components.append("请包含具体示例")
        
        if intent["intent_type"] == "troubleshoot":
            components.append("请列出可能的原因和解决方案")
        elif intent["intent_type"] == "compare":
            components.append("请使用表格或对比列表")
        
        depth_indicators = {
            1: "一句话概括",
            2: "2-3个要点",
            3: "详细说明",
            4: "深入分析",
            5: "全面论述,包含背景、原理、实践"
        }
        
        depth_instruction = depth_indicators.get(intent["expected_depth"], "详细说明")
        
        prompt = f"""回答问题,遵循以下要求:
        - 深度要求:{depth_instruction}
        - 额外要求:{', '.join(components) if components else '无'}
        
        问题:{question}
        
        上下文:{context}
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        
        answer = response.choices[0].message.content
        
        return {
            "answer": answer,
            "intent_analysis": intent,
            "granularity_used": depth_instruction
        }
    
    def update_user_preference(self, user_id: str, feedback: Dict):
        """
        根据反馈更新用户偏好
        """
        if user_id not in self.user_preferences:
            self.user_preferences[user_id] = {"depth_bias": 0}
        
        # 反馈:答案太简单 -> depth_bias +1;太复杂 -> depth_bias -1
        if feedback.get("too_simple"):
            self.user_preferences[user_id]["depth_bias"] += 1
        elif feedback.get("too_complex"):
            self.user_preferences[user_id]["depth_bias"] -= 1
        
        # 限制范围
        self.user_preferences[user_id]["depth_bias"] = max(-2, min(2, 
            self.user_preferences[user_id]["depth_bias"]))

7.4 最佳实践

用户类型 推荐粒度 策略
初学者 详细+示例 解释术语,提供背景
中级用户 平衡 核心信息+关键细节
专家 简洁 直接给出结论或要点
决策者 摘要+关键数据 突出结论和数据
开发者 具体+可执行 提供代码、配置、命令

关键技巧

  1. 提供粒度切换选项(如"简单/详细"按钮)

  2. 答案开头用一句话总结,后续展开

  3. 使用折叠块组织详细内容

  4. 收集用户反馈,个性化调整


八、幻觉问题

8.1 幻觉的类型

  • 事实性幻觉:编造不存在的信息

  • 忠实性幻觉:偏离检索到的文档内容

  • 逻辑幻觉:推理错误或矛盾

8.2 幻觉检测与缓解

python 复制代码
class HallucinationDetector:
    """
    幻觉检测器 - 验证答案的准确性
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
    
    def fact_check(self, answer: str, source_docs: List[str]) -> Dict:
        """
        事实验证:检查答案中的每个声明是否在源文档中有支持
        """
        # 提取答案中的声明
        claim_prompt = f"""提取以下答案中的所有事实性声明(每个声明是一个独立的、可验证的事实):
        
        答案:{answer}
        
        输出每行一个声明:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": claim_prompt}],
            temperature=0.1
        )
        
        claims = response.choices[0].message.content.strip().split('\n')
        
        # 验证每个声明
        verification_results = []
        
        for claim in claims:
            if not claim.strip():
                continue
            
            verify_prompt = f"""判断以下声明是否可以在提供的源文档中找到支持。
            
            声明:{claim}
            
            源文档摘要:
            {chr(10).join([doc[:300] for doc in source_docs[:3]])}
            
            只输出:
            - "SUPPORTED":声明有明确支持
            - "CONTRADICTED":声明与文档矛盾
            - "NOT_FOUND":文档中未找到
            
            输出:"""
            
            verify_response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": verify_prompt}],
                temperature=0.1,
                max_tokens=10
            )
            
            status = verify_response.choices[0].message.content.strip()
            
            verification_results.append({
                "claim": claim,
                "status": status,
                "is_hallucination": status in ["CONTRADICTED", "NOT_FOUND"]
            })
        
        hallucinated_claims = [r for r in verification_results if r["is_hallucination"]]
        
        return {
            "total_claims": len(verification_results),
            "hallucinated_claims": len(hallucinated_claims),
            "hallucination_rate": len(hallucinated_claims) / max(1, len(verification_results)),
            "details": verification_results,
            "is_hallucinated": len(hallucinated_claims) > 0
        }
    
    def self_correction(self, answer: str, source_docs: List[str]) -> str:
        """
        自我纠正:基于验证结果修正答案
        """
        verification = self.fact_check(answer, source_docs)
        
        if not verification["is_hallucinated"]:
            return answer
        
        # 找出幻觉声明
        hallucinated = [v["claim"] for v in verification["details"] if v["is_hallucination"]]
        
        correction_prompt = f"""请修正以下答案,移除或替换其中不真实的部分。
        
        原始答案:{answer}
        
        以下声明无法在源文档中找到支持:{', '.join(hallucinated)}
        
        源文档:{source_docs[0][:1000]}
        
        请输出修正后的答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": correction_prompt}],
            temperature=0.1
        )
        
        return response.choices[0].message.content

class HallucinationMitigator:
    """
    幻觉缓解器 - 多种策略组合
    """
    
    def __init__(self, llm_model):
        self.llm_model = llm_model
        self.detector = HallucinationDetector(llm_model)
    
    def constrained_generation(self, question: str, context: str) -> str:
        """
        约束生成 - 强制模型只使用上下文中的信息
        """
        prompt = f"""【重要】你只能使用以下"上下文"中的信息回答问题。不要添加任何上下文之外的信息。
        如果上下文不足以回答问题,请直接说"根据现有文档,无法回答这个问题"。
        
        问题:{question}
        
        上下文:
        {context}
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            presence_penalty=0.0,  # 禁止引入新信息
            frequency_penalty=0.0
        )
        
        return response.choices[0].message.content
    
    def citation_required(self, question: str, chunks: List[str]) -> Dict:
        """
        强制引用 - 每个事实必须标注来源
        """
        # 为每个chunk添加引用ID
        chunk_with_ids = []
        for i, chunk in enumerate(chunks):
            chunk_with_ids.append(f"[{i+1}] {chunk}")
        
        prompt = f"""回答问题,每个陈述必须标注引用来源的编号[i]。
        只使用提供的chunk中的信息。
        
        问题:{question}
        
        可引用的chunk:
        {chr(10).join(chunk_with_ids)}
        
        答案格式:陈述内容 [编号]
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        answer = response.choices[0].message.content
        
        # 验证引用有效性
        citations = re.findall(r'\[(\d+)\]', answer)
        valid_citations = [c for c in citations if 1 <= int(c) <= len(chunks)]
        
        return {
            "answer": answer,
            "citations_provided": len(citations),
            "valid_citations": len(valid_citations),
            "citation_coverage": len(valid_citations) / max(1, len(citations))
        }
    
    def confidence_scoring(self, question: str, context: str) -> Dict:
        """
        置信度评分 - 标记不确定的信息
        """
        prompt = f"""回答问题,并对每个关键信息标注置信度(高/中/低)。
        
        问题:{question}
        
        上下文:{context}
        
        格式要求:
        答案内容 [置信度: 高/中/低]
        
        示例:
        Python由Guido van Rossum创建 [置信度: 高]
        具体年份存在争议 [置信度: 低]
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2
        )
        
        answer = response.choices[0].message.content
        
        # 解析置信度
        low_conf = re.findall(r'\[置信度:\s*低\]', answer)
        med_conf = re.findall(r'\[置信度:\s*中\]', answer)
        high_conf = re.findall(r'\[置信度:\s*高\]', answer)
        
        return {
            "answer": answer,
            "confidence_breakdown": {
                "high": len(high_conf),
                "medium": len(med_conf),
                "low": len(low_conf)
            },
            "needs_verification": len(low_conf) > 0
        }
    
    def multi_source_consensus(self, question: str, chunks: List[str]) -> Dict:
        """
        多源共识 - 只有当多个chunk支持时才采信
        """
        # 分别让模型基于每个chunk回答
        individual_answers = []
        
        for chunk in chunks[:5]:  # 限制数量
            response = openai.ChatCompletion.create(
                model=self.llm_model,
                messages=[{"role": "user", "content": f"基于以下文本回答问题:{question}\n\n文本:{chunk}\n\n答案:"}],
                temperature=0.1,
                max_tokens=200
            )
            individual_answers.append(response.choices[0].message.content)
        
        # 找出一致性高的信息
        consensus_prompt = f"""以下是5个不同的AI基于不同文档片段对同一问题的回答。
        请找出所有回答中一致认同的事实(至少3个回答提到)。
        
        问题:{question}
        
        回答列表:
        {chr(10).join([f"{i+1}. {ans}" for i, ans in enumerate(individual_answers)])}
        
        共识答案(只包含一致认同的信息):"""
        
        response = openai.ChatCompletion.create(
            model=self.llm_model,
            messages=[{"role": "user", "content": consensus_prompt}],
            temperature=0.1
        )
        
        consensus_answer = response.choices[0].message.content
        
        return {
            "consensus_answer": consensus_answer,
            "individual_answers": individual_answers,
            "num_sources": len(individual_answers),
            "agreement_level": "high" if len(consensus_answer) > 50 else "low"
        }

# 完整RAG管道集成
class RobustRAGPipeline:
    """
    健壮的RAG管道 - 集成所有防幻觉机制
    """
    
    def __init__(self, retriever, llm_model):
        self.retriever = retriever
        self.llm_model = llm_model
        self.detector = HallucinationDetector(llm_model)
        self.mitigator = HallucinationMitigator(llm_model)
    
    def query(self, question: str, enable_verification: bool = True) -> Dict:
        """
        执行RAG查询,带幻觉检测和修正
        """
        # 1. 检索
        retrieved = self.retriever.search(question, top_k=5)
        contexts = [r["content"] for r in retrieved]
        
        # 2. 约束生成
        initial_answer = self.mitigator.constrained_generation(question, "\n\n".join(contexts))
        
        result = {
            "question": question,
            "initial_answer": initial_answer,
            "retrieved_chunks": len(contexts)
        }
        
        # 3. 事实核查
        if enable_verification:
            verification = self.detector.fact_check(initial_answer, contexts)
            result["verification"] = verification
            
            # 4. 如果有幻觉,尝试修正
            if verification["is_hallucinated"]:
                corrected_answer = self.detector.self_correction(initial_answer, contexts)
                result["corrected_answer"] = corrected_answer
                result["was_corrected"] = True
                
                # 再次验证修正后的答案
                re_verification = self.detector.fact_check(corrected_answer, contexts)
                result["re_verification"] = re_verification
            else:
                result["was_corrected"] = False
        
        # 5. 最终答案选择
        if result.get("corrected_answer"):
            final_answer = result["corrected_answer"]
        else:
            final_answer = initial_answer
        
        result["final_answer"] = final_answer
        
        return result

8.3 最佳实践总结

策略 原理 成本 效果
约束生成 强制使用上下文
强制引用 要求标注来源
事实验证 独立检查
自我修正 迭代改进
多源共识 交叉验证 很高
置信度评分 标记不确定性

黄金法则

  1. 永远不要跳过验证:特别是高风险场景(医疗、法律、金融)

  2. 使用系统提示词:在开始时明确告知模型"不要编造信息"

  3. 降低temperature:使用0.1-0.3减少随机性

  4. 实现人机回环:对低置信度答案请求人工确认

  5. 记录幻觉案例:用于改进检索和提示词


第九章:练习题及其答案

9.1 选择题(每题10分)

1. 以下哪个不是RAG系统中文档切分过细导致的问题?

A) 上下文连贯性丢失

B) 检索噪音增加

C) 答案可能误解原意

D) 文档加载速度变慢

答案:D

解析:切分过细主要影响语义完整性,不会直接影响加载速度。加载速度主要取决于文档解析器效率。

2. 混合检索中,RRF(Reciprocal Rank Fusion)的作用是什么?

A) 加速向量检索

B) 融合不同检索器的排名结果

C) 减少内存占用

D) 自动选择最佳切分策略

答案:B

解析:RRF通过公式 1/(k+rank) 融合多个检索结果的排名,避免分数尺度不一致问题。

3. 关于RAG中的幻觉问题,以下哪种说法正确?

A) 使用更大的LLM可以完全消除幻觉

B) 约束生成(constrained generation)可以彻底解决幻觉

C) 事实验证和源引用可以有效降低幻觉率

D) 幻觉只发生在模型训练阶段

答案:C

解析:幻觉是LLM的固有问题,无法完全消除。事实验证和强制引用是业界公认的有效缓解方法。

4. 在处理PDF文档时,对于包含大量表格的文档,应该优先使用哪个解析器?

A) PyPDF2

B) pdfplumber

C) PyMuPDF

D) 直接OCR

答案:B

解析:pdfplumber专门优化了表格提取功能,能较好地保持表格结构。

5. 以下哪个指标最适合评估RAG检索阶段的效果?

A) BLEU分数

B) 召回率@K(Recall@K)

C) 模型参数量

D) 文档压缩率

答案:B

解析:Recall@K衡量Top-K结果中包含相关文档的比例,直接反映检索效果。

9.2 填空题(每题5分)

1. RAG的三个核心步骤是:检索、______、生成。

答案:增强(或Augmented)

2. 在文档切分中,chunk overlap通常设置为chunk size的______%到______%。

答案:10,20

3. 混合检索通常结合______检索和______检索两种方法。

答案:向量,关键词(或BM25)

4. 强制LLM在答案中标注来源编号的技术称为______。

答案:强制引用(或Citation Required)

5. 当检索结果不足以回答问题时,应该返回______而不是强行回答。

答案:无法回答(或降级响应)

9.3 简答题(每题15分)

1. 请简述RAG系统中文档切分粒度过粗和过细分别会导致什么问题?如何选择最优切分策略?

参考答案:

  • 过粗的问题:单个chunk包含过多无关信息,检索时噪音大;可能超出LLM上下文窗口;答案可能包含冗余内容。

  • 过细的问题:丢失上下文连贯性,答案可能看到片段而误解原意;重要信息可能分散在多个chunk中难以整合;增加检索次数和成本。

  • 最优策略选择

    • 基于内容类型:代码用函数级切分,表格保持完整,文本用语义切分

    • 基于应用场景:问答用300-500字,摘要用800-1200字

    • 自适应方法:根据文本复杂度动态调整chunk大小

    • 保留overlap:10-20%的重叠避免边界信息丢失

    • 保留元数据:记录父文档、页码、章节信息

2. 请解释什么是RAG中的"幻觉"问题,并列举至少3种缓解方法。

参考答案:

幻觉定义:LLM生成不基于检索文档的内容,包括编造事实、偏离源文档、逻辑错误等。

缓解方法

  1. 约束生成:在prompt中明确要求"只使用以下上下文中的信息",禁止添加外部知识。

  2. 强制引用:要求每个陈述标注来源编号,便于验证和追溯。

  3. 事实验证:独立检查答案中的每个声明是否在源文档中有支持。

  4. 多源共识:只有当多个chunk支持时才采信信息。

  5. 置信度评分:对不确定的信息标注置信度(高/中/低)。

  6. 降低temperature:使用0.1-0.3减少模型随机性。

9.4 实操题(30分)

题目:实现一个简单的RAG系统,要求包含以下功能:

  1. 文档加载(支持.txt文件)

  2. 文档切分(chunk_size=200, overlap=30)

  3. 向量检索(使用sentence-transformers)

  4. 答案生成(使用OpenAI API或本地LLM)

  5. 包含幻觉检测(检查答案是否基于检索结果)

参考答案代码:

python 复制代码
# 完整的RAG系统实现
import os
from typing import List, Dict
import numpy as np
from sentence_transformers import SentenceTransformer
import openai

class SimpleRAG:
    """
    简易RAG系统 - 包含检索、生成、幻觉检测
    """
    
    def __init__(self, embedding_model="BAAI/bge-small-zh-v1.5"):
        self.embedding_model = SentenceTransformer(embedding_model)
        self.chunks = []
        self.chunk_embeddings = None
    
    def load_document(self, file_path: str) -> str:
        """加载txt文档"""
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    
    def split_chunks(self, text: str, chunk_size: int = 200, overlap: int = 30) -> List[str]:
        """切分文档"""
        chunks = []
        start = 0
        text_length = len(text)
        
        while start < text_length:
            end = min(start + chunk_size, text_length)
            chunk = text[start:end]
            chunks.append(chunk)
            start += chunk_size - overlap
        
        return chunks
    
    def build_index(self, chunks: List[str]):
        """构建向量索引"""
        self.chunks = chunks
        self.chunk_embeddings = self.embedding_model.encode(
            chunks, 
            normalize_embeddings=True,
            show_progress_bar=False
        )
        print(f"Index built: {len(chunks)} chunks")
    
    def retrieve(self, query: str, top_k: int = 3) -> List[Dict]:
        """检索相关chunks"""
        query_embedding = self.embedding_model.encode([query], normalize_embeddings=True)[0]
        
        # 计算相似度
        similarities = np.dot(self.chunk_embeddings, query_embedding)
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        
        results = []
        for idx in top_indices:
            results.append({
                "index": idx,
                "content": self.chunks[idx],
                "score": float(similarities[idx])
            })
        
        return results
    
    def generate_answer(self, question: str, context: str) -> str:
        """生成答案(使用OpenAI)"""
        prompt = f"""【重要】只使用以下上下文回答问题。如果上下文没有相关信息,请回答"无法回答"。
        
        问题:{question}
        
        上下文:
        {context}
        
        答案:"""
        
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            max_tokens=300
        )
        
        return response.choices[0].message.content
    
    def detect_hallucination(self, answer: str, context: str) -> Dict:
        """简单的幻觉检测"""
        # 提取答案中的关键句子
        sentences = answer.split('。')
        
        supported = []
        unsupported = []
        
        for sent in sentences:
            if not sent.strip():
                continue
            
            # 检查句子中的关键词是否在上下文中出现
            words = set(sent.replace(',', ' ').replace('。', ' ').split())
            # 忽略停用词
            stopwords = {'的', '了', '是', '在', '和', '与', '或', '有', '被', '把'}
            content_words = words - stopwords
            
            if not content_words:
                continue
            
            # 如果超过50%的关键词在上下文中,认为有支持
            context_lower = context.lower()
            matched = sum(1 for w in content_words if w.lower() in context_lower)
            support_ratio = matched / len(content_words)
            
            if support_ratio >= 0.5:
                supported.append(sent)
            else:
                unsupported.append(sent)
        
        return {
            "is_hallucinated": len(unsupported) > 0,
            "supported_claims": len(supported),
            "unsupported_claims": len(unsupported),
            "hallucination_rate": len(unsupported) / max(1, len(supported) + len(unsupported))
        }
    
    def query(self, question: str) -> Dict:
        """完整查询流程"""
        # 1. 检索
        retrieved = self.retrieve(question, top_k=3)
        context = "\n\n".join([r["content"] for r in retrieved])
        
        # 2. 生成
        answer = self.generate_answer(question, context)
        
        # 3. 幻觉检测
        hallucination_check = self.detect_hallucination(answer, context)
        
        # 4. 如果检测到幻觉,尝试修正
        if hallucination_check["is_hallucinated"]:
            correction_prompt = f"""原答案中包含无法在上下文中找到支持的内容。请重新回答,只使用以下上下文。
            
            问题:{question}
            上下文:{context}
            
            修正后的答案:"""
            
            response = openai.ChatCompletion.create(
                model="gpt-3.5-turbo",
                messages=[{"role": "user", "content": correction_prompt}],
                temperature=0.1
            )
            corrected_answer = response.choices[0].message.content
        else:
            corrected_answer = answer
        
        return {
            "question": question,
            "retrieved_chunks": len(retrieved),
            "context_used": context[:200] + "...",  # 截断显示
            "initial_answer": answer,
            "corrected_answer": corrected_answer,
            "hallucination_check": hallucination_check,
            "final_answer": corrected_answer if hallucination_check["is_hallucinated"] else answer
        }

# 使用示例
if __name__ == "__main__":
    # 初始化RAG系统
    rag = SimpleRAG()
    
    # 创建测试文档
    test_doc = """
    Python是一种解释型、面向对象的高级编程语言,由Guido van Rossum于1991年首次发布。
    它强调代码的可读性和简洁的语法,使用缩进来表示代码块。
    Python在数据科学、人工智能、Web开发等领域有广泛应用。
    主要的Python发行版包括CPython、PyPy、Jython等。
    """
    
    # 保存测试文件
    with open("test_doc.txt", "w", encoding="utf-8") as f:
        f.write(test_doc)
    
    # 加载和索引
    text = rag.load_document("test_doc.txt")
    chunks = rag.split_chunks(text, chunk_size=200, overlap=30)
    rag.build_index(chunks)
    
    # 提问
    question = "Python是什么时候发布的?"
    result = rag.query(question)
    
    print(f"问题: {result['question']}")
    print(f"最终答案: {result['final_answer']}")
    print(f"幻觉检测: {result['hallucination_check']}")
    
    # 清理
    os.remove("test_doc.txt")

评分标准:

  • 文档加载功能(5分)

  • 切分功能(5分)

  • 向量检索(5分)

  • 答案生成(5分)

  • 幻觉检测(5分)

  • 代码质量和注释(5分)


第十章:总结

RAG 系统作为连接大语言模型与外部知识库的桥梁,其实际落地效果取决于每个环节的精细优化。本文系统梳理了 8 大核心缺陷及其解决方案:

文档加载是基础,选择合适的解析器、实现缓存机制、处理特殊格式,能避免"输入即垃圾"的问题。对于混合格式文档,pdfplumber 处理表格、PyMuPDF 处理纯文本、Unstructured 处理复杂布局,按需组合使用。

文档切分直接影响检索质量。过粗产生噪音,过细丢失上下文。最优策略是:根据内容类型选择切分器(文本用语义切分、代码用函数级切分),设置 10-20% 的重叠,并保留父文档引用。自适应切分能根据文本复杂度动态调整,是进阶方案。

错过排名靠前的文档是检索阶段的核心痛点。混合检索(向量+BM25)+ RRF 融合可将召回率提升 30% 以上。查询改写、HyDE、多路召回等技术能进一步缓解语义漂移问题。重排序模型(Cross-Encoder)作为最后一道关卡,能显著提升 top-k 结果的精度。

格式错误看似琐碎,实则严重影响 LLM 的理解。建立标准化流程:统一编码、规范化表格、修复代码块、重构标题层级。对重度损坏的文档,可调用 LLM 修复作为兜底方案。

答案不完整常源于复杂问题未被分解。通过问题分解、迭代检索、多文档推理,确保答案覆盖所有方面。带引用的答案生成不仅提高可信度,也便于用户验证。

未提取到答案时,不要强行回答。构建证据链、处理表格数据、执行数值计算,或在无法回答时优雅降级,提供相关主题和建议。

答案粒度控制体现了对用户意图的理解。通过关键词检测、用户画像、意图分析,动态调整答案的详细程度。提供多级答案(概要+详细+示例)能满足不同层次用户的需求。

幻觉问题是 RAG 最棘手的挑战,但可管理而非消除。组合使用约束生成、强制引用、事实验证、多源共识等策略,能将幻觉率从 20-30% 降至 5% 以下。关键在于建立验证机制,而非完全依赖模型自身。

实践建议

  1. 从简到繁:先用基础版本验证流程,再逐步添加优化

  2. 监控指标:追踪 Recall@K、MRR、幻觉率、端到端延迟

  3. 测试驱动:构建包含典型问题、边界案例、对抗样本的测试集

  4. 持续迭代:收集用户反馈,定期更新检索索引和提示词

  5. 成本权衡:更复杂的策略(如多源共识)成本更高,根据场景选择

RAG 不是一次性的工程,而是一个持续优化的过程。随着 embedding 模型、LLM 和检索技术的进步,今天的优化方案可能在半年后就不再最优。保持学习、实验和迭代的心态,才能构建真正可靠的 RAG 系统。


🌟 感谢您耐心阅读到这里!

🚀 技术成长没有捷径,但每一次的阅读、思考和实践,都在默默缩短您与成功的距离。

💡 如果本文对您有所启发,欢迎点赞👍、收藏📌、分享📤给更多需要的伙伴!

🗣️ 期待在评论区看到您的想法、疑问或建议,我会认真回复,让我们共同探讨、一起进步~

🔔 关注我,持续获取更多干货内容!

🤗 我们下篇文章见!

相关推荐
chushiyunen2 小时前
python web框架streamlit
开发语言·前端·python
DeepModel2 小时前
机器学习降维核心:奇异值分解 SVD
人工智能·python·机器学习
Agent产品评测局2 小时前
能源行业自动化解决方案选型,安全与降本双提升:2026企业级智能体选型指南
运维·人工智能·安全·ai·chatgpt·自动化
眷蓝天2 小时前
python基础
开发语言·python
Cosmoshhhyyy2 小时前
《Effective Java》解读第46条:优先选择Stream中无副作用的函数
java·windows·python
徽先生2 小时前
注释标准模板
python
gf13211112 小时前
流光剪辑_调用生成图片模型/apimart调用生成视频模型
python
chenglin0162 小时前
Semantic Kernel 内核详解
后端·python·flask
B站_计算机毕业设计之家2 小时前
计算机毕业设计:Python城市地铁网络可视化分析系统 Flask框架 数据分析 可视化 高德地图 数据挖掘 机器学习 爬虫(建议收藏)✅
网络·python·信息可视化·数据挖掘·flask·课程设计·美食