RAG由浅入深02

📦 第一部分:分块策略 —— 如何优雅地切分长文档

分块(Chunking)是RAG的"地基工程"——分块太大,检索噪声多;分块太小,语义不完整。核心目标是在保持语义完整性和控制检索粒度之间找到最佳平衡

🎯 五大主流分块策略

1. 固定字符分块(最基础)

def fixed_size_chunking(text, chunk_size=500, overlap=50):
    """
    按固定字符数切分,带重叠区域防止语义断裂
    """
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap  # 重叠区域,保留上下文
    return chunks
  • 优点:实现简单,计算开销小
  • 缺点:可能切断句子或段落,语义不完整
  • 适用场景:对质量要求不高的快速原型

2. 递归字符分块(LangChain默认方式)

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]  # 优先级递减
)
chunks = splitter.split_text(document)
  • 核心思想:优先在段落、句子边界切分,保持自然语义单元
  • 优点:比固定切分更"智能",保留更多语义完整性
  • 适用场景:通用场景的首选方案

3. 语义分块(基于句子嵌入)

from sentence_transformers import SentenceTransformer
import numpy as np

def semantic_chunking(sentences, similarity_threshold=0.7):
    """
    计算句子间的语义相似度,相似度低的切分
    """
    model = SentenceTransformer('all-MiniLM-L6-v2')
    embeddings = model.encode(sentences)
    
    chunks = []
    current_chunk = [sentences[0]]
    
    for i in range(1, len(sentences)):
        # 计算当前句子与上一句的相似度
        sim = np.dot(embeddings[i], embeddings[i-1]) / (
            np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1])
        )
        if sim < similarity_threshold:
            chunks.append(" ".join(current_chunk))
            current_chunk = [sentences[i]]
        else:
            current_chunk.append(sentences[i])
    
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    return chunks
  • 优点:能智能识别主题边界,分块质量高
  • 缺点:计算开销大,需要调用模型
  • 适用场景:对检索精度要求高的生产环境

4. 文档结构感知分块(针对特定格式)

def markdown_aware_chunking(markdown_text):
    """
    按Markdown的标题层级切分,保持文档结构
    """
    import re
    
    # 按标题(# ## ###)分割
    pattern = r'(#{1,3}\s+[^\n]+\n)'
    sections = re.split(pattern, markdown_text)
    
    chunks = []
    current_header = ""
    current_content = []
    
    for item in sections:
        if re.match(r'^#{1,3}\s+', item):
            # 是标题,保存之前的块
            if current_header and current_content:
                chunks.append(current_header + "\n".join(current_content))
            current_header = item
            current_content = []
        else:
            current_content.append(item)
    
    if current_header and current_content:
        chunks.append(current_header + "\n".join(current_content))
    
    return chunks
  • 适用场景:技术文档、法律文书、财报等结构化文档

5. 大小块混合策略(进阶方案)

def hybrid_chunking(document):
    """
    同时建立大块(用于上下文)和小块(用于检索)
    """
    # 大块:提供完整上下文
    large_chunks = RecursiveCharacterTextSplitter(
        chunk_size=1000, 
        chunk_overlap=100
    ).split_text(document)
    
    # 小块:用于精确检索
    small_chunks = RecursiveCharacterTextSplitter(
        chunk_size=200, 
        chunk_overlap=50
    ).split_text(document)
    
    return {
        "large_chunks": large_chunks,
        "small_chunks": small_chunks,
        "strategy": "父文档检索(Parent Document Retrieval)"
    }
  • 核心思想:检索时用小块精确定位,返回答案时附带大块上下文
  • 优点:兼顾检索精度和回答质量

📊 分块策略选择决策表

文档类型 推荐策略 chunk_size overlap
短文章(<500字) 不切分,整文使用 - -
技术博客/文章 递归字符分块 500-800 50-100
法律/政策文档 结构感知分块 按章节 -
财报/年报 语义分块 动态 -
多主题综合文档 混合策略 200/1000 50/100
用户评论/对话 固定字符分块 200-300 30-50

🎯 第二部分:检索优化 —— 让AI找到最相关的信息

分块只是第一步,更关键的是如何让检索系统精准找到用户问题的相关片段

🔧 检索优化策略矩阵

策略1:多路召回 + 重排序(Reranking)

核心思想:先快速召回多个候选,再用更精细的模型重新排序。

from sentence_transformers import CrossEncoder
import numpy as np

def rerank_search(query, chunks, top_k=10):
    """
    多路召回 + 交叉编码器重排
    """
    # 1. 第一轮:快速召回20个候选(用普通嵌入)
    embeddings = OpenAIEmbeddings()
    query_emb = embeddings.embed_query(query)
    chunk_embs = [embeddings.embed_query(chunk) for chunk in chunks]
    
    # 计算余弦相似度,取前20
    similarities = np.dot(chunk_embs, query_emb)
    top_indices = np.argsort(similarities)[-20:][::-1]
    candidates = [chunks[i] for i in top_indices]
    
    # 2. 第二轮:用交叉编码器精细排序
    reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
    scores = reranker.predict([(query, chunk) for chunk in candidates])
    
    # 返回排序后的top_k
    reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [chunk for chunk, score in reranked[:top_k]]
  • 性能提升:检索精度提升20-30%
  • 适用场景:大规模知识库(>10万文档)

策略2:HyDE(假设文档嵌入)

核心思想:让大模型先"想象"一个理想答案,然后用这个假想答案去检索。

def hyde_search(query, llm, knowledge_base):
    """
    HyDE:先让AI生成假设答案,再检索
    """
    # 1. 生成假设答案(不要求准确,只求相关概念)
    hypothesis = llm.invoke(f"假设你是专家,请针对以下问题写一段可能的答案草稿:{query}")
    
    # 2. 用假设答案进行检索(而非原始问题)
    hypothesis_embed = embeddings.embed_query(hypothesis.content)
    results = knowledge_base.similarity_search_by_vector(hypothesis_embed)
    
    return results
  • 优点:能召回与问题语义相近但关键词不同的文档
  • 缺点:增加LLM调用开销(约100-300ms)

策略3:多查询查询(Multi-Query)

核心思想:将用户问题改写为多个不同视角的问题,分别检索后合并去重。

def multi_query_search(query, llm, knowledge_base):
    """
    生成5个不同视角的问题,合并检索结果
    """
    # 1. 生成多个查询变体
    prompt = f"请将以下问题从不同角度改写为5个不同的问题(保持原意):{query}"
    variants = llm.invoke(prompt).content.split('\n')[:5]
    
    # 2. 每个变体分别检索
    all_results = []
    for q in [query] + variants:
        results = knowledge_base.similarity_search(q, k=3)
        all_results.extend(results)
    
    # 3. 去重并合并(基于内容相似度)
    unique_results = deduplicate_documents(all_results)
    return unique_results
  • 适用场景:用户问题模糊、多义性强的场景

策略4:元数据过滤(Metadata Filtering)

核心思想:为文档块附加元数据(来源、日期、类型等),检索时先按条件过滤。

def metadata_aware_retrieval(query, metadata_filters, knowledge_base):
    """
    带元数据过滤的检索
    """
    # 例:只检索2024年以后的文档,且类型为"政策"
    filters = {
        "year": {"$gte": 2024},
        "doc_type": "policy"
    }
    
    results = knowledge_base.similarity_search(
        query,
        k=5,
        filter=filters  # 先过滤再检索
    )
    return results
  • 优点:大幅减少无关候选,提升精度
  • 适用场景:结构化知识库(如企业文档库)

策略5:自适应检索(Adaptive Retrieval)

核心思想:根据问题的复杂度动态调整检索策略。

def adaptive_search(query, llm, knowledge_base):
    """
    先判断问题复杂度,再选择策略
    """
    # 1. 问题复杂度判断
    complexity_prompt = f"请判断以下问题的复杂度(简单/中等/复杂):{query}"
    level = llm.invoke(complexity_prompt).content.strip()
    
    # 2. 动态策略选择
    if level == "简单":
        return knowledge_base.similarity_search(query, k=3)
    elif level == "中等":
        # 使用多路召回
        return multi_query_search(query, llm, knowledge_base)
    else:  # 复杂
        # 使用HyDE或链式检索
        return hyde_search(query, llm, knowledge_base)

🚀 实战:完整优化方案代码示例

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

def build_optimized_rag():
    """
    构建一个整合了多种优化策略的RAG系统
    """
    # 1. 优化分块策略
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=600,
        chunk_overlap=80,
        separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
    )
    docs = text_splitter.split_documents(documents)
    
    # 2. 创建向量库
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma.from_documents(
        docs,
        embeddings,
        collection_metadata={"hnsw:space": "cosine"}  # 使用余弦相似度
    )
    
    # 3. 基础检索器(多路召回)
    base_retriever = vectorstore.as_retriever(
        search_type="mmr",  # MMR算法平衡相关性和多样性
        search_kwargs={"k": 20, "fetch_k": 50}
    )
    
    # 4. 重排序压缩(LLM链式压缩)
    llm = ChatOpenAI(model="gpt-3.5-turbo")
    compressor = LLMChainExtractor.from_llm(llm)
    
    # 5. 最终检索器
    final_retriever = ContextualCompressionRetriever(
        base_compressor=compressor,
        base_retriever=base_retriever
    )
    
    return final_retriever

# 使用优化后的检索器
retriever = build_optimized_rag()
relevant_docs = retriever.get_relevant_documents("公司年假政策是什么?")

📈 优化效果与性能权衡

优化策略 精度提升 延迟增加 实现难度 推荐指数
递归字符分块 +10% +5ms ⭐⭐⭐⭐⭐
语义分块 +15% +50ms ⭐⭐⭐ ⭐⭐⭐⭐
多路召回+重排 +20-30% +100-200ms ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
HyDE +15-25% +200-300ms ⭐⭐ ⭐⭐⭐⭐
元数据过滤 +25% +10ms ⭐⭐ ⭐⭐⭐⭐⭐
自适应检索 +30% +50-300ms ⭐⭐⭐⭐ ⭐⭐⭐⭐

💡 最佳实践建议

  1. 从小规模开始:先在1000个文档的规模测试分块策略
  2. 建立评测集:准备50-100个标准问题及期望答案,量化评估每个优化策略的效果
  3. 分层优化:先优化分块(影响基础),再优化检索(锦上添花)
  4. 监控关键指标:关注召回的NDCG@10和回答的BLEU/ROUGE分数
  5. 渐进式部署:在生产环境先用简单策略,逐步迭代复杂优化

分块和检索优化没有"银弹",最佳实践是根据你的文档类型、用户查询模式、性能要求做AB测试,找到最适合的组合

posted @ 2026-07-17 21:00  yuangu  阅读(3)  评论(0)    收藏  举报