Spring AI 模块化 RAG 与 TypeSafe Jev:多检索,只保留能回答问题的内容
Spring AI: RAG 文档 | Jev 文档 | 演示
在上一篇文章中,我们介绍了 Spring AI TypeSafe 及其 Jev 模型:输入类型化问题,在几百毫秒内输出经过校准的数字。那篇文章的 SPI 表中有一行值得单独写一篇文章。JevDocumentFilter 和 JevDocumentReranker 是 DocumentPostProcessor,这意味着它们可以直接接入 Spring AI 的 Modular RAG 流水线。
本文将两者结合起来。两次 LLM 调用会在检索前清理并扩展用户的问题。随后,Jev 会在每个检索到的块进入提示词之前对其进行评判。整个流水线就是一个 advisor 构建器。
💡 演示:完整示例是
05-1-modular-rag模块。它紧邻05-rag,即朴素版本,因此你可以对比两者差异。下面引用的每一段输出都来自一次实际运行。
为什么朴素 RAG 不够用
经典的 Spring AI RAG 演示是在向量存储之上使用 QuestionAnswerAdvisor。它嵌入用户文本,获取相似的块,然后将它们塞进提示词。这在演示台上有效,但在现实中会变得不可靠,原因有三:
- 用户不会输入搜索查询。 他们会输入 “我是佛罗里达州居民,去年秋天听说了很多关于风暴的消息。Milton 真的在我所在州登陆了吗?在哪里登陆的?” 逐字嵌入这句话会把所有噪声拖入搜索中。
- 相似并不等于有用。 向量存储返回的是关于该主题的块。一份五次提到“Hurricane Milton”的参考文献列表会得到高分,却什么也回答不了。
- 检索到的文本是不可信输入。 文档中的任何内容都会直接进入提示词,包括为劫持模型而编写的文本。
我们将针对一份关于飓风 Milton 的维基百科 PDF 来回答这个问题,并用一个可插拔组件修复每个问题。
Spring AI 中的 Modular RAG
来自 spring-ai-rag 模块的 RetrievalAugmentationAdvisor 实现了 Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks 中描述的架构。从外部看,它只是另一个 advisor:它位于你的提示词和模型之间,运行检索阶段,并将结果追加到用户消息中。系统提示词和问题原样通过。
[LOADING...]
在内部,每个阶段都是一个可以替换的小型接口:
以下是一次调用的完整流程:
[LOADING...]
检索分支并行运行,每个查询一个分支,并在后处理之前合并。注意一个微妙细节:后处理器和增强器看到的是用户的原始问题,而不是重写后的问题。重写只是为了改进搜索而存在。
快速开始
添加 Spring AI RAG 模块以及上一篇文章中的两个 TypeSafe 构件。Spring AI 的 BOM 管理第一个:
该 starter 会根据你的 API key 自动配置一个 TypeSafeClient bean:
该演示还使用 spring-ai-pdf-document-reader 进行摄取,并使用 spring-ai-starter-model-transformers 进行本地嵌入。
流水线
摄取与任何 Spring AI RAG 应用相同:读取 PDF,将其拆分为块,然后存储它们。
有趣的部分是一个单独的 builder。每个编号注释都是图中的一块砖:
使用它看起来和任何其他 advisor 一样:
DocumentRetriever 和 DocumentPostProcessor 都是单方法接口,因此用一个 lambda 就足以记录流经流水线的数据。该演示包装了 retriever,并在 Jev 阶段周围添加了两个打印型后处理器;下面的输出就来自那里。
后检索阶段中的 Jev
这两个组件都会为每个段落发送一次 Jev 调用,将查询和段落作为 state,并将答案转化为决策。
JevDocumentFilter 会针对每个段落提出四个 Noul 问题,并按顺序应用它们:
这就是上一篇文章中的原子问题思想在发挥作用:四个狭窄的问题、四个阈值、一次调用。矛盾的段落会被有意保留下来。如果用户假设了错误的事情,模型应该看到说明这一点的证据。分类结果存储在文档元数据的 jev.classification 下,而阈值是一个你可以覆盖的 Policy 记录。
JevDocumentReranker 只提出一个问题:这个段落能回答查询吗?,然后按分数排序并保留前 K 个。分数会存入元数据的 jev.rerank.score 下。它的关注点是有意为之的:该段落是否陈述了回答查询的信息,而不仅仅是是否涵盖了同一主题。这正是相似性与有用性之间的差距。
顺序很重要。先过滤,这样重排器只需为幸存者付出代价。
一次实际运行
重写和扩展将一句口语化的话变成了四个聚焦的搜索:
[retrieve] Hurricane Milton 2024 Florida landfall location [retrieve] Hurricane Milton track path across Florida from Gulf Coast to Atlantic and National Hurricane Center landfall report [retrieve] Siesta Key Sarasota County Milton landfall timeline, storm surge, and areas impacted [retrieve] Where did Hurricane Milton make landfall in Florida in October 2024 and at what intensity
joiner 将它们的结果合并为六个唯一块,全部高于 0.5 的相似度阈值:
[retrieved] 0.84 Hurricane Milton ... [retrieved] 0.72 6. "Hurricane Milton Makes Landfall On Florida's West Coast ... [retrieved] 0.70 Hurricane Milton's landfall" (https://www.wesh.com/... [retrieved] 0.69 Archived (http ... [retrieved] 0.59 /news/tropical-storm-milton-forms-gulf-of-mexico ... [retrieved] 0.54 of Key West. [128] Across the state, about 125 homes were d...
其中四个是参考文献列表条目和存档链接。它们大量提到 Milton,却什么也回答不了。JevDocumentFilter 因缺少答案证据而排除了它们,而 JevDocumentReranker 对两个幸存者进行了评分:
[jev-reranked] 0.97 Hurricane Milton ... [jev-reranked] 0.88 6. "Hurricane Milton Makes Landfall On Florida's West Coast ...
因此 topK(3) 只返回了两个块,而这就是重点。模型获得了更少的上下文,但全都相关:
Yes. Hurricane Milton made landfall in Florida, near Siesta Key, on the evening of October 9, 2024. It had weakened to a Category 3 hurricane by then.
向量相似度回答的是 “这段文本是关于什么的?”。Jev 回答的是 “这段文本能回答这个问题吗?”。一个 RAG 流水线两者都需要。
⚠️ 注意事项
默认执行器会让命令行应用保持存活。
RetrievalAugmentationAdvisor在自己的线程池上运行每个查询的检索,而这些线程不是守护线程。该演示打印出答案后便挂起了。通过.taskExecutor(...)传入 Spring Boot 自动配置的TaskExecutor可以解决这个问题,并且在spring.threads.virtual.enabled=true的情况下,你可以免费获得虚拟线程。较新的 Claude 模型会拒绝
temperature。 Spring AI 文档建议查询转换器使用 temperature 0。claude-sonnet-5-5会以 HTTP 400 响应,因此该演示克隆 builder 只是为了将主客户端的 advisor 排除在重写和扩展调用之外。两个 Jev 组件都采用 fail-open 策略。 如果 Jev API 不可达,段落会在未经过筛查或评分的情况下通过,并记录一条警告。你的流水线会降级为普通的 Modular RAG,而不是直接中断。
每一块砖都会增加延迟。 该流水线在检索前增加两次 LLM 调用,在检索后为每个检索到的块增加一次 Jev 调用。Jev 调用默认一次运行四个(
batchOptions(...)可以改变这一点),但在发布前请先测量。
结论
Modular RAG 将检索从一个不透明的 advisor 转变为一条你可以逐块阅读、记录和替换的流水线。以下是一些重要见解:
- 在搜索之前修正问题。 一次重写加上三个变体,比原始句子找到了更好的块。
- 在提示之前修正结果。 相似度找到了六个关于 Milton 的块;Jev 保留了其中两个能够回答的块,并在此过程中筛查了每个段落的提示词注入。
- 先过滤,再重排。 两者都为每个段落花费一次调用,所以先让过滤器缩小列表。
资源
演示
05-1-modular-rag--- 本文中的代码05-rag--- 朴素的QuestionAnswerAdvisor版本,用于比较
Spring AI
- 检索增强生成 --- advisor 和 Modular RAG 组件
- Modular RAG:将 RAG 系统转变为类似 LEGO 的可重构框架 --- 该架构背后的论文
Spring AI TypeSafe
- JevDocumentFilter 和 JevDocumentReranker
- 参考文档 和 GitHub 仓库
- TypeSafe cookbooks --- 重排和 RAG 分类配方
相关 Spring AI 文章