
目录
[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库 | 保留标题层级 |
核心原则:
始终保留文档元数据(页码、章节、来源文件)
实现缓存机制,避免重复解析
大文件(>100MB)采用流式读取
扫描型 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 |
黄金法则:
Overlap 应设置为 chunk_size 的 10-20%,避免关键信息恰好在边界丢失
始终保留父文档引用,支持答案溯源到原始位置
对特殊格式(表格、代码、公式)使用专用切分器
三、错过排名靠前的文档
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 |
关键优化点:
检索不是越多越好:top_k 通常设为 3-10,过大会引入噪音
使用 RRF 替代加权求和,避免分数尺度不一致问题
对特定领域(医疗、法律)微调 embedding 模型
实现检索缓存,相同查询直接返回结果
四、格式错误

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 |
核心建议:
在加载阶段就进行格式标准化,而非检索后
保留原始文本的备份,便于问题追溯
对关键文档(如API文档)使用专用解析器
定期验证格式修复效果,更新规则库
五、答案不完整

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" |
| 开放式问题 | 问题分解+合并 | "深度学习的未来趋势" |
关键要点:
使用问题分解确保覆盖所有方面
要求LLM输出结构化答案(列表、表格)
实现答案验证和迭代完善机制
保留中间检索结果,支持答案溯源
六、未提取到答案

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的优缺点" |
| 未找到 | 优雅降级 | 提供建议、总结相关主题 |
核心原则:
始终尝试多文档融合,而非单文档提取
实现"无法回答"检测,避免幻觉
提供透明的推理过程,便于调试
收集失败案例,持续优化检索策略
七、答案太具体或者太笼统
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 最佳实践
| 用户类型 | 推荐粒度 | 策略 |
|---|---|---|
| 初学者 | 详细+示例 | 解释术语,提供背景 |
| 中级用户 | 平衡 | 核心信息+关键细节 |
| 专家 | 简洁 | 直接给出结论或要点 |
| 决策者 | 摘要+关键数据 | 突出结论和数据 |
| 开发者 | 具体+可执行 | 提供代码、配置、命令 |
关键技巧:
提供粒度切换选项(如"简单/详细"按钮)
答案开头用一句话总结,后续展开
使用折叠块组织详细内容
收集用户反馈,个性化调整
八、幻觉问题
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 最佳实践总结
| 策略 | 原理 | 成本 | 效果 |
|---|---|---|---|
| 约束生成 | 强制使用上下文 | 低 | 中 |
| 强制引用 | 要求标注来源 | 低 | 高 |
| 事实验证 | 独立检查 | 中 | 高 |
| 自我修正 | 迭代改进 | 中 | 高 |
| 多源共识 | 交叉验证 | 高 | 很高 |
| 置信度评分 | 标记不确定性 | 低 | 中 |
黄金法则:
永远不要跳过验证:特别是高风险场景(医疗、法律、金融)
使用系统提示词:在开始时明确告知模型"不要编造信息"
降低temperature:使用0.1-0.3减少随机性
实现人机回环:对低置信度答案请求人工确认
记录幻觉案例:用于改进检索和提示词
第九章:练习题及其答案
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生成不基于检索文档的内容,包括编造事实、偏离源文档、逻辑错误等。
缓解方法:
约束生成:在prompt中明确要求"只使用以下上下文中的信息",禁止添加外部知识。
强制引用:要求每个陈述标注来源编号,便于验证和追溯。
事实验证:独立检查答案中的每个声明是否在源文档中有支持。
多源共识:只有当多个chunk支持时才采信信息。
置信度评分:对不确定的信息标注置信度(高/中/低)。
降低temperature:使用0.1-0.3减少模型随机性。
9.4 实操题(30分)
题目:实现一个简单的RAG系统,要求包含以下功能:
-
文档加载(支持.txt文件)
-
文档切分(chunk_size=200, overlap=30)
-
向量检索(使用sentence-transformers)
-
答案生成(使用OpenAI API或本地LLM)
-
包含幻觉检测(检查答案是否基于检索结果)
参考答案代码:
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% 以下。关键在于建立验证机制,而非完全依赖模型自身。
实践建议:
从简到繁:先用基础版本验证流程,再逐步添加优化
监控指标:追踪 Recall@K、MRR、幻觉率、端到端延迟
测试驱动:构建包含典型问题、边界案例、对抗样本的测试集
持续迭代:收集用户反馈,定期更新检索索引和提示词
成本权衡:更复杂的策略(如多源共识)成本更高,根据场景选择
RAG 不是一次性的工程,而是一个持续优化的过程。随着 embedding 模型、LLM 和检索技术的进步,今天的优化方案可能在半年后就不再最优。保持学习、实验和迭代的心态,才能构建真正可靠的 RAG 系统。
🌟 感谢您耐心阅读到这里!
🚀 技术成长没有捷径,但每一次的阅读、思考和实践,都在默默缩短您与成功的距离。
💡 如果本文对您有所启发,欢迎点赞👍、收藏📌、分享📤给更多需要的伙伴!
🗣️ 期待在评论区看到您的想法、疑问或建议,我会认真回复,让我们共同探讨、一起进步~
🔔 关注我,持续获取更多干货内容!
🤗 我们下篇文章见!