Day18-RAG技术原理
欢迎来到《自学30天掌握AI开发》的第18天!在前面的课程中,我们学习了AI Agent技术和MCP技术的应用。今天,我们将深入探索检索增强生成(RAG)技术的原理。RAG技术是大语言模型应用的一次重要突破,它通过将外部知识融入生成过程,有效克服了大模型的知识局限性,使AI系统能够提供更准确、更可靠的回答。本课程将带你理解RAG的基本概念、技术架构和关键组件,为你后续开发基于RAG的应用打下坚实基础。
🎯 学习目标
完成今天的学习后,你将能够:
- 理解检索增强生成(RAG)的基本概念与工作原理
- 掌握RAG系统的核心组件与技术架构
- 了解RAG在解决大语言模型局限性方面的价值
- 能够分析不同RAG实现方案的优缺点
- 准备为构建自己的RAG系统奠定理论基础
⏱️ 学习建议
今天的内容涉及RAG技术的基础理论,建议按以下方式规划你的学习时间:
学习内容 | 建议时间 |
---|---|
RAG基础概念学习 | 45分钟 |
文本处理与嵌入技术 | 60分钟 |
向量存储与检索原理 | 60分钟 |
上下文构建策略 | 45分钟 |
生成与后处理技术 | 45分钟 |
RAG系统评估方法 | 30分钟 |
实践活动 | 90分钟 |
自测检验 | 30分钟 |
学习方法建议:
- 概念关联法:将RAG技术与之前学习的大语言模型知识关联起来,理解其作为增强技术的价值
- 问题驱动法:思考大模型的局限性问题,以及RAG如何解决这些问题
- 可视化学习:绘制RAG系统的组件流程图,帮助理解各组件间的关系
- 主动思考:针对自己感兴趣的领域,思考RAG技术的潜在应用场景
- 动手实验:尝试使用简单的RAG框架进行基础实验,加深理解
🔑 核心知识点
1. 检索增强生成(RAG)基础概念
1.1 RAG的定义与发展
**检索增强生成(Retrieval-Augmented Generation, RAG)**是一种将信息检索与文本生成相结合的技术框架,它通过在生成过程中引入外部知识源,增强大语言模型的回答能力。
RAG的核心思想可以简单表述为:
最终输出 = 大语言模型能力 + 外部检索到的相关信息
RAG技术的发展历程:
- 2020年:Facebook AI Research(现Meta AI)发表了开创性论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》,首次提出RAG概念
- 2021年:开源社区开始实现各种RAG框架,如LangChain、Haystack等
- 2022年:RAG技术在企业级应用中开始广泛部署,特别是在知识密集型应用场景
- 2023年:高级RAG架构出现,包括多查询RAG、递归RAG和混合检索策略等
- 2024年:RAG与Agent结合,形成智能知识工作者,能够自主检索和推理
与传统问答系统的区别:
维度 | 传统问答系统 | RAG系统 |
---|---|---|
回答生成方式 | 直接检索预定义答案 | 动态结合检索内容生成答案 |
表达灵活性 | 有限,通常是模板化回答 | 高度灵活,自然流畅 |
知识更新 | 需要手动维护答案库 | 只需更新知识库,生成能力不变 |
未见问题处理 | 难以处理未预设的问题 | 能基于相关信息推理出答案 |
实现复杂度 | 相对简单 | 架构较复杂,需要多个组件协同 |
1.2 RAG解决的核心问题
RAG技术主要解决了大语言模型在实际应用中面临的几个关键挑战:
知识局限性问题:
- 大模型知识在预训练截止日期后不再更新
- 专业领域知识可能覆盖不足
- 私有或组织特定信息无法获取
幻觉与事实错误:
- 大模型可能生成看似合理但实际不正确的内容
- 缺乏对生成内容的事实依据
- 无法区分真实知识与推测内容
透明度与可追溯性:
- 传统大模型难以提供信息来源
- 无法验证回答的准确性与可靠性
- 缺乏对结论的解释与支持证据
数据私密性与合规性:
- 直接向大模型提供敏感信息存在风险
- 需要控制模型对专有信息的访问
- 满足行业监管与合规要求
1.3 RAG的基本工作流程
RAG系统的典型工作流程包含以下关键步骤:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 1.查询处理 │ -> │ 2.知识检索 │ -> │ 3.上下文构建 │ -> │ 4.增强生成 │ -> │ 5.后处理优化 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
各步骤详解:
-
查询处理
- 理解用户问题/查询的意图
- 提取关键词和概念
- 查询重写和扩展
- 确定检索策略
-
知识检索
- 在向量数据库中查找相关文档/段落
- 执行语义搜索找到相关信息
- 排序检索结果
- 应用过滤和筛选逻辑
-
上下文构建
- 选择最相关的检索结果
- 组织检索内容形成连贯上下文
- 处理令牌长度限制
- 确保关键信息包含在上下文中
-
增强生成
- 将查询和检索到的上下文提供给LLM
- 应用优化的提示模板引导生成
- 指导模型基于检索到的信息回答
- 控制生成过程的参数
-
后处理优化
- 验证生成内容的准确性
- 添加引用和来源信息
- 格式化和美化输出
- 过滤不当或不相关内容
Python伪代码示例:
def rag_pipeline(user_query):
# 1. 查询处理
processed_query = query_processor.process(user_query)
# 2. 知识检索
relevant_documents = vector_store.search(
query_embedding=embedding_model.embed(processed_query),
top_k=5
)
# 3. 上下文构建
context = context_builder.build(relevant_documents, token_budget=3000)
# 4. 增强生成
prompt = f"""
基于以下信息回答问题。如果信息中没有答案,请说明你不知道,不要编造信息。
信息:
{context}
问题: {user_query}
"""
response = llm.generate(prompt)
# 5. 后处理优化
final_response = post_processor.process(
response=response,
sources=relevant_documents,
query=user_query
)
return final_response
1.4 RAG应用场景与优势
典型应用场景:
-
企业知识库与支持系统
- 接入内部文档、产品手册、知识库
- 提供精准的员工和客户支持
- 保持知识的时效性和准确性
-
专业领域问答系统
- 法律、医疗、金融等专业咨询
- 基于权威资料提供专业建议
- 确保回答有可靠依据
-
个性化教育与学习助手
- 根据教材和课程内容提供解答
- 适应特定学科和课程体系
- 提供有针对性的学习资料
-
研究与创新支持工具
- 快速检索相关研究论文和专利
- 辅助文献综述和研究分析
- 促进跨领域知识发现
RAG相比纯LLM的关键优势:
- 知识时效性:可随时更新知识库,无需重新训练模型
- 可控可靠性:回答基于特定知识源,减少幻觉和错误
- 可解释性:能够提供信息来源,增加透明度
- 成本效益:避免针对特定领域进行大规模微调
- 数据私密性:知识库完全可控,不会泄露给模型提供商
- 可定制性:可以根据具体需求定制知识库内容和范围
- 长尾知识:覆盖大模型训练数据中稀疏的专业内容
2. 文本处理与嵌入技术
RAG系统的基础在于高质量的文本处理和嵌入生成,这直接影响检索的准确性和系统整体性能。
2.1 文本预处理技术
在将文档加入RAG系统前,需要进行一系列预处理步骤,以提高后续检索的效果:
文本清洗与标准化:
- HTML/特殊格式清洗:移除HTML标签、特殊符号和格式标记
- 统一编码与规范化:确保文本使用一致的编码(如UTF-8),处理特殊字符
- 拼写纠正与规范化:修正明显的拼写错误,统一术语表达
- 大小写与标点处理:根据需要统一大小写和标点符号
文本分段与结构化:
- 段落识别与边界检测:根据自然段落或主题边界分割文本
- 标题与层次结构识别:识别文档的层次结构,如标题、子标题
- 表格与列表提取:识别并适当处理表格、列表等结构化内容
- 元数据提取:提取文档标题、作者、日期等元信息
语言学预处理:
- 分词(Tokenization):将文本拆分为词或子词单元
- 停用词过滤:去除常见但意义不大的词(如"的"、“是”、“and”、“the”)
- 词干提取/词形还原:将不同形式的词转换为基本形式
- 词性标注:识别词的语法角色(名词、动词等)
实体识别与关系提取:
- 命名实体识别(NER):检测人名、地点、组织、日期等实体
- 关系抽取:识别实体间的关系(如"X是Y的创始人")
- 关键术语提取:识别文档中的专业术语和重要概念
- 关联信息注解:增强实体与外部知识的关联
Python代码示例:使用NLTK和spaCy进行文本预处理
import nltk
import spacy
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from bs4 import BeautifulSoup
# 加载必要的资源
nltk.download('punkt')
nltk.download('stopwords')
nlp = spacy.load("en_core_web_sm")
stop_words = set(stopwords.words('english'))
stemmer = PorterStemmer()
def preprocess_text(text, clean_html=True, remove_stopwords=True, perform_stemming=False):
"""文本预处理综合函数"""
# HTML清洗
if clean_html:
soup = BeautifulSoup(text, "html.parser")
text = soup.get_text(separator=" ")
# 分词和基础清理
tokens = nltk.word_tokenize(text)
tokens = [token.lower() for token in tokens if token.isalpha()]
# 停用词过滤
if remove_stopwords:
tokens = [token for token in tokens if token not in stop_words]
# 词干提取
if perform_stemming:
tokens = [stemmer.stem(token) for token in tokens]
# 实体识别示例
doc = nlp(" ".join(tokens))
entities = [(ent.text, ent.label_) for ent in doc.ents]
processed_text = " ".join(tokens)
return {
"processed_text": processed_text,
"entities": entities,
"token_count": len(tokens)
}
# 使用示例
sample_text = """
<p>Apple Inc. was founded by Steve Jobs, Steve Wozniak, and Ronald Wayne in April 1976.
The company's first product was the Apple I, a computer designed and hand-built by Wozniak.</p>
"""
result = preprocess_text(sample_text)
print(f"处理后文本: {result['processed_text'][:100]}...")
print(f"识别到的实体: {result['entities']}")
print(f"标记数量: {result['token_count']}")
2.2 文档分块策略
文档分块(Chunking)是RAG系统中至关重要的一步,它直接影响检索的粒度和准确性:
分块的目的与重要性:
- 将长文档切分为适合嵌入和检索的较小单元
- 确保语义完整性,避免关键上下文丢失
- 控制嵌入模型输入大小,遵守模型限制
- 提高检索粒度,精确定位相关信息
常见分块策略:
-
固定大小分块:
- 按固定字符数、词数或标记数分割
- 优点:实现简单,处理一致
- 缺点:可能切分不当,破坏语义完整性
-
语义边界分块:
- 基于段落、句子或自然语义边界分割
- 优点:保持语义完整性
- 缺点:块大小不均匀,可能需要进一步处理
-
混合分块法:
- 首先按语义边界分割,然后应用大小限制
- 优点:平衡语义完整性和大小控制
- 实现:先按段落分,大段落再细分
-
层次分块:
- 创建多层次的分块,包含不同粒度
- 优点:支持多级检索,灵活性高
- 应用:先文档级,再章节,最后段落级检索
分块重叠技术:
为避免关键信息在分块边界丢失,通常采用重叠分块技术:
块1: [.........]
块2: [.........]
块3: [.........]
- 重叠量设置:通常为块大小的10%-20%
- 重叠内容选择:优先确保句子完整性
- 边界处理:特别注意实体和关系不被分割
Python代码示例:实现不同分块策略
def chunk_by_fixed_size(text, chunk_size=1000, overlap=200):
"""固定大小分块,带重叠"""
chunks = []
start = 0
text_len = len(text)
while start < text_len:
# 计算当前块的结束位置
end = min(start + chunk_size, text_len)
# 如果不是最后一块,尝试找到更合适的结束位置(如句号)
if end < text_len:
# 在重叠范围内寻找句子边界
sentence_boundary = text.rfind('. ', end - min(overlap, end-start), end) + 1
if sentence_boundary > start:
end = sentence_boundary
# 添加当前块
chunks.append(text[start:end].strip())
# 移动到下一块的起始位置,考虑重叠
start = end - overlap if end < text_len else text_len
return chunks
def chunk_by_semantic_units(text, max_chunk_size=1500):
"""语义边界分块(段落为单位)"""
# 按段落分割文本
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
chunks = []
current_chunk = ""
for paragraph in paragraphs:
# 如果当前段落加上已有内容不超过最大尺寸,就添加到当前块
if len(current_chunk) + len(paragraph) <= max_chunk_size:
current_chunk += (paragraph + "\n\n")
else:
# 如果当前块非空,添加到结果
if current_chunk:
chunks.append(current_chunk.strip())
# 开始新块
# 如果单个段落超过最大尺寸,需要进一步分割
if len(paragraph) > max_chunk_size:
# 按句子分割过长段落
sentences = paragraph.replace('. ', '.|').split('|')
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) <= max_chunk_size:
current_chunk += (sentence + " ")
else:
chunks.append(current_chunk.strip())
current_chunk = sentence + " "
else:
current_chunk = paragraph + "\n\n"
# 添加最后一个块
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
# 使用示例
text = """这是第一个段落,包含几个句子。这些句子应该被保持在一起。这是该段落的最后一句。
这是第二个段落,讨论不同的主题。它也有多个句子。这些句子形成一个连贯的思想单元。
这是一个很长的段落,可能需要被分割。它包含许多句子,讨论同一个主题的不同方面。这样的长段落在某些文档中很常见。如技术文档、学术论文或详细报告。在RAG系统中,适当处理这类长段落很重要。否则可能会丢失重要上下文。分块策略应该平衡语义完整性和大小限制。"""
fixed_chunks = chunk_by_fixed_size(text)
semantic_chunks = chunk_by_semantic_units(text)
print("固定大小分块结果:")
for i, chunk in enumerate(fixed_chunks):
print(f"块 {i+1} ({len(chunk)} 字符): {chunk[:50]}...")
print("\n语义分块结果:")
for i, chunk in enumerate(semantic_chunks):
print(f"块 {i+1} ({len(chunk)} 字符): {chunk[:50]}...")
2.3 嵌入模型选择
嵌入模型(Embedding Model)是RAG系统的核心组件之一,它将文本转换为向量表示,使语义检索成为可能:
嵌入模型的作用:
- 将自然语言文本转换为向量空间中的数值表示
- 捕捉文本的语义信息,相似内容在向量空间中距离较近
- 支持高效的语义相似度搜索和匹配
主流嵌入模型对比:
嵌入模型 | 维度 | 上下文长度 | 语言支持 | 特点 |
---|---|---|---|---|
OpenAI text-embedding-3 | 1536 | 8192 | 多语言 | 高质量通用嵌入,支持较长文本 |
Cohere embedding-english | 4096 | 512 | 英语为主 | 高维度嵌入,适合精确检索 |
BAAI/bge-large | 1024 | 512 | 多语言 | 开源模型,中英文表现均佳 |
Voyage/voyage-large | 1024 | 4096 | 多语言 | 支持长文本,开源可商用 |
Jina embeddings | 768 | 8192 | 多语言 | 长文档支持,开源可本地部署 |
SentenceTransformers | 384-768 | 512 | 多语言 | 轻量级,适合资源受限场景 |
选择考量因素:
-
语义理解能力:
- 对相似概念的关联能力
- 跨领域理解的泛化性
- 专业术语的表示准确性
-
性能与资源需求:
- 向量维度与存储开销
- 计算速度与硬件需求
- 批处理效率与吞吐量
-
语言与领域支持:
- 多语言支持能力
- 领域特定语言理解
- 跨语言语义匹配
-
接口与集成:
- API可用性与稳定性
- 本地部署可能性
- 与其他组件的集成难度
嵌入测试与评估方法:
评估嵌入模型性能的常用方法:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def evaluate_embedding_model(model, test_pairs, similarity_threshold=0.7):
"""
评估嵌入模型在语义相似性任务上的表现
参数:
- model: 嵌入模型对象
- test_pairs: 列表,每项为(text1, text2, expected_similar),expected_similar为布尔值
- similarity_threshold: 判定为相似的余弦相似度阈值
返回:
- 准确率、精确率、召回率和F1分数
"""
correct = 0
true_positives = 0
false_positives = 0
false_negatives = 0
results = []
for text1, text2, expected in test_pairs:
# 获取嵌入
embedding1 = model.embed(text1)
embedding2 = model.embed(text2)
# 计算余弦相似度
similarity = cosine_similarity([embedding1], [embedding2])[0][0]
# 基于阈值判断相似性
predicted_similar = similarity >= similarity_threshold
# 评估预测
if predicted_similar == expected:
correct += 1
if predicted_similar and expected:
true_positives += 1
elif predicted_similar and not expected:
false_positives += 1
elif not predicted_similar and expected:
false_negatives += 1
results.append({
"text1": text1,
"text2": text2,
"expected": expected,
"predicted": predicted_similar,
"similarity": similarity
})
# 计算指标
accuracy = correct / len(test_pairs)
precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
return {
"accuracy": accuracy,
"precision": precision,
"recall": recall,
"f1": f1,
"detailed_results": results
}
# 使用示例
test_pairs = [
("人工智能正在改变我们的生活方式", "AI技术给日常生活带来巨大变革", True),
("深度学习是机器学习的一个分支", "神经网络是深度学习的基础", True),
("Python是一种编程语言", "苹果是一种水果", False),
("向量数据库用于存储嵌入", "矢量型数据库专门设计用于高效管理向量嵌入", True),
("RAG技术解决了大语言模型的知识局限性", "股票市场今天下跌了2%", False)
]
# 假设我们有一个嵌入模型
# results = evaluate_embedding_model(embedding_model, test_pairs)
# print(f"准确率: {results['accuracy']:.2f}")
# print(f"F1分数: {results['f1']:.2f}")
2.4 嵌入生成最佳实践
在实际RAG系统中,嵌入生成需要考虑多种因素以优化检索效果:
嵌入粒度选择:
- 文档级嵌入:适合整体相关性判断和初筛
- 段落级嵌入:平衡语义完整性和检索精度的常用选择
- 句子级嵌入:适合精确答案搜索,但可能缺乏上下文
- 多粒度混合:综合使用不同粒度嵌入,分层检索
嵌入缓存与批处理:
- 实现嵌入结果缓存,避免重复计算
- 利用批处理提高嵌入生成吞吐量
- 增量更新策略,仅处理新增或修改内容
- 分布式处理大规模文档集合
领域适应技术:
- 使用领域内数据微调通用嵌入模型
- 实施专业术语增强处理
- 结合领域知识图谱改进嵌入质量
- 开发领域特定相似度度量方法
多模态嵌入考虑:
- 文本与图像联合嵌入生成
- 表格、图表等结构化内容特殊处理
- 音频和视频内容的文本转写与嵌入
- 跨模态检索支持
嵌入生成工程最佳实践:
import time
import hashlib
import pickle
from typing import List, Dict, Any
import numpy as np
class EmbeddingManager:
"""高效嵌入生成和管理类"""
def __init__(self, embedding_model, cache_path=None, batch_size=32):
"""
初始化嵌入管理器
参数:
- embedding_model: 嵌入模型对象
- cache_path: 缓存文件路径,None表示不使用缓存
- batch_size: 批处理大小
"""
self.embedding_model = embedding_model
self.cache_path = cache_path
self.batch_size = batch_size
self.embedding_cache = {}
# 加载缓存
if cache_path:
try:
with open(cache_path, 'rb') as f:
self.embedding_cache = pickle.load(f)
print(f"已加载 {len(self.embedding_cache)} 条嵌入缓存")
except (FileNotFoundError, pickle.PickleError):
print("未找到缓存文件或加载失败,将创建新缓存")
def _get_cache_key(self, text: str) -> str:
"""生成文本的唯一缓存键"""
return hashlib.md5(text.encode('utf-8')).hexdigest()
def save_cache(self):
"""保存嵌入缓存到文件"""
if self.cache_path:
with open(self.cache_path, 'wb') as f:
pickle.dump(self.embedding_cache, f)
print(f"已保存 {len(self.embedding_cache)} 条嵌入缓存")
def embed_texts(self, texts: List[str], show_progress=False) -> np.ndarray:
"""
批量生成文本嵌入,利用缓存和批处理
参数:
- texts: 文本列表
- show_progress: 是否显示进度
返回:
- 嵌入向量数组,形状为 (len(texts), embedding_dim)
"""
embeddings = []
texts_to_embed = []
indices_to_embed = []
# 检查哪些文本需要重新嵌入
for i, text in enumerate(texts):
cache_key = self._get_cache_key(text)
if cache_key in self.embedding_cache:
embeddings.append(self.embedding_cache[cache_key])
else:
texts_to_embed.append(text)
indices_to_embed.append(i)
# 如果有需要嵌入的文本
if texts_to_embed:
# 批处理生成嵌入
total_batches = (len(texts_to_embed) + self.batch_size - 1) // self.batch_size
new_embeddings = []
for batch_idx in range(total_batches):
start_idx = batch_idx * self.batch_size
end_idx = min((batch_idx + 1) * self.batch_size, len(texts_to_embed))
batch_texts = texts_to_embed[start_idx:end_idx]
if show_progress:
print(f"处理批次 {batch_idx+1}/{total_batches},包含 {len(batch_texts)} 个文本")
start_time = time.time()
batch_embeddings = self.embedding_model.embed_batch(batch_texts)
elapsed = time.time() - start_time
if show_progress:
print(f"批次嵌入耗时: {elapsed:.2f}秒 ({len(batch_texts)/elapsed:.2f} 文本/秒)")
new_embeddings.extend(batch_embeddings)
# 更新缓存
for i, text in enumerate(batch_texts):
cache_key = self._get_cache_key(text)
self.embedding_cache[cache_key] = batch_embeddings[i]
# 将新生成的嵌入插入到正确位置
for i, idx in enumerate(indices_to_embed):
embeddings.insert(idx, new_embeddings[i])
return np.array(embeddings)
def embed_text(self, text: str) -> np.ndarray:
"""单文本嵌入生成,使用缓存"""
return self.embed_texts([text])[0]
# 使用示例(伪代码)
# embedding_model = SomeEmbeddingModel()
# manager = EmbeddingManager(embedding_model, cache_path="embeddings_cache.pkl")
# documents = ["文档1", "文档2", "文档3", ...]
# embeddings = manager.embed_texts(documents, show_progress=True)
# manager.save_cache()
3. 向量存储与检索技术
在RAG系统中,向量存储和检索是连接文档处理与生成模型的核心环节,它决定了系统能否找到与用户查询最相关的信息。
3.1 向量数据库基础
向量数据库的定义与作用:
向量数据库是专门设计用于存储、管理和检索高维向量数据的数据库系统。在RAG中,它用于存储文档嵌入向量并支持高效的相似性搜索。
向量数据库与传统数据库的区别:
特性 | 传统关系型数据库 | 向量数据库 |
---|---|---|
主要数据类型 | 结构化表格数据 | 高维向量数据 |
查询方式 | 精确匹配(SQL) | 近似相似度搜索 |
索引机制 | B树、哈希等 | HNSW、IVF、LSH等 |
优化目标 | ACID事务、连接效率 | ANN搜索、向量操作 |
扩展性 | 垂直扩展为主 | 水平扩展友好 |
主流向量数据库对比:
数据库 | 类型 | 开源 | 存储方式 | 索引算法 | 特点 |
---|---|---|---|---|---|
Pinecone | 托管服务 | 否 | 云端 | HNSW变体 | 易用性强,无需维护 |
Weaviate | 全栈数据库 | 是 | 本地/云端 | HNSW | 支持多模态,GraphQL接口 |
Milvus | 分布式系统 | 是 | 本地/云端 | 多算法支持 | 高性能,分布式架构 |
Qdrant | 向量搜索引擎 | 是 | 本地/云端 | HNSW | 高度可配置,过滤功能强大 |
Chroma | 轻量级库 | 是 | 内存/持久化 | 基础算法 | 开发友好,易于集成 |
FAISS | 库(非数据库) | 是 | 内存 | 多算法支持 | 高性能,Facebook开发 |
选择向量数据库的考量因素:
- 规模需求:数据量大小和查询频率
- 部署要求:本地、云端或混合部署
- 性能需求:查询延迟和吞吐量要求
- 功能需求:元数据过滤、多模态支持等
- 预算与资源:托管服务成本vs自建维护
3.2 向量索引原理
向量索引是向量数据库高效检索的核心,其目的是避免在查询时与所有向量进行计算,从而加速相似性搜索:
常见索引算法:
-
HNSW (Hierarchical Navigable Small World):
- 原理:构建多层图结构,支持对数时间复杂度的搜索
- 优势:搜索速度快,精度高
- 劣势:内存占用大,构建时间长
- 应用:Qdrant、Weaviate、Pinecone等广泛采用
-
IVF (Inverted File Index):
- 原理:将向量空间分割为多个单元,搜索时仅检查相关单元
- 优势:平衡速度和内存占用
- 劣势:精度受聚类质量影响
- 应用:FAISS的基础索引之一
-
PQ (Product Quantization):
- 原理:将高维向量分解成低维子向量,每个子向量独立量化
- 优势:极大减小存储空间,加快搜索
- 劣势:降低了精度
- 应用:常与IVF结合使用(IVF-PQ)
-
LSH (Locality-Sensitive Hashing):
- 原理:使用特殊哈希函数,相似向量映射到相同桶
- 优势:理论基础扎实,易于理解
- 劣势:精度较低,需要多个哈希表
- 应用:较早的ANN实现方式
索引参数调优:
HNSW索引的关键参数及其影响:
# HNSW索引参数示例(Qdrant格式)
hnsw_config = {
"m": 16, # 每个节点的最大连接数,影响图的连通性,通常8-16
"ef_construct": 100, # 构建时的搜索宽度,影响构建质量,通常50-200
"ef_search": 128, # 搜索时的队列大小,影响搜索精度,通常50-500
"max_elements": 1000000 # 索引最大元素数量估计
}
# 使用示例(伪代码)
# vector_db.create_collection(
# name="documents",
# vectors_config={"size": 1536, "distance": "Cosine"},
# hnsw_config=hnsw_config
# )
向量索引的性能权衡:
构建向量索引时,需要在以下几个维度进行权衡:
-
查询速度 vs. 召回率:
- 更快的查询通常以降低召回率为代价
- 参数调整:降低ef_search加速查询但降低精度
-
构建时间 vs. 索引质量:
- 更高质量的索引需要更长的构建时间
- 参数调整:增加ef_construct提高质量但延长构建时间
-
内存占用 vs. 搜索性能:
- 更好的性能通常需要更多内存
- HNSW比其他算法内存消耗更大,但查询更快
-
索引大小 vs. 向量维度:
- 高维向量索引占用更多空间
- 量化技术(如PQ)可以压缩高维向量
3.3 相似性搜索与距离度量
向量搜索的核心是相似性计算,不同的距离度量方法适用于不同场景:
常用距离度量方法:
-
余弦相似度 (Cosine Similarity):
- 计算公式:cos(θ) = (A·B)/(||A||·||B||)
- 适用场景:关注向量方向而非绝对大小的文本语义搜索
- 特点:值域为[-1,1],常用于文本嵌入
- 使用时需注意:对于L2归一化的向量,等价于欧氏距离
-
欧氏距离 (Euclidean Distance):
- 计算公式:sqrt(Σ(ai-bi)²)
- 适用场景:考虑向量的绝对位置和大小
- 特点:值域为[0,∞),常用于图像特征
- 使用时需注意:对向量长度敏感,通常需要归一化
-
点积 (Dot Product):
- 计算公式:Σ(ai×bi)
- 适用场景:适用于归一化向量的相似度计算
- 特点:计算简单高效
- 使用时需注意:仅适用于已归一化的向量
-
曼哈顿距离 (Manhattan Distance):
- 计算公式:Σ|ai-bi|
- 适用场景:特征具有正交独立性的场合
- 特点:受异常值影响较小
- 使用时需注意:很少用于高维语义空间
Python代码示例:不同相似度计算
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from scipy.spatial.distance import euclidean, cityblock
def calculate_similarities(vec1, vec2):
"""计算不同相似性度量"""
# 确保向量是numpy数组
vec1 = np.array(vec1)
vec2 = np.array(vec2)
# 余弦相似度
cos_sim = cosine_similarity([vec1], [vec2])[0][0]
# 欧氏距离
euc_dist = euclidean(vec1, vec2)
# 点积 (内积)
dot_prod = np.dot(vec1, vec2)
# 曼哈顿距离
man_dist = cityblock(vec1, vec2)
# L2归一化向量的余弦相似度
vec1_norm = vec1 / np.linalg.norm(vec1)
vec2_norm = vec2 / np.linalg.norm(vec2)
norm_cos_sim = cosine_similarity([vec1_norm], [vec2_norm])[0][0]
return {
"cosine_similarity": cos_sim,
"euclidean_distance": euc_dist,
"dot_product": dot_prod,
"manhattan_distance": man_dist,
"normalized_cosine": norm_cos_sim
}
# 使用示例
query_vec = [0.2, 0.5, 0.3, 0.8]
doc_vec = [0.1, 0.6, 0.2, 0.7]
similarities = calculate_similarities(query_vec, doc_vec)
for metric, value in similarities.items():
print(f"{metric}: {value:.4f}")
为RAG选择合适的距离度量:
嵌入类型 | 推荐距离度量 | 原因 |
---|---|---|
文本语义嵌入 | 余弦相似度 | 关注语义方向而非量级 |
已归一化嵌入 | 点积或余弦 | 计算效率更高 |
多模态嵌入 | 余弦或欧氏 | 取决于嵌入空间特性 |
稀疏嵌入 | 杰卡德或余弦 | 处理稀疏性更好 |
3.4 向量检索优化策略
高效的向量检索需要多方面的优化,以下是提升RAG检索质量和性能的关键策略:
检索参数优化:
-
K值选择:
- 控制返回的相似文档数量
- 过小:可能丢失相关信息
- 过大:包含过多不相关内容,增加后续处理量
- 建议:通常从5-10开始,根据需求调整
-
相似度阈值:
- 设置最小相似度要求,过滤低相关性结果
- 动态阈值:根据查询特性自适应调整
- 百分比阈值:仅保留相似度在前X%的结果
-
搜索深度(ef_search):
- 控制检索时探索的节点数量
- 增加可提高召回率,但降低速度
- 根据精度要求和性能预算调整
混合检索策略:
- 关键词与语义混合检索:
- 结合BM25等关键词匹配与向量搜索
- 优势:兼顾确切词汇匹配和语义理解
- 实现:两种搜索结果加权融合
def hybrid_search(query, vector_db, text_index, alpha=0.7):
"""混合检索函数
参数:
- query: 查询文本
- vector_db: 向量数据库连接
- text_index: 文本索引连接
- alpha: 向量搜索权重(0-1)
返回:
- 混合排序的结果列表
"""
# 向量语义搜索
vector_results = vector_db.search(
query_text=query,
top_k=20,
include_metadata=True
)
# 关键词搜索(如BM25)
keyword_results = text_index.search(
query=query,
top_k=20
)
# 结果合并与重排序
combined_results = {}
# 处理向量搜索结果
for item in vector_results:
doc_id = item.id
score = item.score * alpha
combined_results[doc_id] = {
"document": item.metadata,
"score": score
}
# 处理关键词搜索结果
for item in keyword_results:
doc_id = item.id
if doc_id in combined_results:
# 如果已存在,融合得分
combined_results[doc_id]["score"] += item.score * (1-alpha)
else:
# 添加新结果
combined_results[doc_id] = {
"document": item.metadata,
"score": item.score * (1-alpha)
}
# 按得分排序
sorted_results = sorted(
combined_results.items(),
key=lambda x: x[1]["score"],
reverse=True
)
return [item[1]["document"] for item in sorted_results[:10]]
-
多索引检索:
- 为不同长度或类型的内容建立不同索引
- 如:段落级索引+句子级索引
- 优势:同时获取宏观语境和精准匹配
-
多查询技术:
- 从一个用户查询生成多个不同角度的查询
- 聚合多个查询的检索结果
- 优势:提高召回率,减少单一视角限制
元数据过滤与加权:
- 元数据过滤:
- 基于时间、来源、主题等元数据进行预过滤
- 减少搜索空间,提高相关性
- 示例:仅检索最近一年的文档,或特定类别的内容
# 元数据过滤示例(Qdrant格式)
filter_condition = {
"must": [
{"key": "document_type", "match": {"value": "technical_report"}},
{"key": "created_date", "range": {"gte": "2023-01-01"}}
],
"should": [
{"key": "source", "match": {"value": "official_documentation"}},
{"key": "author", "match": {"value": "expert_committee"}}
]
}
# 使用示例(伪代码)
# results = vector_db.search(
# query_vector=query_embedding,
# filter=filter_condition,
# limit=10
# )
- 检索结果加权:
- 根据文档新鲜度、权威性调整相似度分数
- 时间衰减:较新文档获得更高权重
- 来源权重:权威来源获得更高优先级
分布式与并行检索:
-
数据分片:
- 将向量数据分散到多个节点,支持水平扩展
- 每个节点负责子集的索引和搜索
- 结果合并和排序在聚合层完成
-
查询并行处理:
- 并行执行多个不同的查询策略
- 不同参数配置的搜索同时进行
- 聚合各路结果,去重并排序
-
缓存机制:
- 热门查询结果缓存
- 部分结果缓存(如常用文档的嵌入)
- 多级缓存策略(内存、磁盘、分布式)
### 4. 上下文构建策略
上下文构建是RAG系统成功的关键环节。在向量检索返回相关文档后,我们需要将这些检索结果有效地整合为一个连贯的上下文,以供大语言模型生成准确的回答。
#### 4.1 上下文窗口设计
**上下文窗口的概念与重要性**:
上下文窗口(Context Window)是指提供给大语言模型的检索信息与用户查询的组合。设计合理的上下文窗口对RAG系统性能至关重要:
- 窗口过小:可能缺失关键信息,导致不完整回答
- 窗口过大:可能引入噪音,稀释关键信息,并增加token开销
- 丢失关键内容:如果重要信息不在窗口内,模型无法参考使用
**上下文窗口大小优化**:
选择最佳上下文窗口大小需要平衡多种因素:
1. **模型上下文长度限制**:
- 不同模型支持的最大token数不同(如GPT-4支持8K-128K)
- 需要为查询和生成保留足够空间
- 通常检索内容可占用50-70%的token预算
2. **信息密度控制**:
- 确保高相关性内容被优先包含
- 避免重复和低价值信息占用空间
- 保持充分但精简的内容
3. **令牌分配策略**:
- 分配合理比例给检索内容、指令、示例、查询等
- 根据任务复杂性动态调整分配
- 预留足够空间给模型生成回答
**上下文窗口结构示例**:
典型的RAG上下文窗口结构:
┌─────────────────────────────────────────────┐
│ 系统指令 (10-15% token) │
│ - 角色设定、行为指导 │
│ - 输出格式要求 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 检索内容 (50-70% token) │
│ - 来源1: [相关文档片段…] │
│ - 来源2: [相关文档片段…] │
│ - 来源N: [相关文档片段…] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 用户查询 (5-10% token) │
│ - 原始问题或指令 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 预留生成空间 (15-30% token) │
└─────────────────────────────────────────────┘
**上下文窗口模板示例**:
```python
def create_context_window(query, retrieved_documents, max_tokens=3000):
"""创建RAG上下文窗口
参数:
- query: 用户查询
- retrieved_documents: 检索到的文档列表,每项包含内容和元数据
- max_tokens: 上下文窗口的最大token数
返回:
- 格式化的上下文窗口
"""
# 系统指令部分(约500 tokens)
system_instruction = """你是一个基于检索的AI助手。请基于提供的信息回答问题。
如果在提供的信息中找不到答案,请坦诚说明你不知道,不要编造信息。
回答时,请引用信息来源(用[来源X]标记)。确保你的回答准确、全面、有条理。"""
# 为检索内容分配token预算(约70%的剩余空间)
retrieval_token_budget = int((max_tokens - len(system_instruction) - len(query)) * 0.7)
# 选择和格式化检索内容
context_content = ""
for i, doc in enumerate(retrieved_documents):
source_tag = f"[来源{i+1}]: {doc.get('source', '未知来源')}"
formatted_doc = f"{source_tag}\n{doc['content']}\n\n"
# 检查是否超出token预算
if len(context_content + formatted_doc) > retrieval_token_budget:
# 如果这个文档太大,可能需要截断
remaining_budget = retrieval_token_budget - len(context_content)
if remaining_budget > 100: # 确保至少有足够空间添加一些内容
truncated_doc = formatted_doc[:remaining_budget] + "..."
context_content += truncated_doc
break
context_content += formatted_doc
# 构建完整上下文窗口
full_context = f"""{system_instruction}
参考信息:
{context_content}
用户问题: {query}
"""
return full_context
4.2 相关性排序与过滤
从向量数据库获取检索结果后,需要进一步处理这些结果,确保最相关、最有价值的信息被优先包含:
检索结果重排序技术:
-
基础相似度排序:
- 基于向量相似度分数的初步排序
- 缺点:仅考虑语义相似性,忽略其他因素
-
多特征重排序:
- 结合多种特征进行排序:语义相似度、关键词匹配度、信息新鲜度等
- 使用机器学习模型(如LambdaMART)训练重排序器
- 优势:综合考量文档相关性的多个维度
-
交叉编码器重排序:
- 使用交叉编码器(Cross-encoder)直接评估查询-文档对的相关性
- 比双编码器(Bi-encoder)更准确但计算成本更高
- 适用于检索结果的精细重排序(通常处理少量初筛结果)
from sentence_transformers import CrossEncoder
def rerank_with_cross_encoder(query, retrieved_documents, top_k=5):
"""使用交叉编码器重排序检索结果
参数:
- query: 用户查询
- retrieved_documents: 初步检索的文档列表
- top_k: 返回的最终文档数量
返回:
- 重排序后的文档列表
"""
# 加载交叉编码器模型
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# 准备查询-文档对
query_doc_pairs = [(query, doc['content']) for doc in retrieved_documents]
# 使用交叉编码器计算相关性分数
relevance_scores = cross_encoder.predict(query_doc_pairs)
# 将分数与文档关联并排序
scored_docs = [(score, doc) for score, doc in zip(relevance_scores, retrieved_documents)]
scored_docs.sort(reverse=True, key=lambda x: x[0])
# 返回排序后的前top_k个文档
return [doc for _, doc in scored_docs[:top_k]]
冗余信息处理:
检索结果中常包含相似或重复信息,需要进行去重处理:
-
简单去重:
- 基于内容哈希或精确匹配识别完全相同的段落
- 限制:无法处理部分重复或表述不同但内容相似的情况
-
语义去重:
- 计算段落间的语义相似度,合并或过滤高度相似内容
- 算法:最大边际相关性(MMR),平衡相关性与多样性
- 挑选与查询相关且互相差异较大的文档
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def mmr_diversification(query_vector, doc_vectors, doc_contents,
lambda_param=0.5, k=5):
"""
使用最大边际相关性(MMR)算法进行多样化排序
参数:
- query_vector: 查询的向量表示
- doc_vectors: 文档向量列表
- doc_contents: 文档内容列表
- lambda_param: 多样性权重(0-1),越小多样性越高
- k: 返回的文档数量
返回:
- 多样化排序后的文档列表
"""
# 确保输入是numpy数组
query_vector = np.array(query_vector).reshape(1, -1)
doc_vectors = np.array(doc_vectors)
# 文档与查询的相似度
sim_docs_query = cosine_similarity(doc_vectors, query_vector).flatten()
# 已选择和未选择的文档索引
unselected = list(range(len(doc_vectors)))
selected = []
# 先选择与查询最相似的文档
current_idx = np.argmax(sim_docs_query)
selected.append(current_idx)
unselected.remove(current_idx)
# 选择剩余文档
while len(selected) < k and unselected:
# 未选择文档与已选择文档的相似度矩阵
sim_between_docs = cosine_similarity(
doc_vectors[unselected],
doc_vectors[selected]
)
# 每个未选择文档与已选择文档的最大相似度
max_sim_between = np.max(sim_between_docs, axis=1)
# 计算MMR分数: λ*sim(d,q) - (1-λ)*max(sim(d,dj))
mmr_scores = lambda_param * sim_docs_query[unselected] - \
(1 - lambda_param) * max_sim_between
# 选择MMR分数最高的文档
next_idx = unselected[np.argmax(mmr_scores)]
selected.append(next_idx)
unselected.remove(next_idx)
# 返回选定的文档
return [doc_contents[idx] for idx in selected]
信息质量评估:
过滤低质量或干扰性内容,提高上下文质量:
-
信息密度评估:
- 计算每个段落的信息密度(信息量/长度)
- 优先选择信息密度高的段落
- 技术:关键词密度分析、实体密度评估
-
相关段落抽取:
- 仅提取文档中与查询高度相关的段落
- 避免将整个文档填入上下文
- 方法:段落级相似度计算、句子级相关性评分
-
格式适应性处理:
- 处理特殊格式内容(表格、列表、代码)使其适合LLM处理
- 转换复杂格式为文本表示
- 保留关键格式元素以维持信息完整性
4.3 查询增强技术
查询改写与扩展:
通过修改和增强原始查询,提高检索相关性:
-
查询改写(Query Rewriting):
- 使用LLM改写用户查询,使其更精确、更完整
- 修复语法错误、消除歧义、添加上下文信息
- 转换成更接近知识库用语的表述
-
查询扩展(Query Expansion):
- 添加同义词、相关术语扩展原始查询
- 利用领域知识图谱丰富查询
- 融合历史交互上下文中的相关信息
def enhance_query(original_query, conversation_history=None):
"""
使用LLM增强原始查询
参数:
- original_query: 用户原始查询
- conversation_history: 可选的对话历史
返回:
- 增强后的查询
"""
# 构建提示
prompt = """请对以下搜索查询进行改写和扩展,使其更加清晰、准确,包含更丰富的相关术语。
保持原始意图不变,但使查询更适合信息检索系统。
原始查询: "{query}"
"""
# 如果有对话历史,添加到提示中
if conversation_history:
history_context = "\n\n对话历史上下文:\n" + "\n".join(conversation_history[-3:])
prompt += history_context
prompt = prompt.format(query=original_query)
# 调用LLM进行查询增强
enhanced_query = llm.generate(prompt).strip()
return enhanced_query
多查询策略:
生成多个不同视角的查询,以覆盖更广泛的相关信息:
-
分解式多查询:
- 将复杂问题分解为多个简单问题
- 每个子问题生成一组检索结果
- 合并不同子问题的检索结果
-
角度多样化查询:
- 从不同角度、不同层次生成多个查询变体
- 例如:定义性查询、举例性查询、比较性查询等
- 提高检索信息的多样性和全面性
def generate_multi_queries(user_query, num_queries=3):
"""
为用户查询生成多个不同视角的查询变体
参数:
- user_query: 原始用户查询
- num_queries: 生成的查询变体数量
返回:
- 查询变体列表
"""
prompt = f"""请从不同角度和层次,为以下用户问题生成{num_queries}个不同的搜索查询。
这些查询应该覆盖相关信息的不同方面,帮助检索系统找到全面的回答。
每个查询都应该是一个独立、完整的问题或短语。
原始问题: "{user_query}"
请生成以下类型的查询:
1. 直接相关的基础查询
2. 探索定义或概念的查询
3. 寻找具体例子或应用的查询
以"查询1:"、"查询2:"等格式输出。
"""
# 调用LLM生成多个查询
response = llm.generate(prompt)
# 解析生成的查询
query_variants = []
for line in response.strip().split("\n"):
if line.startswith("查询") and ":" in line:
query_text = line.split(":", 1)[1].strip()
if query_text:
query_variants.append(query_text)
# 如果解析失败,至少返回原始查询
if not query_variants:
return [user_query]
return query_variants
HyDE技术:
HyDE(Hypothetical Document Embeddings)是一种特殊的查询增强技术:
-
基本原理:
- 先用LLM生成假设性的"理想回答文档"
- 将这个假设文档而非原始查询进行嵌入
- 用假设文档的嵌入向量进行检索
-
实施步骤:
- 将用户查询发送给LLM生成假设性答案
- 对假设性答案进行嵌入
- 使用此嵌入在向量库中检索真实文档
- 使用检索到的真实文档构建上下文
-
优势:
- 桥接查询意图与实际文档表述的差距
- 减轻词汇不匹配问题
- 增强语义检索能力
def hyde_retrieval(query, embedding_model, vector_db, llm, top_k=5):
"""
使用HyDE(Hypothetical Document Embeddings)技术进行检索
参数:
- query: 用户查询
- embedding_model: 嵌入模型
- vector_db: 向量数据库
- llm: 大语言模型
- top_k: 返回的文档数量
返回:
- 检索到的文档列表
"""
# 1. 生成假设性文档
hyde_prompt = f"""请为以下问题生成一个简洁但信息丰富的假设回答。
这个回答应该像是从一个专业知识库中摘录的片段,包含与问题直接相关的事实和概念。
不需要引用来源,只需提供清晰、准确的内容。内容不超过250字。
问题: {query}
假设性回答:"""
hypothetical_document = llm.generate(hyde_prompt)
# 2. 对假设性文档进行嵌入
hyde_embedding = embedding_model.embed(hypothetical_document)
# 3. 使用假设性文档的嵌入进行检索
retrieved_docs = vector_db.search(
query_vector=hyde_embedding,
top_k=top_k
)
return retrieved_docs
4.4 多文档整合策略
当RAG系统需要从多个文档中获取信息回答复杂问题时,如何有效整合这些信息是关键挑战:
信息融合与排序:
-
主题聚类排序:
- 将检索的段落按主题或概念聚类
- 在上下文中保持相关内容的连贯性
- 方法:对段落进行嵌入并聚类,按主题组织
-
信息完整性重组:
- 识别互补的信息片段并合理组织
- 确保关键概念的所有方面都被覆盖
- 技术:关系抽取、概念映射
-
时序与逻辑排序:
- 按时间顺序或逻辑关系排序信息
- 适用于过程描述、历史事件、因果关系问题
- 方法:元数据利用、文本中的时间线和逻辑标记提取
跨文档一致性处理:
处理多文档中可能出现的冲突或不一致信息:
-
冲突检测:
- 识别不同来源中的矛盾信息
- 基于实体关系比较、数值比较等方法
- 标记不确定或存在争议的内容
-
来源可信度加权:
- 基于来源可靠性、时效性对信息加权
- 优先选择更权威、更新的信息源
- 技术:来源信誉评分系统
-
模糊证据整合:
- 当信息不完全一致时,保留多种可能性
- 表明证据强度和确定性级别
- 避免简单取舍,保留信息丰富度
def integrate_multi_documents(query, retrieved_chunks, max_token_limit=3000):
"""
整合多文档信息为连贯上下文
参数:
- query: 用户查询
- retrieved_chunks: 检索到的文档片段列表
- max_token_limit: 上下文的最大token限制
返回:
- 整合后的上下文文本
"""
# 1. 对文档片段进行分组和聚类(简化示例)
# 实际实现可能使用更复杂的聚类算法
topic_groups = {}
for chunk in retrieved_chunks:
# 简化:使用文档来源或类别作为分组依据
topic = chunk.get('category', 'general')
if topic not in topic_groups:
topic_groups[topic] = []
topic_groups[topic].append(chunk)
# 2. 准备整合的上下文
integrated_context = []
token_count = 0
# 3. 按相关性将各主题组的内容添加到上下文
for topic, chunks in sorted(topic_groups.items(),
key=lambda x: query_topic_relevance(query, x[0]),
reverse=True):
# 对组内文档按相关性排序
sorted_chunks = sorted(chunks,
key=lambda x: chunk_relevance(query, x),
reverse=True)
topic_header = f"== {topic.upper()} 相关信息 =="
integrated_context.append(topic_header)
token_count += len(topic_header.split())
# 添加组内文档,注意token限制
for chunk in sorted_chunks:
chunk_text = f"{chunk['content']} [来源: {chunk.get('source', '未知')}]"
chunk_tokens = len(chunk_text.split())
if token_count + chunk_tokens > max_token_limit:
break
integrated_context.append(chunk_text)
token_count += chunk_tokens
return "\n\n".join(integrated_context)
# 辅助函数(简化实现)
def query_topic_relevance(query, topic):
"""评估查询与主题的相关性(简化)"""
# 实际实现可能使用嵌入相似度或分类器
return 1.0 # 示例返回值
def chunk_relevance(query, chunk):
"""评估查询与文档块的相关性(简化)"""
# 实际实现基于向量相似度或其他相关性指标
return chunk.get('relevance_score', 0.5) # 示例返回值
5. 生成与后处理技术
在RAG系统中,检索完成并构建好上下文后,我们需要引导大语言模型生成高质量回答,并对生成的结果进行适当的后处理,确保回答的准确性和可用性。
5.1 提示工程技术
针对RAG系统,我们需要设计特殊的提示模板,以最大化检索内容的利用效果:
RAG专用提示模板设计:
-
明确指令与角色定义:
- 为模型定义明确的角色(如"检索增强助手")
- 提供清晰的行为指南和输出要求
- 说明如何处理检索信息中的冲突或不确定性
-
检索内容引导:
- 明确指导模型如何利用检索到的信息
- 设定优先级策略(如优先使用检索内容而非内部知识)
- 要求引用信息来源,增强透明度
-
事实边界管理:
- 明确指导模型识别知识边界
- 当检索内容不足时如何响应
- 避免虚构内容的策略指导
RAG提示模板示例:
def create_rag_prompt(query, context, output_format=None):
"""
创建RAG系统的标准提示模板
参数:
- query: 用户查询
- context: 检索到的上下文信息
- output_format: 可选的输出格式要求
返回:
- 格式化的提示文本
"""
system_instruction = """你是一个专业的检索增强型AI助手。你的任务是基于提供的参考信息回答用户问题。
请遵循以下规则:
1. 仅使用提供的参考信息回答问题。如果参考信息不足以完整回答问题,坦率地表明你只能回答部分问题或无法回答。
2. 不要使用你的内部知识编造未在参考信息中出现的内容。
3. 保持回答的准确性、相关性和有条理。
4. 在回答中适当引用参考信息来源(例如[来源1])。
5. 如果参考信息中存在矛盾,请指出这些矛盾并解释可能的原因。
6. 回答应当简洁、清晰,但要包含足够的细节。"""
# 添加特定的输出格式要求
if output_format:
system_instruction += f"\n\n回答必须按照以下格式:\n{output_format}"
# 构建完整提示
full_prompt = f"""{system_instruction}
参考信息:
{context}
用户问题: {query}
请基于以上参考信息回答问题:"""
return full_prompt
少样本提示设计:
在复杂的RAG应用中,可以使用"少样本学习"(Few-shot Learning)提示技术增强生成质量:
def create_few_shot_rag_prompt(query, context, examples):
"""
创建包含示例的少样本RAG提示
参数:
- query: 用户查询
- context: 检索到的上下文信息
- examples: 问答示例列表,每个元素包含'question'、'context'和'answer'
返回:
- 包含示例的提示文本
"""
# 基础指令
system_instruction = """你是一个专业的检索增强型AI助手。你的任务是基于提供的参考信息回答用户问题。
请严格按照以下示例的风格和格式回答问题。特别注意如何仅基于提供的参考信息生成答案,并适当引用信息来源。"""
# 构建示例部分
examples_text = ""
for i, example in enumerate(examples):
examples_text += f"""
示例 {i+1}:
参考信息:
{example['context']}
问题: {example['question']}
回答: {example['answer']}
---
"""
# 构建当前问题部分
current_query_part = f"""
现在请回答新问题:
参考信息:
{context}
问题: {query}
回答:"""
# 组合完整提示
full_prompt = system_instruction + examples_text + current_query_part
return full_prompt
5.2 生成控制参数优化
大语言模型的生成过程可以通过各种参数控制,为不同类型的RAG应用选择合适的参数至关重要:
关键生成参数:
-
温度(Temperature):
- 控制生成的随机性与创造性
- RAG系统通常使用较低温度(0.0-0.3)以增加确定性
- 事实型查询: 0.0-0.1,解释性内容: 0.2-0.4
-
top_p与top_k:
- 控制token选择范围,影响输出多样性
- 事实回答通常设置较低top_p(0.1-0.3)
- 综合性问题可适当增加(0.5-0.7)
-
最大生成长度:
- 根据任务类型和需求设置适当的输出长度
- 考虑用户耐心和信息密度的平衡
- 短回答:150-300 tokens,详细回答:500-1000 tokens
参数动态调整:
为不同类型的查询动态调整生成参数:
def optimize_generation_parameters(query, context_length, query_type):
"""
根据查询特征动态优化生成参数
参数:
- query: 用户查询文本
- context_length: 上下文长度(tokens)
- query_type: 查询类型分类("factual", "explanatory", "creative", "procedural")
返回:
- 优化的生成参数字典
"""
# 基础参数设置
params = {
"temperature": 0.2,
"top_p": 0.4,
"top_k": 40,
"max_tokens": 500,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
# 根据查询类型调整
if query_type == "factual":
# 事实型查询(如"谁发明了电灯")需要高确定性
params.update({
"temperature": 0.0,
"top_p": 0.1,
"max_tokens": min(300, 2000 - context_length), # 简短回答
"presence_penalty": 0.0
})
elif query_type == "explanatory":
# 解释型查询(如"为什么天空是蓝色的")需要详细说明
params.update({
"temperature": 0.3,
"top_p": 0.4,
"max_tokens": min(800, 3000 - context_length), # 详细回答
"presence_penalty": 0.1 # 鼓励覆盖更多方面
})
elif query_type == "creative":
# 创意型查询需要更多多样性
params.update({
"temperature": 0.7,
"top_p": 0.9,
"frequency_penalty": 0.5 # 减少重复
})
elif query_type == "procedural":
# 程序性查询(如"如何做面包")需要结构化步骤
params.update({
"temperature": 0.2,
"max_tokens": min(1000, 3000 - context_length),
"presence_penalty": 0.2 # 确保覆盖所有步骤
})
# 智能调整最大长度
query_length = len(query.split())
if query_length > 20: # 长问题可能需要更详细的回答
params["max_tokens"] = min(params["max_tokens"] * 1.5, 3000 - context_length)
return params
批量生成与选择:
对于关键应用,可以使用批量生成策略提高回答质量:
-
多样性采样:
- 使用不同参数生成多个候选回答
- 通过评估指标选择最佳回答
- 或将多个回答进行整合提炼
-
自我一致性检查:
- 生成多个回答并检查一致性
- 保留在多数回答中出现的信息
- 标记不确定或矛盾的部分
5.3 引用与溯源机制
RAG系统的一个重要优势是能够提供信息来源,增强可信度和透明度:
引用生成策略:
-
内联引用:
- 在回答正文中直接添加引用标记
- 格式如[1]、[来源A]等简洁标记
- 便于阅读的同时提供溯源能力
-
脚注引用:
- 在回答末尾提供详细来源信息
- 包含文档标题、作者、日期等元数据
- 适合正式或学术场景
-
链接引用:
- 为Web应用提供可点击的源文档链接
- 允许用户直接访问原始信息
- 增强用户体验和信任度
引用实现示例:
def process_citations(generated_text, source_documents):
"""
处理生成文本中的引用并添加来源信息
参数:
- generated_text: LLM生成的回答文本
- source_documents: 源文档列表,包含元数据
返回:
- 带格式化引用的最终文本
"""
# 1. 解析文本中的引用标记
citation_pattern = r'\[(?:来源|source)?[ ]*(\d+)\]'
citations = re.findall(citation_pattern, generated_text)
# 如果没有检测到引用标记,尝试添加基于内容匹配的引用
if not citations:
generated_text = add_automatic_citations(generated_text, source_documents)
citations = re.findall(citation_pattern, generated_text)
# 2. 准备引用脚注
footnotes = []
used_sources = set()
for cite_id in citations:
idx = int(cite_id) - 1
if 0 <= idx < len(source_documents):
source = source_documents[idx]
if idx not in used_sources:
footnote = format_source_reference(source)
footnotes.append(f"[{cite_id}] {footnote}")
used_sources.add(idx)
# 3. 将脚注添加到文本末尾
if footnotes:
generated_text += "\n\n来源:\n" + "\n".join(footnotes)
return generated_text
def format_source_reference(source):
"""格式化源文档为引用格式"""
title = source.get('title', '未知标题')
author = source.get('author', '未知作者')
date = source.get('date', '未知日期')
url = source.get('url', '')
reference = f"{title}"
if author != '未知作者':
reference += f", {author}"
if date != '未知日期':
reference += f", {date}"
if url:
reference += f". {url}"
return reference
交互式溯源功能:
为交互式应用设计的高级溯源功能:
-
高亮显示:
- 突出显示回答中直接引用检索内容的部分
- 提供视觉区分原创内容与引用内容
- 增强用户对信息来源的感知
-
证据展示:
- 允许用户点击查看特定声明的证据
- 展示相关的原始文档段落
- 支持原文比对,增强透明度
-
置信度指示:
- 为不同部分的回答提供置信度评分
- 基于检索结果的相关性和一致性
- 帮助用户评估信息可靠性
5.4 事实一致性检查
确保RAG系统输出的事实准确性是关键挑战,需要多层次验证机制:
自动验证技术:
- 答案验证模型:
- 使用专门训练的模型验证生成内容与检索内容的一致性
- 检测添加、遗漏或扭曲的信息
- 标记低置信度或可能错误的内容
def verify_factual_consistency(generated_answer, retrieval_context):
"""
验证生成答案与检索上下文的事实一致性
参数:
- generated_answer: 生成的回答
- retrieval_context: 检索的上下文内容
返回:
- 验证结果和修正建议
"""
verification_prompt = f"""
你的任务是验证AI生成回答的事实准确性。对照提供的参考信息,评估回答中的每个事实性陈述。
标记以下几类问题:
1. 虚构事实:回答中包含参考信息中没有的内容
2. 矛盾:回答中与参考信息冲突的内容
3. 断章取义:回答中对参考信息的错误解读
参考信息:
{retrieval_context}
AI生成的回答:
{generated_answer}
请提供详细分析,标识每个问题,并给出1-10的整体一致性评分(10表示完全一致)。
"""
# 调用验证模型
verification_result = verification_model.generate(verification_prompt)
# 解析验证结果(简化示例)
# 实际实现可能使用更结构化的输出格式和解析逻辑
consistency_score = extract_consistency_score(verification_result)
issues = extract_issues(verification_result)
return {
"consistency_score": consistency_score,
"issues": issues,
"full_analysis": verification_result
}
-
关键事实提取与验证:
- 从生成的回答中提取关键事实声明
- 针对每个事实在检索内容中寻找支持证据
- 对缺乏支持的事实进行标记或重写
-
自动重写与修正:
- 检测到不一致后自动重写问题部分
- 移除未得到支持的断言
- 增加对不确定内容的限定词
人机协作验证:
-
可疑内容标记:
- 自动标记可能存在问题的内容部分
- 提供给人类审核员重点检查
- 减轻人工验证的工作量
-
用户反馈收集:
- 允许最终用户报告可能的错误
- 收集这些反馈用于系统改进
- 建立持续的质量优化循环
-
反例学习:
- 从验证失败的案例中学习模式
- 改进提示模板和过滤机制
- 建立错误模式库进行预防
6. RAG系统评估方法
构建高质量的RAG系统需要全面的评估框架,以确保检索质量和回答准确性。
6.1 评估维度与指标
RAG系统的评估应该覆盖多个关键维度:
检索评估指标:
-
准确率(Precision)与召回率(Recall):
- 准确率:检索结果中相关文档的比例
- 召回率:成功检索到的相关文档占所有相关文档的比例
- F1分数:准确率和召回率的调和平均值
-
排序质量指标:
- 平均倒数排名(MRR):相关文档倒数排名的平均值
- 规范化衰减累积收益(NDCG):考虑排序位置的相关性度量
- 前K位准确率(P@K):前K个结果中相关文档的比例
-
语义相关性:
- 检索结果与查询的语义相关度
- 使用人工评分或自动相关性模型
- BERTScore等基于嵌入的相关性指标
生成评估指标:
-
事实准确性:
- 生成内容中正确事实陈述的比例
- 幻觉率:生成内容中未在检索内容出现的断言比例
- 支持率:能从检索内容中找到支持的陈述比例
-
内容质量:
- 回答完整性:覆盖问题所有相关方面
- 简洁性:无不必要冗余或重复
- 连贯性:逻辑流畅,结构合理
-
效用指标:
- 直接回答率:直接回答问题而非周围讨论
- 相关性:回答与原始问题的相关程度
- 可执行性:对操作类问题,提供可执行的步骤
端到端评估指标:
-
用户满意度:
- 用户评分(1-5星等)
- 满意率:认为回答满足需求的用户比例
- 首次解决率:首次回答即解决问题的比例
-
任务成功指标:
- 查询解决率:成功回答查询的比例
- 任务完成时间:完成特定任务所需交互次数
- 额外查询需求:需要后续澄清的查询比例
-
系统性能指标:
- 延迟:从查询到回答的时间
- 吞吐量:单位时间内可处理的查询数
- 资源使用效率:计算和存储需求
评估方法综合框架:
def evaluate_rag_system(test_queries, ground_truth, rag_system):
"""
综合评估RAG系统性能
参数:
- test_queries: 测试查询列表
- ground_truth: 参考答案和相关文档
- rag_system: 待评估的RAG系统
返回:
- 多维度评估结果
"""
results = {
"retrieval_metrics": {},
"generation_metrics": {},
"end_to_end_metrics": {},
"query_level_results": []
}
# 检索相关指标
precision_sum = 0
recall_sum = 0
mrr_sum = 0
# 生成相关指标
factual_accuracy_sum = 0
hallucination_sum = 0
completeness_sum = 0
# 端到端指标
success_count = 0
avg_latency = 0
for i, query in enumerate(test_queries):
# 获取参考信息
reference_answer = ground_truth[i]["answer"]
relevant_docs = ground_truth[i]["relevant_documents"]
# 记录开始时间
start_time = time.time()
# 执行RAG流程
retrieved_docs = rag_system.retrieve(query)
generated_answer = rag_system.generate(query, retrieved_docs)
# 计算延迟
latency = time.time() - start_time
avg_latency += latency
# 评估检索性能
retrieval_metrics = evaluate_retrieval(retrieved_docs, relevant_docs)
precision_sum += retrieval_metrics["precision"]
recall_sum += retrieval_metrics["recall"]
mrr_sum += retrieval_metrics["mrr"]
# 评估生成性能
generation_metrics = evaluate_generation(
generated_answer, reference_answer, retrieved_docs)
factual_accuracy_sum += generation_metrics["factual_accuracy"]
hallucination_sum += generation_metrics["hallucination_rate"]
completeness_sum += generation_metrics["completeness"]
# 端到端成功判断
if is_successful_answer(generated_answer, reference_answer):
success_count += 1
# 记录查询级结果
results["query_level_results"].append({
"query": query,
"retrieval_metrics": retrieval_metrics,
"generation_metrics": generation_metrics,
"latency": latency,
"retrieved_docs": retrieved_docs,
"generated_answer": generated_answer
})
# 计算平均指标
n = len(test_queries)
results["retrieval_metrics"] = {
"precision": precision_sum / n,
"recall": recall_sum / n,
"mrr": mrr_sum / n,
"f1": 2 * (precision_sum/n) * (recall_sum/n) / ((precision_sum/n) + (recall_sum/n))
}
results["generation_metrics"] = {
"factual_accuracy": factual_accuracy_sum / n,
"hallucination_rate": hallucination_sum / n,
"completeness": completeness_sum / n
}
results["end_to_end_metrics"] = {
"success_rate": success_count / n,
"avg_latency": avg_latency / n
}
return results
6.2 评估数据集构建
高质量的评估数据集是准确评估RAG系统的基础:
测试集设计原则:
-
多样性覆盖:
- 包含不同类型的查询(事实型、解释型、指导型等)
- 覆盖不同难度级别的问题
- 包括各种主题领域和知识深度
-
现实代表性:
- 反映真实用户可能提出的查询
- 包含自然语言变体和不同表达方式
- 包含模糊、不完整或复杂的查询
-
挑战性查询:
- 包含需要多跳推理的问题
- 包含需要整合多个信息片段的问题
- 包含需要识别矛盾信息的问题
测试集构建方法:
-
手动精选:
- 由领域专家构建核心测试集
- 确保关键场景和边缘情况的覆盖
- 创建预期答案和检索结果
-
用户查询采样:
- 从真实用户查询中抽样
- 移除个人识别信息
- 按类型分类并平衡分布
-
自动合成:
- 使用LLM生成多样化问题
- 基于知识库内容自动生成查询
- 对生成的查询进行人工验证和筛选
参考答案创建:
-
多专家合作:
- 多名专家独立创建参考答案
- 通过讨论解决分歧
- 确保答案的准确性和完整性
-
结构化标注:
- 标注必须包含的关键事实点
- 标注每个事实点的重要性权重
- 创建多级评分标准
-
相关文档标注:
- 标识应当被检索到的关键文档
- 为每个文档标记相关性等级
- 标注文档中支持答案的具体段落
6.3 自动与人工评估平衡
RAG系统评估需要结合自动评估和人工评估的优势:
自动评估方法:
-
基于参考的自动评估:
- 使用BLEU、ROUGE等文本相似度指标
- BERTScore等语义相似度指标
- METEOR等考虑同义词的指标
-
LLM辅助评估:
- 使用能力更强的LLM评估答案质量
- 评估事实准确性、相关性、完整性等维度
- 提供结构化评分和失败原因分析
def llm_based_evaluation(query, generated_answer, ground_truth, retrieval_context):
"""
使用大语言模型进行答案质量评估
参数:
- query: 用户查询
- generated_answer: RAG系统生成的答案
- ground_truth: 标准参考答案
- retrieval_context: 检索到的上下文内容
返回:
- 详细的评估结果
"""
evaluation_prompt = f"""作为专业的AI评估专家,你的任务是评估一个检索增强型AI系统的回答质量。
请按照以下维度进行分析,使用1-5分进行评分(5分为最高):
1. 事实准确性: 回答中的事实陈述是否与参考信息和标准答案一致
2. 相关性: 回答是否直接相关并解决了用户问题
3. 完整性: 回答是否涵盖了所有相关方面和关键信息
4. 简洁性: 回答是否简洁明了,没有不必要的冗余
5. 可用性: 回答的格式和结构是否利于用户理解和使用
用户问题:
{query}
检索到的参考信息:
{retrieval_context}
系统生成的回答:
{generated_answer}
标准参考答案:
{ground_truth}
请提供详细分析,指出优点和问题,并给出每个维度的分数。最后计算总分(满分25分)。
"""
# 调用评估模型
evaluation_result = evaluation_llm.generate(evaluation_prompt)
# 解析评估结果
scores = extract_scores(evaluation_result)
analysis = extract_analysis(evaluation_result)
return {
"scores": scores,
"total_score": sum(scores.values()),
"analysis": analysis,
"full_evaluation": evaluation_result
}
- 无参考评估:
- 事实一致性检测(检测与检索内容不一致的陈述)
- 自我矛盾检测(检测回答内部的逻辑矛盾)
- 语言质量评估(流畅度、连贯性等)
人工评估方法:
-
主观评分:
- 人类评估人员对回答进行多维度评分
- 主要关注准确性、有用性、完整性等
- 创建标准化评分指南确保一致性
-
偏好比较:
- A/B测试比较不同系统或配置的回答
- 双盲评估,评估者不知道哪个是哪个系统生成
- 记录偏好选择和偏好强度
-
细粒度标注:
- 标注回答中的每个事实断言
- 评估其准确性、相关性和来源
- 标注错误类型和严重程度
评估工作流程:
结合自动和人工评估的综合工作流程:
-
初步筛选阶段:
- 使用自动指标进行大规模系统评估
- 筛选出表现最好的配置和参数
- 标识需要深入人工评估的样本
-
深入评估阶段:
- 对关键样本进行人工专家评估
- 特别关注自动评估难以判断的维度
- 提供详细的质量分析和改进建议
-
持续改进循环:
- 收集评估结果并识别共同问题
- 改进系统组件或配置
- 重复评估验证改进效果
6.4 在线评估与监控
将评估融入到生产环境中,实现持续改进:
在线评估机制:
-
A/B测试:
- 将用户流量分配给不同系统配置
- 比较用户参与度和任务成功率
- 统计显著性分析确保结果可靠
-
用户反馈收集:
- 在回答后提供简单的点赞/点踩机制
- 选择性的详细反馈收集
- 自动分类反馈类型和严重程度
-
交互行为分析:
- 跟踪用户查询模式和变化
- 分析跟进问题和重新查询模式
- 识别用户满意和不满意的信号
系统监控指标:
-
质量监控:
- 幻觉率趋势监控
- 回答拒绝率(无法回答的比例)
- 用户报告的错误率
-
性能监控:
- 检索延迟和生成延迟
- 系统负载和资源使用
- 查询队列长度和等待时间
-
使用模式监控:
- 热门查询主题和领域
- 查询复杂度分布
- 用户会话长度和深度
持续学习循环:
-
失败案例收集:
- 自动识别和收集系统失败案例
- 创建失败模式数据库
- 优先处理高频和高影响失败
-
知识库改进:
- 基于查询模式优化知识库内容
- 填补知识库中被识别的空白
- 更新过时或不准确的信息
-
系统自适应:
- 基于用户互动自动调整参数
- 学习用户偏好和行为模式
- 持续优化提示模板和检索策略
💻 实践活动
实践1:基础RAG系统实现
在本实践活动中,我们将构建一个简单但功能完整的RAG系统,帮助你理解关键组件如何协同工作。
步骤1:环境准备
首先,我们需要安装必要的库和依赖:
# 安装必要的库
!pip install langchain sentence-transformers chromadb openai
然后创建一个基本的工作目录:
import os
# 创建工作目录
work_dir = "rag_workshop"
data_dir = os.path.join(work_dir, "data")
db_dir = os.path.join(work_dir, "db")
os.makedirs(data_dir, exist_ok=True)
os.makedirs(db_dir, exist_ok=True)
print(f"工作目录已创建: {work_dir}")
步骤2:准备示例文档
让我们准备一些示例文档用于构建知识库:
# 创建一些示例文档
documents = [
{
"title": "RAG技术概述",
"content": """检索增强生成(RAG)是一种将信息检索与文本生成相结合的技术。它通过在生成过程中引入外部知识,增强大语言模型的回答能力。
RAG技术解决了大模型的知识时效性问题,因为知识库可以随时更新,无需重新训练模型。此外,它还降低了幻觉产生的可能性,因为生成过程基于检索到的事实信息。"""
},
{
"title": "嵌入模型简介",
"content": """嵌入模型是RAG系统的核心组件之一,它将文本转换为向量表示。优质的嵌入模型能够捕捉文本的语义信息,使相似内容在向量空间中距离较近。
常见的嵌入模型包括OpenAI的text-embedding-3、Cohere的embedding模型以及开源的BAAI/bge系列模型等。选择模型时需要考虑语义理解能力、性能与资源需求、语言支持等因素。"""
},
{
"title": "向量数据库基础",
"content": """向量数据库是专门设计用于存储、管理和检索高维向量数据的数据库系统。在RAG中,它用于存储文档嵌入向量并支持高效的相似性搜索。
主流向量数据库包括Pinecone、Weaviate、Milvus、Qdrant和Chroma等。这些系统通常使用HNSW或IVF等索引算法来加速向量搜索过程,避免在查询时与所有向量进行计算。"""
},
{
"title": "提示工程在RAG中的应用",
"content": """在RAG系统中,提示工程至关重要。有效的提示模板应该明确指导模型如何利用检索到的信息,并设定优先级策略。
典型的RAG提示模板包括系统指令、检索内容、用户查询三个部分。系统指令定义模型角色并提供行为指南;检索内容部分包含从知识库中获取的相关文档;用户查询部分则包含原始问题。"""
}
]
# 将文档保存到文件中
import json
for i, doc in enumerate(documents):
with open(os.path.join(data_dir, f"doc_{i}.json"), "w", encoding="utf-8") as f:
json.dump(doc, f, ensure_ascii=False, indent=2)
print(f"已创建 {len(documents)} 个示例文档")
步骤3:文本处理与嵌入生成
实现文档处理和嵌入生成流程:
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# 文档加载和处理
def process_documents(data_dir):
# 从保存的文件加载文档
docs = []
for filename in os.listdir(data_dir):
if filename.endswith(".json"):
with open(os.path.join(data_dir, filename), "r", encoding="utf-8") as f:
doc = json.load(f)
# 添加元数据
docs.append({
"content": doc["content"],
"metadata": {"title": doc["title"], "source": filename}
})
# 文本分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
# 处理每个文档并分块
all_chunks = []
for doc in docs:
chunks = text_splitter.split_text(doc["content"])
# 为每个块添加元数据
doc_chunks = [{"content": chunk, "metadata": doc["metadata"]} for chunk in chunks]
all_chunks.extend(doc_chunks)
return all_chunks
# 嵌入生成与向量存储
def create_vector_store(chunks, db_dir):
# 初始化嵌入模型
embeddings = HuggingFaceEmbeddings(model_name="shibing624/text2vec-base-chinese")
# 提取内容和元数据
texts = [chunk["content"] for chunk in chunks]
metadatas = [chunk["metadata"] for chunk in chunks]
# 创建向量存储
vectorstore = Chroma.from_texts(
texts=texts,
embedding=embeddings,
metadatas=metadatas,
persist_directory=db_dir
)
return vectorstore
# 执行处理流程
chunks = process_documents(data_dir)
vectorstore = create_vector_store(chunks, db_dir)
print(f"处理完成,共生成 {len(chunks)} 个文本块")
步骤4:构建RAG查询流程
实现完整的RAG查询流程:
from langchain_openai import ChatOpenAI
# 请替换为你的API密钥
import os
os.environ["OPENAI_API_KEY"] = "你的OpenAI API密钥"
# 初始化大语言模型
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
# 创建RAG提示模板
from langchain.prompts import ChatPromptTemplate
template = """你是一个专业的AI助手。请基于以下提供的信息回答用户问题。
如果提供的信息不足以回答问题,请说明你不知道,不要编造内容。
请引用信息来源。
信息:
{context}
用户问题: {query}
"""
prompt = ChatPromptTemplate.from_template(template)
# RAG查询函数
def rag_query(query, vectorstore, top_k=3):
# 检索相关文档
results = vectorstore.similarity_search(query, k=top_k)
# 构建上下文
contexts = []
for i, doc in enumerate(results):
source = doc.metadata.get("title", "未知文档")
contexts.append(f"[文档{i+1}: {source}]\n{doc.page_content}")
context_text = "\n\n".join(contexts)
# 生成提示
formatted_prompt = prompt.format(context=context_text, query=query)
# 生成回答
response = llm.invoke(formatted_prompt)
return {
"query": query,
"response": response.content,
"source_documents": results
}
# 测试RAG系统
test_queries = [
"什么是RAG技术?它有什么优势?",
"嵌入模型在RAG系统中的作用是什么?",
"向量数据库有哪些常见选择?"
]
for query in test_queries:
result = rag_query(query, vectorstore)
print(f"\n问题: {query}")
print(f"\n回答: {result['response']}")
print("\n--------------------------------------------------")
步骤5:评估与优化
设计简单的评估机制:
def evaluate_rag_response(response, ground_truth):
"""简单评估RAG系统回答质量"""
from langchain_openai import ChatOpenAI
eval_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
eval_prompt = f"""请评估以下AI助手回答的质量,根据以下标准打分(1-5分):
1. 事实准确性: 回答中的事实是否正确
2. 相关性: 回答是否与问题相关
3. 完整性: 回答是否涵盖了问题的所有方面
问题: {response['query']}
AI回答: {response['response']}
参考标准答案: {ground_truth}
请给出每项的评分和简要分析。
"""
evaluation = eval_llm.invoke(eval_prompt)
return evaluation.content
# 示例用法(需要标准答案)
# ground_truth = "RAG(检索增强生成)是..."
# eval_result = evaluate_rag_response(result, ground_truth)
# print(f"\n评估结果:\n{eval_result}")
实践2:RAG优化实验
在这个实践中,我们将探索不同的RAG优化技术,并比较它们的效果:
# 1. 查询重写技术
from langchain_openai import ChatOpenAI
def rewrite_query(original_query):
"""使用LLM重写原始查询以提高检索效果"""
rewrite_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2)
rewrite_prompt = f"""请对以下搜索查询进行改写,使其更加清晰、具体,并包含更多可能的关键词,以便更好地进行文档检索。
保持原始查询的核心意图不变,但使其更适合信息检索系统。
原始查询: {original_query}
改写后的查询:"""
response = rewrite_llm.invoke(rewrite_prompt)
return response.content.strip()
# 2. 实现多查询RAG
def multi_query_rag(original_query, vectorstore, llm, top_k=2):
"""生成多个查询变体并合并检索结果"""
# 生成查询变体
variant_prompt = f"""为以下用户问题生成3个不同的搜索查询变体,以便从文档库中检索信息。
这些变体应该从不同角度表达相同的信息需求,使用不同的关键词或表述方式。
原始问题: {original_query}
请直接输出三个查询变体,每行一个,不要有编号或其他说明。"""
variants_response = llm.invoke(variant_prompt)
query_variants = variants_response.content.strip().split('\n')
query_variants = [q.strip() for q in query_variants if q.strip()]
query_variants.append(original_query) # 添加原始查询
# 从每个变体中检索文档
all_docs = []
for query in query_variants:
results = vectorstore.similarity_search(query, k=top_k)
all_docs.extend(results)
# 去重
unique_docs = []
seen_contents = set()
for doc in all_docs:
if doc.page_content not in seen_contents:
unique_docs.append(doc)
seen_contents.add(doc.page_content)
# 排序(简化版,实际可能需要更复杂的排序逻辑)
sorted_docs = unique_docs[:top_k*2] # 限制最终文档数量
# 构建上下文和生成回答的过程与之前相同
contexts = []
for i, doc in enumerate(sorted_docs):
source = doc.metadata.get("title", "未知文档")
contexts.append(f"[文档{i+1}: {source}]\n{doc.page_content}")
context_text = "\n\n".join(contexts)
formatted_prompt = prompt.format(context=context_text, query=original_query)
response = llm.invoke(formatted_prompt)
return {
"query": original_query,
"response": response.content,
"source_documents": sorted_docs
}
# 比较不同RAG策略的效果
def compare_rag_strategies(query):
print(f"原始查询: {query}\n")
# 标准RAG
standard_result = rag_query(query, vectorstore)
print(f"标准RAG回答:\n{standard_result['response']}\n")
# 查询重写RAG
rewritten_query = rewrite_query(query)
print(f"重写后查询: {rewritten_query}\n")
rewritten_result = rag_query(rewritten_query, vectorstore)
print(f"查询重写RAG回答:\n{rewritten_result['response']}\n")
# 多查询RAG
multi_result = multi_query_rag(query, vectorstore, llm)
print(f"多查询RAG回答:\n{multi_result['response']}\n")
print("--------------------------------------------------")
# 测试不同的查询
test_query = "RAG和传统问答系统有什么区别?"
compare_rag_strategies(test_query)
🧠 自测问题
-
概念理解:解释RAG系统的核心工作流程,并说明每个环节的主要功能。
-
技术选择:在设计RAG系统时,如何选择合适的嵌入模型和向量数据库?列出至少三个考量因素。
-
优化策略:描述至少三种提高RAG系统检索准确性的方法,并解释它们的工作原理。
-
应用场景:分析RAG技术在哪些场景中比纯LLM解决方案更有优势,并解释原因。
-
评估方法:如何全面评估RAG系统的性能?设计一个评估框架,包括关键指标和测试方法。
📚 拓展资源
学习资料
开源工具
- LangChain - 构建RAG应用的流行框架
- LlamaIndex - 数据框架,简化RAG开发
- Chroma - 轻量级开源向量数据库
- FAISS - 高效向量搜索库
视频教程
📝 课后作业
-
RAG系统设计与实现:基于本课程所学知识,设计并实现一个针对特定领域(如技术文档、法律文本、教育资料等)的RAG系统。记录实现过程中遇到的挑战和解决方案。
-
性能对比实验:比较至少三种不同的检索策略(如基础向量检索、混合检索、多查询检索等)对RAG系统性能的影响。使用标准测试集评估并分析结果。
-
RAG系统优化:针对自己实现的RAG系统,应用至少两种优化技术(如上下文处理优化、提示工程改进等),并量化评估优化效果。
-
RAG与纯LLM对比:选择一个专业领域的问答任务,比较RAG系统与纯LLM(不使用外部知识)的回答质量差异。分析在哪些类型的问题上差异最显著。
-
RAG评估框架开发:设计并实现一个自动化评估RAG系统的框架,包括检索质量和生成质量的多维度评估。测试该框架在实际RAG系统上的应用效果。
明日预告:明天我们将学习如何构建完整的RAG应用,实践今天所学的理论知识,开发能够处理私有数据的智能问答系统。