这篇笔记的目标,不是继续解释
RAG的概念,而是把问题直接落到“怎么起一个最小可行项目”上:如果现在要做一个能接文档、能入库、能检索、能回答、还能继续迭代的 RAG 服务,第一版到底应该长什么样。
这篇内容会有明确的技术偏向:优先给出 Java Spring Boot / Spring AI 的落地方案,向量库侧默认从
pgvector起步,因为它对很多已有 Java 后端团队来说心智成本最低。内容重点放在 MVP 方案、工程拆解和升级路线,不把重点放在复杂的 agent 编排上。
参考资料:
官方文档:Spring AI - Retrieval Augmented Generation 、 Spring AI - Vector Databases 、 Spring AI - PGvector 、 Spring AI - ETL Pipeline
官方仓库:pgvector/pgvector
实践参考:Microsoft Learn - Develop a RAG application with Spring AI and Azure OpenAI 、 Datawhale - All-in-RAG 第一节 RAG 简介
[TOC]
一、先定边界:什么叫“最小 RAG 项目”
这里说的“最小”,不是指只完成一次性演示,而是指:
- 已经有基本的数据接入能力
- 已经有向量化和检索能力
- 已经能通过接口问答
- 已经能定位问题出在入库、检索还是生成
- 但还没有把系统做复杂
因此,一个最小可行的 RAG 项目至少应具备下面 7 个能力:
- 能导入一批文档
- 能把文档切块并写入向量库
- 能按问题做相似度检索
- 能把检索结果拼到 prompt 里生成答案
- 能返回来源信息
- 能按元数据过滤
- 能单独验证入库和检索是否正常
如果缺少第 5 到第 7 项,这个系统通常还停留在可演示阶段,很难视为可落地方案。
二、第一版为什么建议走 Spring Boot + Spring AI + pgvector
对于 Java 团队,第一版更适合从下面这条技术线起步:
1
2
3
4
5
Spring Boot
+ Spring AI
+ PostgreSQL / pgvector
+ 一个 Embedding 模型
+ 一个 Chat 模型
选择这条技术线,并不是因为它在所有场景下都最优,而是因为:
- Java 团队已有 Spring Boot 经验
- Spring AI 已经把模型调用、向量存储、RAG Advisor 这层胶水补上了
pgvector可以复用现有 PostgreSQL 体系- 第一版不需要额外引入专用向量库运维成本
- 后续要升级到
Qdrant、Milvus也不是推倒重来
更务实的启动姿势可以概括为:
先用 Spring AI 把 RAG 主链跑通,向量存储先用 pgvector 验证价值。
三、第一版架构保持最小链路即可
一个最小 RAG 项目,建议先拆成两条链路:
graph LR
A[文档来源] --> B[文本提取]
B --> C[切块]
C --> D[Embedding]
D --> E[pgvector]
Q[用户问题] --> R[Query Embedding]
R --> E
E --> S[Top K 检索]
S --> T[Prompt 组装]
T --> U[LLM 生成]
U --> V[答案与来源]
这张图对应的就是第一版最小链路:离线侧负责把文档变成可检索的向量数据,在线侧负责把用户问题转成检索请求,再把召回结果拼进 prompt 生成答案。
离线链路:文档入库
1
2
3
4
5
文档来源
-> 文本提取
-> 切块
-> Embedding
-> 写入 pgvector
在线链路:检索问答
1
2
3
4
5
6
7
用户问题
-> Query Embedding
-> pgvector 相似度检索
-> 取回 top k 文档片段
-> 拼进 Prompt
-> LLM 生成答案
-> 返回答案和来源
这就是最常见的 Naive RAG 形态,对于项目第一版通常已经足够。
第一版通常不必优先引入:
- 多路召回
- rerank
- query rewrite
- Agentic Workflow
- GraphRAG
如果最小链路本身还没有稳定,这些升级件只会进一步增加排障难度。
四、项目目录的拆分原则:先把“入库”和“问答”分开
一个比较务实的 Spring Boot 项目结构,可以先长这样:
1
2
3
4
5
6
7
8
9
10
src/main/java
/config
/controller
/service
/rag
/ingest
/retrieve
/prompt
/repository
/model
这里最关键的是职责不要混:
ingest负责读文档、切块、入库retrieve负责搜索和过滤prompt负责组装上下文和回答约束controller只负责暴露接口
如果一开始就把这些职责全部集中在一个 Service 里,后面很难判断到底是哪一层出了问题。
五、最小数据模型怎么设计:不要只存 embedding
第一版最容易犯的错,就是只想着“把向量存进去”。
但真正可用的文档片段,至少应该带下面这些信息:
iddocumentIdchunkIdcontentmetadataembedding
其中 metadata 建议至少包含:
- 文档名
- 来源路径或 URL
- 标题
- 页码
- 文档类型
- 租户 / 业务域
- 更新时间
为什么元数据重要?
因为后面常见需求通常会落到这些方向:
- 只查某个知识库
- 只查某类文档
- 只看某个租户
- 回答里要标出处
- 某份文档更新后需要删除旧版本
如果一开始不留元数据,后面几乎一定要返工。
六、Spring AI 这条线最核心的几个对象
如果准备用 Spring AI 做第一版 RAG,建议先把下面几个对象关系看清楚。
ChatClient
它负责和聊天模型交互,是最终发起 prompt 调用的入口。
EmbeddingModel
它负责把文本转成向量。
无论是文档切块,还是用户 query,向量都要由它生成。
VectorStore
它负责把文档和向量存进去,并支持相似度检索。
在 pgvector 场景里,Spring AI 已经提供了 PgVectorStore 对应支持。
QuestionAnswerAdvisor
这是 Spring AI 官方提供的最直接 RAG 入口。
它会在用户提问时:
- 先去向量库查相关文档
- 把检索结果拼到用户问题里
- 再交给模型生成答案
如果只是第一版最小项目,QuestionAnswerAdvisor 已经足够。
RetrievalAugmentationAdvisor
这是比 QuestionAnswerAdvisor 更模块化的一层。
这一层允许把流程拆成:
- pre-retrieval
- retrieval
- post-retrieval
- generation
如果只是最小项目,先不用它也没问题;但如果后面要加改写、重排、空上下文策略,它会更好扩展。
七、依赖如何配置:先用官方 starter,不要自己拼太多胶水
如果按 Spring AI 官方思路走,第一版通常至少需要这几类依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
如果后面想从简单问答升级到模块化 RAG,再补:
1
2
3
4
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
这里有个现实建议:
- 第一版不宜同时兼容太多模型厂商
- 先固定一个 embedding 模型
- 再固定一个 chat 模型
- 先把链路稳定性打出来
八、数据库如何起步:pgvector 是第一版很合适的起点
为什么第一版更推荐 pgvector
它更适合回答一个很常见的问题:
已有 Java + Spring Boot + PostgreSQL 团队,是否可以先不引入独立向量库?
在很多 MVP 场景下,答案是可以。
它的优势很直接:
- 部署和运维成本低
- 可复用现有 Postgres 账号、权限、备份
- 业务表和向量表在同一数据库体系内
- 元数据过滤和业务字段联动更自然
当然,它也不是没边界:
- 超大规模检索不一定是最优
- 专用向量库在分布式、复杂检索上可能更强
但这些问题对第一版 MVP 往往不是核心矛盾。
本地起库可以先用 Docker
比如:
1
2
3
4
5
6
7
8
9
services:
postgres:
image: pgvector/pgvector:pg16
ports:
- "5432:5432"
environment:
POSTGRES_DB: rag
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
手工建表大致示意
Spring AI 文档里给出了 pgvector 典型 schema,大意如下:
1
2
3
4
5
6
7
8
9
10
11
12
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS vector_store (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
content text,
metadata json,
embedding vector(1536)
);
CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);
如果使用 Spring AI 的自动 schema 初始化,也可以让框架完成这部分工作,但要注意:
- 新版本需要显式开启
initialize-schema - embedding 维度要和所选模型一致
除此之外,还要注意 pgvector 的两个工程边界:
- 一旦使用
HNSW或IVFFlat这类近似索引,检索结果就是速度与召回率的权衡,不再等同于精确最近邻 - 当近似索引与强元数据过滤叠加时,过滤通常发生在索引扫描之后,
top k结果可能不足,尤其在多租户共享同一索引时更明显
九、配置如何编写:先保证“能接模型、能连数据库、能自动建表”
一个最小可行配置,大致可以写成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
datasource:
url: jdbc:postgresql://localhost:5432/rag
username: postgres
password: postgres
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
embedding:
options:
model: text-embedding-3-small
vectorstore:
pgvector:
initialize-schema: true
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
如果使用的是 Azure OpenAI 或其他兼容实现,主要替换的是模型配置,不影响整体思路。
这里还需要额外注意两件事:
dimensions可以显式声明,也可以由EmbeddingModel推导;如果后续切换到不同维度的 embedding 模型,通常需要重建vector_store表并重新向量化历史数据- 批量导入量较大时,可以结合
max-document-batch-size控制单批写入规模,避免一次性写入过大批次
十、离线入库如何实现:最小实现先把文档读、切、存跑通
文档来源先收敛
第一版强烈建议先只支持 1 到 2 种输入格式,比如:
- Markdown
不建议在第一阶段同时支持:
- Word
- 网页抓取
- 图片 OCR
- Excel
- 飞书 / Confluence / 钉钉
因为 RAG 的第一版痛点往往不在“格式不够多”,而在“已有格式能不能稳定进来”。
切块策略先保持稳定
第一版可以先采用:
- 按段落 / 标题切
- 配合固定 token 上限
- 保留少量 overlap
最重要的是:
- 不要把整页一次性塞成大 chunk
- 不要切得太碎导致上下文断掉
切块参数一旦确定,应尽量保持稳定。否则同一批测试问题在不同切块方案下会得到完全不同的召回结果,后续评估很难判断问题到底来自切块、检索还是生成。
Spring AI 的 ETL Pipeline 可以作为默认入库骨架
如果希望尽量贴近 Spring AI 官方提供的抽象,离线入库可以先按下面这条链路理解:
1
2
3
DocumentReader
-> DocumentTransformer(如 TokenTextSplitter)
-> DocumentWriter(如 VectorStore)
这条链路的价值在于职责非常明确:
DocumentReader负责把外部文件转成DocumentDocumentTransformer负责切块、格式整理和内容变换DocumentWriter负责把处理后的文档写入目标存储
在 Spring AI 的官方 ETL Pipeline 中,MarkdownDocumentReader、PDF Reader、TokenTextSplitter 与 VectorStore 这些对象已经形成了比较自然的最小组合。第一版即使不把这条链路完全框架化,也建议按这个职责模型拆实现。
入库服务示意
下面这个示意代码,不追求 API 100% 完整,而是把职责边界摆清楚:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class RagIngestService {
private final VectorStore vectorStore;
public RagIngestService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void ingest(List<RagChunk> chunks) {
List<Document> documents = chunks.stream()
.map(chunk -> new Document(
chunk.content(),
Map.of(
"documentId", chunk.documentId(),
"title", chunk.title(),
"page", String.valueOf(chunk.page()),
"source", chunk.source()
)))
.toList();
vectorStore.add(documents);
}
}
这里的重点不是 vectorStore.add() 这一行本身,而是入库前已经把:
- chunk 内容
- 文档身份
- 来源信息
- 位置元数据
都准备好了。
入库接口建议单独做
比如至少提供一个:
POST /api/rag/ingest
或者一个后台任务入口:
POST /api/rag/rebuild
原因很简单:
- 入库是批处理动作
- 问答是在线请求
- 两者生命周期和故障处理方式完全不同
不建议把它们合并成同一个接口。
文档更新、删除与重建策略要提前确定
第一版即使不做完整的文档生命周期系统,也至少要先回答下面几个问题:
- 同一份文档更新后,是先按
documentId删除旧 chunk,再重建,还是通过版本号过滤只保留当前版本 - 误导入、重复导入或脏数据写入后,是否有单文档级别的回滚与重建入口
- embedding 模型切换后,旧向量是否继续保留,还是整库重建
如果这些问题没有提前约束,系统很容易出现“新旧版本混检”“历史脏数据长期残留”“换模型后召回结果失真”这类问题。
十一、在线问答如何实现:Spring AI 最小方案可以直接使用 QuestionAnswerAdvisor
这是 Spring AI 官方给出的最直接 RAG 方式,也是第一版最容易落地的方式。
最小问答链路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class RagChatService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagChatService(ChatModel chatModel, VectorStore vectorStore) {
this.chatClient = ChatClient.builder(chatModel).build();
this.vectorStore = vectorStore;
}
public String ask(String question) {
var advisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.75d)
.build())
.build();
return chatClient.prompt()
.advisors(advisor)
.user(question)
.call()
.content();
}
}
这段代码背后的链路是:
- 用户输入问题
QuestionAnswerAdvisor去向量库做相似度搜索- 把召回内容拼到 prompt
- 模型基于上下文生成回答
这已经是一条完整的 RAG 问答主链。
如果需要按知识库过滤
Spring AI 支持在 SearchRequest 里加过滤表达式。
例如:
1
2
3
4
5
6
7
var advisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.75d)
.filterExpression("kb == 'employee-handbook'")
.build())
.build();
这样就可以按:
- 知识库
- 文档类型
- 租户
- 业务域
做基础隔离。
如果需要运行时动态过滤
Spring AI 也支持在请求时动态传 filter。
这在多知识库、多租户场景里非常实用,因为不需要为每个知识库都创建一套固定 client。
例如,可以把静态 QuestionAnswerAdvisor 和运行时过滤参数组合起来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.75d)
.build())
.build();
return chatClient.prompt()
.advisors(qaAdvisor)
.advisors(a -> a.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION,
"kb == 'employee-handbook'"))
.user(question)
.call()
.content();
十二、如果希望更像一个接口服务,建议至少暴露这 3 类 API
文档入库
1
POST /api/rag/documents
负责:
- 上传文档
- 解析
- 切块
- 入库
相似度检索调试
1
GET /api/rag/search?q=...
负责:
- 只返回检索结果
- 不调用 LLM
这是线上排问题非常重要的接口。
因为当线上反馈“答得不对”时,必须先知道:
- 是没召回到
- 还是召回到了但没用好
问答接口
1
POST /api/rag/ask
输入:
- 用户问题
- 可选知识库
- 可选过滤参数
输出:
- 回答文本
- 命中的来源
- 可选调试信息
十三、Prompt 如何约束:第一版一定要强约束
最小 RAG 项目里,prompt 不求花哨,但一定要清楚。
这类 prompt 至少要表达 4 件事:
- 只能基于检索到的内容回答
- 证据不足时明确说不知道
- 尽量引用来源
- 不要把外部常识直接混入
例如:
1
2
3
4
你是一个知识库问答助手。
请严格基于提供的上下文回答问题。
如果上下文不足以支持回答,请直接说明“不知道”或“资料不足”。
回答尽量简洁,并在最后列出使用到的来源标题。
如果没有这层约束,很容易出现:
- 检索证据明显不足时,模型仍然继续生成看似完整的回答
- 资料和常识冲突时,模型自行引入上下文之外的补充信息
十四、第一版最值得做的,不是优化回答,而是先把可观测性补上
真正落地时,最有价值的往往不是“再换个大模型”,而是能不能看到这几个东西:
- 本次 query 是什么
- top k 召回了哪些 chunk
- 每个 chunk 的分数是多少
- 最终拼给模型的上下文是什么
- 回答引用了哪些来源
- 使用的 embedding 模型和 chat 模型版本是什么
- 检索过滤条件与链路总耗时是什么
如果没有这些信息,后面一旦出现“答错了”的反馈,排查基本只能靠猜。
所以第一版就建议加上最基础的日志和调试开关。
十五、如何判断第一版是不是合格:先看这 5 个问题
文档是否真的入库成功
不要只看接口 200。
至少确认:
- 向量表里是否真的有记录
- metadata 是否正确
- chunk 数量是否符合预期
检索接口是否能召回对的片段
这一步必须脱离 LLM 单独验证。
回答是否真的使用了检索结果
不要只看“像不像对”,而要看:
- 回答能否被检索片段支撑
资料不足时是否会拒答
如果资料不足仍持续生成无依据内容,第一版就不能算合格。
来源是否能回显
哪怕第一版只返回:
- 文档标题
- 来源地址
都比完全不给出处强很多。
十六、Java Spring Boot / Spring AI 的推荐落地路线如何选择
如果把“现在真的要做一个 Java 版 RAG 服务,最推荐哪条路”落成具体方案,可以先从下面这个组合开始。
先把三个阶段放到一张表里看,会更容易判断当前项目所处的位置:
| 阶段 | 核心组合 | 主要目标 | 关键能力 | 适用场景 |
|---|---|---|---|---|
| 第一阶段:最小可行方案 | Spring Boot + Spring AI + pgvector + QuestionAnswerAdvisor |
先把 RAG 主链跑通 | 文档入库、向量检索、基础问答、来源返回 | 快速验证知识库问答价值,团队主体是 Java,且已有 PostgreSQL |
| 第二阶段:增强方案 | Spring Boot + Spring AI + pgvector/专用向量库 + RetrievalAugmentationAdvisor |
提升检索可控性和扩展性 | 元数据过滤、检索调试接口、模块化 retrieval、为 query rewrite / rerank 预留扩展点 | 需要多知识库、多租户和更细粒度检索策略 |
| 第三阶段:生产增强 | 在前两阶段基础上补齐工程能力 | 把可用系统推进到可持续运营 | 异步入库、版本控制、删除重建索引、多路召回、缓存、评估集、链路观测 | 已进入生产迭代,需要稳定性、可观测性和持续优化能力 |
第一阶段:最小可行方案
1
2
3
4
5
Spring Boot
+ Spring AI
+ OpenAI / Azure OpenAI 兼容模型
+ pgvector
+ QuestionAnswerAdvisor
适用场景:
- 想快速验证知识库问答价值
- 团队主体是 Java
- 已有 PostgreSQL
- 文档量仍处在中小规模
第二阶段:增强方案
1
2
3
4
5
6
Spring Boot
+ Spring AI
+ pgvector 或专用向量库
+ RetrievalAugmentationAdvisor
+ 元数据过滤
+ 检索调试接口
适用场景:
- 需要更精细的检索控制
- 需要模块化扩展 query rewrite / rerank
- 开始进入多知识库、多租户、多场景
第三阶段:生产增强
再逐步补:
- 异步入库
- 文档版本控制
- 删除 / 重建索引
- 多路召回
- rerank
- 缓存
- 评估集
- 观测与链路追踪
这才是比较现实的演进顺序。
十七、什么时候该从 QuestionAnswerAdvisor 升级到模块化 RAG
如果已经遇到下面这些问题,就说明该升级了:
- 不同问题需要不同检索策略
- 只靠 top k 效果波动很大
- 需要加 query rewrite
- 需要空上下文兜底策略
- 需要后处理或重排
这时就可以考虑把简单模式:
QuestionAnswerAdvisor
升级成更可编排的:
RetrievalAugmentationAdvisor
但顺序一定要对:
- 先把最小链路稳定下来
- 再增加模块化能力
不要反过来。
十八、第一版最常见的坑,往往都很朴素
文档没切好
导致定义、结论、限制条件被拆开,最后召回内容不完整。
只返回答案,不返回来源
一旦答错,几乎没法排查。
把检索和问答绑死
导致无法单独验证检索质量。
元数据设计太弱
后面想做知识库隔离和过滤时直接卡住。
直接上专用向量库,结果项目复杂度先爆炸
第一版最怕的不是技术不够高级,而是链路太长,问题定位不了。
embedding 模型切换后仍沿用旧向量
如果更换了 embedding 模型,尤其是维度、分布或语义空间发生变化之后,继续沿用旧向量数据通常会让召回结果出现明显漂移。此时需要关注的不是“配置是否已经改完”,而是历史数据是否已经完成重新向量化。
近似索引叠加强过滤,导致 top k 不足
在 pgvector 的近似索引模式下,元数据过滤通常不会在索引扫描前完全生效。过滤条件越强,越容易出现召回数不够、结果波动增大或不同租户之间互相影响召回效果的问题。
十九、如果从 0 开一个 Java RAG 小项目,可以按这个顺序做
- 起一个 Spring Boot 工程
- 接入 Spring AI 和一个稳定模型
- 本地拉起 PostgreSQL + pgvector
- 打通
VectorStore.add()入库 - 先确定
documentId、版本号与删除 / 重建策略 - 单独做一个
/search接口验证检索 - 再接
QuestionAnswerAdvisor做/ask - 补来源返回和基础日志
- 准备 20 到 50 条真实问答样本做人工验证
如果这 9 步都稳定了,再谈:
- 重排
- 混合检索
- 复杂编排
- 多模型切换
二十、最后总结:最小项目真正要证明的,不是“会调 AI API”
全文可以概括为:
最小 RAG 项目要证明的,不是“模型能回答”,而是“文档能被稳定入库、稳定召回、稳定约束生成”。
而在 Java Spring Boot 生态里,更务实的起步方案可以概括为:
Spring Boot + Spring AI + pgvector + QuestionAnswerAdvisor。
这条路线的价值不在于一步到位,而在于它特别适合第一版:
- 容易启动
- 容易理解
- 容易排查
- 容易往上升级
顺着这条线继续展开,比较值得单独深入的两个专题是:
Spring AI里的QuestionAnswerAdvisor/RetrievalAugmentationAdvisor执行链与扩展点- Java RAG 项目里的文档入库、切块、版本管理和评估体系