最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Spring AI+Milvus的RAG混合檢索的實(shí)戰(zhàn)指南

 更新時(shí)間:2026年06月02日 09:05:21   作者:輪子飛了  
這篇文章主要給大家記錄了從零搭建企業(yè)級(jí) RAG 知識(shí)庫問答系統(tǒng)的工程,涵蓋意圖路由、混合檢索、RRF 融合、query 改寫、rerank 精排全鏈路,并通過代碼示例講解的非常詳細(xì),需要的朋友可以參考下

1. 背景與動(dòng)機(jī)

1.1 業(yè)務(wù)場(chǎng)景

我們要為一個(gè)垂直業(yè)務(wù)平臺(tái)構(gòu)建智能客服助手。用戶描述自己遇到的產(chǎn)品問題——“產(chǎn)品A 無法啟動(dòng)怎么排查”、“產(chǎn)品B 多久保養(yǎng)一次”——系統(tǒng)需要給出準(zhǔn)確的解決方案。

數(shù)據(jù)側(cè),我們已經(jīng)整理了大量的產(chǎn)品技術(shù)文檔(PDF、DOCX),按產(chǎn)品線(產(chǎn)品A、產(chǎn)品B、產(chǎn)品C、產(chǎn)品D、產(chǎn)品E)分類。現(xiàn)在需要一套檢索增強(qiáng)生成(RAG)系統(tǒng),把這些文檔"喂"給 LLM,生成結(jié)構(gòu)化的解決方案。

1.2 核心挑戰(zhàn)

直接做一個(gè)"用戶問 → 向量檢索 → LLM 回答"的樸素 RAG 夠用嗎?不夠。

實(shí)際場(chǎng)景中我們面臨幾個(gè)關(guān)鍵問題:

  1. 意圖多樣:不是所有問題都需要 RAG。“你好”、"我之前提交的工單怎樣了"這類不需要檢索,直接走 LLM 或者查數(shù)據(jù)庫即可。每次都觸發(fā)全套檢索鏈路是浪費(fèi)。
  2. 精準(zhǔn)召回難:純向量檢索對(duì)專業(yè)術(shù)語的敏感性有限。"產(chǎn)品C 運(yùn)行異常"和"產(chǎn)品C 系統(tǒng)故障"在語義空間里可能很近,但純關(guān)鍵詞匹配可能漏掉。
  3. 檢索噪聲:召回 40 條候選里,可能有 50% 以上和問題無關(guān)。直接塞給 LLM 不僅浪費(fèi) token,還容易讓 LLM 被噪聲帶偏。
  4. 領(lǐng)域術(shù)語差異:用戶說"設(shè)備發(fā)熱",文檔寫的是"機(jī)身溫度過高";用戶說"接口松動(dòng)",文檔寫的是"連接端口接觸不良"。不做 query 擴(kuò)展根本搜不到。

1.3 選型

組件選型理由
框架Spring Boot 3.4.5 + Spring AI 1.1.2Java 生態(tài)成熟,Spring AI 封裝了向量存儲(chǔ)和 LLM 調(diào)用
向量數(shù)據(jù)庫Milvus 2.5.6原生支持 BM25 內(nèi)置函數(shù),免去額外 ES 依賴
嵌入模型DashScope text-embedding-v21536 維,中文效果好
LLMDashScope Qwen兩階段調(diào)用(意圖分類 + 答案生成),同一模型不同 system prompt
RerankDashScope Rerank與 LLM 同供應(yīng)商,延遲可控

2. 整體架構(gòu)概覽

整個(gè)問答系統(tǒng)的核心 pipeline 分為兩階段

┌─────────────────────────────────────────────────────────┐
│                    用戶輸入(問題文本)                      │
└─────────────────────┬───────────────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────────────┐
│              Phase 1: 意圖分類(輕量 LLM 調(diào)用)              │
│                                                         │
│  ┌─────────────┐  ┌──────────────────┐  ┌────────────┐ │
│  │GENERAL      │  │VIEW_RECENT       │  │FIND        │ │
│  │_CONSULT     │  │_FOLLOWUP_TASKS   │  │_COMPONENT  │ │
│  │一般咨詢      │  │查看歷史記錄       │  │查找解決方案  │ │
│  └──────┬──────┘  └────────┬─────────┘  └─────┬──────┘ │
└─────────┼──────────────────┼──────────────────┼────────┘
          │                  │                  │
          ▼                  ▼                  ▼
   LLM 直接回復(fù)      查數(shù)據(jù)庫返回         觸發(fā) Phase 2
   (可選同步 QA 庫)   歷史記錄             RAG 檢索鏈路
                                             │
                                             ▼
                          ┌─────────────────────────────────┐
                          │    Phase 2: RAG 混合檢索          │
                          │                                 │
                          │  Query 改寫 → 向量檢索 + BM25     │
                          │       → RRF 融合 → Rerank        │
                          │       → 后處理門控 → LLM 生成     │
                          └─────────────────────────────────┘

關(guān)鍵設(shè)計(jì)決策:意圖優(yōu)先,RAG 按需觸發(fā)。只有 FIND_COMPONENT 意圖才走完整的混合檢索鏈路,其他意圖直接走輕型路徑。這樣做的好處是:

  • 減少不必要的 LLM 調(diào)用次數(shù)(Phase 1 和 Phase 2 各自只調(diào)一次 LLM)
  • 降低平均延遲(大部分請(qǐng)求不需要 RAG)
  • Phase 1 分類 + Phase 2 生成的兩次 LLM 調(diào)用可針對(duì)各自任務(wù)分別調(diào)參

核心服務(wù)類關(guān)系

層級(jí)關(guān)鍵類職責(zé)
Web 層AssistantController, RagControllerREST 接口
核心 pipelineDoctorAssistantService兩階段 LLM pipeline
RAG 檢索RagQueryService混合檢索編排
Query 改寫RagQueryRewriteService同義擴(kuò)展 + 領(lǐng)域詞映射
文檔攝入DocumentIngestService文檔上傳、文本提取、切片
Milvus BM25MilvusBuiltInBm25KeywordSearchMilvus 內(nèi)置 BM25 全文檢索
Milvus 建表MilvusBm25CollectionProvisionerBM25 集合 schema 自動(dòng)創(chuàng)建
Milvus v2 寫入MilvusBm25V2DocumentWriter適配 BM25 函數(shù)字段的數(shù)據(jù)寫入

3. 意圖路由:輕量分類 + RAG 按需觸發(fā)

3.1 方案選型

在要不要先做意圖分類這個(gè)問題上,我們的權(quán)衡是這樣的:

  • 不做分類,直接 RAG:簡(jiǎn)單粗暴,但每個(gè)"你好"都要跑一遍檢索鏈路,浪費(fèi)算力和延遲。
  • 兩階段 LLM:Phase 1 用輕量 prompt 做分類,Phase 2 只在需要時(shí)才觸發(fā)。

我們選擇了后者。Phase 1 的意圖分類 prompt 要求 LLM 返回嚴(yán)格的 JSON:

你是產(chǎn)品技術(shù)支持領(lǐng)域的意圖識(shí)別專家。根據(jù)用戶問題,識(shí)別意圖類型。
必須嚴(yán)格返回JSON格式:
{"intentType": "GENERAL_CONSULT"|"FIND_COMPONENT"|"VIEW_RECENT_FOLLOWUP_TASKS"}

這個(gè)階段 LLM 的推理負(fù)擔(dān)極輕——不需要理解全文,不需要檢索上下文,只需要判斷用戶想干什么。

3.2 三種意圖的處理路徑

意圖處理方式延遲
GENERAL_CONSULTLLM 直接回答,可選同步到 QA 庫
VIEW_RECENT_FOLLOWUP_TASKS調(diào)用外部用戶服務(wù)查歷史數(shù)據(jù)
FIND_COMPONENT觸發(fā)完整 RAG pipeline → LLM 生成解決方案

只有 FIND_COMPONENT 一個(gè)意圖會(huì)觸發(fā) RAG。而且這里還有一個(gè)前置校驗(yàn)——通過外部服務(wù)確認(rèn)用戶確實(shí)關(guān)聯(lián)了某種產(chǎn)品配置。如果用戶沒有相關(guān)產(chǎn)品卻問"產(chǎn)品A 故障怎么辦",系統(tǒng)不會(huì)浪費(fèi)時(shí)間去檢索文檔。

3.3 后處理門控

即使 RAG 召回了文檔,LLM 生成了回答,還有一個(gè)后處理門控:

  1. LLM 返回的答案中包含 componentCode(產(chǎn)品類型編碼)
  2. 系統(tǒng)檢查這個(gè)編碼是否與用戶實(shí)際關(guān)聯(lián)的產(chǎn)品類型一致
  3. 如果不一致——比如用戶關(guān)聯(lián)的是產(chǎn)品B,但 RAG 召回了產(chǎn)品A 的相關(guān)文檔——系統(tǒng)將此回答降級(jí)為 GENERAL_CONSULT,即只給一般性建議,不給具體操作指引

這個(gè)門控很重要,因?yàn)樵跇I(yè)務(wù)場(chǎng)景下,給錯(cuò)產(chǎn)品線的操作建議比不給更危險(xiǎn)

4. 混合檢索 pipeline:向量 + BM25 + RRF 融合

混合檢索是 RAG 系統(tǒng)的核心。我們來詳細(xì)拆解每一步。

4.1 為什么要混合檢索

純向量檢索(dense retrieval)的問題是:

  • 向量相似 ≠ 關(guān)鍵詞匹配。"產(chǎn)品C 故障"和"產(chǎn)品C 運(yùn)行異常"語義相近,但用戶搜索"產(chǎn)品C"這個(gè)詞時(shí),向量檢索不一定能準(zhǔn)確區(qū)分"產(chǎn)品C"和"產(chǎn)品A系列"的文檔。
  • 對(duì)稀有詞、專業(yè)名詞的召回容易漂移。向量模型在訓(xùn)練時(shí)可能沒見過足夠的該領(lǐng)域語料。

純關(guān)鍵詞檢索(BM25 sparse retrieval)的問題是:

  • 詞匯不匹配就沒結(jié)果。用戶說"設(shè)備發(fā)熱",BM25 搜不到寫了"機(jī)身溫度過高"的文檔。
  • 無法理解語義。"產(chǎn)品A 卡頓"和"設(shè)備無響應(yīng)"沒有共同詞,BM25 得分是 0。

兩者結(jié)合,優(yōu)勢(shì)互補(bǔ)。

4.2 技術(shù)選型:Milvus 2.5 內(nèi)置 BM25

市面上常見的混合檢索方案通常需要兩個(gè)引擎:Milvus/Qdrant 做向量 + Elasticsearch 做 BM25。維護(hù)兩套索引,還要在兩個(gè)結(jié)果集之間做融合,架構(gòu)復(fù)雜度不小。

Milvus 2.5 開始支持 內(nèi)置 BM25——通過 FunctionType.BM25 在 schema 中定義一個(gè)函數(shù),將文本字段自動(dòng)轉(zhuǎn)換為稀疏向量。檢索時(shí)用 EmbeddedText 將查詢文本發(fā)給 Milvus,服務(wù)端自動(dòng)分詞并計(jì)算 BM25 分?jǐn)?shù)。

好處很明顯:

  • 單一數(shù)據(jù)庫,不需要維護(hù) ES 集群
  • 向量檢索和 BM25 檢索可以復(fù)用同一套過濾條件(documentId、documentSource)
  • 部署簡(jiǎn)單

代價(jià)是:

  • BM25 參數(shù)調(diào)優(yōu)受限于 Milvus 的暴露程度(我們可以在建索引時(shí)配置 k1、b,但運(yùn)行時(shí)不可動(dòng)態(tài)調(diào)整)
  • 分詞依賴 Milvus 內(nèi)置的 analyzer(我們用的是 Chinese analyzer)

4.3 檢索 pipeline 代碼拆解

核心入口在 RagQueryService.retrieveForRag()

                     用戶問題
                        │
                        ▼
              queryRewriteService
                .rewriteForRetrieval()     ← 領(lǐng)域改寫(見第5節(jié))
                        │
                        ▼
          ┌─────────────┴─────────────┐
          │      hybrid enabled?       │
          └─────────────┬─────────────┘
                  Yes   │   No
          ┌─────────────┴─────────────┐
          ▼                           ▼
   ┌──────────────┐           ┌──────────────┐
   │ 并行雙路檢索   │           │ 純向量檢索     │
   │              │           │ topK=N       │
   │ 向量池: 40條 │           │ minSim=0.15  │
   │ BM25池: 24條 │           └──────────────┘
   └──────┬───────┘
          ▼
     RRF 融合 (k=60)
          │
          ▼
    截?cái)嗟?max(topK, 48)
          │
          ▼
    DashScope Rerank
          │
          ▼
    多重過濾 → 返回 topK

如果 hybrid.enabled=false,整個(gè)鏈路退化為純向量檢索,用于做對(duì)照實(shí)驗(yàn)。

4.4 向量檢索

使用 Spring AI 封裝的 Milvus 向量存儲(chǔ),配置 IVF_FLAT 索引 + COSINE 相似度:

MilvusSearchRequest request = MilvusSearchRequest.milvusBuilder()
    .query(retrievalQuery)
    .topK(topK)
    .searchParamsJson("{\"nprobe\":128}")
    .similarityThreshold(0.15)  // 過濾低分噪聲
    .build();
return milvusVectorStore.similaritySearch(request);

幾個(gè)要點(diǎn):

  • nprobe=128:IVF_FLAT 索引搜索時(shí)掃描 128 個(gè)最近的聚類中心,在召回率和速度間取平衡
  • similarityThreshold=0.15:COSINE 相似度低于 0.15 的直接丟棄,減少噪聲
  • 向量池取 max(topK, 40) 條——即使請(qǐng)求只需要 5 條,也要多召回一些給后續(xù)融合留空間

4.5 Milvus BM25 檢索

BM25 檢索通過 MilvusBuiltInBm25KeywordSearch 實(shí)現(xiàn),直接調(diào)用 Milvus v2 gRPC API:

SearchReq.builder()
    .collectionName(collectionName)
    .annsField("sparse_bm25")           // 稀疏向量字段名
    .data(List.of(new EmbeddedText(query)))  // 服務(wù)端自動(dòng)分詞
    .topK(topK)
    .metricType(IndexMetricType.BM25)
    .outputFields(List.of("id", "content", "metadata"))
    .filter(filter)                      // 與向量檢索一致的過濾條件
    .build();

關(guān)鍵設(shè)計(jì)點(diǎn):

  1. 使用 EmbeddedText 而非手動(dòng) BM25 向量化:把原始查詢文本發(fā)給 Milvus,讓服務(wù)端用建庫時(shí)相同的 Chinese analyzer 分詞后做 BM25 匹配。這保證了詞法對(duì)齊——入庫分析和查詢分析用的同一套 tokenizer。
  2. 詞法池取 max(topK, 24):比向量池的 40 小,因?yàn)閷?shí)際場(chǎng)景中查詢?cè)~一般較短(一兩句話),BM25 能有效匹配的文檔數(shù)本來就有限。
  3. BM25 絕對(duì)分過濾:我們加了一個(gè) minBm25AbsoluteScore=1.0 的閾值。BM25 分?jǐn)?shù)低于 1.0 的文檔基本沒有有意義的詞法匹配,屬于噪聲。
  4. BM25 相對(duì)分過濾:同一批次內(nèi),BM25_score / max(BM25_score) 低于 0.8 的丟棄。這個(gè)閾值可以配置。

4.6 RRF 融合

Reciprocal Rank Fusion(RRF)是一種簡(jiǎn)潔有效的分?jǐn)?shù)融合方法:

// 偽代碼
Map<docId, rrfScore> scores = new HashMap<>();

int rank = 1;
for (doc : vectorRankedList) {
    scores.merge(doc.getId(), 1.0 / (rrfK + rank), Double::sum);
    rank++;
}

rank = 1;
for (doc : keywordRankedList) {
    scores.merge(doc.getId(), 1.0 / (rrfK + rank), Double::sum);
    rank++;
}

return scores.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .limit(rerankCandidateMax)
    .toList();

RRF 公式:RRF_score(d) = Σ 1 / (k + rank_i(d)),其中 k 是阻尼常數(shù)。

為什么用 RRF 而不是簡(jiǎn)單的分?jǐn)?shù)歸一化?

傳統(tǒng)分?jǐn)?shù)融合需要做 score normalization——比如 min-max 歸一化。但問題是:

  • 向量相似度和 BM25 分?jǐn)?shù)的分布完全不同,線性歸一化的假設(shè)不成立
  • BM25 分?jǐn)?shù)理論上無上限,向量 COSINE 相似度在 -1 到 1 之間,歸一化后的"權(quán)重"很難解釋

RRF 不關(guān)心原始分?jǐn)?shù)的絕對(duì)值,只關(guān)心相對(duì)排名,天然適合異構(gòu)檢索結(jié)果的融合。

k 值的選擇:我們?cè)O(shè) rrfK=60。k 越大,排名差異的影響越小,更偏向于"同時(shí)出現(xiàn)在兩個(gè)列表中的文檔得分高"。k=60 意味著:

  • 只在向量列表中排第 1 名的文檔:1/(60+1) = 0.0164
  • 在向量列表排第 10、BM25 列表排第 5 的文檔:1/(60+10) + 1/(60+5) = 0.0143 + 0.0154 = 0.0297
  • 同時(shí)在兩個(gè)列表排第 1 的文檔:1/61 + 1/61 = 0.0328

可以看到,出現(xiàn)在兩個(gè)列表中的文檔 RRF 分明顯更高——這正是我們想要的:兩個(gè)檢索器"共識(shí)"的文檔排前面。

5. Query 改寫:領(lǐng)域詞擴(kuò)展與同義映射

5.1 為什么需要改寫

前面提到,用戶的自然語言和專業(yè)文檔的書面表述之間存在 gap。一個(gè)典型的例子:

用戶問題文檔表述
“設(shè)備發(fā)熱”“機(jī)身溫度過高”
“接口松動(dòng)”“連接端口接觸不良”
“多久換一次”“配件更換周期/更換頻率”

如果不做改寫,向量檢索和 BM25 檢索都可能漏掉真正相關(guān)的文檔。

5.2 改寫策略

RagQueryRewriteService.rewriteForRetrieval() 執(zhí)行三步處理:

Step 1: 文本歸一化(Normalize)

String normalized = original.trim().replaceAll("\\s+", " ");

最簡(jiǎn)單的清洗:去首尾空白、合并多余空格。

Step 2: 規(guī)則驅(qū)動(dòng)的詞擴(kuò)展

維護(hù)了一個(gè)包含 27 條規(guī)則的映射表,每條規(guī)則定義了一組觸發(fā)詞和對(duì)應(yīng)的擴(kuò)展詞:

rule(
    new String[] {"保養(yǎng)周期", "保養(yǎng)頻率", "多久保養(yǎng)", "檢查周期",
                  "多長(zhǎng)時(shí)間檢測(cè)", "維護(hù)頻率", "多久檢查"},
    new String[] {"常規(guī)保養(yǎng)間隔", "使用期間檢查頻率", "保養(yǎng)周期",
                  "安裝后維護(hù)", "常規(guī)保養(yǎng)"}
)

匹配邏輯是:如果用戶問題中出現(xiàn)任意一個(gè)觸發(fā)詞,就將所有擴(kuò)展詞(未在問題原文中出現(xiàn)的)追加到檢索 query 后面。

為什么用規(guī)則而不是 LLM 做改寫?

  • 確定性:規(guī)則擴(kuò)寫的結(jié)果是可預(yù)測(cè)、可調(diào)試的。LLM 改寫可能引入不確定的語義漂移。
  • 低延遲:規(guī)則匹配是 O(n) 字符串查找,毫秒級(jí)完成。LLM 改寫需要一次完整的推理,延遲 1-3 秒。
  • 可維護(hù)性:新增一個(gè)領(lǐng)域詞映射只需要加一條規(guī)則,不需要重新訓(xùn)練或調(diào) prompt。
  • 夠用:該業(yè)務(wù)領(lǐng)域的同義詞匯是有限的、可枚舉的?;ɡ锖诘?LLM 改寫是 overkill。

當(dāng)一個(gè)領(lǐng)域的同義詞是可枚舉且有限的,規(guī)則引擎就是最好的選擇。

Step 3: 產(chǎn)品上下文擴(kuò)展

如果檢測(cè)到問題涉及特定的產(chǎn)品線,自動(dòng)追加對(duì)應(yīng)術(shù)語:

if (question.contains("產(chǎn)品A") || question.contains("A系列")) {
    expansionTerms.add("產(chǎn)品A");
    expansionTerms.add("產(chǎn)品A系列(企業(yè)級(jí)高性能型號(hào))");
    // 追加產(chǎn)品A相關(guān)的故障關(guān)鍵詞
    expansionTerms.addAll(getKeywordsForProduct(PRODUCT_A));
}

最終,改寫后的檢索 query 是:

原始問題 + 擴(kuò)展詞1 擴(kuò)展詞2 擴(kuò)展詞3 ...

改寫 query 只用于檢索,不用于 LLM 生成。生成階段的 prompt 仍然使用用戶的原始問題原文。

6. DashScope Rerank 精排與多重過濾

6.1 為什么需要 Rerank

RRF 融合的結(jié)果是基于兩個(gè)檢索器各自排名的折中,但它不知道哪個(gè)文檔真正回答了用戶的問題

Rerank 模型把每個(gè)候選文檔和用戶問題做一對(duì)一的語義相關(guān)性打分,比向量相似度的 rank 更精細(xì)。DashScope 的 Rerank 模型(gte-rerank)專門為這個(gè)任務(wù)優(yōu)化過。

6.2 Rerank 調(diào)用

RRF 融合后,候選列表先截?cái)嗟?max(topK, 48) 條,然后調(diào)用 DashScope Rerank:

RerankResponse response = rerankModel.call(new RerankRequest(
    retrievalQuery,
    fusedDocs,  // candidate documents
    DashScopeRerankOptions.builder()
        .withTopN(topK)
        .build()
));

注意這里的 retrievalQuery改寫后的 query(包含擴(kuò)展詞),不是原始問題。因?yàn)?Rerank 階段希望獲得更多的檢索信號(hào)來排序。

6.3 雙重過濾

Rerank 返回的結(jié)果還要經(jīng)過兩層過濾,而且是 AND 關(guān)系:

相對(duì)分過濾(batch-normalized):

double maxScore = results.stream()
    .mapToDouble(RerankResult.Result::getRelevanceScore)
    .max().orElse(1.0);

results.stream()
    .filter(r -> r.getRelevanceScore() / maxScore >= 0.8)
    .toList();

以同一批次中最高分為基準(zhǔn),相對(duì)分低于 0.8 的丟棄。這保證了返回的文檔與問題的相關(guān)性不低于"最佳匹配文檔"的 80%。

絕對(duì)分過濾

results.stream()
    .filter(r -> r.getRelevanceScore() >= 0.3)
    .toList();

Rerank 分?jǐn)?shù)低于 0.3 表示文檔基本與問題無關(guān),直接扔掉。

兩重過濾的關(guān)系是 AND:一個(gè)文檔必須同時(shí)滿足相對(duì)分 >= 0.8×max 和絕對(duì)分 >= 0.3 才會(huì)被保留。

6.4 熱切換與降級(jí)

Rerank 是可熱切換的:

RerankModel rerank = hybridProperties.isUseRerankWhenAvailable()
    ? rerankModel.getIfAvailable() : null;

if (rerank == null) {
    // 無 rerank,直接用 RRF 結(jié)果截?cái)喾祷?
    return fused.stream().limit(topK).toList();
}

use-rerank-when-available 配置項(xiàng)設(shè)為 false 即可跳過 rerank,這對(duì)于壓測(cè)和故障降級(jí)很有用。

降級(jí)策略:如果 rerank 調(diào)用返回空結(jié)果,系統(tǒng)回退到 RRF 融合列表截?cái)?topK 輸出。但如果 rerank 返回了結(jié)果但全被過濾掉了(雙重過濾導(dǎo)致),則不降級(jí)——返回空結(jié)果,不兜底。因?yàn)?quot;找到但全不相關(guān)"比"假裝找到了"更誠(chéng)實(shí),也避免 LLM 拿到無關(guān)上下文后胡編亂造。

7. 文檔攝入:分詞策略與 Milvus BM25 自動(dòng)入庫

7.1 上傳與文本提取

文檔上傳通過 REST 接口:

POST /api/rag/documents
Content-Type: multipart/form-data

file: 產(chǎn)品手冊(cè).pdf
documentId: product_manual_v2
documentSource: product_kb

后端使用 Apache Tika 做文本提取,支持 PDF、DOCX、XLSX、HTML 等常見格式:

String rawText = TikaTextExtractor.extract(inputStream, filename);

7.2 兩種切片策略

DocumentIngestService 提供兩種切片策略,通過 jmyzt.rag.ingest.strategy 配置:

FIXED_LENGTH(固定長(zhǎng)度滑動(dòng)窗口)

int maxSize = 1200;   // 每塊最多 1200 字符
int overlap = 150;    // 塊間重疊 150 字符

int start = 0;
while (start < text.length()) {
    int end = Math.min(text.length(), start + maxSize);
    String chunk = text.substring(start, end).strip();
    start = Math.max(end - overlap, start + 1);  // 避免死循環(huán)
}

適合結(jié)構(gòu)規(guī)整的文檔。1200 字符大約 400-600 個(gè)中文字,是一個(gè)比較適中的上下文窗口。

PARAGRAPH_THEN_FIXED(先按段落切,長(zhǎng)段落再固定切)

// 先按空行切段落
String[] paragraphs = text.split("\\n\\s*\\n+");
for (String para : paragraphs) {
    if (para.length() <= maxChunkChars) {
        chunks.add(para);  // 短段落直接作為一個(gè) chunk
    } else {
        // 長(zhǎng)段落遞歸按固定長(zhǎng)度切
        splitLongParagraphFixedRecursive(para, chunks);
    }
}

適合有明確段落結(jié)構(gòu)的產(chǎn)品技術(shù)文檔。保留了文檔的自然段落邊界,檢索時(shí)上下文的連貫性更好。

7.3 解決 Spring AI v1 InsertParam 與 BM25 函數(shù)字段的兼容問題

這是整個(gè)項(xiàng)目最"工程化"的一個(gè)問題。

Milvus 的 BM25 函數(shù)字段(FunctionType.BM25)是由服務(wù)端從 content 字段自動(dòng)生成的。在插入數(shù)據(jù)時(shí),不應(yīng)該手動(dòng)給這個(gè)字段賦值。

但 Spring AI 1.x 的 MilvusVectorStore 使用的是 Milvus v1 gRPC 客戶端(InsertParam),它會(huì)嘗試給 schema 中定義的所有字段賦值。當(dāng)遇到 sparse_bm25 字段時(shí),Spring AI 沒有對(duì)應(yīng)的數(shù)據(jù),就報(bào)錯(cuò)了:

The field: sparse_bm25 is not provided

解決方案:繞過 Spring AI,直接使用 MilvusClientV2.insert(),在構(gòu)建插入請(qǐng)求時(shí)故意不包含 sparse_bm25 字段

// MilvusBm25V2DocumentWriter.insertDocuments()
for (Document doc : documents) {
    JsonObject row = new JsonObject();
    row.addProperty("id", doc.getId());
    row.addProperty("content", doc.getText());
    row.addProperty("metadata", metadataJson);
    row.add("embedding", embeddingArray);  // 手動(dòng)嵌入
    // 注意:不添加 sparse_bm25 字段!
    // Milvus 服務(wù)端 BM25 函數(shù)會(huì)自動(dòng)填充
    data.add(row);
}

InsertReq insertReq = InsertReq.builder()
    .collectionName(collectionName)
    .data(data)
    .build();

milvusClientV2.insert(insertReq);

嵌入向量的生成也是手動(dòng)調(diào) EmbeddingModel.embed() 完成的,繞開了 Spring AI 的自動(dòng)嵌入流程。

7.4 Collection Schema 自動(dòng)建表

MilvusBm25CollectionProvisioner 在應(yīng)用啟動(dòng)時(shí)自動(dòng)執(zhí)行,支持三種模式(milvus-bm25-collection-bootstrap 配置):

模式行為
none不做任何操作,假設(shè)集合已存在
create-if-missing如果集合不存在則創(chuàng)建(生產(chǎn)推薦)
recreate刪除已有集合并重建(數(shù)據(jù)丟失! 僅開發(fā)用)

創(chuàng)建的集合 schema 包含:

字段名類型說明
idVarChar(36)主鍵
contentVarChar(65535)文本內(nèi)容,Chinese analyzer 分詞
metadataJSON文檔元信息(documentId, documentName 等)
embeddingFloatVector(1536)文本嵌入向量
sparse_bm25SparseFloatVectorBM25 函數(shù)輸出字段

BM25 函數(shù)定義:

schema.addFunction(CreateCollectionReq.Function.builder()
    .name("content_bm25")
    .functionType(FunctionType.BM25)
    .inputFieldNames(List.of("content"))       // 輸入:文本字段
    .outputFieldNames(List.of("sparse_bm25"))  // 輸出:稀疏向量字段
    .build());

BM25 索引參數(shù):

Map<String, Object> sparseParams = new HashMap<>();
sparseParams.put("inverted_index_algo", "DAAT_MAXSCORE");
sparseParams.put("bm25_k1", 1.2);    // 詞頻飽和度參數(shù)
sparseParams.put("bm25_b", 0.75);    // 文檔長(zhǎng)度歸一化參數(shù)

k1=1.2 和 b=0.75 是 Okapi BM25 的標(biāo)準(zhǔn)默認(rèn)值。在這個(gè)中文長(zhǎng)文檔場(chǎng)景中我們沒有做特別調(diào)整,因?yàn)闃?biāo)準(zhǔn)參數(shù)對(duì)于中文長(zhǎng)文檔通常表現(xiàn)不錯(cuò)。

索引就緒等待:創(chuàng)建集合后,啟動(dòng)時(shí)會(huì)輪詢 describeIndex 等待索引構(gòu)建完成(最多 120 次 × 500ms = 60 秒),避免索引未就緒時(shí)查詢返回空結(jié)果。

8. 踩過的坑與調(diào)參經(jīng)驗(yàn)

8.1 向量池和 BM25 池不宜過大

最開始我們?cè)O(shè)向量池 100、BM25 池 100,覺得"多召回一些,反正后續(xù)有 rerank 篩選"。結(jié)果:

  • Milvus 檢索時(shí)間從幾十毫秒飆到幾百毫秒
  • Rerank 階段需要處理近 200 條候選,單次 rerank 調(diào)用耗時(shí) 3-5 秒
  • 大部分候選文檔的分?jǐn)?shù)很低,徒增延遲

現(xiàn)在穩(wěn)定在向量池 40、BM25 池 24、rerank 候選上限 48。實(shí)測(cè)延遲降到 1.2 秒以內(nèi)(全鏈路)。

原則:多路檢索的池大小是"夠用就好",不要貪多。

8.2 Rerank 后的絕對(duì)分過濾比相對(duì)分過濾更重要

一開始我們只設(shè)置了相對(duì)分過濾(minRelativeRetrievalScore=0.8),沒有絕對(duì)分過濾。發(fā)現(xiàn)在某些邊緣 query 上——用戶問"今天天氣怎么樣"——Rerank 返回的所有文檔分?jǐn)?shù)都很低(~0.1),歸一化后最高分也只有 0.1,相對(duì)分過濾形同虛設(shè)(0.1/0.1=1.0,全通過)。

加上 minRerankAbsoluteScore=0.3 后,這種情況會(huì)被正確攔截——所有 rerank 分低于 0.3 的文檔直接丟棄,返回空列表,LLM 至少知道"沒有找到相關(guān)文檔"而不是對(duì)著無關(guān)內(nèi)容瞎編。

8.3 Spring AI 版本不匹配問題

Spring AI 1.1.2 的 Milvus 集成基于 v1 gRPC 客戶端,而 Milvus 2.5 的部分功能(EmbeddedText、BM25 函數(shù))只在 v2 gRPC 中可用。這導(dǎo)致了兩個(gè) API 路徑共存:

  • 向量檢索:走 Spring AI(v1)
  • BM25 檢索:直接調(diào) MilvusClientV2(v2)
  • 文檔寫入:繞開 Spring AI,用自定義的 v2 writer

教訓(xùn):框架的封裝層不一定總是夠用。對(duì)于前沿功能(如 Milvus 2.5 內(nèi)置 BM25),要做好繞過框架直接調(diào)底層 SDK 的準(zhǔn)備。 我們做了封裝隔離——只在底層交互類中依賴 v2 SDK,上層業(yè)務(wù)邏輯仍然通過統(tǒng)一接口調(diào)用。

8.4 Query 改寫的擴(kuò)展詞要有"節(jié)制"

最開始我們給每條規(guī)則加了很多擴(kuò)展詞,比如"故障"觸發(fā)詞映射到十幾個(gè)擴(kuò)展詞。結(jié)果某些 query 改寫得非常長(zhǎng)(幾百字),反而稀釋了核心關(guān)鍵詞的信號(hào)。

現(xiàn)在的策略是每條規(guī)則 3-5 個(gè)精準(zhǔn)的擴(kuò)展詞。寧可少寫,不要多寫。

8.5 RRF 的 k 值調(diào)優(yōu)

k=60 不是拍腦袋定的。我們?cè)陂_發(fā)環(huán)境做了小規(guī)模對(duì)比:

k 值效果
k=0極端重視高 rank,排第一的文檔權(quán)重極大。結(jié)果:幾乎退化為"取兩個(gè)列表的交集排序",單路表現(xiàn)差的 query 全掛
k=60當(dāng)前的平衡點(diǎn),共識(shí)文檔有優(yōu)勢(shì),但單路高排名的文檔也不會(huì)被完全埋沒
k=∞所有 rank 權(quán)重相同,退化為"在兩個(gè)列表中都出現(xiàn)的排前面,都沒出現(xiàn)的隨機(jī)排"

k=60 在開發(fā)和測(cè)試階段的表現(xiàn)穩(wěn)定,所以沿用到了生產(chǎn)。

8.6 中文分詞的 BM25 分析器選擇

Milvus 的 Chinese analyzer 對(duì)中文文本的處理包括分詞和停用詞過濾。我們發(fā)現(xiàn)通用 Chinese analyzer 對(duì)垂直領(lǐng)域的專業(yè)術(shù)語分詞效果有一些偏差——例如 “產(chǎn)品A系列” 可能被切為 “產(chǎn)品” + “A” + “系列”,“部件故障” 可能被切為 “部件” + “故障”。

目前通過在 query 改寫階段追加完整的專業(yè)詞匯作為緩解手段(即使分析器誤切,也能通過擴(kuò)展后的完整詞匹配到),后續(xù)考慮使用自定義詞典來進(jìn)一步優(yōu)化。

9. 總結(jié)與下一步

9.1 這套方案的核心價(jià)值

  1. 意圖優(yōu)先,RAG 按需觸發(fā):不是所有請(qǐng)求都需要全套檢索,兩階段 pipeline 在延遲和效果之間取得了好的平衡。
  2. 向量 + BM25 混合檢索:?jiǎn)我粩?shù)據(jù)庫(Milvus 2.5 內(nèi)置 BM25)搞定,不需要維護(hù)多個(gè)存儲(chǔ)引擎。
  3. 規(guī)則驅(qū)動(dòng)的 query 改寫:領(lǐng)域知識(shí)顯式編碼,可調(diào)試、可維護(hù),延遲為零。
  4. 多層過濾漏斗:從向量相似度閾值 → BM25 分過濾 → RRF 融合 → Rerank 雙重過濾,每層砍掉一部分噪聲,最終喂給 LLM 的是高度相關(guān)的上下文。
  5. 后處理門控:答案與用戶實(shí)際情況交叉校驗(yàn),降低風(fēng)險(xiǎn)。

9.2 當(dāng)前局限與改進(jìn)方向

  • Query 改寫依賴規(guī)則維護(hù):新增產(chǎn)品線時(shí)需要手動(dòng)加規(guī)則。未來可以考慮用 LLM 生成候選擴(kuò)展詞 + 人工審核的半自動(dòng)化流程。
  • BM25 分詞精度:通用 Chinese analyzer 對(duì)領(lǐng)域?qū)I(yè)術(shù)語的分詞有偏差,可以引入自定義詞典。
  • Rerank 是唯一的外部依賴:如果 DashScope Rerank 服務(wù)不可用,系統(tǒng)雖有降級(jí)策略但排序質(zhì)量會(huì)下降??紤]評(píng)估本地 Cross-Encoder 模型作為備選。
  • 評(píng)估體系缺失:目前靠人工抽樣評(píng)估檢索質(zhì)量。下一步需要構(gòu)建標(biāo)注數(shù)據(jù)集和自動(dòng)化評(píng)估

9.3 關(guān)鍵配置速查

配置項(xiàng)用途
hybrid.vector-pool-top-k40向量召回候選數(shù)
hybrid.keyword-pool-top-k24BM25 召回候選數(shù)
hybrid.rrf-k60RRF 阻尼常數(shù)
hybrid.rerank-candidate-max48Rerank 輸入上限
hybrid.min-vector-similarity0.15向量相似度閾值
hybrid.min-bm25-absolute-score1.0BM25 絕對(duì)分閾值
hybrid.min-relative-retrieval-score0.8Rerank 相對(duì)分閾值
hybrid.min-rerank-absolute-score0.3Rerank 絕對(duì)分閾值
rag.ingest.max-chunk-chars1200切片大小
rag.ingest.overlap-chars150切片重疊量

以上就是基于Spring AI+Milvus的RAG混合檢索的實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于Spring AI Milvus RAG混合檢索的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java?IO與NIO高效的輸入輸出操作深入探究

    Java?IO與NIO高效的輸入輸出操作深入探究

    這篇文章主要為大家介紹了Java?IO與NIO高效的輸入輸出操作深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • idea撤銷git?commit操作詳解

    idea撤銷git?commit操作詳解

    這篇文章主要為大家介紹了idea撤銷git?commit操作詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • springboot訪問請(qǐng)求404的原因及解決辦法

    springboot訪問請(qǐng)求404的原因及解決辦法

    在使用Spring Boot開發(fā)應(yīng)用程序時(shí),有時(shí)可能會(huì)遇到訪問請(qǐng)求出現(xiàn)404錯(cuò)誤的情況,即請(qǐng)求的資源未找到,這篇文章主要給大家介紹了關(guān)于springboot訪問請(qǐng)求404的原因及解決辦法,需要的朋友可以參考下
    2023-09-09
  • springboot中如何通過main方法調(diào)用service或dao

    springboot中如何通過main方法調(diào)用service或dao

    這篇文章主要介紹了springboot中如何通過main方法調(diào)用service或dao,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • 一文教你學(xué)會(huì)搭建SpringBoot分布式項(xiàng)目

    一文教你學(xué)會(huì)搭建SpringBoot分布式項(xiàng)目

    這篇文章主要為大家詳細(xì)介紹了搭建SpringBoot分布式項(xiàng)目的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-01-01
  • SpringBoot對(duì)數(shù)據(jù)庫用戶名和密碼進(jìn)行加密的兩種方案

    SpringBoot對(duì)數(shù)據(jù)庫用戶名和密碼進(jìn)行加密的兩種方案

    在當(dāng)今互聯(lián)網(wǎng)時(shí)代,?數(shù)據(jù)庫安全的重要性不言而喻,作為Java開發(fā)的主流框架,Spring Boot項(xiàng)目通常需要連接數(shù)據(jù)庫,而數(shù)據(jù)庫的?用戶名和密碼作為最敏感的信息之一,所以本文給大家詳細(xì)介紹兩種主流的數(shù)據(jù)源配置加密方案,需要的朋友可以參考下
    2025-12-12
  • java 操作gis geometry類型數(shù)據(jù)方式

    java 操作gis geometry類型數(shù)據(jù)方式

    這篇文章主要介紹了java 操作gis geometry類型數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • SpringMVC中的handlerMappings對(duì)象用法

    SpringMVC中的handlerMappings對(duì)象用法

    這篇文章主要介紹了SpringMVC中的handlerMappings對(duì)象用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • SpringBoot整合ELK做日志超完整詳細(xì)教程

    SpringBoot整合ELK做日志超完整詳細(xì)教程

    本文詳細(xì)介紹了如何在springboot中快速接入ELK的過程,ELK可以說在實(shí)際項(xiàng)目中具有很好的適用價(jià)值,不管是小項(xiàng)目,還是中大型項(xiàng)目,都具備普適參考性,值得深入了解和學(xué)習(xí),感興趣的朋友一起看看吧
    2024-01-01
  • Mybatis-plus默認(rèn)不能更新null字段的問題及解決過程

    Mybatis-plus默認(rèn)不能更新null字段的問題及解決過程

    Mybatis-plus默認(rèn)忽略null值導(dǎo)致update失敗,解決方案包括單個(gè)配置(每個(gè)字段加注解)和全局配置(統(tǒng)一配置),全局配置簡(jiǎn)單高效,但需要靈活配置時(shí)可結(jié)合單個(gè)配置,注意配置的是update-strategy
    2025-11-11

最新評(píng)論

精河县| 微博| 天峨县| 日照市| 灌阳县| 平乐县| 甘孜| 彰武县| 沛县| 翁牛特旗| 嘉兴市| 南开区| 太白县| 常州市| 吉首市| 金阳县| 大冶市| 莱西市| 屏南县| 徐闻县| 土默特左旗| 昂仁县| 乐安县| 贵定县| 广德县| 姚安县| 孝义市| 海原县| 定州市| 莒南县| 怀远县| 青神县| 白银市| 嘉善县| 太原市| 浙江省| 漯河市| 邻水| 仪征市| 翁源县| 桐乡市|