Skip to content

Spring Boot + Spring AI:知识库、分片、向量化、召回、重排与向量数据库实战

很多人第一次做 Spring AI 知识库问答时,容易把事情理解成:

  1. 把文档塞进向量库
  2. 问问题时做一次相似度检索
  3. 把结果拼进 Prompt

这个流程不能说错,但离真实项目还差不少。

真正决定效果的,往往不是“有没有接上 VectorStore”,而是下面这些环节有没有串顺:

  1. 文档怎么清洗
  2. 文档怎么切分
  3. 用什么 EmbeddingModel 做向量化
  4. 检索时怎么召回
  5. 候选结果要不要过滤、去重、重排
  6. 最后哪些片段真的送进模型

所以这篇不只讲一个最小 RAG demo,而是把 Spring Boot + Spring AI 项目里知识库落地最常见的一条主线讲清楚。

1. 把“知识库问答”拆成几层

在 Spring 项目里,知识库问答通常至少包含 4 层:

  1. 文档入库层:读取原始资料、清洗、分片、向量化、写入索引
  2. 检索召回层:根据用户问题取回候选片段
  3. 排序过滤层:做元数据过滤、去重、Rerank 精排
  4. 生成回答层:把最终上下文拼进 Prompt,再让模型回答

🌟 要注意的是:知识库 不等于 向量数据库

知识库 指的是一整条“资料准备 -> 索引构建 -> 检索 -> 回答”的链路;
向量数据库 只是这条链路里负责存储和检索向量的一层基础设施。

2. 一条完整链路到底长什么样

mermaid
flowchart TD
    A[原始文档: PDF / Markdown / FAQ / 工单] --> B[清洗与标准化]
    B --> C[按段落或语义切分 Chunk]
    C --> D[补元数据: 来源/时间/类型/权限]
    D --> E[调用 EmbeddingModel 生成向量]
    E --> F[写入 VectorStore]
    G[用户问题] --> H[问题改写/补充检索词]
    H --> I[按向量或混合方式召回]
    I --> J[元数据过滤/去重]
    J --> K[Rerank 精排]
    K --> L[选出 TopN 上下文]
    L --> M[拼接 Prompt]
    M --> N[模型生成答案]
    N --> O[返回答案与引用来源]

这条链路里最容易被低估的几个点是:

  1. 切分不是越细越好,也不是越大越好
  2. 召回出来的内容不等于就适合直接送给模型
  3. 没有元数据过滤时,知识库很容易把不该给当前用户的内容也一起查出来
  4. Rerank 往往直接影响最终答案质量

3. 文档入库不是“直接 add”

3.1 文档入库通常要先做什么

一份原始资料进入知识库前,通常至少要经过这些步骤:

  1. 去掉页眉页脚、导航噪音、空白段落
  2. 把文档切成更适合检索的 Chunk
  3. 给每个片段补元数据
  4. 再做向量化并写入向量库

如果一上来就把整篇文档直接入库,后面常见的问题通常是:

  1. 召回粒度太粗,一个片段里混了太多无关信息
  2. 相似度看起来命中,但真正相关的内容埋在长文本中间
  3. 无法按文档类型、知识来源做过滤

3.2 一个更贴近工程的入库 Service

java
package com.example.ai.knowledge;

import java.util.List;
import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.stereotype.Service;

/**
 * 负责把原始资料整理后写入知识库。
 */
@Service
public class KnowledgeIngestionService {

    private final VectorStore vectorStore;

    public KnowledgeIngestionService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    /**
     * 导入一篇原始文档。
     *
     * @param sourceId 文档唯一标识
     * @param title 文档标题
     * @param rawContent 清洗后的正文
     */
    public void ingest(String sourceId, String title, String rawContent) {
        // 先用语义化元数据把文档身份补齐,后面过滤和溯源都要靠它。
        Document source = new Document(rawContent, Map.of(
                "sourceId", sourceId,
                "title", title,
                "docType", "manual",
                "category", "knowledge-base"
        ));

        // 演示里直接使用 Token 切分器;真实项目里通常要按文档类型再做定制。
        TokenTextSplitter splitter = new TokenTextSplitter(300, 80, 20, 10000, true);
        List<Document> chunks = splitter.apply(List.of(source));

        // 给每个片段补一个 chunkIndex,方便排障和命中回溯。
        for (int i = 0; i < chunks.size(); i++) {
            chunks.get(i).getMetadata().put("chunkIndex", i);
        }

        vectorStore.add(chunks);
    }
}

这段代码真正想说明的是:

  1. VectorStore.add(...) 前面还有一段非常重要的预处理链路
  2. 元数据不是附属品,它决定后面检索和权限边界能不能做
  3. 切分策略应该独立成一层,而不是散落在 Controller 里

4. 分片怎么做,效果差别很大

分片,更贴近知识库语境的说法是:Chunk 切分

它要解决的是:原始文档太长,不能直接整篇拿去检索和拼 Prompt,那应该按什么粒度拆开。

4.1 常见切分方式对比

方式做法适合场景风险
固定长度切分按字符数或 token 数切快速起步、实现简单可能把一个完整语义硬切开
段落切分按标题、段落、列表切文档结构清晰的资料长段落仍可能过大
滑动窗口切分当前块和前后块保留重叠FAQ、说明文档、规则类文本冗余会增多,存储成本更高
语义切分尽量按完整主题或段意拆高质量知识库、复杂文档实现更复杂,需要更细致清洗

4.2 一个实战里更常见的经验

🌟 一开始通常不用追求很复杂的切分算法,把下面 3 件事做好,收益往往更高:

  1. 控制单个 Chunk 不要太大
  2. 让相邻片段保留少量重叠
  3. 尽量不要把标题和正文拆散

一个比较常见的起步思路是:

  1. Chunk 大小先从 300 ~ 600 tokens 附近试起
  2. 重叠部分先从 50 ~ 100 tokens 左右试起
  3. FAQ、制度、操作手册三类文档分开调参数

要注意的是:切分参数没有通用最优值,必须结合你自己的文档类型调。

5. 向量化不是“随便换个模型都一样”

Embedding 可以理解成“把文本变成向量表示”。

在知识库场景里,它解决的是:
怎么把语义相近的文本,在向量空间里尽量放得更近。

5.1 选 EmbeddingModel 时看什么

更实用的判断维度通常是:

  1. 中文效果怎么样
  2. 向量维度多大
  3. 成本和吞吐能不能接受
  4. 查询文本和文档文本是否适合同一模型
  5. 和当前向量数据库的兼容性是否顺手

5.2 一个向量化阶段该补的工程认知

  1. 入库文本和查询文本最好使用同一套向量模型
  2. 换向量模型后,旧索引通常不能直接复用
  3. 一旦向量维度变化,底层表结构或索引配置也可能要调整
  4. 不是维度越高效果就一定越好,业务语料更重要

6. 检索、召回、排序分别在做什么

很多人会把 检索召回排序 混在一起。

可以按下面这个顺序理解:

  1. 召回:先从大规模候选里粗筛一批“可能相关”的内容
  2. 排序:在这批候选里继续判断谁更相关
  3. 检索:通常是对这整条查找过程的统称

6.1 一个问题进入系统后常见会发生什么

  1. 先对用户问题做一次检索
  2. 用向量检索或混合检索召回 TopK
  3. 根据文档类型、来源、时间范围做过滤
  4. 对候选结果做去重和 Rerank
  5. 选出真正要送给模型的 TopN

6.2 一个更贴近项目的查询 Service

java
package com.example.ai.knowledge;

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

/**
 * 负责知识库问答主链路:召回、过滤、重排、生成。
 */
@Service
public class KnowledgeQaService {

    private final VectorStore vectorStore;
    private final ChatClient chatClient;
    private final KnowledgeRerankService knowledgeRerankService;

    public KnowledgeQaService(
            VectorStore vectorStore,
            ChatClient.Builder chatClientBuilder,
            KnowledgeRerankService knowledgeRerankService) {
        this.vectorStore = vectorStore;
        this.chatClient = chatClientBuilder.build();
        this.knowledgeRerankService = knowledgeRerankService;
    }

    /**
     * 基于知识库资料回答问题。
     *
     * @param question 用户问题
     * @return 最终答案
     */
    public String answer(String question) {
        SearchRequest request = SearchRequest.builder()
                .query(question)
                .topK(12)
                .similarityThreshold(0.75d)
                .build();

        List<Document> recalled = vectorStore.similaritySearch(request);
        List<Document> reranked = knowledgeRerankService.rerank(question, recalled);
        List<Document> topDocuments = reranked.stream().limit(4).toList();

        String context = topDocuments.stream()
                .map(doc -> "【%s】%s".formatted(doc.getMetadata().get("title"), doc.getText()))
                .collect(Collectors.joining("\n\n"));

        return chatClient.prompt()
                .system("你是企业知识库助手,必须优先基于资料回答;资料不足时要明确说明。")
                .user("""
                        用户问题:
                        %s

                        检索资料:
                        %s
                        """.formatted(question, context))
                .call()
                .content();
    }
}

6.3 一个简单的重排服务骨架

java
package com.example.ai.knowledge;

import java.util.Comparator;
import java.util.List;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;

/**
 * 负责把召回结果再排一次序。
 *
 * 这里用占位逻辑表达职责边界:
 * 真实项目里可以接独立 Rerank 模型、搜索引擎精排能力,或你自己的打分服务。
 */
@Service
public class KnowledgeRerankService {

    /**
     * 对召回结果按相关性重新排序。
     *
     * @param query 用户问题
     * @param recalled 召回候选
     * @return 精排后的结果
     */
    public List<Document> rerank(String query, List<Document> recalled) {
        return recalled.stream()
                .sorted(Comparator.comparingInt(doc -> estimateScore(query, doc)).reversed())
                .toList();
    }

    private int estimateScore(String query, Document document) {
        String text = document.getText();
        int score = 0;
        if (text.contains(query)) {
            score += 20;
        }
        if (Boolean.TRUE.equals(document.getMetadata().get("isFaq"))) {
            score += 5;
        }
        return score;
    }
}

这里故意没有把 Rerank 写成某个固定厂商 SDK,因为这段代码更想传达的是职责边界:

  1. 召回负责“先找一批”
  2. 重排负责“从这批里重新排顺”
  3. 最终送给模型的上下文,通常不应该直接等于最初召回结果

7. 为什么很多项目还要做混合检索

单纯向量检索有时会碰到这些问题:

  1. 产品名、版本号、报错码这类关键词不够敏感
  2. 用户问题很短时,语义信息本来就不丰富
  3. 某些业务强依赖精确词命中

所以更常见的做法是:

  1. 关键词检索召回一批
  2. 向量检索再召回一批
  3. 候选集合并后做 Rerank
  4. 最后再截取 TopN 送给模型

🌟 如果你的知识库里有很多 错误码接口名SQL 关键字产品版本号,混合检索通常会比纯向量检索更稳。

8. 向量数据库怎么选

VectorStore 是 Spring AI 里的抽象接口;
真正落地时,你还要选择底层向量数据库或检索引擎。

8.1 常见方案怎么判断

方案特点更适合什么场景真正要注意的点
PGVector基于 PostgreSQL,接入门槛低中小规模、业务数据本来就在 PG检索能力和扩展性不是无限上升
Milvus更偏专业向量检索向量规模较大、检索要求高运维成本更高,体系也更重
Elasticsearch / OpenSearch关键词检索和向量检索都能做本来就要做混合检索向量能力要结合版本和索引策略看
Redis接入快,适合低延迟场景已经大量使用 Redis,数据规模可控不要把它当成所有知识库场景的通用最优解

8.2 一个更务实的选型思路

如果只是做公司内部知识库的第一版,通常可以按下面顺序判断:

  1. 现有技术栈里有没有现成基础设施
  2. 召回规模大不大
  3. 是否强依赖混合检索
  4. 是否要做复杂过滤和权限边界
  5. 团队能不能接受新的运维成本

很多时候,第一版选型最重要的不是“功能最强”,而是“能否稳定接入、方便观测、便于后续演进”。

9. 在 Spring AI 项目里,哪些职责应该拆开

如果知识库能力准备长期维护,建议至少拆成下面几层:

  1. KnowledgeIngestionService:负责入库和更新索引
  2. KnowledgeRetrievalService:负责召回、过滤、混合检索
  3. KnowledgeRerankService:负责精排
  4. KnowledgeQaService:负责把上下文拼进 Prompt 并生成答案
  5. KnowledgeController:只暴露接口,不直接写检索逻辑

9.1 为什么不要把这些全塞进一个 Service

因为一旦混在一起,后面很快就会出现这些问题:

  1. 切分参数没地方调
  2. 检索质量不好时不知道该查哪一层
  3. 重排逻辑和生成逻辑互相缠住
  4. 很难做单测和效果对比

10. 一个更完整的知识库接口骨架

java
package com.example.ai.web;

import com.example.ai.knowledge.KnowledgeIngestionService;
import com.example.ai.knowledge.KnowledgeQaService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 知识库问答与入库接口示例。
 */
@RestController
public class KnowledgeController {

    private final KnowledgeIngestionService knowledgeIngestionService;
    private final KnowledgeQaService knowledgeQaService;

    public KnowledgeController(
            KnowledgeIngestionService knowledgeIngestionService,
            KnowledgeQaService knowledgeQaService) {
        this.knowledgeIngestionService = knowledgeIngestionService;
        this.knowledgeQaService = knowledgeQaService;
    }

    @PostMapping("/demo/ai/knowledge/ingest")
    public void ingest(@RequestBody KnowledgeDocumentRequest request) {
        knowledgeIngestionService.ingest(
                request.sourceId(),
                request.title(),
                request.content()
        );
    }

    @PostMapping("/demo/ai/knowledge/ask")
    public String ask(@RequestParam String q) {
        return knowledgeQaService.answer(q);
    }
}

这个接口骨架里最重要的不是代码量,而是职责切分:

  1. 入库和问答分开
  2. 知识库能力和普通聊天能力分开
  3. 元数据和过滤条件不要等到最后才补

11. 最容易踩的坑

11.1 以为“接上向量库”就等于知识库完成了

真正会影响效果的还有:

  1. 文档质量
  2. 切分策略
  3. 召回参数
  4. Rerank
  5. 上下文拼接方式

11.2 切分过细或过粗都不行

过细会让上下文碎片化;
过粗会让召回命中看起来对,但真正相关的信息被长文本稀释掉。

11.3 不做元数据设计

没有 sourceIdtitledocType 这类元数据,后面通常会遇到:

  1. 无法回溯答案来源
  2. 无法做定向过滤
  3. 不方便排查为什么召回结果不稳定

11.4 不做引用来源返回

如果回答里没有来源,真实业务里很难:

  1. 让用户信任结果
  2. 做效果排查
  3. 让运营或业务同学定位哪份资料该修

11.5 把知识库和 Agent 混成一层

RAG 负责把资料找回来,Agent 负责做多步任务执行。
两者可以协作,但最好不要在第一版里直接混成一个“超级智能服务”。

12. 推荐怎么读这几篇 Spring AI 笔记

如果你想按一个更自然的项目落地顺序来读,建议这样走:

  1. Spring Boot + Spring AI:最小接入、聊天接口与流式输出
  2. 再看 Spring Boot + Spring AI:Function Call、工具链调用与 MCP
  3. 再看这篇 Spring Boot + Spring AI:知识库、分片、向量化、召回、重排与向量数据库实战
  4. 最后看 Spring Boot + Spring AI:RAG、Agent 与稳健性治理

这样顺序会更顺:

  1. 把基础聊天主线跑通
  2. 再把外部工具和标准化对接方式补上
  3. 然后把知识库问答这条链路单独做扎实
  4. 最后再把 Agent 和治理能力串起来

13. 一段更稳的总结

Spring AI 里的知识库能力,不是“向量数据库 + Prompt 拼接”这么简单。真正能把效果做稳的,是一条完整链路:文档清洗、分片策略、向量化、召回、过滤、重排、上下文拼接,再加上合适的向量数据库选型和清楚的工程分层。只有这条链路顺了,后面的 RAGAgent 和业务问答才有稳定基础。

基于 VitePress 构建的个人技术笔记。