similarityThreshold的取值必须落在 0~1。写超过 1 或小于 0,SearchRequest.Builder里的Assert会先拦下来抛异常。另一头更隐蔽:阈值调太高(比如 0.8)召回 0 条,RetrievalAugmentationAdvisor默认不允许空上下文,模型就固定回一句“无法根据提供的信息回答”。
Issue & Env(案发现场与环境)
- 业务场景:给 RAG 检索调参。有人想“过掉不相关结果”,把阈值写到 1 甚至更大;也有人调高阈值后,回答质量反而变差。
- 环境:Spring AI 1.1.x,
QuestionAnswerAdvisor或RetrievalAugmentationAdvisor搭配VectorStoreDocumentRetriever。
StackTrace(堆栈日志)
阈值越界时,构造 SearchRequest 这一步就抛异常:
java.lang.IllegalArgumentException: Similarity threshold must be in [0,1] range.
at org.springframework.ai.vectorstore.SearchRequest$Builder.similarityThreshold(SearchRequest.java:186)
还有一种是“没有异常”的版本:检索命中 0 条文档,模型回一句“无法根据提供的信息回答该问题”。日志干干净净,最容易当成模型变笨。
Reproduce(复现代码)
// 越界:构造时就抛异常
SearchRequest request = SearchRequest.builder()
.query("退款流程")
.similarityThreshold(1.2d) // 大于 1,Assert 拦下
.topK(4)
.build();
// 静默失忆:阈值偏高 + topK 很小,召回可能为空
SearchRequest strict = SearchRequest.builder()
.query("退款流程")
.similarityThreshold(0.8d)
.topK(2)
.build();
把 strict 交给 QuestionAnswerAdvisor 或 VectorStoreDocumentRetriever,就可能一条都命中不了。
Root Cause(根因分析)
一是取值范围的硬校验
similarityThreshold 的语义是相似度下限,取值是 0~1 的小数。Javadoc 说得很直白:0.0 表示接受任意相似度、相当于关掉阈值过滤;1.0 表示要求完全匹配。文档里的常量 SIMILARITY_THRESHOLD_ACCEPT_ALL 就是 0.0。源码里对应这一行:
Assert.isTrue(threshold >= 0 && threshold <= 1, "Similarity threshold must be in [0,1] range.");
越界直接抛 IllegalArgumentException,不会帮你裁剪到边界。
二是阈值与 topK 是两个维度
similarityThreshold:质量下限,低于它的结果一律丢。topK:条数上限,最多返回几条。
阈值越高,留下的越少;再叠一个很小的 topK,很容易一条不剩。一个管“够不够像”,一个管“要几条”,别混着调。
三是空上下文触发了兜底话术
RetrievalAugmentationAdvisor 默认不允许检索上下文为空。官方原话是:上下文为空时,它会指示模型不要回答用户问题(it instructs the model not to answer the user query)。于是你看到的,就是一句固定的“我无法根据提供的信息回答”。
The Fix(解决方案)
1. 阈值先落回 0~1
想看全部结果就用 0.0:
SearchRequest.builder()
.query("退款流程")
.similarityThreshold(0.0d)
.topK(4)
.build();
还有个等价写法 similarityThresholdAll(),效果就是把阈值设回 0.0。
2. 调参顺序:先松后紧
先用 0.0 或 0.5 看召回,再一点点收紧。别一上来就给 0.8。
QuestionAnswerAdvisor 的常规配置长这样:
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(6).build())
.build();
3. 允许空上下文时自己兜底
确实想允许召回为空、由应用自行处理,就打开 allowEmptyContext。注意它挂在 ContextualQueryAugmenter 上:
Advisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build();
4. 先确认库里真的有数据
召回为空有时跟阈值无关:文档根本没灌进去,或者写入和查询用的不是同一个 embedding 模型。先确认数据在库里,再谈阈值。
