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 模型。先確認數據在庫裡,再談閾值。
