第三阶段_大模型应用开发-Day 4: RAG检索增强生成技术

Day 4: RAG检索增强生成技术

学习目标

  • 理解RAG(检索增强生成)的基本原理和重要性
  • 掌握知识库构建和向量检索的方法
  • 学习RAG系统的设计和实现步骤
  • 了解提示工程在RAG中的应用
  • 掌握RAG系统的评估和优化方法

1. RAG基础概念

1.1 什么是RAG

RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合检索系统和生成模型的技术,通过从外部知识库检索相关信息来增强大语言模型的回答质量。

RAG的核心思想:

  • 不依赖模型参数中存储的知识
  • 从外部知识库检索相关信息
  • 将检索到的信息与用户查询结合
  • 生成基于事实、最新且可溯源的回答

RAG的优势:

  • 知识更新:无需重新训练模型即可更新知识
  • 减少幻觉:提供事实依据,减少模型生成虚假信息
  • 可溯源性:回答可以追溯到具体信息来源
  • 领域适应:通过更换知识库快速适应不同领域
  • 成本效益:比完全微调模型更经济高效

1.2 RAG的工作流程

RAG系统通常包含以下核心步骤:

  1. 索引构建:

    • 收集和处理文档
    • 将文档分割成适当大小的块
    • 使用嵌入模型生成文本块的向量表示
    • 将向量存储在向量数据库中
  2. 查询处理:

    • 接收用户查询
    • 使用相同的嵌入模型生成查询的向量表示
    • 在向量数据库中检索与查询最相似的文本块
    • 根据相似度排序检索结果
  3. 上下文增强:

    • 将检索到的文本块与原始查询结合
    • 构建提示模板,指导模型如何使用检索信息
  4. 生成回答:

    • 将增强的提示发送给大语言模型
    • 模型生成基于检索内容的回答
    • 可能包含引用或来源信息

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. 不满足(1分):回答完全不准确或不相关
  2. 部分满足(2分):回答部分准确或相关,但有明显缺陷
  3. 基本满足(3分):回答基本准确和相关,但可能不完整
  4. 良好(4分):回答准确、相关且相当完整
  5. 优秀(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技能:

  1. 数据处理经验:

    • 文本解析和处理
    • 数据清洗和转换
    • 批处理和流处理
  2. 系统设计能力:

    • 模块化设计
    • 接口定义
    • 可扩展架构
  3. 性能优化技巧:

    • 缓存策略
    • 并行处理
    • 资源管理
  4. 质量保证实践:

    • 单元测试
    • 集成测试
    • 性能测试

6.3 开发流程对比

JAVA应用开发流程:

  1. 需求分析
  2. 系统设计
  3. 数据库设计
  4. 编码实现
  5. 测试验证
  6. 部署维护

RAG系统开发流程:

  1. 需求分析
  2. 知识库设计
  3. 向量存储设计
  4. 检索和生成实现
  5. 评估优化
  6. 部署维护

6.4 实践建议

从JAVA到RAG的过渡:

  1. 利用已有知识:

    • 应用数据处理经验到文本处理
    • 使用系统设计原则构建RAG架构
    • 应用性能优化技巧提高RAG效率
  2. 重点学习领域:

    • 向量嵌入和相似度计算
    • 提示工程技术
    • 大语言模型特性和限制
    • Python生态系统工具
  3. 开发习惯调整:

    • 从确定性结果到概率性输出
    • 从精确匹配到语义相似度
    • 从规则逻辑到模型推理
    • 从静态数据到动态知识库
  4. 工具链转换:

    • Maven/Gradle → pip/conda
    • Spring Boot → FastAPI/Flask
    • Hibernate → LangChain/LlamaIndex
    • JUnit → pytest

7. 实践练习

练习1:基本RAG系统构建

  1. 收集5-10篇关于特定主题的文档(如Python编程、机器学习等)
  2. 实现文本分块和向量嵌入
  3. 构建简单的向量存储
  4. 实现基本的检索和回答生成
  5. 测试系统回答相关问题的能力

练习2:使用LangChain构建RAG

  1. 使用LangChain加载和处理文档
  2. 创建向量存储并实现检索
  3. 设计提示模板
  4. 构建完整的RAG问答链
  5. 评估系统性能并进行优化

练习3:高级RAG技术实践

  1. 实现混合检索(关键词+向量)
  2. 添加查询转换或分解功能
  3. 实现回答引用来源功能
  4. 添加结果缓存机制
  5. 进行系统评估和错误分析

8. 总结与反思

  • RAG(检索增强生成)是一种结合检索系统和生成模型的技术,通过从外部知识库检索相关信息来增强大语言模型的回答质量
  • 知识库构建是RAG系统的基础,包括数据收集、文本分块、向量嵌入和向量存储
  • RAG系统实现可以从基本架构开始,逐步添加高级功能,如混合检索、重排序、查询转换等
  • 提示工程在RAG中扮演重要角色,良好的提示模板和上下文增强技术可以显著提高系统性能
  • RAG系统评估需要考虑检索质量、回答质量和系统性能等多个方面,通过错误分析可以持续改进系统
  • JAVA开发者可以迁移许多已有技能到RAG开发中,同时需要学习新的概念和工具

9. 预习与延伸阅读

预习内容

  • 大模型应用开发模式
  • 大模型应用安全性考虑
  • 多模态模型应用开发
  • 大模型应用部署和监控

延伸阅读

  1. Jerry Liu等,《LlamaIndex: A Comprehensive Guide》
  2. Harrison Chase等,《LangChain: Building applications with LLMs through composability》
  3. Lewis Tunstall等,《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》
  4. Pinecone,《Vector Database Guide》
  5. Chip Huyen,《Designing Machine Learning Systems》(第7章:Data Distribution Shifts and Monitoring)

10. 明日预告

明天我们将学习大模型应用开发模式,包括Agent架构、工具使用、多模态应用等高级主题。我们将探讨如何设计和实现更复杂的大模型应用,如何让模型使用工具和API,以及如何处理多模态输入和输出。我们还将讨论大模型应用的安全性考虑,包括提示注入防御、输出过滤和隐私保护等内容。

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
大勇前进4 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu4 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile4 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白804 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙4 天前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发4 天前
软件工程SOLID 五大设计原则
后端·软件工程