优化 RAG 规模化:分块、检索与贝叶斯搜索

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

构建一个检索增强生成(RAG)系统非常简单,但在生产环境中实现规模化却异常困难。大多数开发者从“基础 RAG”开始:将文档切分为 512 令牌(token)的块,使用标准模型进行嵌入,并执行简单的 top-k 向量搜索。虽然这对于演示项目来说已经足够,但一旦遇到复杂的法律合同、技术 API 文档或细微的客户支持工单,这种方法就会失效。

n1n.ai 协助客户优化规模化系统时,我们发现 70% 的召回率与 95% 的召回率之间的差距,全在于检索流水线的设计。本指南将介绍如何从“语义搜索 + 运气”转向一个基于高级分块、混合检索和贝叶斯优化的、可度量且可调优的检索引擎。

分块危机:为什么固定窗口会失效

在生产环境中,固定的 512 令牌窗口往往是相关性的敌人。如果一个法律条款在中间被切断,嵌入向量就会丢失该条款的义务背景。如果 API 参考文档的块太大,特定功能的“信号”就会被淹没在周围的模板化“噪音”中。

为了解决这个问题,我们实施了分层分块策略。通过使用 n1n.ai 访问 Claude 3.5 Sonnet 或 OpenAI o3 等高推理能力模型,你甚至可以实现“智能体分块”(Agentic Chunking),由 LLM 决定文档的语义边界。

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Chunk:
    text: str
    metadata: dict
    token_count: int
    chunk_id: str

class ChunkingStrategy(ABC):
    @abstractmethod
    def chunk(self, document: str, metadata: dict) -> list[Chunk]: ...

class RecursiveChunker(ChunkingStrategy):
    """尊重结构:Markdown 标题、代码块、段落"""
    def __init__(self, separators=["\n## ", "\n### ", "\n\n", "\n", " "], chunk_size=512):
        self.separators = separators
        self.chunk_size = chunk_size

专业提示: 对于技术文档,使用能够识别代码块的 RecursiveChunker。对于对话数据,确保重叠度(Overlap)至少在 15-20% 之间,以保持对话流的连贯性。

混合检索:结合两者的优势

纯向量搜索(稠密检索)擅长处理同义词,但在处理 SKU 编号、错误代码或特定函数名等精确匹配时表现糟糕。相反,BM25(稀疏检索)擅长关键词匹配,但会错过查询的概念性含义。

我们采用混合检索器(Hybrid Retriever),结合了这两者,随后使用交叉编码器(Cross-Encoder)进行重排序(Rerank)。这种“漏斗式”方法确保我们不仅能找到数学上相似的文档,还能找到真正相关的文档。

class HybridRetriever:
    def __init__(self, vector_store, bm25_index, reranker, weights=(0.4, 0.3, 0.3)):
        self.vector = vector_store
        self.bm25 = bm25_index
        self.reranker = reranker
        self.weights = weights

    async def retrieve(self, query: str, k=20, final_k=5):
        # 第一阶段:并行检索
        vector_results = await self.vector.search(query, k=k)
        bm25_results = await self.bm25.search(query, k=k)

        # 第二阶段:互惠排名融合 (RRF)
        fused = self._rrf(vector_results, bm25_results, k=60)

        # 第三阶段:交叉编码器重排序 (Top 50 → Top 5)
        # 使用 n1n.ai 的高速端点访问重排序模型
        reranked = await self.reranker.rerank(query, fused[:50])

        return reranked[:final_k]

通过从单模态搜索转向这种混合方法,我们通常能看到 Recall@10 提升 10-15%。交叉编码器是这里的“秘密武器”;虽然双编码器(标准嵌入)与真实相关性的相关性约为 0.75,但交叉编码器可以达到 0.92。

查询转换:帮助用户更好地表达

众所周知,用户并不擅长编写搜索查询。他们经常使用模糊的术语,或者提出多部分的问题,而没有任何一个单独的文档块能够回答。因此,查询扩展(Query Expansion)和分解(Decomposition)至关重要。

通过利用 n1n.ai,你可以将这些转换任务路由到 DeepSeek-V3 或 GPT-4o-mini 等更小、更快的模型,在保持低延迟的同时,显著提高搜索覆盖率。

策略Recall@10成本系数延迟影响
单次查询78%1x+0ms
查询扩展 (3x)94%3x+40ms
任务分解96%2x+60ms

贝叶斯优化:调优黑盒

为什么要选择 chunk_size=512?为什么向量权重是 0.4?大多数团队只是在猜测。我们将这些视为超参数,并使用 Optuna 通过贝叶斯搜索进行优化。这使我们能够找到“帕累托前沿”(Pareto Frontier)——即召回率与延迟之间的完美平衡。

def objective(trial: optuna.Trial) -> tuple[float, float]:
    config = RetrievalConfig(
        chunk_size=trial.suggest_categorical("chunk_size", [256, 512, 1024]),
        vector_weight=trial.suggest_float("vector_weight", 0.1, 0.9),
    )
    # 在黄金数据集上评估
    recall, latency = evaluate_config(config, golden_set)
    return recall, latency / 1000

在我们的法律数据集测试中,这种自动化调优将 p95 延迟降低了 62%,同时将召回率从 78% 提高到了 95%。

总结

检索不是一个“设置后即可忘记”的组件,它是核心基础设施。要构建世界一流的 RAG 系统,你必须:

  1. 根据文档结构而非任意限制进行分块。
  2. 实施带有重排序步骤的混合搜索。
  3. 使用贝叶斯优化来停止对参数的猜测。
  4. 监控一切——从嵌入延迟到召回率抽样。

通过在 n1n.ai 提供的稳定、高速的 LLM 基础设施之上构建,你可以专注于这些高层架构的改进,而无需担心 API 的可用性或模型的速率限制。

n1n.ai 获取免费 API 密钥。