大模型 RAG 后端架构:向量数据库选型与检索优化

cover

一、大模型的知识边界:训练数据截止与领域知识缺失

大语言模型的知识来源于训练数据,存在两个根本性限制:训练数据有截止日期(无法获取最新信息),且通用模型缺乏垂直领域的专业知识。RAG(Retrieval-Augmented Generation)通过"先检索、后生成"的范式弥补这些缺陷——先从外部知识库中检索相关文档,再将检索结果注入模型上下文,让模型基于真实数据生成回答。

RAG 的后端架构核心是向量数据库的选型与检索优化。向量数据库负责存储文档的嵌入向量并支持高效的相似度检索,其性能直接决定 RAG 系统的响应延迟和检索质量。但向量数据库的选型不是简单的"性能对比"——不同的数据规模、查询模式和部署约束,适合不同的数据库方案。

二、RAG 后端架构与向量检索流程

RAG 系统的后端架构分为离线索引和在线检索两条路径。离线阶段负责文档的采集、分块、嵌入编码和索引构建;在线阶段处理用户查询的实时嵌入、向量检索和上下文注入。

flowchart TB
    A[文档数据源] --> B[文档采集与清洗]
    B --> C[文本分块策略]
    C --> D[嵌入编码]
    D --> E[向量数据库写入]

    F[用户查询] --> G[查询嵌入编码]
    G --> H[向量相似度检索]
    E --> H
    H --> I[Top-K 候选文档]
    I --> J[语义重排序]
    J --> K[上下文窗口截断]
    K --> L[Prompt 组装]
    L --> M[LLM 生成回答]

    subgraph 向量数据库选型
        N[小规模: FAISS/Chroma]
        O[中规模: Milvus/Qdrant]
        P[大规模: Pinecone/Weaviate]
    end

    E --> N
    E --> O
    E --> P

上图展示了 RAG 系统的完整数据流。向量数据库选型是架构决策的核心:FAISS 适合单机小规模场景(< 100 万条),Milvus 适合分布式中等规模(100 万-1 亿条),Pinecone 等托管服务适合不想自运维的团队。

三、生产级实现:RAG 检索引擎

// RAGRetrievalEngine.java — RAG 检索引擎核心实现
import java.util.*;
import java.util.concurrent.*;

// 文档分块记录
record DocumentChunk(
    String id,
    String content,
    String source,
    Map<String, String> metadata,
    float[] embedding
) {}

// 检索结果
record RetrievalResult(
    DocumentChunk chunk,
    double score,
    String rerankReason
) {}

// 文档分块器:按语义边界切分
// 设计意图:固定长度分块会截断语义完整的段落,
// 按段落和句子边界分块可以保留语义完整性
class SemanticChunker {
    private static final int MAX_CHUNK_TOKENS = 400;
    private static final int OVERLAP_TOKENS = 50;

    List<String> chunk(String text) {
        List<String> paragraphs = Arrays.asList(text.split("\\n{2,}"));
        List<String> chunks = new ArrayList<>();
        StringBuilder currentChunk = new StringBuilder();

        for (String para : paragraphs) {
            if (currentChunk.length() + para.length() > MAX_CHUNK_TOKENS * 2) {
                if (currentChunk.length() > 0) {
                    chunks.add(currentChunk.toString().trim());
                    // 保留重叠区域
                    String overlap = currentChunk.substring(
                        Math.max(0, currentChunk.length() - OVERLAP_TOKENS * 2)
                    );
                    currentChunk = new StringBuilder(overlap);
                }
            }
            currentChunk.append(currentChunk.length() > 0 ? "\n\n" : "").append(para);
        }

        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString().trim());
        }
        return chunks;
    }
}

// 检索引擎:向量检索 + 语义重排序
class RAGRetrievalEngine {
    private final EmbeddingClient embeddingClient;
    private final VectorStore vectorStore;
    private final Reranker reranker;
    private final int topK;
    private final int rerankTopN;

    RAGRetrievalEngine(EmbeddingClient embeddingClient,
                       VectorStore vectorStore,
                       Reranker reranker,
                       int topK, int rerankTopN) {
        this.embeddingClient = embeddingClient;
        this.vectorStore = vectorStore;
        this.reranker = reranker;
        this.topK = topK;
        this.rerankTopN = rerankTopN;
    }

    // 完整检索流程:查询嵌入 → 向量检索 → 重排序 → 截断
    // 设计意图:向量检索是粗筛(召回率高但精度有限),
    // 重排序是精排(基于交叉编码器提升精度)
    List<RetrievalResult> retrieve(String query) {
        // 步骤 1:查询嵌入
        float[] queryEmbedding = embeddingClient.embed(query);

        // 步骤 2:向量相似度检索(粗筛,取 topK 条)
        List<RetrievalResult> candidates = vectorStore.search(
            queryEmbedding, topK
        );

        // 步骤 3:语义重排序(精排,取 rerankTopN 条)
        // 设计意图:向量检索使用双编码器(Bi-Encoder),
        // 查询和文档独立编码,速度虽快但精度有限。
        // 重排序使用交叉编码器(Cross-Encoder),
        // 联合编码查询和文档,精度更高但速度较慢。
        List<RetrievalResult> reranked = reranker.rerank(
            query, candidates, rerankTopN
        );

        return reranked;
    }

    // 构建注入 LLM 的上下文
    // 设计意图:检索到的文档可能超过 LLM 上下文窗口限制,
    // 需要按优先级截断并标注来源
    String buildContext(List<RetrievalResult> results, int maxContextTokens) {
        StringBuilder context = new StringBuilder();
        int usedTokens = 0;

        for (RetrievalResult result : results) {
            int chunkTokens = estimateTokens(result.chunk().content());
            if (usedTokens + chunkTokens > maxContextTokens) {
                break;  // 超出上下文窗口,截断
            }
            context.append("[来源: ").append(result.chunk().source())
                   .append("]\n")
                   .append(result.chunk().content())
                   .append("\n\n");
            usedTokens += chunkTokens;
        }

        return context.toString();
    }

    private int estimateTokens(String text) {
        return (int) Math.ceil(text.length() / 2.0);
    }
}

// 向量存储接口:抽象不同向量数据库的实现
interface VectorStore {
    void upsert(List<DocumentChunk> chunks);
    List<RetrievalResult> search(float[] queryEmbedding, int topK);
    void deleteBySource(String source);
}

// 重排序器接口
interface Reranker {
    List<RetrievalResult> rerank(String query, List<RetrievalResult> candidates, int topN);
}

四、边界分析与架构权衡

RAG 后端架构在生产落地中需要正视以下 Trade-off:

向量数据库的选型决策。FAISS 是最简单的方案(单机内存索引,零运维),但无法水平扩展且不支持持久化。Milvus 支持分布式部署和持久化,但运维复杂度高(依赖 etcd、MinIO、Pulsar)。Pinecone 是全托管服务,零运维但成本较高(按向量数量和查询次数计费)。选型建议:< 50 万条用 FAISS,50 万-5000 万条用 Milvus/Qdrant,> 5000 万条或不想运维用 Pinecone。

检索精度与延迟的矛盾。双编码器检索延迟低(< 10ms),但精度有限;交叉编码器重排序精度高,但延迟高(每条 50-100ms)。Top-K=20 的重排序需要 1-2 秒。建议:粗筛取 Top-50,重排序取 Top-5,在精度和延迟间取得平衡。

分块策略的影响。分块大小直接影响检索质量:块太大导致嵌入向量"稀释"关键信息,块太小丢失上下文。固定长度分块最简单但效果最差,按段落分块保留语义完整性,按语义边界分块(使用 NLP 句子分割器)效果最好但实现复杂。建议从段落分块开始,根据检索效果逐步优化。

适用边界:RAG 最适合知识密集型问答场景(技术文档、法律条文、产品手册)。对于需要精确匹配的场景(如库存查询、价格计算),传统数据库查询比 RAG 更可靠。

五、总结

RAG 后端架构将大模型从"封闭推理"扩展到"开放检索"。核心要点:向量数据库选型需根据数据规模和运维能力决策,双编码器粗筛 + 交叉编码器精排的两阶段检索在精度和延迟间取得平衡,分块策略直接影响检索质量。落地建议:第一,从 FAISS + 段落分块快速验证 RAG 效果;第二,根据数据规模决定是否迁移到分布式向量数据库;第三,引入重排序提升检索精度,但需控制延迟在可接受范围内。关键原则:RAG 的价值不在于"检索到文档",而在于"检索到正确的文档"——检索质量决定生成质量。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐