Christian Tzolov 在 2026-10-02 的博文里,把 Spring AI Modular RAG 与 TypeSafe Jev 接到同一条 RetrievalAugmentationAdvisor 管道:检索前改写并扩展问题,检索后用 JevDocumentFilter 与 JevDocumentReranker 筛掉无法回答的 chunk,再喂进 prompt。站内尚无 TypeSafe / Jev / Modular RAG 专文。本稿按官博、RAG/Jev 文档与 demo 核对,配置与输出未在本地跑通。
朴素 RAG 卡在哪
经典写法是 QuestionAnswerAdvisor:嵌入原句、取相似片段塞进 prompt。线上常见:闲聊噪声拖累搜索;相近不等于能答(参考文献分数高却无证据);检索文本不受信,索引投毒可能直进 prompt。
官博 demo 用 Hurricane Milton Wikipedia PDF;对照 05-rag 与 05-1-modular-rag。
Modular RAG 管道
spring-ai-rag 的 RetrievalAugmentationAdvisor 实现 Modular RAG:外看仍是 Advisor;系统提示与用户原问原样穿过,检索结果追加到用户消息。各阶段可替换。
| 阶段 | 接口 | 本 demo 实现 |
|---|---|---|
| 检索前 | QueryTransformer | RewriteQueryTransformer |
| 检索前 | QueryExpander | MultiQueryExpander |
| 检索 | DocumentRetriever | VectorStoreDocumentRetriever |
| 检索 | DocumentJoiner | 默认 ConcatenationDocumentJoiner |
| 检索后 | DocumentPostProcessor | JevDocumentFilter、JevDocumentReranker |
| 生成 | QueryAugmenter | ContextualQueryAugmenter |
各查询检索并行,join 后再 post-process。Post-processor 与 augmenter 看到的是用户原问,改写只服务搜索。
依赖与配置
官博与 demo README 对齐 TypeSafe 0.3.0。spring-ai-rag 由 Spring AI BOM 管理:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-starter-typesafe</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>typesafe-spring-ai</artifactId>
<version>0.3.0</version>
</dependency>
spring.ai.typesafe.api-key=${TYPESAFE_API_KEY}
Starter 按 API key 自动配置 TypeSafeClient。Demo 另用 PDF 读入与本地 transformers 嵌入,并需聊天模型密钥(demo 用 Anthropic)。
一条 builder 串起全流程
入库与朴素 RAG 相同。关键在 Advisor builder:
var ragClientBuilder = chatClientBuilder.clone();
var modularRag = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ragClientBuilder)
.build())
.queryExpander(MultiQueryExpander.builder()
.chatClientBuilder(ragClientBuilder)
.numberOfQueries(3)
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(4)
.build())
.documentPostProcessors(
JevDocumentFilter.builder(typeSafeClient).build(),
JevDocumentReranker.builder(typeSafeClient).topK(3).build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.taskExecutor(taskExecutor)
.build();
String answer = chatClient.prompt()
.advisors(modularRag)
.user("I'm a Florida resident and heard a lot about storms last fall. "
+ "Did Milton actually make landfall in my state, and where?")
.call()
.content();
clone() 让改写/扩展不继承主客户端 Advisor。较新 Claude 会拒 temperature,demo 因此省略文档建议的低温。CLI 应传入 Boot 的 TaskExecutor(默认池非 daemon,答完可能挂住);可开 spring.threads.virtual.enabled=true。
Jev:先滤后排
两者都是 DocumentPostProcessor:每段一次 Jev,状态为 query + passage。
JevDocumentFilter 问四个 Noul,阈值按序、先命中先生效:
| 问题 | 默认阈值 | 结果 |
|---|---|---|
contains_prompt_injection | > 0.70 | 丢弃 |
contradicts_query_premise | > 0.70 | 保留,标 CONFLICTING |
is_relevant | < 0.45 | 丢弃 |
contains_answer_evidence | > 0.55 | 保留,否则丢弃 |
与用户前提冲突的段落刻意保留,便于模型反证。分类写入 jev.classification,阈值可用 Policy 覆盖。
JevDocumentReranker 问「这段能否回答该查询?」,按分排序并保留 top K,分数在 jev.rerank.score。焦点是能否作答,不是是否同主题。
顺序固定:先 Filter,再 Reranker。两边都是每段一调用;先缩小集合再打分更省,也避免注入文本进入排序。
官博实跑摘录
改写与扩展产出多条检索式。Joiner 留下 6 个相似度 ≥ 0.5 的 chunk,多条是参考文献与归档链接。Filter 丢掉无证据段后,Reranker 给两段约 0.97 / 0.88;topK(3) 实际只剩 2 段。模型答出 Milton 于 2024-10-09 傍晚在 Siesta Key 附近以 Category 3 登陆。
向量相似度回答「这段在说什么」;Jev 回答「这段能不能答这个问题」。
落地注意
- Fail-open:Jev 不可达时段落原样通过并打警告;管道退化为普通 Modular RAG。应对该警告做告警,否则注入筛查可能静默失效。
- 成本:检索前多 2 次 LLM;检索后每 chunk 各一轮 Filter/Reranker(默认并发 4)。服务端应共用
batchOptionsexecutor。 - 空上下文:demo 用
allowEmptyContext(true);默认空上下文时指示模型不要答。
参考
- 主源:Spring AI Modular RAG and TypeSafe Jev(Christian Tzolov,2026-10-02)
- 背景:Spring AI and TypeSafe Jev
- 文档:Spring AI RAG、JevDocumentFilter、JevDocumentReranker
- Demo:
05-1-modular-rag(可与同仓库05-rag对照)
一起交流
分享你的思考,让讨论更进一步。