Day 4: RAG检索增强生成技术
学习目标
- 理解RAG(检索增强生成)的基本原理和重要性
- 掌握知识库构建和向量检索的方法
- 学习RAG系统的设计和实现步骤
- 了解提示工程在RAG中的应用
- 掌握RAG系统的评估和优化方法
1. RAG基础概念
1.1 什么是RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合检索系统和生成模型的技术,通过从外部知识库检索相关信息来增强大语言模型的回答质量。
RAG的核心思想:
- 不依赖模型参数中存储的知识
- 从外部知识库检索相关信息
- 将检索到的信息与用户查询结合
- 生成基于事实、最新且可溯源的回答
RAG的优势:
- 知识更新:无需重新训练模型即可更新知识
- 减少幻觉:提供事实依据,减少模型生成虚假信息
- 可溯源性:回答可以追溯到具体信息来源
- 领域适应:通过更换知识库快速适应不同领域
- 成本效益:比完全微调模型更经济高效
1.2 RAG的工作流程
RAG系统通常包含以下核心步骤:
-
索引构建:
- 收集和处理文档
- 将文档分割成适当大小的块
- 使用嵌入模型生成文本块的向量表示
- 将向量存储在向量数据库中
-
查询处理:
- 接收用户查询
- 使用相同的嵌入模型生成查询的向量表示
- 在向量数据库中检索与查询最相似的文本块
- 根据相似度排序检索结果
-
上下文增强:
- 将检索到的文本块与原始查询结合
- 构建提示模板,指导模型如何使用检索信息
-
生成回答:
- 将增强的提示发送给大语言模型
- 模型生成基于检索内容的回答
- 可能包含引用或来源信息
1.3 RAG与传统方法对比
RAG vs 微调:
特性 | RAG | 微调 |
---|---|---|
知识更新 | 简单(更新知识库) | 复杂(重新训练) |
内存需求 | 低(模型参数不变) | 高(存储完整模型) |
计算需求 | 中等(检索+推理) | 高(训练过程) |
可解释性 | 高(可提供来源) | 低(黑盒知识) |
领域适应 | 快速(更换知识库) | 慢速(领域微调) |
实时性 | 高(可包含最新信息) | 低(受训练数据限制) |
RAG vs 提示工程:
特性 | RAG | 提示工程 |
---|---|---|
知识范围 | 广泛(外部知识库) | 有限(上下文窗口) |
提示长度 | 可动态扩展 | 受上下文窗口限制 |
信息准确性 | 高(基于检索) | 中(依赖模型记忆) |
实现复杂度 | 高(需要检索系统) | 低(仅需提示设计) |
运行成本 | 高(检索+生成) | 低(仅生成) |
1.4 RAG的应用场景
适合RAG的场景:
- 问答系统:基于企业文档、产品手册等回答用户问题
- 客户支持:根据支持文档提供准确的技术支持
- 内容创作:基于参考资料生成文章、报告或摘要
- 研究助手:检索和综合学术论文或研究资料
- 个性化推荐:基于用户历史和偏好提供建议
- 事实核查:验证信息的准确性并提供来源
不太适合RAG的场景:
- 创意写作(如小说、诗歌)
- 纯主观意见表达
- 简单的指令执行任务
- 实时交互式对话(延迟要求极高)
2. 知识库构建
2.1 数据收集与处理
构建高质量知识库的第一步是收集和处理相关数据:
数据来源:
- 企业内部文档(手册、报告、政策)
- 网站内容(FAQ、博客、产品描述)
- 公开数据集(维基百科、学术论文)
- API和数据库(结构化数据)
- 用户生成内容(问答、评论)
数据处理步骤:
python
import os
import re
from typing import List, Dict, Any
def load_documents(directory: str) -> List[Dict[str, Any]]:
"""从目录加载文档"""
documents = []
for root, _, files in os.walk(directory):
for file in files:
if file.endswith(('.txt', '.md', '.pdf', '.docx')):
file_path = os.path.join(root, file)
# 根据文件类型选择适当的加载方法
if file.endswith('.txt') or file.endswith('.md'):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
elif file.endswith('.pdf'):
content = extract_text_from_pdf(file_path)
elif file.endswith('.docx'):
content = extract_text_from_docx(file_path)
# 创建文档对象
doc = {
'content': content,
'metadata': {
'source': file_path,
'title': os.path.splitext(file)[0],
'file_type': file.split('.')[-1],
'created_at': os.path.getctime(file_path),
'modified_at': os.path.getmtime(file_path)
}
}
documents.append(doc)
return documents
def clean_text(text: str) -> str:
"""清理文本内容"""
# 移除多余的空白字符
text = re.sub(r'\s+', ' ', text).strip()
# 移除特殊字符
text = re.sub(r'[^\w\s.,?!:;()\[\]{}\-\'\"]+', '', text)
# 规范化引号
text = text.replace(''', "'").replace(''', "'")
text = text.replace('"', '"').replace('"', '"')
return text
def preprocess_documents(documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""预处理文档集合"""
processed_docs = []
for doc in documents:
# 清理文本
cleaned_content = clean_text(doc['content'])
# 创建处理后的文档
processed_doc = {
'content': cleaned_content,
'metadata': doc['metadata']
}
processed_docs.append(processed_doc)
return processed_docs
2.2 文本分块
将长文档分割成适当大小的文本块是构建高效RAG系统的关键步骤:
分块策略:
- 固定大小分块:按字符或标记数量分割
- 语义分块:按段落、句子或语义单元分割
- 递归分块:根据文档结构递归分割
- 滑动窗口分块:使用重叠窗口确保上下文连贯性
分块实现:
python
from typing import List, Dict, Any
def split_by_character_count(text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
"""按字符数分块,带重叠"""
chunks = []
start = 0
text_length = len(text)
while start < text_length:
# 确定当前块的结束位置
end = min(start + chunk_size, text_length)
# 如果不是最后一块且没有到达文本末尾,尝试在句子边界分割
if end < text_length:
# 查找最后一个句子结束符
last_period = text.rfind('.', start, end)
if last_period > start + chunk_size // 2: # 确保块不会太小
end = last_period + 1
# 提取当前块
chunk = text[start:end]
chunks.append(chunk)
# 更新下一块的起始位置,考虑重叠
start = end - overlap
return chunks
def split_by_markdown_structure(text: str) -> List[str]:
"""按Markdown结构分块"""
# 按标题分割
import re
# 匹配Markdown标题
header_pattern = r'^#{1,6}\s+.+$'
# 分割文本
lines = text.split('\n')
chunks = []
current_chunk = []
for line in lines:
if re.match(header_pattern, line) and current_chunk:
# 遇到新标题,保存当前块
chunks.append('\n'.join(current_chunk))
current_chunk = [line]
else:
current_chunk.append(line)
# 添加最后一个块
if current_chunk:
chunks.append('\n'.join(current_chunk))
return chunks
def create_document_chunks(documents: List[Dict[str, Any]],
chunk_size: int = 1000,
overlap: int = 200) -> List[Dict[str, Any]]:
"""将文档集合分块"""
chunked_documents = []
for doc in documents:
content = doc['content']
metadata = doc['metadata']
# 根据文件类型选择分块策略
if metadata['file_type'] == 'md':
chunks = split_by_markdown_structure(content)
else:
chunks = split_by_character_count(content, chunk_size, overlap)
# 为每个块创建新的文档对象
for i, chunk in enumerate(chunks):
chunk_doc = {
'content': chunk,
'metadata': {
**metadata,
'chunk_id': i,
'chunk_total': len(chunks)
}
}
chunked_documents.append(chunk_doc)
return chunked_documents
2.3 向量嵌入
向量嵌入是将文本转换为数值向量的过程,使计算机能够理解和比较文本的语义相似性:
嵌入模型选择:
- 通用模型:Sentence-BERT、OpenAI Embeddings、BERT等
- 领域特定模型:针对特定领域微调的嵌入模型
- 多语言模型:支持多语言的嵌入模型
- 轻量级模型:适合资源受限环境的小型模型
生成嵌入:
python
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List, Dict, Any
class EmbeddingGenerator:
def __init__(self, model_name: str = 'all-MiniLM-L6-v2'):
"""初始化嵌入生成器"""
self.model = SentenceTransformer(model_name)
def generate_embeddings(self, texts: List[str]) -> np.ndarray:
"""为文本列表生成嵌入"""
embeddings = self.model.encode(texts, show_progress_bar=True)
return embeddings
def embed_documents(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""为文档集合生成嵌入"""
# 提取文本内容
texts = [doc['content'] for doc in documents]
# 生成嵌入
embeddings = self.generate_embeddings(texts)
# 将嵌入添加到文档中
embedded_documents = []
for i, doc in enumerate(documents):
embedded_doc = {
**doc,
'embedding': embeddings[i]
}
embedded_documents.append(embedded_doc)
return embedded_documents
# 使用OpenAI的嵌入API
import openai
def generate_openai_embeddings(texts: List[str], model: str = "text-embedding-ada-002") -> List[List[float]]:
"""使用OpenAI API生成嵌入"""
embeddings = []
# 由于API限制,可能需要分批处理
batch_size = 100
for i in range(0, len(texts), batch_size):
batch = texts[i:i+batch_size]
response = openai.Embedding.create(
input=batch,
model=model
)
batch_embeddings = [item["embedding"] for item in response["data"]]
embeddings.extend(batch_embeddings)
return embeddings
2.4 向量存储
向量存储是专门设计用于高效存储和检索向量数据的数据库系统:
常见向量数据库:
- Faiss:Facebook AI开发的高效相似性搜索库
- Pinecone:专为向量搜索设计的托管服务
- Milvus:开源向量数据库,支持大规模向量检索
- Weaviate:语义搜索引擎,支持向量和结构化数据
- Chroma:专为RAG设计的嵌入式向量数据库
- Qdrant:高性能向量搜索引擎
使用Faiss存储向量:
python
import faiss
import numpy as np
import pickle
from typing import List, Dict, Any, Tuple
class FaissVectorStore:
def __init__(self, dimension: int):
"""初始化Faiss向量存储"""
self.dimension = dimension
self.index = faiss.IndexFlatL2(dimension) # L2距离索引
self.documents = []
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
"""添加文档到向量存储"""
# 提取嵌入
embeddings = np.array([doc['embedding'] for doc in documents], dtype=np.float32)
# 添加到索引
self.index.add(embeddings)
# 存储文档(不包括嵌入)
for doc in documents:
doc_without_embedding = {k: v for k, v in doc.items() if k != 'embedding'}
self.documents.append(doc_without_embedding)
def search(self, query_embedding: np.ndarray, top_k: int = 5) -> List[Dict[str, Any]]:
"""搜索最相似的文档"""
# 确保查询嵌入是二维数组
if query_embedding.ndim == 1:
query_embedding = np.expand_dims(query_embedding, axis=0)
# 执行搜索
distances, indices = self.index.search(query_embedding, top_k)
# 获取结果
results = []
for i, idx in enumerate(indices[0]):
if idx < len(self.documents): # 确保索引有效
result = {
**self.documents[idx],
'score': float(distances[0][i])
}
results.append(result)
return results
def save(self, index_path: str, documents_path: str) -> None:
"""保存索引和文档"""
# 保存Faiss索引
faiss.write_index(self.index, index_path)
# 保存文档
with open(documents_path, 'wb') as f:
pickle.dump(self.documents, f)
@classmethod
def load(cls, index_path: str, documents_path: str) -> 'FaissVectorStore':
"""加载索引和文档"""
# 加载Faiss索引
index = faiss.read_index(index_path)
# 加载文档
with open(documents_path, 'rb') as f:
documents = pickle.load(f)
# 创建实例
vector_store = cls(index.d)
vector_store.index = index
vector_store.documents = documents
return vector_store
使用Chroma存储向量:
python
import chromadb
from typing import List, Dict, Any
class ChromaVectorStore:
def __init__(self, collection_name: str, persist_directory: str = None):
"""初始化Chroma向量存储"""
# 创建客户端
if persist_directory:
self.client = chromadb.PersistentClient(path=persist_directory)
else:
self.client = chromadb.Client()
# 获取或创建集合
self.collection = self.client.get_or_create_collection(name=collection_name)
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
"""添加文档到向量存储"""
# 准备数据
ids = [str(i) for i in range(len(documents))]
texts = [doc['content'] for doc in documents]
embeddings = [doc['embedding'].tolist() for doc in documents]
metadatas = [doc['metadata'] for doc in documents]
# 添加到集合
self.collection.add(
ids=ids,
documents=texts,
embeddings=embeddings,
metadatas=metadatas
)
def search(self, query_embedding: List[float], top_k: int = 5) -> List[Dict[str, Any]]:
"""搜索最相似的文档"""
# 执行搜索
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=top_k
)
# 处理结果
documents = []
for i in range(len(results['ids'][0])):
doc = {
'content': results['documents'][0][i],
'metadata': results['metadatas'][0][i],
'score': results['distances'][0][i] if 'distances' in results else None
}
documents.append(doc)
return documents
3. RAG系统实现
3.1 基本RAG架构
RAG系统的基本架构包括以下组件:
核心组件:
- 文档处理器:处理和分块文档
- 嵌入生成器:生成文本的向量表示
- 向量存储:存储和检索向量
- 检索器:根据查询检索相关文档
- 提示构建器:构建包含检索内容的提示
- 生成模型:生成最终回答
- 后处理器:格式化和增强回答
简单RAG实现:
python
from typing import List, Dict, Any
import numpy as np
from transformers import AutoTokenizer, AutoModelForCausalLM
class SimpleRAG:
def __init__(self,
vector_store,
embedding_generator,
model_name: str = "gpt2"):
"""初始化简单RAG系统"""
self.vector_store = vector_store
self.embedding_generator = embedding_generator
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(model_name)
def retrieve(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
"""检索相关文档"""
# 生成查询嵌入
query_embedding = self.embedding_generator.generate_embeddings([query])[0]
# 检索相关文档
documents = self.vector_store.search(query_embedding, top_k=top_k)
return documents
def generate_prompt(self, query: str, documents: List[Dict[str, Any]]) -> str:
"""构建提示"""
# 构建上下文
context = "\n\n".join([doc['content'] for doc in documents])
# 构建提示
prompt = f"""请基于以下信息回答问题。如果无法从提供的信息中找到答案,请说明你不知道。
信息:
{context}
问题: {query}
回答:"""
return prompt
def answer(self, query: str, top_k: int = 3) -> str:
"""回答问题"""
# 检索相关文档
documents = self.retrieve(query, top_k=top_k)
# 构建提示
prompt = self.generate_prompt(query, documents)
# 生成回答
inputs = self.tokenizer(prompt, return_tensors="pt")
outputs = self.model.generate(
inputs["input_ids"],
max_length=512,
num_return_sequences=1,
do_sample=True,
temperature=0.7
)
# 解码回答
answer = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取生成的回答部分
answer = answer[len(prompt):]
return answer
3.2 使用LangChain实现RAG
LangChain是一个用于构建LLM应用的框架,提供了丰富的组件和工具,特别适合构建RAG系统:
python
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
from langchain.document_loaders import TextLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.llms import HuggingFacePipeline
from langchain.prompts import PromptTemplate
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
# 1. 加载文档
loader = DirectoryLoader('./documents', glob="**/*.txt", loader_cls=TextLoader)
documents = loader.load()
# 2. 分割文档
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)
# 3. 创建嵌入
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# 4. 创建向量存储
vectorstore = FAISS.from_documents(chunks, embeddings)
# 5. 设置检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 6. 设置语言模型
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_length=512
)
llm = HuggingFacePipeline(pipeline=pipe)
# 7. 设置提示模板
template = """请基于以下信息回答问题。如果无法从提供的信息中找到答案,请说明你不知道。
信息:
{context}
问题: {question}
回答:"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
# 8. 创建QA链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
chain_type_kwargs={"prompt": prompt}
)
# 9. 回答问题
query = "什么是向量数据库?"
result = qa_chain({"query": query})
print(result["result"])
3.3 使用LlamaIndex实现RAG
LlamaIndex是另一个专门为RAG设计的框架,提供了更多针对索引和检索的优化:
python
from llama_index import SimpleDirectoryReader, VectorStoreIndex, ServiceContext
from llama_index.llms import HuggingFaceLLM
from llama_index.embeddings import HuggingFaceEmbedding
import torch
# 1. 加载文档
documents = SimpleDirectoryReader('./documents').load_data()
# 2. 设置嵌入模型
embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")
# 3. 设置语言模型
llm = HuggingFaceLLM(
model_name="gpt2",
tokenizer_name="gpt2",
context_window=2048,
max_new_tokens=256,
generate_kwargs={"temperature": 0.7, "do_sample": True}
)
# 4. 创建服务上下文
service_context = ServiceContext.from_defaults(
llm=llm,
embed_model=embed_model
)
# 5. 创建索引
index = VectorStoreIndex.from_documents(
documents,
service_context=service_context
)
# 6. 创建查询引擎
query_engine = index.as_query_engine()
# 7. 回答问题
query = "什么是向量数据库?"
response = query_engine.query(query)
print(response)
3.4 高级RAG技术
基本RAG系统可以通过多种技术进行增强:
1. 混合检索:
python
from langchain.retrievers import BM25Retriever
from langchain.retrievers.ensemble import EnsembleRetriever
# 创建BM25检索器(基于关键词)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# 创建向量检索器(基于语义)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 创建集成检索器
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5]
)
# 使用集成检索器
docs = ensemble_retriever.get_relevant_documents(query)
2. 重排序:
python
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# 创建基础检索器
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 创建LLM压缩器
compressor = LLMChainExtractor.from_llm(llm)
# 创建上下文压缩检索器
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever
)
# 使用压缩检索器
compressed_docs = compression_retriever.get_relevant_documents(query)
3. 查询转换:
python
from langchain.retrievers import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
# 定义元数据结构
metadata_field_info = [
AttributeInfo(
name="source",
description="文档的来源文件",
type="string",
),
AttributeInfo(
name="title",
description="文档的标题",
type="string",
),
]
# 创建自查询检索器
self_query_retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
"文档集合包含各种技术文档",
metadata_field_info,
verbose=True
)
# 使用自查询检索器
docs = self_query_retriever.get_relevant_documents(
"查找关于Python的文档,但不包括教程"
)
4. 多步检索:
python
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
# 查询分解提示
decompose_prompt = PromptTemplate(
input_variables=["question"],
template="""请将以下复杂问题分解为2-3个简单的子问题,这些子问题的答案可以帮助回答原始问题。
问题: {question}
子问题:"""
)
# 创建查询分解链
decompose_chain = LLMChain(llm=llm, prompt=decompose_prompt)
# 分解查询
result = decompose_chain.run(query)
sub_questions = [q.strip() for q in result.split('\n') if q.strip()]
# 对每个子问题进行检索和回答
sub_answers = []
for sub_q in sub_questions:
sub_docs = retriever.get_relevant_documents(sub_q)
sub_context = "\n\n".join([doc.page_content for doc in sub_docs])
sub_prompt = f"""基于以下信息回答问题:
信息:
{sub_context}
问题: {sub_q}
回答:"""
sub_answer = llm(sub_prompt)
sub_answers.append({"question": sub_q, "answer": sub_answer})
# 合成最终答案
synthesis_prompt = f"""基于以下子问题的答案,回答原始问题:
原始问题: {query}
子问题和答案:
"""
for item in sub_answers:
synthesis_prompt += f"问题: {item['question']}\n"
synthesis_prompt += f"答案: {item['answer']}\n\n"
synthesis_prompt += "最终答案:"
final_answer = llm(synthesis_prompt)
4. 提示工程与RAG
4.1 提示模板设计
提示模板是RAG系统的关键组件,影响着生成回答的质量和相关性:
基本提示模板:
python
basic_template = """请基于以下信息回答问题。如果无法从提供的信息中找到答案,请说明你不知道。
信息:
{context}
问题: {question}
回答:"""
引用来源的提示模板:
python
citation_template = """请基于以下信息回答问题。对于回答中的每个事实,请在方括号中引用相应的来源编号。如果无法从提供的信息中找到答案,请说明你不知道。
信息:
{context_with_sources}
问题: {question}
回答:"""
思维链提示模板:
python
cot_template = """请基于以下信息回答问题。首先分析问题,思考如何从提供的信息中找到答案,然后给出最终回答。如果无法从提供的信息中找到答案,请说明你不知道。
信息:
{context}
问题: {question}
思考过程:"""
角色扮演提示模板:
python
role_template = """你是一位专业的研究助手,擅长基于提供的信息回答问题。请基于以下信息回答问题。如果无法从提供的信息中找到答案,请诚实地说明你不知道,而不是猜测。
信息:
{context}
问题: {question}
专业回答:"""
4.2 上下文增强技术
上下文增强技术可以提高RAG系统的性能:
1. 元数据增强:
python
def enhance_context_with_metadata(documents):
"""使用元数据增强上下文"""
enhanced_context = ""
for i, doc in enumerate(documents):
# 添加来源信息
source = doc['metadata'].get('source', 'Unknown')
title = doc['metadata'].get('title', 'Untitled')
date = doc['metadata'].get('created_at', 'Unknown date')
# 格式化增强上下文
enhanced_context += f"[文档 {i+1}] 来源: {source}, 标题: {title}, 日期: {date}\n"
enhanced_context += f"内容: {doc['content']}\n\n"
return enhanced_context
2. 相关性排序:
python
def sort_by_relevance(documents, query, embedding_generator):
"""按相关性排序文档"""
# 生成查询嵌入
query_embedding = embedding_generator.generate_embeddings([query])[0]
# 计算相似度分数
for doc in documents:
if 'embedding' in doc:
doc_embedding = doc['embedding']
# 计算余弦相似度
similarity = np.dot(query_embedding, doc_embedding) / (
np.linalg.norm(query_embedding) * np.linalg.norm(doc_embedding)
)
doc['relevance_score'] = similarity
# 按相关性排序
sorted_docs = sorted(documents, key=lambda x: x.get('relevance_score', 0), reverse=True)
return sorted_docs
3. 信息去重:
python
from sklearn.metrics.pairwise import cosine_similarity
def remove_redundant_information(documents, threshold=0.85):
"""移除冗余信息"""
if not documents:
return []
# 提取嵌入
embeddings = np.array([doc['embedding'] for doc in documents if 'embedding' in doc])
# 计算相似度矩阵
similarity_matrix = cosine_similarity(embeddings)
# 标记要保留的文档
to_keep = [True] * len(documents)
# 检查相似度
for i in range(len(documents)):
if not to_keep[i]:
continue
for j in range(i + 1, len(documents)):
if similarity_matrix[i, j] > threshold:
# 标记为冗余
to_keep[j] = False
# 过滤文档
unique_docs = [doc for i, doc in enumerate(documents) if to_keep[i]]
return unique_docs
4.3 回答生成策略
不同的回答生成策略可以满足不同的需求:
1. 直接回答:
python
def generate_direct_answer(llm, prompt, query, context):
"""生成直接回答"""
full_prompt = prompt.format(context=context, question=query)
return llm(full_prompt)
2. 带引用的回答:
python
def generate_answer_with_citations(llm, prompt, query, documents):
"""生成带引用的回答"""
# 准备带来源的上下文
context_with_sources = ""
for i, doc in enumerate(documents):
source = doc['metadata'].get('source', 'Unknown')
context_with_sources += f"[{i+1}] 来源: {source}\n{doc['content']}\n\n"
# 生成回答
full_prompt = prompt.format(context_with_sources=context_with_sources, question=query)
return llm(full_prompt)
3. 思维链回答:
python
def generate_chain_of_thought_answer(llm, prompt, query, context):
"""生成思维链回答"""
full_prompt = prompt.format(context=context, question=query)
# 生成思维过程
thinking = llm(full_prompt)
# 添加最终回答提示
full_response = full_prompt + "\n" + thinking + "\n\n基于以上思考,最终回答是:"
# 生成最终回答
return full_response + "\n" + llm(full_response)
4. 多模型协作:
python
def generate_collaborative_answer(retrieval_model, reasoning_model, generation_model, query, documents):
"""多模型协作生成回答"""
# 1. 检索模型选择最相关的文档
context = "\n\n".join([doc['content'] for doc in documents])
# 2. 推理模型分析信息
reasoning_prompt = f"""分析以下信息,找出与问题"{query}"最相关的关键点:
{context}
关键点:"""
key_points = reasoning_model(reasoning_prompt)
# 3. 生成模型创建最终回答
generation_prompt = f"""基于以下关键点回答问题:
关键点:
{key_points}
问题: {query}
回答:"""
return generation_model(generation_prompt)
5. RAG系统评估
5.1 评估指标
评估RAG系统需要考虑多个方面:
1. 检索质量指标:
- 召回率(Recall):检索到的相关文档占所有相关文档的比例
- 精确率(Precision):检索到的相关文档占检索结果的比例
- F1分数:精确率和召回率的调和平均
- 平均倒数排名(MRR):相关文档排名的倒数的平均值
- 归一化折扣累积增益(NDCG):考虑排序质量的指标
2. 回答质量指标:
- 正确性:回答是否准确
- 相关性:回答是否与问题相关
- 完整性:回答是否完整覆盖问题
- 简洁性:回答是否简洁明了
- 引用准确性:引用是否准确
3. 系统性能指标:
- 延迟:生成回答的时间
- 吞吐量:单位时间处理的查询数
- 资源使用:CPU、GPU、内存使用情况
5.2 自动评估方法
自动评估可以提高评估效率:
python
from rouge import Rouge
from bert_score import score
import numpy as np
import time
def evaluate_retrieval(retrieved_docs, relevant_docs):
"""评估检索质量"""
# 计算精确率
precision = len(set(retrieved_docs) & set(relevant_docs)) / len(retrieved_docs)
# 计算召回率
recall = len(set(retrieved_docs) & set(relevant_docs)) / len(relevant_docs)
# 计算F1分数
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
return {
"precision": precision,
"recall": recall,
"f1": f1
}
def evaluate_answer_quality(generated_answer, reference_answer):
"""评估回答质量"""
# 使用ROUGE评估
rouge = Rouge()
rouge_scores = rouge.get_scores(generated_answer, reference_answer)[0]
# 使用BERTScore评估
P, R, F1 = score([generated_answer], [reference_answer], lang="zh")
return {
"rouge-1": rouge_scores["rouge-1"]["f"],
"rouge-2": rouge_scores["rouge-2"]["f"],
"rouge-l": rouge_scores["rouge-l"]["f"],
"bert_score": F1.item()
}
def evaluate_rag_system(rag_system, test_queries, ground_truth):
"""评估RAG系统"""
results = []
for i, query in enumerate(test_queries):
# 记录开始时间
start_time = time.time()
# 生成回答
answer = rag_system.answer(query)
# 计算延迟
latency = time.time() - start_time
# 评估回答质量
quality_scores = evaluate_answer_quality(answer, ground_truth[i]["answer"])
# 评估检索质量
retrieved_docs = [doc["metadata"]["id"] for doc in rag_system.retrieve(query)]
retrieval_scores = evaluate_retrieval(retrieved_docs, ground_truth[i]["relevant_docs"])
# 记录结果
results.append({
"query": query,
"answer": answer,
"reference": ground_truth[i]["answer"],
"latency": latency,
"quality_scores": quality_scores,
"retrieval_scores": retrieval_scores
})
# 计算平均分数
avg_results = {
"avg_latency": np.mean([r["latency"] for r in results]),
"avg_rouge_1": np.mean([r["quality_scores"]["rouge-1"] for r in results]),
"avg_rouge_2": np.mean([r["quality_scores"]["rouge-2"] for r in results]),
"avg_rouge_l": np.mean([r["quality_scores"]["rouge-l"] for r in results]),
"avg_bert_score": np.mean([r["quality_scores"]["bert_score"] for r in results]),
"avg_precision": np.mean([r["retrieval_scores"]["precision"] for r in results]),
"avg_recall": np.mean([r["retrieval_scores"]["recall"] for r in results]),
"avg_f1": np.mean([r["retrieval_scores"]["f1"] for r in results])
}
return results, avg_results
5.3 人工评估方法
人工评估可以提供更全面的质量评估:
评估维度:
- 事实准确性:回答中的事实是否准确
- 相关性:回答是否与问题相关
- 完整性:回答是否完整覆盖问题
- 有用性:回答是否对用户有帮助
- 清晰度:回答是否清晰易懂
评分标准示例:
- 不满足(1分):回答完全不准确或不相关
- 部分满足(2分):回答部分准确或相关,但有明显缺陷
- 基本满足(3分):回答基本准确和相关,但可能不完整
- 良好(4分):回答准确、相关且相当完整
- 优秀(5分):回答完全准确、高度相关且全面
人工评估表格:
问题ID | 问题 | 系统回答 | 事实准确性(1-5) | 相关性(1-5) | 完整性(1-5) | 有用性(1-5) | 清晰度(1-5) | 总分 | 评价意见 |
---|---|---|---|---|---|---|---|---|---|
1 | ... | ... | 4 | 5 | 3 | 4 | 5 | 21 | ... |
2 | ... | ... | 3 | 4 | 4 | 3 | 4 | 18 | ... |
5.4 错误分析与改进
错误分析是改进RAG系统的关键步骤:
常见错误类型:
- 检索错误:未检索到相关文档
- 排序错误:相关文档排序不佳
- 理解错误:模型未正确理解问题
- 生成错误:生成的回答不准确或不相关
- 幻觉:生成不在检索内容中的信息
错误分析流程:
python
def analyze_errors(evaluation_results):
"""分析错误案例"""
# 按总分排序
sorted_results = sorted(evaluation_results, key=lambda x: sum(x["quality_scores"].values()))
# 选择最差的案例
worst_cases = sorted_results[:10]
error_analysis = []
for case in worst_cases:
# 检索错误分析
retrieval_error = case["retrieval_scores"]["recall"] < 0.5
# 生成错误分析
generation_error = case["quality_scores"]["bert_score"] < 0.7
# 延迟分析
latency_issue = case["latency"] > 2.0 # 超过2秒
error_type = []
if retrieval_error:
error_type.append("检索错误")
if generation_error:
error_type.append("生成错误")
if latency_issue:
error_type.append("延迟问题")
error_analysis.append({
"query": case["query"],
"answer": case["answer"],
"reference": case["reference"],
"error_type": error_type,
"retrieval_scores": case["retrieval_scores"],
"quality_scores": case["quality_scores"],
"latency": case["latency"]
})
return error_analysis
改进策略:
- 检索改进:使用混合检索、查询扩展、重排序等
- 提示优化:改进提示模板、添加示例、使用思维链等
- 模型升级:使用更强大的基础模型或领域适应模型
- 知识库增强:扩充知识库、提高知识质量、优化分块策略
- 后处理优化:添加事实检查、引用验证、格式优化等
6. 从JAVA开发者视角理解RAG
6.1 概念对比
JAVA概念与RAG概念对比:
JAVA概念 | RAG概念 | 说明 |
---|---|---|
数据库查询 | 向量检索 | 从存储中获取信息 |
索引优化 | 向量索引 | 提高检索效率 |
缓存机制 | 结果缓存 | 提高响应速度 |
数据预处理 | 文本分块 | 准备数据以便处理 |
模板引擎 | 提示模板 | 生成结构化输出 |
微服务架构 | RAG组件 | 系统架构设计 |
日志分析 | 错误分析 | 系统改进方法 |
6.2 技能迁移
可迁移的JAVA技能:
-
数据处理经验:
- 文本解析和处理
- 数据清洗和转换
- 批处理和流处理
-
系统设计能力:
- 模块化设计
- 接口定义
- 可扩展架构
-
性能优化技巧:
- 缓存策略
- 并行处理
- 资源管理
-
质量保证实践:
- 单元测试
- 集成测试
- 性能测试
6.3 开发流程对比
JAVA应用开发流程:
- 需求分析
- 系统设计
- 数据库设计
- 编码实现
- 测试验证
- 部署维护
RAG系统开发流程:
- 需求分析
- 知识库设计
- 向量存储设计
- 检索和生成实现
- 评估优化
- 部署维护
6.4 实践建议
从JAVA到RAG的过渡:
-
利用已有知识:
- 应用数据处理经验到文本处理
- 使用系统设计原则构建RAG架构
- 应用性能优化技巧提高RAG效率
-
重点学习领域:
- 向量嵌入和相似度计算
- 提示工程技术
- 大语言模型特性和限制
- Python生态系统工具
-
开发习惯调整:
- 从确定性结果到概率性输出
- 从精确匹配到语义相似度
- 从规则逻辑到模型推理
- 从静态数据到动态知识库
-
工具链转换:
- Maven/Gradle → pip/conda
- Spring Boot → FastAPI/Flask
- Hibernate → LangChain/LlamaIndex
- JUnit → pytest
7. 实践练习
练习1:基本RAG系统构建
- 收集5-10篇关于特定主题的文档(如Python编程、机器学习等)
- 实现文本分块和向量嵌入
- 构建简单的向量存储
- 实现基本的检索和回答生成
- 测试系统回答相关问题的能力
练习2:使用LangChain构建RAG
- 使用LangChain加载和处理文档
- 创建向量存储并实现检索
- 设计提示模板
- 构建完整的RAG问答链
- 评估系统性能并进行优化
练习3:高级RAG技术实践
- 实现混合检索(关键词+向量)
- 添加查询转换或分解功能
- 实现回答引用来源功能
- 添加结果缓存机制
- 进行系统评估和错误分析
8. 总结与反思
- RAG(检索增强生成)是一种结合检索系统和生成模型的技术,通过从外部知识库检索相关信息来增强大语言模型的回答质量
- 知识库构建是RAG系统的基础,包括数据收集、文本分块、向量嵌入和向量存储
- RAG系统实现可以从基本架构开始,逐步添加高级功能,如混合检索、重排序、查询转换等
- 提示工程在RAG中扮演重要角色,良好的提示模板和上下文增强技术可以显著提高系统性能
- RAG系统评估需要考虑检索质量、回答质量和系统性能等多个方面,通过错误分析可以持续改进系统
- JAVA开发者可以迁移许多已有技能到RAG开发中,同时需要学习新的概念和工具
9. 预习与延伸阅读
预习内容
- 大模型应用开发模式
- 大模型应用安全性考虑
- 多模态模型应用开发
- 大模型应用部署和监控
延伸阅读
- Jerry Liu等,《LlamaIndex: A Comprehensive Guide》
- Harrison Chase等,《LangChain: Building applications with LLMs through composability》
- Lewis Tunstall等,《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》
- Pinecone,《Vector Database Guide》
- Chip Huyen,《Designing Machine Learning Systems》(第7章:Data Distribution Shifts and Monitoring)
10. 明日预告
明天我们将学习大模型应用开发模式,包括Agent架构、工具使用、多模态应用等高级主题。我们将探讨如何设计和实现更复杂的大模型应用,如何让模型使用工具和API,以及如何处理多模态输入和输出。我们还将讨论大模型应用的安全性考虑,包括提示注入防御、输出过滤和隐私保护等内容。