Appearance
Spring Boot + Spring AI:知识库、分片、向量化、召回、重排与向量数据库实战
很多人第一次做 Spring AI 知识库问答时,容易把事情理解成:
- 把文档塞进向量库
- 问问题时做一次相似度检索
- 把结果拼进 Prompt
这个流程不能说错,但离真实项目还差不少。
真正决定效果的,往往不是“有没有接上 VectorStore”,而是下面这些环节有没有串顺:
- 文档怎么清洗
- 文档怎么切分
- 用什么
EmbeddingModel做向量化 - 检索时怎么召回
- 候选结果要不要过滤、去重、重排
- 最后哪些片段真的送进模型
所以这篇不只讲一个最小 RAG demo,而是把 Spring Boot + Spring AI 项目里知识库落地最常见的一条主线讲清楚。
1. 把“知识库问答”拆成几层
在 Spring 项目里,知识库问答通常至少包含 4 层:
- 文档入库层:读取原始资料、清洗、分片、向量化、写入索引
- 检索召回层:根据用户问题取回候选片段
- 排序过滤层:做元数据过滤、去重、
Rerank精排 - 生成回答层:把最终上下文拼进 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[返回答案与引用来源]这条链路里最容易被低估的几个点是:
- 切分不是越细越好,也不是越大越好
- 召回出来的内容不等于就适合直接送给模型
- 没有元数据过滤时,知识库很容易把不该给当前用户的内容也一起查出来
Rerank往往直接影响最终答案质量
3. 文档入库不是“直接 add”
3.1 文档入库通常要先做什么
一份原始资料进入知识库前,通常至少要经过这些步骤:
- 去掉页眉页脚、导航噪音、空白段落
- 把文档切成更适合检索的
Chunk - 给每个片段补元数据
- 再做向量化并写入向量库
如果一上来就把整篇文档直接入库,后面常见的问题通常是:
- 召回粒度太粗,一个片段里混了太多无关信息
- 相似度看起来命中,但真正相关的内容埋在长文本中间
- 无法按文档类型、知识来源做过滤
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);
}
}这段代码真正想说明的是:
VectorStore.add(...)前面还有一段非常重要的预处理链路- 元数据不是附属品,它决定后面检索和权限边界能不能做
- 切分策略应该独立成一层,而不是散落在 Controller 里
4. 分片怎么做,效果差别很大
分片,更贴近知识库语境的说法是:Chunk 切分。
它要解决的是:原始文档太长,不能直接整篇拿去检索和拼 Prompt,那应该按什么粒度拆开。
4.1 常见切分方式对比
| 方式 | 做法 | 适合场景 | 风险 |
|---|---|---|---|
| 固定长度切分 | 按字符数或 token 数切 | 快速起步、实现简单 | 可能把一个完整语义硬切开 |
| 段落切分 | 按标题、段落、列表切 | 文档结构清晰的资料 | 长段落仍可能过大 |
| 滑动窗口切分 | 当前块和前后块保留重叠 | FAQ、说明文档、规则类文本 | 冗余会增多,存储成本更高 |
| 语义切分 | 尽量按完整主题或段意拆 | 高质量知识库、复杂文档 | 实现更复杂,需要更细致清洗 |
4.2 一个实战里更常见的经验
🌟 一开始通常不用追求很复杂的切分算法,把下面 3 件事做好,收益往往更高:
- 控制单个
Chunk不要太大 - 让相邻片段保留少量重叠
- 尽量不要把标题和正文拆散
一个比较常见的起步思路是:
Chunk大小先从300 ~ 600 tokens附近试起- 重叠部分先从
50 ~ 100 tokens左右试起 - FAQ、制度、操作手册三类文档分开调参数
要注意的是:切分参数没有通用最优值,必须结合你自己的文档类型调。
5. 向量化不是“随便换个模型都一样”
Embedding 可以理解成“把文本变成向量表示”。
在知识库场景里,它解决的是:怎么把语义相近的文本,在向量空间里尽量放得更近。
5.1 选 EmbeddingModel 时看什么
更实用的判断维度通常是:
- 中文效果怎么样
- 向量维度多大
- 成本和吞吐能不能接受
- 查询文本和文档文本是否适合同一模型
- 和当前向量数据库的兼容性是否顺手
5.2 一个向量化阶段该补的工程认知
- 入库文本和查询文本最好使用同一套向量模型
- 换向量模型后,旧索引通常不能直接复用
- 一旦向量维度变化,底层表结构或索引配置也可能要调整
- 不是维度越高效果就一定越好,业务语料更重要
6. 检索、召回、排序分别在做什么
很多人会把 检索、召回、排序 混在一起。
可以按下面这个顺序理解:
召回:先从大规模候选里粗筛一批“可能相关”的内容排序:在这批候选里继续判断谁更相关检索:通常是对这整条查找过程的统称
6.1 一个问题进入系统后常见会发生什么
- 先对用户问题做一次检索
- 用向量检索或混合检索召回 TopK
- 根据文档类型、来源、时间范围做过滤
- 对候选结果做去重和
Rerank - 选出真正要送给模型的 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,因为这段代码更想传达的是职责边界:
- 召回负责“先找一批”
- 重排负责“从这批里重新排顺”
- 最终送给模型的上下文,通常不应该直接等于最初召回结果
7. 为什么很多项目还要做混合检索
单纯向量检索有时会碰到这些问题:
- 产品名、版本号、报错码这类关键词不够敏感
- 用户问题很短时,语义信息本来就不丰富
- 某些业务强依赖精确词命中
所以更常见的做法是:
- 关键词检索召回一批
- 向量检索再召回一批
- 候选集合并后做
Rerank - 最后再截取 TopN 送给模型
🌟 如果你的知识库里有很多 错误码、接口名、SQL 关键字、产品版本号,混合检索通常会比纯向量检索更稳。
8. 向量数据库怎么选
VectorStore 是 Spring AI 里的抽象接口;
真正落地时,你还要选择底层向量数据库或检索引擎。
8.1 常见方案怎么判断
| 方案 | 特点 | 更适合什么场景 | 真正要注意的点 |
|---|---|---|---|
PGVector | 基于 PostgreSQL,接入门槛低 | 中小规模、业务数据本来就在 PG | 检索能力和扩展性不是无限上升 |
Milvus | 更偏专业向量检索 | 向量规模较大、检索要求高 | 运维成本更高,体系也更重 |
Elasticsearch / OpenSearch | 关键词检索和向量检索都能做 | 本来就要做混合检索 | 向量能力要结合版本和索引策略看 |
Redis | 接入快,适合低延迟场景 | 已经大量使用 Redis,数据规模可控 | 不要把它当成所有知识库场景的通用最优解 |
8.2 一个更务实的选型思路
如果只是做公司内部知识库的第一版,通常可以按下面顺序判断:
- 现有技术栈里有没有现成基础设施
- 召回规模大不大
- 是否强依赖混合检索
- 是否要做复杂过滤和权限边界
- 团队能不能接受新的运维成本
很多时候,第一版选型最重要的不是“功能最强”,而是“能否稳定接入、方便观测、便于后续演进”。
9. 在 Spring AI 项目里,哪些职责应该拆开
如果知识库能力准备长期维护,建议至少拆成下面几层:
KnowledgeIngestionService:负责入库和更新索引KnowledgeRetrievalService:负责召回、过滤、混合检索KnowledgeRerankService:负责精排KnowledgeQaService:负责把上下文拼进 Prompt 并生成答案KnowledgeController:只暴露接口,不直接写检索逻辑
9.1 为什么不要把这些全塞进一个 Service
因为一旦混在一起,后面很快就会出现这些问题:
- 切分参数没地方调
- 检索质量不好时不知道该查哪一层
- 重排逻辑和生成逻辑互相缠住
- 很难做单测和效果对比
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);
}
}这个接口骨架里最重要的不是代码量,而是职责切分:
- 入库和问答分开
- 知识库能力和普通聊天能力分开
- 元数据和过滤条件不要等到最后才补
11. 最容易踩的坑
11.1 以为“接上向量库”就等于知识库完成了
真正会影响效果的还有:
- 文档质量
- 切分策略
- 召回参数
Rerank- 上下文拼接方式
11.2 切分过细或过粗都不行
过细会让上下文碎片化;
过粗会让召回命中看起来对,但真正相关的信息被长文本稀释掉。
11.3 不做元数据设计
没有 sourceId、title、docType 这类元数据,后面通常会遇到:
- 无法回溯答案来源
- 无法做定向过滤
- 不方便排查为什么召回结果不稳定
11.4 不做引用来源返回
如果回答里没有来源,真实业务里很难:
- 让用户信任结果
- 做效果排查
- 让运营或业务同学定位哪份资料该修
11.5 把知识库和 Agent 混成一层
RAG 负责把资料找回来,Agent 负责做多步任务执行。
两者可以协作,但最好不要在第一版里直接混成一个“超级智能服务”。
12. 推荐怎么读这几篇 Spring AI 笔记
如果你想按一个更自然的项目落地顺序来读,建议这样走:
- 看 Spring Boot + Spring AI:最小接入、聊天接口与流式输出
- 再看 Spring Boot + Spring AI:Function Call、工具链调用与 MCP
- 再看这篇 Spring Boot + Spring AI:知识库、分片、向量化、召回、重排与向量数据库实战
- 最后看 Spring Boot + Spring AI:RAG、Agent 与稳健性治理
这样顺序会更顺:
- 把基础聊天主线跑通
- 再把外部工具和标准化对接方式补上
- 然后把知识库问答这条链路单独做扎实
- 最后再把
Agent和治理能力串起来
13. 一段更稳的总结
Spring AI 里的知识库能力,不是“向量数据库 + Prompt 拼接”这么简单。真正能把效果做稳的,是一条完整链路:文档清洗、分片策略、向量化、召回、过滤、重排、上下文拼接,再加上合适的向量数据库选型和清楚的工程分层。只有这条链路顺了,后面的 RAG、Agent 和业务问答才有稳定基础。