RAG 答非所问/幻觉怎么破?Spring AI QuestionAnswerAdvisor 召回优化实战(TopK/重排/混合检索)
系列:Spring AI 深入浅出(第 11 篇)
版本:Spring AI 2.0.1 / Spring Boot 4.1.x / JDK 21
标签:#SpringAI #RAG #QuestionAnswerAdvisor #幻觉
前置阅读:第 9、10 篇
预计字数:6500 字|可复制源码 + 召回参数表 + 幻觉排查清单
本文将解决 :RAG 效果差,90% 在召回不在生成 。本文讲清 QuestionAnswerAdvisor 的检索增强链路,给出一套召回参数调优 + 幻觉排查清单。
你将学到:
- 检索增强链路:召回 → 上下文注入 → 生成
QuestionAnswerAdvisor的用法- 召回参数调优(topK / similarityThreshold / filterExpression)
- 分块大小与召回命中率的关系
- 重排序与混合检索的价值
- 一张"答非所问/幻觉"排查清单
一、版本标注
本文基于 Spring AI 2.0.1 / Boot 4.1 / JDK 21。QuestionAnswerAdvisor 与 SearchRequest 的具体构造签名以 2.0.1 官方文档为准。
二、检索增强链路
第 9 篇搭好了 ETL,第 10 篇选好了库,现在把它们接到对话上。QuestionAnswerAdvisor 做的事:
#mermaid-svg-Di8w1p0iXJoWb9b6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Di8w1p0iXJoWb9b6 .error-icon{fill:#552222;}#mermaid-svg-Di8w1p0iXJoWb9b6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Di8w1p0iXJoWb9b6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .marker.cross{stroke:#333333;}#mermaid-svg-Di8w1p0iXJoWb9b6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Di8w1p0iXJoWb9b6 p{margin:0;}#mermaid-svg-Di8w1p0iXJoWb9b6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster-label text{fill:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster-label span{color:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster-label span p{background-color:transparent;}#mermaid-svg-Di8w1p0iXJoWb9b6 .label text,#mermaid-svg-Di8w1p0iXJoWb9b6 span{fill:#333;color:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .node rect,#mermaid-svg-Di8w1p0iXJoWb9b6 .node circle,#mermaid-svg-Di8w1p0iXJoWb9b6 .node ellipse,#mermaid-svg-Di8w1p0iXJoWb9b6 .node polygon,#mermaid-svg-Di8w1p0iXJoWb9b6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .rough-node .label text,#mermaid-svg-Di8w1p0iXJoWb9b6 .node .label text,#mermaid-svg-Di8w1p0iXJoWb9b6 .image-shape .label,#mermaid-svg-Di8w1p0iXJoWb9b6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Di8w1p0iXJoWb9b6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .rough-node .label,#mermaid-svg-Di8w1p0iXJoWb9b6 .node .label,#mermaid-svg-Di8w1p0iXJoWb9b6 .image-shape .label,#mermaid-svg-Di8w1p0iXJoWb9b6 .icon-shape .label{text-align:center;}#mermaid-svg-Di8w1p0iXJoWb9b6 .node.clickable{cursor:pointer;}#mermaid-svg-Di8w1p0iXJoWb9b6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .arrowheadPath{fill:#333333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Di8w1p0iXJoWb9b6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Di8w1p0iXJoWb9b6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Di8w1p0iXJoWb9b6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster text{fill:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 .cluster span{color:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Di8w1p0iXJoWb9b6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Di8w1p0iXJoWb9b6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Di8w1p0iXJoWb9b6 .icon-shape,#mermaid-svg-Di8w1p0iXJoWb9b6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Di8w1p0iXJoWb9b6 .icon-shape p,#mermaid-svg-Di8w1p0iXJoWb9b6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Di8w1p0iXJoWb9b6 .icon-shape rect,#mermaid-svg-Di8w1p0iXJoWb9b6 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Di8w1p0iXJoWb9b6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Di8w1p0iXJoWb9b6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Di8w1p0iXJoWb9b6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户问题
向量化
相似度检索 TopK
检索结果注入 Prompt
模型基于资料生成
它是个 Advisor(第 6 篇机制):请求进入时,把问题拿去检索,把命中的文档拼进 prompt,再交给模型。
三、QuestionAnswerAdvisor 用法
java
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
@Bean
public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) {
return builder
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build())
.build();
}
挂上后,用户提问会自动检索知识库并增强回答。要精细控制召回,用 SearchRequest:
java
SearchRequest searchRequest = SearchRequest.builder()
.topK(4) // 取最相似的 4 条
.similarityThreshold(0.7) // 相似度阈值,低于 0.7 的不采用
.build();
QuestionAnswerAdvisor advisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(searchRequest)
.build();
四、召回参数调优
| 参数 | 含义 | 太小 | 太大 | 建议 |
|---|---|---|---|---|
| topK | 取前 K 条最相似 | 漏召回、答不全 | 噪声多、塞进无关内容 | 3~5 起步 |
| similarityThreshold | 相似度下限 | 漏掉相关 | 混入无关、易幻觉 | 0.6~0.8 起步 |
核心矛盾:topK 和阈值共同决定"召回多少、多相关"。调优方向------
- 答不全 → 加大 topK、降低阈值
- 答非所问/幻觉 → 减小 topK、提高阈值,让模型只看到高相关内容
五、分块大小与召回命中率
第 9 篇提过 chunk 是头号变量,这里说它和召回的关系:
- 块太大:一个 chunk 里塞了多个话题,检索命中的"颗粒"太粗,注入时带进无关内容。
- 块太小:一个完整语义被切碎,检索只能命中碎片,上下文不全。
- 实践:按知识粒度切(FAQ 按条、文档按段),让每个 chunk 是"一个完整可独立回答的单元",召回命中率自然高。
六、重排序与混合检索(进阶)
6.1 重排序(Rerank)
向量检索"召回"快但排序粗,可以加一层 rerank 模型对 TopK 结果重新打分排序,把最相关的放最前:
- 适用:知识库大、TopK 只能召回"大致相关"时
- 代价:多一次 rerank 模型调用,增加延迟与成本
6.2 混合检索(向量 + 关键词)
向量检索擅长语义,但对精确关键词(如型号"X-2000"、编号"PRD-001")不如传统全文检索。混合检索 = 向量 + 关键词(BM25)两路召回再融合:
- 适用:知识库里有大量精确术语/编号的场景
- 实现:ES 等支持向量+全文混合的库天然占优(第 10 篇提过)
七、幻觉排查清单
遇到"答非所问 / 胡说八道",按这个顺序排查:
- 召回有没有命中:打日志看检索到了什么。没命中 → 调 topK/阈值、换 embedding。
- 命中但内容不对:看 chunk 是否完整、是否带无关内容 → 调分块、加过滤。
- 内容对但答案错:问题在 prompt 拼装/模型 → 检查注入格式、降低 temperature。
- 答案对但没出处:加引用溯源(第 12 篇),让用户可核对。
- 特定领域一直胡说:知识库里根本没有这块资料 → 补数据,不是调参能救的。
牢记:RAG 的幻觉很多是"数据/召回"问题,不是"模型"问题。先查召回,再查 prompt,最后才怀疑模型。
八、原理图解:召回优化的闭环
#mermaid-svg-4BZQPPGqDeUjMZ1Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .error-icon{fill:#552222;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .marker.cross{stroke:#333333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z p{margin:0;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster-label text{fill:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster-label span{color:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster-label span p{background-color:transparent;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .label text,#mermaid-svg-4BZQPPGqDeUjMZ1Z span{fill:#333;color:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .node rect,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node circle,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node ellipse,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node polygon,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .rough-node .label text,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node .label text,#mermaid-svg-4BZQPPGqDeUjMZ1Z .image-shape .label,#mermaid-svg-4BZQPPGqDeUjMZ1Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .rough-node .label,#mermaid-svg-4BZQPPGqDeUjMZ1Z .node .label,#mermaid-svg-4BZQPPGqDeUjMZ1Z .image-shape .label,#mermaid-svg-4BZQPPGqDeUjMZ1Z .icon-shape .label{text-align:center;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .node.clickable{cursor:pointer;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .arrowheadPath{fill:#333333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4BZQPPGqDeUjMZ1Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4BZQPPGqDeUjMZ1Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster text{fill:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .cluster span{color:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4BZQPPGqDeUjMZ1Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .icon-shape,#mermaid-svg-4BZQPPGqDeUjMZ1Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .icon-shape p,#mermaid-svg-4BZQPPGqDeUjMZ1Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .icon-shape rect,#mermaid-svg-4BZQPPGqDeUjMZ1Z .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4BZQPPGqDeUjMZ1Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4BZQPPGqDeUjMZ1Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4BZQPPGqDeUjMZ1Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没命中
命中但乱
命中且准
用户问题
召回结果如何?
降阈值/加TopK/换embedding
调分块/加过滤
进入生成
模型作答
九、踩坑 FAQ
坑 1:topK 太小漏召回
现象 :答案缺关键信息,或干脆答"不知道"。
原因 :topK 太小,相关内容没被召回。
解决:加大 topK(3~5 起步),必要时降阈值。
坑 2:相似度阈值太高,零召回
现象 :无论问什么,检索结果都为空。
原因 :similarityThreshold 设太高(如 0.9),没有 chunk 达标。
解决:降到 0.6~0.8,观察召回。
坑 3:检索命中但答案仍错
现象 :日志显示召回了相关内容,但答案不对。
原因 :问题在 prompt 拼装或模型,不在召回。
解决:检查注入格式、降 temperature、加强 prompt 约束。
坑 4:相关性得分普遍偏低
现象 :相似度分数都很低,召回不稳定。
原因 :embedding 模型与语料不匹配(如英文模型处理中文)。
解决:换中文友好的 embedding 模型。
坑 5:精确关键词检索不到
现象 :问"PRD-001 的规格",向量检索找不到。
原因 :精确编号/术语不适合纯向量语义检索。
解决:上混合检索(向量 + 关键词)或 rerank。
十、小结与合集导航
本篇把 RAG 从"能跑"推进到"答得准":
QuestionAnswerAdvisor完成召回→注入→生成SearchRequest调 topK / similarityThreshold- 分块、rerank、混合检索三管齐下
- 一张幻觉排查清单:先查召回,再查 prompt,最后查模型
下一篇:《RAG 生产级进阶》------多租户隔离、增量更新、引用溯源与召回评测。
十一、互动钩子
投票:你的 RAG 最头疼哪个问题?
- 召回不准确
- 幻觉/胡说
- 精确关键词搜不到
- 知识库更新不及时
评论区聊聊你的 RAG 翻车现场;源码见置顶评论。