RAG 与向量数据库集成:Spring Boot 中的检索增强生成架构实战

一、大模型的知识边界:为什么"参数化记忆"不够用

大模型的知识来源于训练数据,存在三个根本性限制:其一,训练数据有截止日期,模型无法回答训练之后发生的事件;其二,模型对专业领域知识(如企业内部文档、行业法规)的掌握深度不足;其三,模型可能产生"幻觉"——自信地编造不存在的事实。

检索增强生成(Retrieval-Augmented Generation, RAG)通过在推理时动态检索外部知识库,将相关文档注入 Prompt 上下文,弥补模型的知识盲区。但 RAG 的工程实现远不止"查向量数据库 + 拼接到 Prompt"这么简单——文档切分策略、Embedding 模型选型、检索精度优化、上下文窗口管理,每个环节都直接影响最终效果。

二、RAG 架构:从文档摄入到检索增强生成

完整的 RAG 系统包含两条链路:离线的文档摄入管线和在线的检索生成管线。

flowchart LR
    subgraph 离线摄入
        A[原始文档] --> B[文档解析<br/>PDF/Word/HTML]
        B --> C[文本切分<br/>Chunk Strategy]
        C --> D[Embedding 向量化]
        D --> E[写入向量数据库]
    end

    subgraph 在线检索
        F[用户查询] --> G[查询向量化]
        G --> H[向量相似度检索]
        H --> I[结果重排序<br/>Reranking]
        I --> J[上下文组装]
        J --> K[调用大模型生成]
        K --> L[返回增强响应]
    end

    E -.-> H

关键设计决策在于 Chunk 策略和检索策略的配合。Chunk 过大导致检索精度下降(无关信息稀释关键内容),Chunk 过小导致语义不完整(关键信息被切断)。检索策略需要平衡召回率和精度——召回太少可能遗漏关键文档,召回太多会超出上下文窗口。

三、生产级代码实现:文档摄入、向量检索与上下文组装

3.1 文档摄入管线

@Service
public class DocumentIngestionService {

    private final DocumentParser parser;
    private final ChunkStrategy chunkStrategy;
    private final EmbeddingModel embeddingModel;
    private final VectorStore vectorStore;

    @Async("ingestionExecutor")
    public CompletableFuture<Void> ingest(Resource document,
                                           Map<String, Object> metadata) {
        // 1. 解析文档为纯文本
        String text = parser.parse(document);

        // 2. 按语义边界切分
        List<TextChunk> chunks = chunkStrategy.split(text,
            new ChunkConfig(500, 50));  // 500 Token, 50 Token 重叠

        // 3. 向量化并存储
        List<Document> documents = chunks.stream()
            .map(chunk -> {
                float[] embedding = embeddingModel.embed(chunk.getText());
                Document doc = new Document(chunk.getText(), metadata);
                doc.setEmbedding(embedding);
                return doc;
            })
            .toList();

        vectorStore.add(documents);
        return CompletableFuture.completedFuture(null);
    }
}

3.2 语义切分策略

@Component
public class SemanticChunkStrategy implements ChunkStrategy {

    private final EmbeddingModel embeddingModel;

    @Override
    public List<TextChunk> split(String text, ChunkConfig config) {
        // 先按段落切分
        List<String> paragraphs = Arrays.asList(text.split("\n\n+"));

        List<TextChunk> chunks = new ArrayList<>();
        StringBuilder currentChunk = new StringBuilder();
        int currentTokens = 0;

        for (String para : paragraphs) {
            int paraTokens = estimateTokens(para);

            if (currentTokens + paraTokens > config.getMaxTokens()
                    && currentTokens > 0) {
                chunks.add(new TextChunk(currentChunk.toString().trim()));
                // 保留重叠部分,避免语义断裂
                String overlap = getOverlap(currentChunk.toString(),
                    config.getOverlapTokens());
                currentChunk = new StringBuilder(overlap);
                currentTokens = estimateTokens(overlap);
            }
            currentChunk.append(para).append("\n\n");
            currentTokens += paraTokens;
        }

        if (currentTokens > 0) {
            chunks.add(new TextChunk(currentChunk.toString().trim()));
        }
        return chunks;
    }
}

3.3 检索与上下文组装

@Service
public class RAGService {

    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;
    private final ChatClient chatClient;
    private final PromptTemplateService promptTemplateService;

    public String query(String userQuery, int topK) {
        // 1. 向量检索相关文档
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.query(userQuery)
                .withTopK(topK)
                .withSimilarityThreshold(0.75)
        );

        // 2. 组装上下文
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n---\n"));

        // 3. 渲染 Prompt 模板
        Map<String, Object> variables = Map.of(
            "context", context,
            "question", userQuery
        );
        String prompt = promptTemplateService.render(
            "rag-qa-template", variables);

        // 4. 调用大模型
        return chatClient.call(prompt);
    }
}

四、RAG 架构的精度瓶颈与工程权衡

检索精度的不确定性:向量相似度检索基于 Embedding 空间的距离度量,但语义相似不等于问题相关。用户问"如何配置 SSL 证书",检索可能返回"SSL 握手原理"的文档而非"SSL 证书配置步骤"。Reranking 模型(如 Cohere Rerank)可以提升精度,但增加了额外的推理延迟和成本。

上下文窗口的容量限制:检索到的文档加上用户查询和系统 Prompt,总长度可能超出模型上下文窗口。简单的截断策略可能丢失关键信息,需要根据文档与查询的相关性排序后,从高到低填充直到窗口上限。

Embedding 模型的领域适配:通用 Embedding 模型(如 text-embedding-3-small)在专业领域(如法律、医疗)的语义表达能力有限,可能导致检索精度下降。领域适配需要微调 Embedding 模型,但标注数据获取成本高,且微调后的模型可能丧失通用能力。

文档更新的实时性:向量数据库中的文档与源文档之间的同步存在延迟。当源文档更新后,需要重新摄入并替换旧的向量,这个过程的延迟取决于摄入管线的吞吐量。对于实时性要求高的场景(如新闻资讯),需要设计增量更新机制。

五、总结

RAG 的本质是将"参数化记忆"扩展为"参数化记忆 + 外部知识库"的混合架构,通过检索弥补模型的知识盲区。本文方案的核心链路为:文档解析 → 语义切分 → 向量化存储 → 检索重排序 → 上下文组装 → 模型生成。落地时需重点关注三个参数:Chunk 大小(建议 300-500 Token)、检索 topK 值(建议 3-5)、相似度阈值(建议 0.7-0.8)。建议从高质量的小规模知识库(如 FAQ 文档)开始验证,逐步扩展到大文档库,并在每个阶段评估检索准确率和生成质量。

Logo

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

更多推荐