1. 本期目标
上一篇文章分析了 ai_agent 项目的默认 RAG 流程。默认流程主要是:
本地 Markdown 文档
↓
Document 加载与元数据增强
↓
SimpleVectorStore 本地向量库
↓
QueryRewriter 查询重写
↓
QuestionAnswerAdvisor 检索增强回答
这一期继续往下看 RAG 的高级扩展部分。项目的 rag 目录下除了基础的文档加载、关键词增强和本地向量库配置外,还包含 LoveAppRagCustomAdvisorFactory、LoveAppContextualQueryAugmenterFactory、PgVectorVectorStoreConfig 和 LoveAppRagCloudAdvisorConfig 等扩展类。也就是说,这个项目并不只停留在本地 SimpleVectorStore Demo,而是预留了过滤检索、PgVector 持久化向量库和云知识库三类扩展方向。(GitHub)
本期主要解决几个问题:
1. 为什么默认 QuestionAnswerAdvisor 还不够?
2. 什么是按 status 过滤检索?
3. LoveAppRagCustomAdvisorFactory 做了什么?
4. similarityThreshold 和 topK 分别控制什么?
5. LoveAppContextualQueryAugmenterFactory 如何处理空上下文?
6. PgVectorVectorStoreConfig 如何连接 PostgreSQL + PgVector?
7. PgVector 相比 SimpleVectorStore 有什么优势?
8. LoveAppRagCloudAdvisorConfig 如何接入云知识库?
9. 这三类 RAG 扩展分别适合什么场景?
2. 为什么需要 RAG 高级扩展?
默认 RAG 流程已经可以工作,但它更适合作为入门版本。
默认版本的问题主要有三个:
第一,默认 QuestionAnswerAdvisor 会在整个向量库中检索,容易召回不符合用户状态的文档。
第二,SimpleVectorStore 更适合本地学习和 Demo,不适合大规模持久化知识库。
第三,当知识库没有检索到相关内容时,需要更明确的兜底策略,避免模型自由发挥。
在恋爱咨询场景中,用户状态非常重要。单身用户、恋爱中用户、已婚用户面对的问题不同,回答策略也不同。如果用户处于"单身"状态,系统最好优先检索"单身篇"文档,而不是把"已婚篇"或"恋爱篇"的内容也混进来。
所以,RAG 高级扩展的目标可以概括为:
让检索更准,
让知识库更可扩展,
让无相关上下文时的回答更可控。
3. LoveApp 中预留的多种 RAG 方案
在 LoveApp 的 doChatWithRag() 方法中,当前默认启用的是:
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))
但是源码中还保留了几个注释掉的 RAG 方案:
// .advisors(loveAppRagCloudAdvisor)
// .advisors(new QuestionAnswerAdvisor(pgVectorVectorStore))
// .advisors(
// LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(
// loveAppVectorStore, "单身"
// )
// )
这说明项目已经预留了三条扩展路线:云知识库 RAG、PgVector 向量库存储 RAG,以及自定义 RAG 检索增强服务。(GitHub)
可以把它们理解为:
默认 RAG:
QuestionAnswerAdvisor + SimpleVectorStore
自定义 RAG:
RetrievalAugmentationAdvisor + VectorStoreDocumentRetriever + status 过滤
PgVector RAG:
QuestionAnswerAdvisor + PgVectorVectorStore
云知识库 RAG:
RetrievalAugmentationAdvisor + DashScopeDocumentRetriever
这几种方式不是互斥关系,而是面向不同阶段的选择。
4. 默认 QuestionAnswerAdvisor 的局限
Spring AI 官方文档说明,QuestionAnswerAdvisor 会根据用户问题查询向量数据库,并把检索到的文档内容附加到用户文本中,为模型生成回答提供上下文。它也支持通过搜索请求设置相似度阈值、返回数量和过滤表达式。(Home)
默认写法通常是:
.advisors(new QuestionAnswerAdvisor(vectorStore))
这种写法简单,但控制能力比较弱。
在当前项目中,如果直接使用:
new QuestionAnswerAdvisor(loveAppVectorStore)
它会在 loveAppVectorStore 的全部文档中做相似度检索。对于小型 Demo 来说可以接受,但对于垂直场景来说,还可以继续细化。
例如,用户的问题是:
"我现在单身,不知道怎么认识合适的人。"
理想检索范围应该是:
status = 单身
而不是:
单身篇 + 恋爱篇 + 已婚篇
所以项目提供了自定义 RAG 工厂,用于支持更精细的检索控制。
5. 自定义 RAG:LoveAppRagCustomAdvisorFactory
自定义 RAG 的核心类是:
LoveAppRagCustomAdvisorFactory
它提供了一个静态方法:
public static Advisor createLoveAppRagCustomAdvisor(VectorStore vectorStore, String status)
这个方法接收两个参数:
vectorStore:
要检索的向量库。
status:
用户状态,例如"单身""恋爱""已婚"。
源码中,该方法先创建一个过滤表达式:
Filter.Expression expression = new FilterExpressionBuilder()
.eq("status", status)
.build();
然后构建 VectorStoreDocumentRetriever,设置 vectorStore、filterExpression、similarityThreshold(0.5) 和 topK(3),最后用 RetrievalAugmentationAdvisor.builder() 创建 Advisor。(GitHub)
整体流程可以写成:
传入 status
↓
构造 metadata 过滤条件
↓
只检索 status 匹配的文档
↓
设置相似度阈值和返回数量
↓
构建 RetrievalAugmentationAdvisor
6. status 过滤检索的意义
前面第六期讲过,项目在加载 Markdown 文档时已经从文件名中提取了 status 元数据,例如:
单身
恋爱
已婚
现在 LoveAppRagCustomAdvisorFactory 正好利用了这个元数据。
例如:
LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(
loveAppVectorStore,
"单身"
)
这表示检索时只召回:
metadata.status == "单身"
的文档。
这样做的好处是很明显的:
第一,减少不相关文档干扰。
第二,让回答更符合用户当前状态。
第三,让知识库分类信息真正参与检索。
第四,提升垂直场景 Agent 的可控性。
这也是从"能检索"走向"有条件地检索"的关键一步。
7. VectorStoreDocumentRetriever 是什么?
默认 QuestionAnswerAdvisor 是一个比较方便的封装。
而 VectorStoreDocumentRetriever 更像是检索阶段的具体执行器。Spring AI 文档说明,RetrievalAugmentationAdvisor 可以通过 VectorStoreDocumentRetriever 构建常见 RAG 流程,并且 VectorStoreDocumentRetriever 支持用 FilterExpression 根据元数据过滤检索结果。(Home)
在项目中,它的配置是:
DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.filterExpression(expression)
.similarityThreshold(0.5)
.topK(3)
.build();
可以逐项理解:
vectorStore:
指定在哪个向量库里检索。
filterExpression:
指定元数据过滤条件。
similarityThreshold:
指定相似度阈值。
topK:
指定最多返回几个相关文档。
这比默认 QuestionAnswerAdvisor 更清楚地暴露了检索控制参数。
8. similarityThreshold 的作用
similarityThreshold(0.5) 表示相似度阈值。
可以理解为:
只有相似度达到一定程度的文档,才会被认为是相关文档。
阈值太低,可能召回很多不相关内容。
阈值太高,可能召回不到文档。
例如:
similarityThreshold = 0.2:
召回更宽松,可能噪声更多。
similarityThreshold = 0.8:
召回更严格,可能漏掉一些有用文档。
当前项目设置为:
.similarityThreshold(0.5)
这是一个相对折中的设置,适合先跑通功能,再根据实际检索效果调参。(GitHub)
9. topK 的作用
topK(3) 表示最多返回 3 个相关文档。
可以理解为:
从满足条件的文档中,选出最相关的前 3 个。
topK 太小,可能上下文信息不够。
topK 太大,可能把过多文档塞进 prompt,增加 token 成本,也可能引入干扰信息。
当前项目设置为:
.topK(3)
这说明项目希望每次 RAG 只提供少量高相关文档,而不是把大量知识片段全部交给模型。(GitHub)
10. RetrievalAugmentationAdvisor:更模块化的 RAG Advisor
LoveAppRagCustomAdvisorFactory 最后返回的是:
RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever)
.queryAugmenter(LoveAppContextualQueryAugmenterFactory.createInstance())
.build();
这说明自定义 RAG 由两部分组成:
documentRetriever:
负责找文档。
queryAugmenter:
负责把文档上下文增强到用户问题中。
Spring AI 官方文档中也说明,RetrievalAugmentationAdvisor 提供了一种模块化 RAG 架构,可以使用不同模块构建自己的 RAG 流程。(Home)
这比默认 QuestionAnswerAdvisor 更灵活。
可以理解为:
QuestionAnswerAdvisor:
适合快速接入简单 RAG。
RetrievalAugmentationAdvisor:
适合构建更可控、更模块化的 RAG。
11. 空上下文问题
RAG 系统中有一个常见问题:
如果向量库没有检索到相关文档,模型应该怎么办?
默认情况下,模型可能会继续凭借自身知识回答。
这在开放问答中问题不大,但在垂直智能体中可能不合适。
例如,恋爱咨询智能体如果用户问:
"帮我写一段 Java 多线程代码。"
这不属于恋爱咨询范围。
理想结果不是让模型回答 Java 代码,而是提示用户:
当前智能体只回答恋爱相关问题。
所以,空上下文处理是 RAG 可控性的重要部分。
12. LoveAppContextualQueryAugmenterFactory
项目通过:
LoveAppContextualQueryAugmenterFactory
来处理空上下文情况。
源码中,它创建了一个 ContextualQueryAugmenter,并设置了:
.allowEmptyContext(false)
.emptyContextPromptTemplate(emptyContextPromptTemplate)
同时,emptyContextPromptTemplate 要求模型输出固定兜底内容:"抱歉,我只能回答恋爱相关的问题,别的没办法帮到您哦,有问题可以联系客服"。(GitHub)
可以理解为:
如果检索不到相关上下文:
不允许模型自由发挥
使用固定模板提示用户
这让 RAG 系统的边界更加清晰。
13. allowEmptyContext(false) 的意义
allowEmptyContext(false) 表示不允许空上下文继续进入正常回答流程。
Spring AI 文档中也提到,RetrievalAugmentationAdvisor 默认不允许检索上下文为空;当上下文为空时,会指示模型不要回答用户查询,也可以通过 ContextualQueryAugmenter 调整空上下文行为。(Home)
在这个项目中,这个设置和业务场景结合得比较自然:
检索到恋爱相关知识:
基于上下文回答。
没有检索到恋爱相关知识:
提示只能回答恋爱相关问题。
这比"什么都回答"的通用聊天机器人更符合垂直 Agent 的定位。
14. 自定义 RAG 的完整流程
综合 LoveAppRagCustomAdvisorFactory 和 LoveAppContextualQueryAugmenterFactory,自定义 RAG 流程可以写成:
用户问题
↓
传入 status,例如"单身"
↓
构造 filterExpression:status == 单身
↓
VectorStoreDocumentRetriever 在向量库中检索
↓
只召回 status 匹配且相似度足够的文档
↓
最多返回 topK = 3 个文档
↓
ContextualQueryAugmenter 把文档增强到问题中
↓
如果上下文为空,则输出兜底提示
↓
模型生成最终回答
这条链路比默认 RAG 更适合业务场景。
它不仅问"哪些文档相似",还问:
哪些文档既相似,又符合当前用户状态?
15. PgVectorVectorStoreConfig:从本地向量库到数据库向量库
除了自定义检索,项目还预留了 PgVector 向量库存储。
对应配置类是:
PgVectorVectorStoreConfig
源码中,它定义了一个 Bean:
@Bean
public VectorStore pgVectorVectorStore(
JdbcTemplate jdbcTemplate,
EmbeddingModel dashscopeEmbeddingModel
)
然后通过:
PgVectorStore.builder(jdbcTemplate, dashscopeEmbeddingModel)
构建 PgVector 向量库,并设置 dimensions(1536)、distanceType(COSINE_DISTANCE)、indexType(HNSW)、initializeSchema(true)、schemaName("public")、vectorTableName("vector_store") 和 maxDocumentBatchSize(10000)。(GitHub)
可以理解为:
SimpleVectorStore:
向量数据主要保存在本地内存中,适合 Demo。
PgVectorStore:
向量数据保存在 PostgreSQL + pgvector 中,适合持久化和生产扩展。
16. PgVector 配置逐项解释
项目中的 PgVector 配置可以拆成几项理解。
.dimensions(1536)
表示向量维度是 1536。Spring AI 文档说明,PgVector 的 embedding 维度用于创建表中的向量列,如果维度变化,需要重新创建 vector_store 表。(Home)
.distanceType(COSINE_DISTANCE)
表示使用余弦距离进行相似度检索。Spring AI 的 PgVector 配置中,距离类型默认也是 COSINE_DISTANCE。(Home)
.indexType(HNSW)
表示使用 HNSW 索引。Spring AI 文档说明,HNSW 构建较慢、内存使用较高,但查询性能通常更好;它也不需要像 IVFFlat 那样先有训练步骤。(Home)
.initializeSchema(true)
表示启动时初始化所需 schema。Spring AI 文档说明,PgVectorStore 可以初始化所需 schema,但需要显式启用 initializeSchema。(Home)
17. PgVector 相比 SimpleVectorStore 的优势
SimpleVectorStore 的优点是简单,适合入门。
但正式系统往往需要:
知识库持久化
多实例共享
大量文档检索
增量更新
数据库备份
元数据过滤
更稳定的向量索引
PgVector 的优势就在这里。
它把向量和文档内容保存在 PostgreSQL 中。Spring AI 的 PgVector 文档中也说明,vector_store 表通常包含 id、content、metadata 和 embedding 字段,并可以基于 HNSW 创建向量索引。(Home)
所以可以这样理解:
SimpleVectorStore:
适合"先把 RAG 跑起来"。
PgVectorStore:
适合"把 RAG 做成可持续运行的知识库服务"。
18. 当前 PgVector 配置还缺什么?
需要注意的是,PgVectorVectorStoreConfig 当前只是创建了 pgVectorVectorStore 这个 Bean。
它并没有像 LoveAppVectorStoreConfig 那样加载 Markdown 文档、关键词增强并写入 PgVector。
也就是说,当前配置更像是:
准备好了 PgVector 向量库连接
但还缺少:
把文档写入 PgVector 的初始化流程
如果后续要真正使用 PgVector,需要补充类似流程:
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
List<Document> enrichedDocuments = myKeywordEnricher.enrichDocuments(documents);
pgVectorVectorStore.add(enrichedDocuments);
否则即使在 LoveApp 中启用:
new QuestionAnswerAdvisor(pgVectorVectorStore)
也可能因为 PgVector 中没有数据而检索不到有效内容。
19. LoveAppRagCloudAdvisorConfig:云知识库方案
项目还提供了云知识库 RAG 配置:
LoveAppRagCloudAdvisorConfig
源码中,它读取配置:
@Value("${spring.ai.dashscope.api-key}")
private String dashScopeApiKey;
然后创建:
DashScopeApi dashScopeApi = DashScopeApi.builder()
.apiKey(dashScopeApiKey)
.build();
接着指定云知识库索引名:
final String KNOWLEDGE_INDEX = "恋爱大师";
最后使用 DashScopeDocumentRetriever 创建文档检索器,并通过 RetrievalAugmentationAdvisor.builder() 构建 loveAppRagCloudAdvisor。(GitHub)
完整理解就是:
DashScope API Key
↓
DashScopeApi
↓
云端知识库索引:恋爱大师
↓
DashScopeDocumentRetriever
↓
RetrievalAugmentationAdvisor
20. 云知识库方案适合什么场景?
云知识库方案适合把知识库托管到平台侧。
本地 RAG 的知识库通常在项目代码或本地数据库中。
云知识库则更像:
文档上传到云平台
平台负责索引和检索
项目只负责调用检索服务
它的优势是:
本地项目更轻量
知识库管理可以平台化
适合多人维护知识库
更容易使用平台提供的检索能力
但它也有需要注意的地方:
依赖外部云服务
需要配置 API Key
知识库索引名称要和云端一致
网络和权限会影响可用性
调试时不如本地向量库直观
在当前项目中,云知识库的索引名写死为:
恋爱大师
这说明项目默认假设云端已经存在这个知识库索引。(GitHub)
21. 三种 RAG 方案对比
可以用一段话来理解这三种方案。
SimpleVectorStore 默认 RAG:
适合学习 RAG 主流程,部署简单,成本低。
自定义过滤 RAG:
适合垂直业务,需要根据用户状态、文档类型或业务标签做精细检索。
PgVector RAG:
适合正式系统,需要持久化、可扩展和多实例共享。
云知识库 RAG:
适合依赖平台知识库服务,希望把文档管理和检索能力交给云端。
如果按照项目演进顺序,可以这样走:
第一阶段:
SimpleVectorStore 跑通本地 RAG。
第二阶段:
加入 status 过滤,让检索更准。
第三阶段:
切换 PgVector,让知识库持久化。
第四阶段:
根据需求接入云知识库或混合知识库。
22. 更适合当前项目的 RAG 演进路线
对于 ai_agent 这个学习项目,我认为最合理的扩展路线不是直接上云知识库,而是先把本地 RAG 做扎实。
推荐顺序是:
第一步:保留 SimpleVectorStore,先验证默认 RAG 回答效果。
第二步:启用 LoveAppRagCustomAdvisorFactory,根据 status 做过滤检索。
第三步:补充检索日志,观察召回文档、相似度、filename、status、keywords。
第四步:把同一套文档写入 PgVector,验证持久化向量库。
第五步:在 PgVector 稳定后,再考虑云知识库方案。
这个顺序更适合学习。
因为 RAG 的难点不只是"接入某个向量库",而是要先理解:
检索到了什么?
为什么检索到这些?
这些文档是否真的能支撑回答?
23. 当前实现可以改进的地方
23.1 status 不应该写死为"单身"
当前 LoveApp 注释中的自定义 RAG 示例是:
LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(
loveAppVectorStore,
"单身"
)
这适合演示,但正式场景中,status 应该来自用户画像、前端选择、对话识别或模型分类。
更合理的流程是:
用户问题
↓
判断用户状态:单身 / 恋爱 / 已婚
↓
把 status 传入自定义 RAG
↓
只检索对应文档
这样才能真正实现动态过滤检索。
23.2 检索参数需要可配置化
当前 similarityThreshold(0.5) 和 topK(3) 写死在代码中。(GitHub)
后续可以放到配置文件:
rag:
similarity-threshold: 0.5
top-k: 3
这样调参时不用改代码。
23.3 PgVector 需要补充文档入库逻辑
当前 PgVectorVectorStoreConfig 主要完成 PgVectorStore Bean 的创建,没有看到将 Markdown 文档写入 pgVectorVectorStore 的流程。(GitHub)
后续可以补充:
启动时导入文档
手动接口导入文档
定时同步知识库
检测文档变更后增量更新
否则 PgVector 只是准备好了存储,还没有真正承载知识库。
23.4 云知识库索引名应该配置化
当前云知识库索引名是:
final String KNOWLEDGE_INDEX = "恋爱大师";
它写在代码中。(GitHub)
后续可以改成配置项:
rag:
dashscope:
knowledge-index: 恋爱大师
这样更方便切换不同知识库。
23.5 需要增加 RAG 观测能力
RAG 系统最需要观测的不是最终回答,而是中间检索结果。
后续可以增加日志:
原始问题
改写问题
使用的 status
召回文档数量
每个文档的 filename
每个文档的 status
每个文档的 keywords
相似度分数
最终进入 prompt 的上下文
这样才能判断 RAG 出错时到底是:
问题改写错了
过滤条件错了
向量库没数据
文档切分不合理
相似度阈值太高
模型没有正确利用上下文
24. 我的理解
我认为这一期的核心不是"项目支持多少种向量库",而是理解 RAG 从简单到复杂的演进逻辑。
默认 RAG 解决的是:
模型可以参考知识库回答。
自定义过滤 RAG 解决的是:
模型参考更符合业务条件的知识回答。
PgVector 解决的是:
知识库可以持久化、可扩展、可共享。
云知识库解决的是:
知识库管理和检索能力可以交给外部平台。
所以,这几个模块对应的是 RAG 工程化的不同层次。
25. 本期重点理解
这一期最重要的是理解 RAG 高级扩展的三个方向:
第一,LoveAppRagCustomAdvisorFactory 通过 status 元数据过滤、similarityThreshold 和 topK 控制检索范围。
第二,LoveAppContextualQueryAugmenterFactory 通过 allowEmptyContext(false) 和兜底模板控制空上下文回答。
第三,PgVectorVectorStoreConfig 用 PostgreSQL + PgVector 替代本地 SimpleVectorStore,面向持久化和生产扩展。
第四,LoveAppRagCloudAdvisorConfig 通过 DashScopeDocumentRetriever 接入云知识库索引。
第五,RAG 的关键不是只接入向量库,而是控制检索范围、检索质量和无上下文时的回答边界。
一句话概括:
ai_agent 的 RAG 高级扩展,核心是从"能检索"进一步走向"可过滤、可持久、可托管、可控制"的工程化 RAG。
26. 本期小结
本期主要分析了 ai_agent 项目中的 RAG 高级扩展。项目在默认 QuestionAnswerAdvisor + SimpleVectorStore 的基础上,进一步预留了自定义过滤检索、PgVector 向量数据库和云知识库三类方案。LoveAppRagCustomAdvisorFactory 通过 status 元数据过滤检索范围,并使用 similarityThreshold(0.5) 和 topK(3) 控制召回文档质量与数量;LoveAppContextualQueryAugmenterFactory 通过 allowEmptyContext(false) 和自定义空上下文模板,让智能体在没有检索到相关内容时输出可控提示;PgVectorVectorStoreConfig 通过 PgVectorStore.builder() 构建 PostgreSQL + PgVector 向量库,为知识库持久化和生产扩展打基础;LoveAppRagCloudAdvisorConfig 则通过 DashScopeDocumentRetriever 接入名为"恋爱大师"的云知识库索引。
这一期可以用一句话总结:
RAG 高级扩展的核心价值,是让 LoveApp 不只会"查知识库",还能够按业务状态精准查、用数据库持久查、用云知识库托管查,并在查不到内容时保持回答边界。
下一期可以继续分析:
AI Agent 项目学习笔记(八):Tool Calling 工具调用机制总览
下一期重点分析 ToolRegistration、ToolCallback[]、@Tool、ToolContext 以及搜索、抓取、下载、文件操作、终端执行、PDF 生成等工具模块,理解项目如何让模型从"会回答"进一步变成"能执行任务"的智能体。