展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > 基于SpringAI+Milvus的RAG混合检索的实战指南

基于SpringAI+Milvus的RAG混合检索的实战指南

Air Explorer Pro
Air Explorer Pro

Air Explorer Pro是一款跨平台多网盘管理工具,可实现一站式完成不同网盘间文件的互传、搜索和同步等操作。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

¥99
Windows/macOS 2026-08-22
正版软件 Windows macOS软件 Pro 文件搜索工具 Pro下载 Pro正版软件 Pro购买
立即下载

1. 背景与动机 1.1 业务场景 这次要解决的问题很明确:给一个垂直业务平台搭建智能客服助手。用户会直接描述问题,比如“产品A启动不了该怎么办”,或者“产品B的保养周期是多久”,系统需要给出准确、可操作的解决方案。 数据方面已经有了大量按产品线(A、B、C、D、E)分类的技术文档,主要是PDF和D

本文目录

    1. 背景与动机

    1.1 业务场景

    这次要解决的问题很明确:给一个垂直业务平台搭建智能客服助手。用户会直接描述问题,比如“产品A启动不了该怎么办”,或者“产品B的保养周期是多久”,系统需要给出准确、可操作的解决方案。

    基于SpringAI+Milvus的RAG混合检索的实战指南

    数据方面已经有了大量按产品线(A、B、C、D、E)分类的技术文档,主要是PDF和DOCX格式。现在要做的是搭一套检索增强生成(RAG)系统,把这些文档“喂”给大模型,让它能输出结构化的解决方案,而不是天马行空地乱说。

    1.2 核心挑战

    如果只是做一个“用户提问 → 向量检索 → LLM回答”的常规RAG,数据能洗干净吗?坦白讲,不够。

    实际落地会碰到几个硬骨头:

    1. 意图太杂:用户不是说每句话都需要翻文档的。像“你好”这种打招呼,或者“我上次的工单怎么样了”这种查记录,直接走LLM或者查数据库就行。每次无差别触发全套检索,完全是浪费资源。
    2. 精准召回是个老大难:纯向量检索对专业术语的理解能力有限。Most of the time,用户说“产品C运行异常”和文档里的“产品C系统故障”,在语义空间里可能很近,但纯向量一搜,关键信息可能就漏掉了。
    3. 检索噪声大:一次召回40条候选,里面可能超过一半是与问题不相关的。把这些噪音一股脑塞给大模型,不但费钱(token消耗),还容易把大模型带偏。
    4. 用户和文档说话的方式不一样:用户嘴里说“设备发热”,但写文档的人用的是“机身温度过高”;用户说“接口松动”,文档写的是“连接端口接触不良”。不做点query扩展,根本检索不到目标内容。

    1.3 选型

    组件选型理由
    框架Spring Boot 3.4.5 + Spring AI 1.1.2Ja va生态成熟,Spring AI对向量存储和LLM调用有很好的封装
    向量数据库Milvus 2.5.6原生支持BM25内置函数,省去了额外维护一个ES
    嵌入模型DashScope text-embedding-v21536维,中文效果不错
    LLMDashScope Qwen两阶段调用,意图分类和答案生成用同一模型,切换system prompt就行
    RerankDashScope Rerank跟LLM同一家供应商,延迟比较可控

    2. 整体架构概览

    整个问答系统的主流程分两大阶段:

    ┌─────────────────────────────────────────────────────────┐
    │                    用户输入(问题文本)                    │
    └─────────────────────┬───────────────────────────────────┘
                          ▼
    ┌─────────────────────────────────────────────────────────┐
    │              Phase 1: 意图分类(轻量 LLM 调用)           │
    │                                                         │
    │  ┌─────────────┐  ┌──────────────────┐  ┌────────────┐ │
    │  │GENERAL      │  │VIEW_RECENT       │  │FIND        │ │
    │  │_CONSULT     │  │_FOLLOWUP_TASKS   │  │_COMPONENT  │ │
    │  │一般咨询      │  │查看历史记录       │  │查找解决方案  │ │
    │  └──────┬──────┘  └────────┬─────────┘  └─────┬──────┘ │
    └─────────┼──────────────────┼──────────────────┼────────┘
              │                  │                  │
              ▼                  ▼                  ▼
       LLM 直接回复      查数据库返回         触发 Phase 2
       (可选同步 QA 库)   历史记录             RAG 检索链路
                                                 │
                                                 ▼
                              ┌─────────────────────────────────┐
                              │    Phase 2: RAG 混合检索         │
                              │                                 │
                              │  Query 改写 → 向量检索 + BM25    │
                              │     → RRF 融合 → Rerank          │
                              │     → 后处理门控 → LLM 生成      │
                              └─────────────────────────────────┘
    

    核心设计思路是:意图优先,RAG按需触发。只有判断为FIND_COMPONENT意图时,才会走完整的混合检索链路。这带来了几个好处:

    • 不必要的LLM调用次数大幅减少(Phase 1和Phase 2各自只调一次LLM)
    • 平均延迟显著降低(大部分请求根本不需要走RAG)
    • 两个阶段的LLM调用可以针对各自的轻量分类和重量生成分别调参

    核心服务类的职责分工:

    层级关键类职责
    Web 层AssistantController, RagControllerREST 接口
    核心 pipelineDoctorAssistantService两阶段 LLM pipeline
    RAG 检索RagQueryService混合检索编排
    Query 改写RagQueryRewriteService同义扩展 + 领域词映射
    文档摄入DocumentIngestService文档上传、文本提取、切片
    Milvus BM25MilvusBuiltInBm25KeywordSearchMilvus 内置 BM25 全文检索
    Milvus 建表MilvusBm25CollectionProvisionerBM25 集合 schema 自动创建
    Milvus v2 写入MilvusBm25V2DocumentWriter适配 BM25 函数字段的数据写入

    3. 意图路由:轻量分类 + RAG 按需触发

    3.1 方案选型

    要不要在入口处先做意图分类?这个问题的权衡其实很直接:

    • 不做分类,直接RAG:简单是真简单,但每一个“你好”都要跑一遍检索,不仅浪费算力,延迟也上去了。
    • 两阶段LLM:Phase 1用轻量prompt做分类,Phase 2只在确认需要时触发。

    我们选了后者。Phase 1的意图分类prompt强制LLM返回严格的JSON格式:

    你是产品技术支持领域的意图识别专家。根据用户问题,识别意图类型。
    必须严格返回JSON格式:
    {"intentType": "GENERAL_CONSULT"|"FIND_COMPONENT"|"VIEW_RECENT_FOLLOWUP_TASKS"}
    

    这个阶段的推理负担极轻——不需要理解全文,不需要检索上下文,只需要判断用户到底想干什么。

    3.2 三种意图的处理路径

    意图处理方式延迟
    GENERAL_CONSULTLLM 直接回答,可选同步到 QA 库低
    VIEW_RECENT_FOLLOWUP_TASKS调用外部用户服务查历史数据低
    FIND_COMPONENT触发完整 RAG pipeline → LLM 生成解决方案高

    只有FIND_COMPONENT这一个意图会触发RAG。这里还有一道前置校验——通过外部服务确认用户确实关联了某种产品配置。如果用户没有相关产品却来问“产品A故障怎么办”,系统压根不会浪费时间去检索文档。

    3.3 后处理门控

    即使RAG召回了文档,LLM生成了回答,最后还有一关要过:

    1. LLM返回的答案中必须包含componentCode(产品类型编码)
    2. 系统检查这个编码是否与用户实际关联的产品类型一致
    3. 如果不一致——比如用户用的是产品B,但RAG召回了产品A的文档——系统会将回答降级为GENERAL_CONSULT,也就是只给一般性建议,绝不给具体操作指引

    这个门控很关键,因为在业务场景下,给错产品线的操作建议,比不给建议更危险。

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

    混合检索是整个RAG系统的核心,我们来逐个步骤拆解。

    4.1 为什么要混合检索

    纯向量检索(dense retrieval)的问题在于:

    • 向量相似 ≠ 关键词匹配。 “产品C故障”和“产品C运行异常”语义相近,但用户搜“产品C”这个词时,向量检索不一定能准确把“产品C”和“产品A系列”的文档区分开。
    • 对稀有词、专业名词的召回容易跑偏。向量模型在训练时很可能没见过足够多的该领域语料。

    纯关键词检索(BM25 sparse retrieval)的问题在于:

    • 词汇不匹配就没结果。用户说“设备发热”,BM25搜不到写了“机身温度过高”的文档。
    • 无法理解语义。“产品A卡顿”和“设备无响应”没有共同词,BM25得分直接是0。

    两者结合,才能优势互补。

    4.2 技术选型:Milvus 2.5 内置 BM25

    市面上的混合检索方案,通常需要两个引擎:Milvus/Qdrant做向量 + Elasticsearch做BM25。维护两套索引,还要在两个结果集之间做融合,架构复杂度不小。

    Milvus 2.5开始支持内置BM25——通过FunctionType.BM25在schema中定义一个函数,将文本字段自动转换为稀疏向量。检索时用EmbeddedText将查询文本发给Milvus,服务端自动分词并计算BM25分数。

    好处很明显:

    • 单一数据库,不需要维护ES集群
    • 向量检索和BM25检索可以复用同一套过滤条件(documentId、documentSource)
    • 部署简单,运维成本低

    当然也有代价:

    • BM25参数调优受限于Milvus的暴露程度(可以在建索引时配置k1、b,但运行时不可动态调整)
    • 分词依赖Milvus内置的analyzer(我们用的是Chinese analyzer)

    4.3 检索 pipeline 代码拆解

    核心入口在RagQueryService.retrieveForRag():

                         用户问题
                            │
                            ▼
                  queryRewriteService
                    .rewriteForRetrieval()     ← 领域改写(见第5节)
                            │
                            ▼
              ┌─────────────┴─────────────┐
              │      hybrid enabled?       │
              └─────────────┬─────────────┘
                      Yes   │   No
              ┌─────────────┴─────────────┐
              ▼                           ▼
       ┌──────────────┐           ┌──────────────┐
       │ 并行双路检索   │           │ 纯向量检索     │
       │              │           │ topK=N       │
       │ 向量池: 40条 │           │ minSim=0.15  │
       │ BM25池: 24条 │           └──────────────┘
       └──────┬───────┘
              ▼
         RRF 融合 (k=60)
              │
              ▼
        截断到 max(topK, 48)
              │
              ▼
        DashScope Rerank
              │
              ▼
        多重过滤 → 返回 topK
    

    如果hybrid.enabled=false,整个链路会退化为纯向量检索,方便做对照实验。

    4.4 向量检索

    使用Spring AI封装的Milvus向量存储,配置IVF_FLAT索引 + COSINE相似度:

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

    几个要点:

    • nprobe=128:IVF_FLAT索引搜索时扫描128个最近的聚类中心,在召回率和速度之间取平衡
    • similarityThreshold=0.15:COSINE相似度低于0.15的文档直接丢弃,从源头减少噪声
    • 向量池取max(topK, 40)条——即使请求只需要5条,也要多召回一些,给后续融合留出空间

    4.5 Milvus BM25 检索

    BM25检索通过MilvusBuiltInBm25KeywordSearch实现,直接调用Milvus v2 gRPC API:

    SearchReq.builder()
        .collectionName(collectionName)
        .annsField("sparse_bm25")           // 稀疏向量字段名
        .data(List.of(new EmbeddedText(query)))  // 服务端自动分词
        .topK(topK)
        .metricType(IndexMetricType.BM25)
        .outputFields(List.of("id", "content", "metadata"))
        .filter(filter)                      // 与向量检索一致的过滤条件
        .build();
    

    关键设计点:

    1. 使用EmbeddedText而非手动BM25向量化:把原始查询文本发给Milvus,让服务端用建库时相同的Chinese analyzer分词后做BM25匹配。这保证了词法对齐——入库分析和查询分析用的是同一套tokenizer。
    2. 词法池取max(topK, 24)条:比向量池的40小,因为实际场景中查询词一般较短(一两句话),BM25能有效匹配的文档数本就有限。
    3. BM25绝对分过滤:加了minBm25AbsoluteScore=1.0的阈值。BM25分数低于1.0的文档基本没有有意义的词法匹配,属于噪声。
    4. BM25相对分过滤:同一批次内,BM25_score / max(BM25_score)低于0.8的丢弃。这个阈值可以按需配置。

    4.6 RRF 融合

    Reciprocal Rank Fusion(RRF)是一种简洁有效的分数融合方法:

    // 伪代码
    Map 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.comparingByValue().reversed())
        .limit(rerankCandidateMax)
        .toList();
    

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

    为什么用RRF,而不是简单的分数归一化?

    传统分数融合需要做score normalization——比如min-max归一化。但问题是:

    • 向量相似度和BM25分数的分布完全不同,线性归一化的假设不成立
    • BM25分数理论上无上限,向量COSINE相似度在 -1 到 1 之间,归一化后的“权重”很难解释

    RRF不关心原始分数的绝对值,只关心相对排名,天然适合异构检索结果的融合。

    k值的选择:我们设rrfK=60。k越大,排名差异的影响越小,更偏向于“同时出现在两个列表中的文档得分高”。k=60意味着:

    • 只在向量列表中排第1名的文档:1/(60+1) = 0.0164
    • 在向量列表排第10、BM25列表排第5的文档:1/(60+10) + 1/(60+5) = 0.0143 + 0.0154 = 0.0297
    • 同时在两个列表排第1的文档:1/61 + 1/61 = 0.0328

    可以看到,出现在两个列表中的文档RRF分明显更高——这正是我们想要的:两个检索器“共识”的文档排前面。

    5. Query 改写:领域词扩展与同义映射

    5.1 为什么需要改写

    前面提到,用户的自然语言和专业文档的书面表述之间存在gap。一个典型的例子:

    用户问题文档表述
    “设备发热”“机身温度过高”
    “接口松动”“连接端口接触不良”
    “多久换一次”“配件更换周期/更换频率”

    如果不做改写,无论是向量检索还是BM25检索,都可能漏掉真正相关的文档。

    5.2 改写策略

    RagQueryRewriteService.rewriteForRetrieval()执行三步处理:

    Step 1: 文本归一化(Normalize)

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

    最简单的清洗:去首尾空白、合并多余空格。

    Step 2: 规则驱动的词扩展

    维护了一个包含27条规则的映射表,每条规则定义了一组触发词和对应的扩展词:

    rule(
        new String[] {"保养周期", "保养频率", "多久保养", "检查周期",
                      "多长时间检测", "维护频率", "多久检查"},
        new String[] {"常规保养间隔", "使用期间检查频率", "保养周期",
                      "安装后维护", "常规保养"}
    )
    

    匹配逻辑是:如果用户问题中间出现任意一个触发词,就将所有扩展词(未在问题原文中间出现的)追加到检索query后面。

    为什么用规则而不是LLM做改写?

    • 确定性:规则扩写的结果是可预测、可调试的。LLM改写可能引入不确定的语义漂移。
    • 低延迟:规则匹配是O(n)字符串查找,毫秒级完成。LLM改写需要一次完整的推理,延迟1-3秒。
    • 可维护性:新增一个领域词映射只需要加一条规则,不需要重新训练或调prompt。
    • 够用:该业务领域的同义词汇是有限的、可枚举的。花里胡哨的LLM改写是overkill。

    当一个领域的同义词是可枚举且有限的,规则引擎就是最好的选择。

    Step 3: 产品上下文扩展

    如果检测到问题涉及特定产品线,自动追加对应术语:

    if (question.contains("产品A") || question.contains("A系列")) {
        expansionTerms.add("产品A");
        expansionTerms.add("产品A系列(企业级高性能型号)");
        // 追加产品A相关的故障关键词
        expansionTerms.addAll(getKeywordsForProduct(PRODUCT_A));
    }
    

    最终,改写后的检索query是:

    原始问题 + 扩展词1 扩展词2 扩展词3 ...
    

    改写后的query只用于检索,不用于LLM生成。生成阶段的prompt仍然使用用户原始的提问文本。

    6. DashScope Rerank 精排与多重过滤

    6.1 为什么需要 Rerank

    RRF融合的结果是基于两个检索器各自排名的折中,但它不知道哪个文档真正回答了用户的问题。

    Rerank模型把每个候选文档和用户问题做一对一的语义相关性打分,比向量相似度的rank更精细。DashScope的Rerank模型(gte-rerank)专门为这个任务优化过。

    6.2 Rerank 调用

    RRF融合后,候选列表先截断到max(topK, 48)条,然后调用DashScope Rerank:

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

    注意这里的retrievalQuery是改写后的query(包含扩展词),不是原始问题。因为Rerank阶段希望获得更多的检索信号来排序。

    6.3 双重过滤

    Rerank返回的结果还要经过两层过滤,而且是AND关系:

    相对分过滤(batch-normalized):

    double maxScore = results.stream()
        .mapToDouble(RerankResult.Result::getRelevanceScore)
        .max().orElse(1.0);
    
    results.stream()
        .filter(r -> r.getRelevanceScore() / maxScore >= 0.8)
        .toList();
    

    以同一批次中最高分为基准,相对分低于0.8的丢弃。这保证了返回的文档相关性不低于“最佳匹配文档”的80%。

    绝对分过滤:

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

    Rerank分数低于0.3的文档基本与问题无关,直接扔掉。

    两重过滤的关系是AND:一个文档必须同时满足相对分 >= 0.8×max 和绝对分 >= 0.3 才会被保留。

    6.4 热切换与降级

    Rerank是可热切换的:

    RerankModel rerank = hybridProperties.isUseRerankWhenA vailable()
        ? rerankModel.getIfA vailable() : null;
    
    if (rerank == null) {
        // 无 rerank,直接用 RRF 结果截断返回
        return fused.stream().limit(topK).toList();
    }
    

    use-rerank-when-a vailable配置项设为false即可跳过rerank,这对于压测和故障降级很有用。

    降级策略:如果rerank调用返回空结果,系统回退到RRF融合列表截断topK输出。但如果rerank返回了结果但全被过滤掉了(双重过滤导致),则不降级——返回空结果,不兜底。因为“找到但全不相关”比“假装找到了”更诚实,也避免LLM拿到无关上下文后胡编乱造。

    7. 文档摄入:分词策略与 Milvus BM25 自动入库

    7.1 上传与文本提取

    文档上传通过REST接口:

    POST /api/rag/documents
    Content-Type: multipart/form-data
    
    file: 产品手册.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(固定长度滑动窗口):

    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);  // 避免死循环
    }
    

    适合结构规整的文档。1200字符大约400-600个中文字,是一个比较适中的上下文窗口。

    PARAGRAPH_THEN_FIXED(先按段落切,长段落再固定切):

    // 先按空行切段落
    String[] paragraphs = text.split("\n\s*\n+");
    for (String para : paragraphs) {
        if (para.length() <= maxChunkChars) {
            chunks.add(para);  // 短段落直接作为一个 chunk
        } else {
            // 长段落递归按固定长度切
            splitLongParagraphFixedRecursive(para, chunks);
        }
    }
    

    适合有明确段落结构的产品技术文档。保留了文档的自然段落边界,检索时上下文的连贯性更好。

    7.3 解决 Spring AI v1 InsertParam 与 BM25 函数字段的兼容问题

    这是整个项目中最“工程化”的一个坑。

    Milvus的BM25函数字段(FunctionType.BM25)是由服务端从content字段自动生成的。在插入数据时,不应该手动给这个字段赋值。

    但Spring AI 1.x的MilvusVectorStore使用的是Milvus v1 gRPC客户端(InsertParam),它会尝试给schema中定义的所有字段赋值。当遇到sparse_bm25字段时,Spring AI没有对应的数据,直接就报错了:

    The field: sparse_bm25 is not provided
    

    解决方案:绕过Spring AI,直接使用MilvusClientV2.insert(),在构建插入请求时故意不包含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);  // 手动嵌入
        // 注意:不添加 sparse_bm25 字段!
        // Milvus 服务端 BM25 函数会自动填充
        data.add(row);
    }
    
    InsertReq insertReq = InsertReq.builder()
        .collectionName(collectionName)
        .data(data)
        .build();
    
    milvusClientV2.insert(insertReq);
    

    嵌入向量的生成也是手动调EmbeddingModel.embed()完成的,绕开了Spring AI的自动嵌入流程。

    7.4 Collection Schema 自动建表

    MilvusBm25CollectionProvisioner在应用启动时自动执行,支持三种模式(milvus-bm25-collection-bootstrap配置):

    模式行为
    none不做任何操作,假设集合已存在
    create-if-missing如果集合不存在则创建(生产推荐)
    recreate删除已有集合并重建(数据丢失! 仅开发用)

    创建的集合schema包含:

    字段名类型说明
    idVarChar(36)主键
    contentVarChar(65535)文本内容,Chinese analyzer 分词
    metadataJSON文档元信息(documentId, documentName 等)
    embeddingFloatVector(1536)文本嵌入向量
    sparse_bm25SparseFloatVectorBM25 函数输出字段

    BM25函数定义:

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

    BM25索引参数:

    Map sparseParams = new HashMap<>();
    sparseParams.put("inverted_index_algo", "DAAT_MAXSCORE");
    sparseParams.put("bm25_k1", 1.2);    // 词频饱和度参数
    sparseParams.put("bm25_b", 0.75);    // 文档长度归一化参数
    

    k1=1.2和b=0.75是Okapi BM25的标准默认值。在这个中文长文档场景中没有做特别调整,因为标准参数对于中文长文档通常表现不错。

    索引就绪等待:创建集合后,启动时会轮询describeIndex等待索引构建完成(最多120次 × 500ms = 60秒),避免索引未就绪时查询返回空结果。

    8. 踩过的坑与调参经验

    8.1 向量池和 BM25 池不宜过大

    最开始设向量池100、BM25池100,觉得“多召回一些,反正后续有rerank筛选”。结果:

    • Milvus检索时间从几十毫秒飙到几百毫秒
    • Rerank阶段需要处理近200条候选,单次rerank调用耗时3-5秒
    • 大部分候选文档的分数很低,徒增延迟

    现在稳定在向量池40、BM25池24、rerank候选上限48。实测延迟降到1.2秒以内(全链路)。

    原则:多路检索的池大小是“够用就好”,不要贪多。

    8.2 Rerank 后的绝对分过滤比相对分过滤更重要

    一开始只设置了相对分过滤(minRelativeRetrievalScore=0.8),没有绝对分过滤。发现在某些边缘query上——用户问“今天天气怎么样”——Rerank返回的所有文档分数都很低(~0.1),归一化后最高分也只有0.1,相对分过滤形同虚设(0.1/0.1=1.0,全通过)。

    加上minRerankAbsoluteScore=0.3后,这种情况会被正确拦截——所有rerank分低于0.3的文档直接丢弃,返回空列表,LLM至少知道“没有找到相关文档”而不是对着无关内容瞎编。

    8.3 Spring AI 版本不匹配问题

    Spring AI 1.1.2的Milvus集成基于v1 gRPC客户端,而Milvus 2.5的部分功能(EmbeddedText、BM25函数)只在v2 gRPC中可用。这导致了两个API路径共存:

    • 向量检索:走Spring AI(v1)
    • BM25检索:直接调MilvusClientV2(v2)
    • 文档写入:绕开Spring AI,用自定义的v2 writer

    教训:框架的封装层不一定总是够用。对于前沿功能(如Milvus 2.5内置BM25),要做好绕过框架直接调底层SDK的准备。 我们做了封装隔离——只在底层交互类中依赖v2 SDK,上层业务逻辑仍然通过统一接口调用。

    8.4 Query 改写的扩展词要有“节制”

    最开始给每条规则加了很多扩展词,比如“故障”触发词映射到十几个扩展词。结果某些query改写得非常长(几百字),反而稀释了核心关键词的信号。

    现在的策略是每条规则3-5个精准的扩展词。宁可少写,不要多写。

    8.5 RRF 的 k 值调优

    k=60不是拍脑袋定的。在开发环境做了小规模对比:

    k 值效果
    k=0极端重视高 rank,排第一的文档权重极大。结果:几乎退化为“取两个列表的交集排序”,单路表现差的 query 全挂
    k=60当前的平衡点,共识文档有优势,但单路高排名的文档也不会被完全埋没
    k=∞所有 rank 权重相同,退化为“在两个列表中都出现的排前面,都没出现的随机排”

    k=60在开发和测试阶段的表现稳定,所以沿用到了生产。

    8.6 中文分词的 BM25 分析器选择

    Milvus的Chinese analyzer对中文文本的处理包括分词和停用词过滤。我们发现通用Chinese analyzer对垂直领域的专业术语分词效果有一些偏差——例如“产品A系列”可能被切为“产品” + “A” + “系列”,“部件故障”可能被切为“部件” + “故障”。

    目前通过在query改写阶段追加完整的专业词汇作为缓解手段(即使分析器误切,也能通过扩展后的完整词匹配到),后续考虑使用自定义词典来进一步优化。

    9. 总结与下一步

    9.1 这套方案的核心价值

    1. 意图优先,RAG按需触发:不是所有请求都需要全套检索,两阶段pipeline在延迟和效果之间取得了好的平衡。
    2. 向量 + BM25混合检索:单一数据库(Milvus 2.5内置BM25)搞定,不需要维护多个存储引擎。
    3. 规则驱动的query改写:领域知识显式编码,可调试、可维护,延迟为零。
    4. 多层过滤漏斗:从向量相似度阈值 → BM25分过滤 → RRF融合 → Rerank双重过滤,每层砍掉一部分噪声,最终喂给LLM的是高度相关的上下文。
    5. 后处理门控:答案与用户实际情况交叉校验,降低风险。

    9.2 当前局限与改进方向

    • Query改写依赖规则维护:新增产品线时需要手动加规则。未来可以考虑用LLM生成候选扩展词 + 人工审核的半自动化流程。
    • BM25分词精度:通用Chinese analyzer对领域专业术语的分词有偏差,可以引入自定义词典。
    • Rerank是唯一的外部依赖:如果DashScope Rerank服务不可用,系统虽有降级策略但排序质量会下降。考虑评估本地Cross-Encoder模型作为备选。
    • 评估体系缺失:目前靠人工抽样评估检索质量。下一步需要构建标注数据集和自动化评估。

    9.3 关键配置速查

    配置项值用途
    hybrid.vector-pool-top-k40向量召回候选数
    hybrid.keyword-pool-top-k24BM25 召回候选数
    hybrid.rrf-k60RRF 阻尼常数
    hybrid.rerank-candidate-max48Rerank 输入上限
    hybrid.min-vector-similarity0.15向量相似度阈值
    hybrid.min-bm25-absolute-score1.0BM25 绝对分阈值
    hybrid.min-relative-retrieval-score0.8Rerank 相对分阈值
    hybrid.min-rerank-absolute-score0.3Rerank 绝对分阈值
    rag.ingest.max-chunk-chars1200切片大小
    rag.ingest.overlap-chars150切片重叠量
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 AI
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Aperty macOS版
    Aperty macOS版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirServer Windows版
    AirServer Windows版

    AirServer Windows版可把电脑变成AirPlay、Google Cast和Miracast无线投屏接收端,适合会议演示、课堂教学及家庭大屏分享。发送设备通常无需另装客户端,并可同时接收和智能排列多个镜像会话。

    即将离开本站
    您即将前往第三方网站,请确认是否继续?