第17章:Spring AI 2.0 RAG 主管道——RetrievalAugmentationAdvisor

发布时间:2026/10/14 17:41:21

第17章:Spring AI 2.0 RAG 主管道——RetrievalAugmentationAdvisor 上章思考题答案护栏 order 为 -1、日志为 0 时护栏更靠外。敏感问题在日志的adviseCall之前就返回了日志不会执行。计数仍是 0。记忆 order 为 2排在护栏后面before不会执行记忆里没有这句话。检索顾问若排在护栏后面敏感问题被护栏短路不会去查向量库。无依据文案若由更靠内的顾问返回最外层日志只要自己的 order 更小并调用了nextCall就看得到这句文案。模型次数仍是 0 的前提是内层没有再调用模型。1. 项目背景1.1 业务场景售后机器人若用 if 先检索再手写提示词退款接口和超时接口就会各写一份拼接。有的忘了放条款编号有的把四条条款全部塞进去。邻里仓要求检索成为一条固定管道用户问题进来取出条款把条款写进即将送给模型的用户消息回答里必须出现元数据中的编号例如R-01。天气这种没有命中的问题不能改成「请根据常识回答」。本章把检索放进RetrievalAugmentationAdvisor。它实现BaseAdvisor在before里完成取出文档和改写用户消息。空结果的拒答文案在本节只要求不打开自由发挥不在这里展开截断和去重。1.2 痛点手写拼接时有人把Document.getText()拼上了编号留在元数据里没人读回答里没有R-01测试无法验收。有人命中为空仍把原问题送给模型并在系统提示词里写「可以结合常识」。模型会编一条退款规则条款编号对不上任何手册。管道若只存在于某一个控制器库存接口又会退回 if。编号和条件必须落在同一段里否则退款接口会引用库存条款的半句。没有主管道时的路径各接口自己检索 → 编号丢失或条款串门 → 空结果仍自由发挥 → 无法用同一条顾问验收。库存接口若仍手写拼接退款接口已经带上 R-01库存回答却引用退款条款。测试要按接口分头看提示词而不是只看最终语气像不像客服。退款接口的用户文本有 R-01库存接口的用户文本有 S-01两边都不能同时出现。管道若把 topK 留成 4两个接口会拿到同一堆条款编号断言就会互相污染。用户问题RetrievalAugmentationAdvisor.beforeVectorStoreDocumentRetriever条款列表QueryAugmenter改写后的用户消息DeepSeek2. 项目设计小胖说「这不就是把档案柜嵌进窗口的流水线吗客人问一句柜子自动抽出一页贴在问句下面再递给师傅。以前我在每个窗口自己抽抽到哪页算哪页。」小白问「抽出的页只有正文页眉上的编号谁来贴没抽出任何一页时是把原问题递进去还是换一张『没有这页』的条子师傅若被允许凭经验答编号从哪来」小胖又说「没抽到就让模型自己说客户至少有一句答复。」小白追问「那句答复没有条款编号。验收要的是编号不是一句像客服的话。自由发挥和没抽到必须是两条路。」大师把管道上的零件命名「RetrievalAugmentationAdvisor.builder()需要documentRetriever。本章用VectorStoreDocumentRetriever里面是第 13 章的SimpleVectorStore带上topK和similarityThreshold。顾问的before用用户原文构造Query调用retrieve再交给QueryAugmenter。默认的ContextualQueryAugmenter把文档正文填进{context}和{query}一起变成新的用户文本并通过prompt.augmentUserMessage替换原用户消息。默认的文档格式化只用Document.getText()不会自动带上元数据。条款编号要出现在上下文里就要自定义documentFormatter把clauseId和正文一起格式化。allowEmptyContext保持默认 false。它为 true 时空列表会把原问题原样送回模型等于没检索。false 时使用空上下文模板而不是邀请模型用常识作答。空模板的中文拒答句留到第 19 章再换。」技术映射流水线是RetrievalAugmentationAdvisor抽档案的是VectorStoreDocumentRetriever把页贴上问句的是QueryAugmenter。页眉编号要自己写进格式化结果。小白问替换「augmentUserMessage是追加还是换掉原来的用户句历史里的上一句还在不在」小胖说「换掉最干净省 token。」小白说「换掉的是这一次的用户文本。上一轮的订单号若只活在被换掉的那一句里这一轮就丢了。管道应该改写当前问句不应该清掉整个消息列表。」大师确认替换范围「被改写的是当前用户消息的文本系统消息和其他历史消息还在。本章的验收问题是单轮『怎么退款』提示词里要能看到R-01和条款正文。多轮压缩是下一章的事。测试可以不连 DeepSeek计数模型的call收到的Prompt里用户文本必须包含R-01。连网时再断言最终回答也包含R-01。两种断言都不要比较整段口语。」技术映射贴上去的是这一张问句。整本对话记录不因为贴了一页就被换掉。小白又问上下文键「文档放进DOCUMENT_CONTEXT之后模型还看不看得到这个键日志若只打印模型正文编号没被说出来时还能不能知道检索命中了哪一条」小胖说「模型没说就当没检索到。」小白说「模型省略编号和检索没中是两件事。键在上下文里就说明顾问已经取到了文档。」大师说「模型看见的是格式化之后的用户文本不是这个 Map 键。键给日志和测试用。验收时两处都看用户文本含R-01上下文里的文档元数据也是R-01。只看口语模型漏说编号会被误判成检索失败。」小胖问空结果「默认空模板是英文的『outside your knowledge base』。App 是中文本章要不要先换成中文拒答」小白问「换成中文之后模型仍会被调用去朗读这句模板。这算自由发挥吗本章和下一章的边界在哪」大师把边界划在开关上「自由发挥指的是allowEmptyContext(true)原问题直接进模型系统侧再鼓励它用常识。本章禁止这个开关。默认空模板仍然会导致一次模型调用这是顾问的现状before改写查询后链继续往下走。本章的测试断言两件事命中时用户文本含R-01构造一个allowEmptyContext(true)的对照augmenter时空文档得到的文本就是原问题。中文固定拒答、以及空结果零调用放在第 19 章。不要在本章为了省事打开 true。命中看编号没命中看是不是原问题。中文拒答和零调用留到第 19 章检索顾问的 order 写 3大于护栏的 1。」技术映射没有档案时不许把原问题当成已经查过。allowEmptyContext为 true 就是这条禁路。小白问「order 写成 3 之后护栏仍是 1敏感问题不会进入检索。这条沿用上一章的测试本章不再造敏感词。」大师说「格式化必须把条款编号和正文拼在一起。只拼正文用户文本里没有 R-01验收过不了。」3. 项目实战3.1 环境准备依赖增加spring-ai-rag版本由 BOM 管理。嵌入仍用TransformersEmbeddingModelMetadataMode.NONE缓存目录在target/onnx-cache。手册沿用四条短条款每条Document的元数据有clauseId。检索阈值先用 0.45若本机把「怎么退款」滤空按第 13 章的办法取中点只改这一处。密钥只用于可选冒烟本地测试用CountingChatModel查看它收到的提示词。计数模型必须把Prompt里的用户文本存下来。只返回固定回答测试就看不见编号有没有注入。存下来的文本用于断言不写入永久日志。四条条款的正文保持第 15 章的写法编号只在元数据里用来逼迫 formatter 去读clauseId。若有人把编号写进正文来让默认 formatter 蒙混过关单独再断言元数据仍是编号来源。3.2 分步实现步骤 1检索器只取一条步骤目标「怎么退款」取回的文档含R-01。VectorStoreDocumentRetrieverretrieverVectorStoreDocumentRetriever.builder().vectorStore(store).topK(1).similarityThreshold(0.45).build();ListDocumenthitsretriever.retrieve(newQuery(怎么退款));运行结果文字描述hits大小为 1元数据clauseId为R-01正文含「未发货」。天气问题在同一阈值下为空列表。这一步还没有顾问。可能的坑topK用默认 4阈值用 0天气问题也会取出条款。管道会把不相关正文写进提示词。两个参数都要显式写出。步骤 2格式化时带上编号步骤目标送给模型的上下文含有R-01而不是只有正文。ContextualQueryAugmenteraugmenterContextualQueryAugmenter.builder().allowEmptyContext(false).documentFormatter(docs-docs.stream().map(d-d.getMetadata().get(clauseId)\nd.getText()).collect(Collectors.joining(\n))).build();Queryaugmentedaugmenter.augment(newQuery(怎么退款),hits);运行结果文字描述augmented.text()包含R-01和「未发货」也包含原来的问句「怎么退款」。不包含S-01。可能的坑不设documentFormatter默认只拼接getText()。正文若没有把编号写进去提示词里就没有R-01。编号留在元数据是对的格式化时必须读出来。另一个坑是把四条文档都传给 augmenter。本章的检索器topK已经是 1不要在外面再拼接未过阈值的邻居。步骤 3挂上顾问并看模型收到的文本步骤目标顾问改写用户消息而不是另起一个 if。RetrievalAugmentationAdvisoradvisorRetrievalAugmentationAdvisor.builder().documentRetriever(retriever).queryAugmenter(augmenter).order(3).build();ChatClientclientChatClient.builder(countingModel).defaultAdvisors(advisor).build();Stringanswerclient.prompt().user(怎么退款).call().content();order 设为 3是为了排在第 16 章的日志 0、护栏 1、记忆 2 之后。本章的测试可以只挂这一个顾问。计数模型在call里把收到的用户文本存下来。运行结果文字描述计数为 1。保存下来的用户文本包含R-01。返回给调用方的answer是计数模型的固定句。冒烟换成DeepSeekChatModel后真实回答也要包含R-01。可能的坑顾问 order 小于护栏。成本价问题会先检索再被拒绝向量库被白查。生产客户端按第 16 章的顺序检索顾问放在护栏和记忆之后、模型之前。本章单测只验证检索注入。步骤 4空结果不许原题直达步骤目标看清allowEmptyContext(true)就是自由发挥入口。用空的文档列表调用两个 augmenter。false 的那个返回的文本不是原问题「今天天气怎么样」而是空上下文模板。true 的那个返回的文本就是原问题。测试断言这一个差别。不要把 true 的结果送进计数模型。运行结果文字描述false 的输出不包含「请根据常识」。true 的输出等于原问题。本章不把 true 接到客户端上。可能的坑为了让天气问题也有「回答」而打开 true。模型会生成没有编号的退款建议。处理保持 false空模板的中文固定句在第 19 章替换。命中路径和空路径不要共用一个计数模型却不重置。先问退款再问天气计数从 1 变成 2会让人以为空路径也调用了模型。天气路径在本章只比较 augmenter 的返回文本不调用模型。退款路径单独新建计数模型。冒烟若打开只追加断言回答含R-01不要把本地已经断言过的提示词全文再锁一次。本地构建不配置密钥这条冒烟跳过时提示词测试仍要执行。把整个测试类标成需要密钥会让编号注入在 CI 里消失。检索顾问的回归和聊天冒烟分成两个类或至少分成两个方法一个不带条件注解。失败时只打印用户文本里是否含有R-01和S-01不把整段条款打进构建日志。3.3 完整代码清单RefundRag、手册文档、测试。源码对照spring-ai-rag/.../advisor/RetrievalAugmentationAdvisor.javaspring-ai-rag/.../retrieval/search/VectorStoreDocumentRetriever.javaspring-ai-rag/.../generation/augmentation/QueryAugmenter.java命中的文档还会放进上下文键RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT供后面的日志读取编号而不必再解析模型正文。3.4 测试验证TestvoidaugmentedPromptContainsClauseId(){CountingChatModelmodelnewCountingChatModel();ChatClientclientRefundRag.client(model,store);client.prompt().user(怎么退款).call().content();assertThat(model.lastUserText()).contains(R-01);assertThat(model.lastUserText()).doesNotContain(S-01);assertThat(model.calls()).isEqualTo(1);}TestvoidemptyContextDoesNotReturnOriginalQuery(){QueryemptyRefundRag.augmenter(false).augment(newQuery(今天天气怎么样),List.of());QueryfreeRefundRag.augmenter(true).augment(newQuery(今天天气怎么样),List.of());assertThat(empty.text()).doesNotContain(今天天气怎么样);assertThat(free.text()).isEqualTo(今天天气怎么样);}CountingChatModel需要把最后一次Prompt的用户文本存进lastUserText()。没有这个存档就只能看见返回句看不见编号有没有被注入。mvn -DtestRefundRagTest test不需要密钥。可选冒烟在有DEEPSEEK_API_KEY时断言真实回答含R-01。本地失败时先看lastUserText。文本里没有编号是 formatter 没读元数据。文本里有编号而检索列表为空是测试看错了对象。空结果对照若变成原问题是allowEmptyContext被打开了。三处分开记不要合成一句检索顾问失效。退款用例的用户文本必须同时含R-01和条款里的「未发货」。只含编号、不含条件说明切块把标题和正文拆开了回到第 14 章把块收紧。天气对照只比较 augmenter 的返回值allowEmptyContext为 false 时不是原问题为 true 时等于原问题。不要对天气路径调用chatClient.call否则计数会从 0 变成 1和「空上下文仍会往下走」的结论搅在一起。检索顾问的 order 读出来应是 3。格式化结果里编号和「未发货」必须出现在同一段用户文本中。两段分开出现说明切块把标题和条件拆开了。库存接口要单独建一个检索器topK 仍是 1断言用户文本含S-01且不含R-01。两个接口共用一个 topK 为 4 的检索器编号会互相出现。4. 项目总结4.1 优点与缺点对比项每个接口手写拼接RetrievalAugmentationAdvisor编号容易忘formatter 统一读元数据空结果各写各的 ifallowEmptyContext 开关插入位置控制器顾问 order模型是否仍被调用看 if命中时会调用空模板默认也会调用多轮问句不管本章不压缩优点检索和注入集中在一个顾问里。条款编号从元数据进入提示词不靠模型编造。可以只取 topK 为 1避免串条款。自由发挥被一个布尔值挡住并能用测试锁定。命中文档留在DOCUMENT_CONTEXT日志能记编号。缺点默认格式化不含元数据不改就会丢编号。空结果默认仍会继续调用模型只是换了模板。检索顾问的 order 放错敏感问题也会查库。阈值仍要随嵌入模型校准。单轮管道不会理解「那单呢」。4.2 适用场景适用退款、超时、库存都要引用条款编号。希望所有接口共用一条检索管道。需要证明提示词里真有编号而不只看最终口语。空结果不能退化成常识问答。本地用计数模型验收注入结果。不适用用户问题依赖上一轮才看得懂。先做第 18 章的压缩。空结果要求零模型调用的固定中文拒答。那是第 19 章。4.3 注意事项documentFormatter要输出clauseId。allowEmptyContext保持 false。检索顾问的 order 大于护栏。冒烟只断言编号不断言全文。4.4 常见踩坑回答很流利但没有 R-01。根因是默认 formatter 没读元数据。处理格式化时拼上clauseId并断言用户文本。天气问题得到一条退款建议。根因是allowEmptyContext(true)或阈值是 0。处理关掉开关并设置阈值。成本价问题的日志里出现了向量检索。根因是检索顾问的 order 小于护栏。处理护栏 1检索 3。4.5 思考题用户第二轮只说「那单呢」。当前管道把这四个字拿去检索。它为什么很难稳定命中 R-01要在检索之前增加哪一种变换空上下文模板仍然是英文并且链还会调用模型。若要求用户看到的是一句固定中文、且计数为 0还缺哪一层答案在第 18 章开头。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析
延伸阅读

更多相关文章

2026/10/14 17:36:17

PTZ-C8T6.zip 云台控制 C 工程解析:编译、协议与避坑指南

简介:PTZ-C8T6.zip 是一份面向嵌入式开发初学者与 STM32 学习者的云台控制项目工程源码,基于 STM32F103 系列芯片与 HAL 库构建,适合用于课程设计、毕业设计或电赛类云台控制方案的参考与二次开发。压缩包共 98 个文件,约 675KB&a…

2026/10/14 17:36:17

Python 读取 Excel 并写入 zip:自动化报表打包实战

简介:这份资源面向需要处理表格数据的Python学习者与开发者,聚焦Excel文件的读取与写入这一常见任务,帮助解决数据分析、数据清洗与自动化报告中的表格操作问题。压缩包共4个文件,全部为py脚本,整体约6KB,涵…

2026/10/14 21:28:21

滑动窗口算法解析:LeetCode最小覆盖子串问题

1. 问题背景与核心挑战这道来自LeetCode的Hard题目要求我们在字符串S中找到包含字符串T所有字符的最短连续子串。举个例子,如果S"ADOBECODEBANC",T"ABC",那么正确结果应该是"BANC",因为它同时包含A…

2026/10/14 21:28:21

在Hive上构建MOLAP:预聚合加速大数据多维分析

1. 先聊透:Hive上做MOLAP到底解决什么问题第一次听到"在Hive上做MOLAP"这个说法时,我的第一反应也是愣了一下。MOLAP是传统的多维数据库玩法,Hive是跑离线大数据SQL的数据仓库工具,两者给人的印象差着辈分。但认真做完一…

2026/10/14 21:28:21

SQLite context-mode:基于FTS5与BM25的语义化上下文检索机制

1. 项目概述:什么是 context-mode?它到底在解决什么问题?“context-mode”这个词乍一看像某个新出的编程模式或IDE插件开关,但实际翻遍主流开发文档、语言规范和知名开源项目仓库,你根本找不到一个叫“context-mode”的…

2026/10/14 21:28:21

Vue自定义指令实现高性能下拉加载

1. 项目背景与核心价值下拉加载是现代Web应用中极其常见的交互模式,特别是在内容流、社交动态、商品列表等场景中。作为前端开发者,我们经常需要重复实现类似的逻辑:监听滚动事件、计算触底距离、触发数据加载、处理加载状态...传统实现方式存…

2026/10/14 21:28:20

JMeter JSON提取器配置与JSONPath实战指南

1. 为什么JSON提取器是JMeter压测脚本的“呼吸阀”在某次电商大促前的全链路压测中,我负责的订单创建接口TPS始终卡在800左右,远低于目标值2500。监控显示后端服务CPU和数据库连接池都未见瓶颈,但JMeter聚合报告里大量请求响应时间突增、错误…

2026/10/14 21:23:18

Flask开源项目实战指南:后台管理、人脸识别与自动化运维选型

前阵子有个刚转后端的朋友找我聊天,说他想在简历上写点“有面儿”的项目,又不想从零手撸一整套Web框架,问我有没有现成的Flask开源项目可以直接拿来改造。我翻了翻自己的收藏夹和这些年跟进过的仓库,发现一个挺有意思的现象&#…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/14 0:00:33

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:33

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:33

SCORM课件演示与测试全解析:从API通信到自动化验证

简介:这是一套遵循SCORM标准的交互式课件演示与测试资源,面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者,用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件,约12.07MB,以swf动…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑