# Milvus Hybrid Search 接入对照清单 > **已过时(2026-09-29)**:RAG 模块已抽离为独立 py-rag 知识服务,进程内 Milvus > (`MilvusHybridKnowledgeStore` / `VectorSearchService`)与本文描述的接入路径已整体删除。 > 当前检索架构与契约映射见 [../../architecture/RAG知识检索架构.md](../../architecture/RAG知识检索架构.md)。 > 本文仅作历史决策追溯。 **日期**:2026-07-27 **前提**:旧 Milvus SDK 直连检索路径后续废弃,不作为长期实现基础 **目标**:在现有 `lookup_knowledge` pipeline 上接入 dense + sparse/BM25 混合检索,融合优先走服务端 RRF **关联文档**: - `RAG排序-多路召回与RRF.md`(排序与多路召回判断框架) - 本文后续实现讨论以本节 **「交付拆分:分块去重 + Hybrid 同规划」** 为基线 --- ## 1. 结论先说 可以接,而且和前面讨论的多路召回 / RRF 高度一致。 但(写作当时)项目 **还不具备 hybrid 运行条件**,缺的不是“再调一次 search”,而是: ```text 1. schema 只有 dense,没有 sparse/BM25 字段 2. 写入只产 dense embedding 3. 检索抽象仍以单路 similaritySearch 为中心 4. 后处理仍承担了过多“伪融合”职责 5. 证据去重粒度偏文档/source 级,同文档多 chunk 会被吞掉 ``` **接入原则:** ```text - 不继续加厚旧 SDK search 分支 - 以“检索端口 + 写入端口”抽象为准 - hybrid 融合尽量下沉到向量库(RRFRanker) - 应用层保留:filter 策略、chunk 去重、return-n、阈值、投影 - 分块去重与 hybrid 同规划、分里程碑交付(先共用地基,再开 hybrid) ``` ```mermaid flowchart TB subgraph gaps["写作时的缺口"] G1[无 sparse/BM25 schema] G2[写入只有 dense] G3[单路 similaritySearch] G4[后处理伪融合] G5[source 级去重吞 chunk] end subgraph principles["接入原则"] P1[端口抽象 · 不堆旧 SDK] P2[融合下沉向量库 RRF] P3[应用层:filter/dedup/return-n/投影] P4[先地基后 hybrid 分里程碑] end gaps --> principles ``` ## 实现状态(2026-07-27,后续已完成) | 里程碑 | 状态 | 说明 | |---|---|---| | 交付 1 chunk 身份/去重/SearchPort | **已完成并归档** | `2026-07-27-rag-chunk-evidence-identity-dedup` | | 交付 2a 应用层 multi-path+RRF | **已完成并归档** | `2026-07-27-rag-hybrid-search-rrf`(已被 2b 取代为生产路径) | | 交付 2b 真 BM25 hybrid + 废弃 SDK | **已完成并归档** | `2026-07-27-rag-bm25-hybrid-drop-sdk` | ```mermaid flowchart LR D1[交付1
chunk 身份/去重] --> D2a[交付2a
app RRF] D2a --> D2b[交付2b
真 BM25 hybrid] D2b --> NOW[生产:V2 store + hybrid mode] ``` **当前生产知识路径:** ```text VectorIndexService / VectorSearchService -> MilvusHybridKnowledgeStore (MilvusClientV2 only) collection: milvus.collection (default biz) mode: retrieval.search.mode = dense | hybrid hybrid: dense ANN + BM25 sparse ANN + RRFRanker ``` ```mermaid flowchart TB subgraph write["写入"] UP[upload / init / rebuild] --> VIS[VectorIndexService] VIS --> STORE[MilvusHybridKnowledgeStore] end subgraph read["检索"] LK[lookup_knowledge] --> VSS[VectorSearchService] VSS -->|dense| SD[searchDense] VSS -->|hybrid| SH[searchHybrid + RRF] SD --> STORE SH --> STORE end STORE --> COL[(Milvus collection biz
dense + BM25 schema)] ``` **运维必做:** 全量重灌知识库到 hybrid schema collection(配置名以 `milvus.collection` 为准,常见 `biz`);旧纯 dense collection 不能直接当 hybrid 用。 --- ## 1.1 交付拆分:分块去重 + Hybrid 同规划 > 实现讨论基线。后续排期、拆 PR、评审范围,默认按本节两个交付理解。 ### 判断 **可以一起做,而且应该绑在同一条改造主线上**; 但不要理解成「一个 PR 把 hybrid 全做完」。 更准确的表述: ```text 同一条演进线,两层交付: 交付 1:共用地基(证据身份 + chunk 去重 + 检索裁剪 + 端口雏形) 交付 2:hybrid(schema/写入/查询/RRF + 阈值校准) ``` ### 为什么必须同规划 两边改的是同一条链上的相邻环节: ```text 检索命中 -> 候选身份(docId / chunkIndex / evidenceKey) ← hybrid 要,去重也要 -> 去重 / 单文档 chunk 上限 ← 分块去重 -> 排序融合(现在规则 / 以后库内 RRF) ← hybrid -> return-n / Agent 投影 ``` ```mermaid flowchart TB HIT[检索命中] --> ID[候选身份
docId / chunkIndex / evidenceKey] ID --> DEDUP[chunk 去重 · 每文档上限] DEDUP --> FUSE[排序融合 · 库内 RRF] FUSE --> RET[return-n] RET --> PROJ[Agent 投影] ID -.->|交付1 地基| D1[chunk identity] DEDUP -.-> D1 FUSE -.->|交付2| D2[hybrid] ``` 若拆开且顺序错误: | 只做一项 | 后果 | |---|---| | 只做 hybrid,不做 chunk 去重 | 多路召回更多同文档片段,仍被 source 级去重吞掉,**hybrid 收益被吃掉** | | 只做去重,完全不管候选/端口结构 | 能立刻改善,但接 hybrid 时往往还要再改一遍 DTO 与映射 | 因此: > **分块去重不是 hybrid 的可选项,而是 hybrid 生效的前提。** > 设计上当一件事;代码上分两个可独立验证的里程碑。 ### 必须放进同一批(交付 1 公共地基) 这些强烈建议同一波完成,作为后续实现讨论的最小必选范围: | 项 | 原因 | |---|---| | 候选补 `docId` / `chunkIndex` / `evidenceKey` | 去重 key 与 hybrid hit 身份统一 | | 后处理按 `docId#chunkIndex`(或 vector id fallback)去重 | 修复「同文档多 chunk 被吞」 | | `maxChunksPerDocument` | 放开多 chunk 后防止单文档刷屏 | | `retrieve-k` / `return-n` 分离 | hybrid 扩召回时必需;现在 K=3 也不该三者混用 | | Projector 去重语义对齐 | 后处理放出的多 chunk,不能在投影阶段再按 `source` 砍成 1 条 | | `SearchHit` / `RetrievedEvidenceCandidate` 字段对齐 | 避免 hybrid 再引入第三套结果结构 | | (建议)`KnowledgeSearchPort` 雏形 | 检索调用面先稳定,后续只换实现 | 可称为: ```text 「检索结果身份与裁剪契约」 ``` **不上 hybrid 也有独立价值**,并且为交付 2 铺路。 ### 不要硬塞进交付 1 的同一 PR 可同规划、建议第二波(交付 2): | 项 | 原因 | |---|---| | 新 collection + sparse/BM25 schema | 数据迁移/重灌,风险独立 | | 全量重索引 | 耗时长,需单独验证 | | 打开 `search.mode=hybrid` | 依赖 sparse 数据已就绪 | | fused score 阈值重标定 | 要 hybrid 跑起来后有样本 | | 删除旧 SDK 读路径 | 最后做,降低回滚成本 | 否则单个交付会同时碰:业务排序逻辑 + 数据迁移 + 基础设施,评审、回滚、评测都困难。 ### 交付 1:chunk 级证据身份 + 去重 + 检索裁剪 **主题:** 让同一次检索内,同文档多个相关 chunk 能作为独立证据存活,并为 hybrid 统一 hit 模型。 **范围(实现讨论默认包含):** ```text 1. RetrievedEvidenceCandidate / EvidenceBlock - 补 docId、chunkIndex、evidenceKey 2. KnowledgeDocumentRetriever - 从 metadata 抽取 docId/chunkIndex - evidenceKey 规则: docId + "#chunk-" + chunkIndex fallback: "vector:" + id fallback: "rank:" + originalRank 3. KnowledgeEvidencePostProcessor - 去重 key = evidenceKey(不再 source/title 优先) - maxChunksPerDocument(建议默认 2) - 同 key 才 merge;merge 不覆盖更高分 content 4. RagResultProjector - 按 evidence 身份去重(chunk 级 document_id 或显式 chunk 身份) - 禁止再仅用 source 当“每文档一条”的唯一键 5. 配置 - rag.retrieve-k - rag.return-n - rag.max-chunks-per-document - 逐步弱化/废弃单一 rag.top-k 身兼多职 6. (建议同批)KnowledgeSearchPort / SearchRequest / SearchHit 雏形 - 即使底层暂时仍是 dense-only,调用面先稳定 7. 单测 - 同 doc 两 chunk 都保留 - 同 doc+chunk 真重复只留一条 - 超 maxChunksPerDocument 裁掉低分 - projector 不再误杀同 source 不同 chunk ``` **明确不包含:** ```text - sparse/BM25 schema - 全量重灌 - hybridSearch 开关 - 旧 SDK 删除 ``` **完成定义(交付 1 Done):** ```text [ ] 同文档多相关 chunk 可同时出现在内部 evidenceBlocks [ ] Agent 投影后仍能看到多于 1 条同文档片段(未超预算时) [ ] retrieve-k / return-n 可配置且行为可测 [ ] 候选身份字段稳定,足够支撑后续 hybrid hit 映射 [ ] 不依赖旧 SDK 新增逻辑 ``` ### 交付 2:Hybrid 写入 + 查询 **主题:** 在交付 1 的身份/裁剪契约稳定后,打开 dense + sparse/BM25 与库内 RRF。 **范围:** ```text 1. 新 collection schema(dense + sparse/BM25 + 必要标量字段) 2. 写入 dense + sparse,doc_id 级删除与重灌 3. KnowledgeSearchPort 实现 hybrid 模式 4. ranker = RRF(默认)/ Weighted(可配路权) 5. category filter 策略: - 唯一 domain 时 filtered hybrid - 低质时 unfiltered 兜底(或双路径轻量合并) 6. 分数语义区分 fused/dense/sparse,重标定 found/relevance 7. 回归评测与延迟对比 8. 冻结并最终删除旧 SDK 读路径 ``` **完成定义(交付 2 Done):** ```text [ ] 可配置 dense | hybrid 切换 [ ] hybrid 默认 RRF,术语类与语义类回归不回退 [ ] filter 误杀有兜底 [ ] 应用层仍按 chunk 身份去重,hybrid 多命中不会被 source 级逻辑误伤 [ ] 新逻辑不再依赖旧 SDK search ``` ### 节奏与评审方式 ```text 规划:一件事(检索质量主线) 设计评审:按交付 1 + 交付 2 两章看 开发: 先合并交付 1(可独立上线/验证) 再做交付 2(数据迁移 + hybrid 开关) 验收: 交付 1 用“同文档多 chunk”用例 交付 2 用“术语/语义/filter 兜底/延迟”用例 ``` ### 反模式(实现讨论时直接否决) ```text ❌ 一个大 PR:去重 + schema 重灌 + hybrid 开关 + 删 SDK ❌ 先上 hybrid、后补 chunk 去重 ❌ 交付 1 仍按 source 去重,只把 hybrid 分数接进来 ❌ 为 hybrid 新建第三套与 candidate/EvidenceBlock 并行的结果模型长期共存 ❌ 在旧 SDK search 实现里继续堆 hybrid 细节作为长期方案 ``` --- ## 2. 现状对照 | 层级 | 当前实现 | Hybrid 需要 | |---|---|---| | Collection | `id / vector / content / metadata` | 至少再有 sparse/BM25 文本检索能力 | | 写入 | `VectorIndexService` 只写 dense | 同步维护 dense + sparse/BM25 | | 检索门面 | `VectorSearchService`:`sdk \| spring \| auto` | 单端口:`search(query, options)`,内部可 hybrid | | 旧 SDK 路径 | `MilvusServiceClient.search` | **废弃,不再作为主实现** | | Spring AI 路径 | `VectorStore.similaritySearch` | 可作过渡 dense 读路径,但 hybrid 能力要单独确认/扩展 | | 后处理 | 规则 boost + source 去重 | 融合交给库;后处理做裁剪/等级/打包 | | Agent 投影 | `RagResultProjector` | 基本不动 | 当前关键文件: ```text 写入: VectorIndexService DocumentChunkService VectorEmbeddingService MilvusClientFactory # schema/index 创建(旧) 读取: VectorSearchService # 门面,含 sdk/spring 路由 KnowledgeDocumentRetriever KnowledgeEvidencePostProcessor LookupKnowledgeTool 配置: retrieval.vector-store.mode retrieval.kb-scope rag.top-k ``` --- ## 3. 目标架构(不绑旧 SDK) ```text ┌─────────────────────────┐ upload/init │ KnowledgeWritePort │ chunk + embed -> │ - upsertChunks() │ │ - deleteByDocId() │ └───────────┬─────────────┘ │ ▼ Vector DB dense + sparse/BM25 metadata filters ▲ ┌───────────┴─────────────┐ lookup_knowledge │ KnowledgeSearchPort │ query + options-> │ - search() │ │ - mode: DENSE/HYBRID │ └───────────┬─────────────┘ │ ▼ KnowledgeDocumentRetriever │ ▼ PostProcess(dedup/chunk cap/threshold/pack) │ ▼ LookupResult / Projector ``` ```mermaid flowchart TB subgraph write_port["写入边界"] W[KnowledgeWritePort
upsert / deleteByDocId] end subgraph search_port["检索边界"] S[KnowledgeSearchPort
mode DENSE / HYBRID] end W --> VDB[(Vector DB
dense + BM25 sparse
metadata filter)] S --> VDB UP[upload/init] --> W LK[lookup_knowledge] --> S S --> RET[DocumentRetriever] RET --> POST[PostProcess
dedup / cap / threshold / pack] POST --> PROJ[LookupResult / Projector] PROJ --> AG[Agent] ``` 说明: - **Port** 是应用边界,实现可换成 Spring AI、Milvus 新客户端、或其他封装。 - 旧 `MilvusServiceClient` 检索实现可以暂时留着,但 **新功能不要往里堆**。 - Hybrid 是 `KnowledgeSearchPort` 的一种 mode,不是再开一套平行 tool。 --- ## 4. Schema 改造清单 ### 4.1 建议逻辑模型 ```text id string PK # chunk 级唯一 id doc_id string # 文档 id(从 metadata 提升为一等字段更稳) chunk_index int content text/varchar # 原始 chunk 正文(给 BM25 / 返回) title string nullable breadcrumb string nullable category string nullable kb_scope string nullable dense_vector float vector # embedding(title/path/content) sparse_vector sparse vector # BM25 或 sparse embedding metadata json # 兼容扩展字段 ``` ### 4.2 和现状差异 | 字段 | 现状 | 建议 | |---|---|---| | `vector` | 有 | 可改名 `dense_vector`,或保留别名兼容 | | `content` | 有,仅存储/返回 | 同时作为 BM25 输入文本 | | `sparse_vector` | 无 | **新增,hybrid 必需** | | `docId/chunkIndex` | 塞在 JSON metadata | 建议提升为可过滤/可排序字段 | | `category/kb_scope` | metadata JSON | 建议提升,filter 更稳 | ### 4.3 索引 ```text dense_vector -> 向量索引(COSINE/IP/L2,与 embedding 一致) sparse_vector -> 稀疏倒排 / BM25 索引 category/kb_scope/doc_id -> 标量过滤索引(如需要) ``` ### 4.4 迁移策略 不要幻想“只改 search 方法”: 1. **新建 collection 或新版本 collection**(推荐) 2. 全量重灌知识库(dense + sparse) 3. 双写一段时间(可选) 4. 切换读路径到 hybrid 5. 下线旧 collection / 旧 SDK 读路径 就地改老 collection 风险高:已有数据无 sparse,历史 metadata 形态也不统一。 --- ## 5. 写入路径改造清单 ### 5.1 需要动的职责 | 类/模块 | 现在 | 改造 | |---|---|---| | `DocumentChunkService` | 产出 chunk 正文/title/breadcrumb | 基本可复用 | | `VectorEmbeddingService` | 只做 dense embed | 保留;sparse/BM25 另算或交给库 | | `VectorIndexService` | 组装 metadata + insert dense | 升级为 write port 实现:dense+sparse 一并 upsert | | 删除逻辑 | 按 `metadata.docId` / `_source` 删 | 统一按 `doc_id` 删,避免路径不一致 | ### 5.2 写入时每条 chunk 必须具备 ```text - dense_vector: embed(buildEmbeddingText(chunk)) - sparse 输入: 建议用“可检索文本” title + breadcrumb + content 而不是只丢 raw content - doc_id / chunk_index / category / kb_scope - 稳定 chunk id(doc_id + chunk_index 派生) ``` ```mermaid flowchart LR CHUNK[DocumentChunk] --> EMB[dense embed] CHUNK --> ST[search_text
title+path+content] CHUNK --> META[docId/chunkIndex
category/kb_scope] EMB --> ROW[upsert row] ST --> ROW META --> ROW ROW --> FN[BM25 Function
search_text → sparse] ROW --> COL[(collection)] FN --> COL ``` ### 5.3 注意 - embedding 文本可以继续拼 `Title/Path/Content` - **返回给 Agent 的 content 仍应是原文 chunk**,不要返回 embedding 拼接串 - BM25 文本建议包含 title/breadcrumb,否则专有名词在标题里时字面路会弱 --- ## 6. 检索路径改造清单 ### 6.1 新检索端口(建议) 不要继续扩: ```text searchSimilarDocuments(query, topK, category) ``` 建议收敛成: ```text SearchRequest { query: string retrieveK: int # 例如 20 returnN: int # 例如 5,可在后处理裁 mode: DENSE | HYBRID categoryFilter?: string kbScope?: string ranker: RRF | WEIGHTED rrfK: int # 默认 60 weights?: {dense, sparse} } SearchHit { id, docId, chunkIndex content, title, breadcrumb source, category scores: { fused?, denseRank?, sparseRank?, raw?... } metadata } ``` ```mermaid flowchart TB REQ[SearchRequest
query · retrieveK · mode
filter · rrfK] --> PORT[KnowledgeSearchPort] PORT -->|DENSE| D[dense ANN only] PORT -->|HYBRID| H[dense + BM25 + RRF] D --> HIT[SearchHit 列表
id/docId/chunk · content · ranks] H --> HIT HIT --> APP[后处理 / 投影] ``` ### 6.2 `VectorSearchService` 怎么演进 短期: ```text 保留门面类名也可 但内部: - 不再把 sdk 当长期分支 - 增加 hybridSearch(...) 能力 - mode 配置改为: dense | hybrid (spring 仅作 dense 兼容实现) ``` 中期: ```text VectorSearchService 实现 KnowledgeSearchPort 旧 sdk 分支删除或仅 test/fallback 开关默认关 ``` ### 6.3 Hybrid 查询语义 ```text 路 A: dense(query_embedding) limit=retrieveK 路 B: bm25/sparse(query_text) limit=retrieveK 可选过滤: category / kb_scope 融合: RRFRanker(k=60) 或 WeightedRanker 输出: top retrieveK/returnN ``` 对应我们之前的公式: ```text RRF_w(d) = Σ w_i / (k + rank_i(d)) ``` - 用 RRF:先不调权重 - 用 Weighted:调的是 **dense/sparse 路权**,不是 keyword contains 加分 ### 6.4 filtered + unfiltered 还要不要? 还要,但定位变了: | 能力 | 放哪 | |---|---| | dense + bm25 融合 | **库内 hybrid** | | category filter 开/关 | 应用策略层,可变成两次 hybrid 或 filter 参数 | | chunk 去重 / 每文档上限 | 应用后处理 | | found / relevanceLevel | 应用后处理 | 推荐策略: ```text if 唯一 domain: hybrid(query, filter=category) # 主路 若低质量: hybrid(query, filter=null) # 兜底 或并行两条 hybrid 再做一次轻量合并 else: hybrid(query, filter=null) ``` 注意:这里的“两条”是 **filter 策略双路径**,不是再手写一套 dense/bm25 融合。 --- ## 7. 和现有 pipeline 的衔接(按类) ### 7.1 基本不动 | 类 | 原因 | |---|---| | `LookupKnowledgeTool` | 继续编排 transform → retrieve → post → pack | | `KnowledgeQueryTransformer` | 仍产 categoryFilter / hints | | `KnowledgeContextPacker` | 仍做字符预算 | | `LookupResultAssembler` | 仍组装内部结果 | | `RagToolAdapter` / `RagResultProjector` | Agent 契约保持稳定 | ### 7.2 要改 | 类 | 改什么 | |---|---| | `KnowledgeDocumentRetriever` | 调新 search port;透传 retrieveK/mode;把 docId/chunkIndex 提成候选一等字段 | | `KnowledgeEvidencePostProcessor` | 弱化“跨路融合”职责;保留 dedup、chunk cap、阈值、轻精排 | | `VectorSearchService` | 成为 hybrid 入口,去掉对旧 sdk 的依赖增长 | | `VectorIndexService` | 写入 dense+sparse,统一 doc_id 删除 | | schema 工厂/初始化 | 新 collection 定义与索引 | ### 7.3 后处理职责重新划分 **交给 Milvus hybrid:** - dense/sparse 多路召回 - RRF / weighted 融合 - 基础 topK **留给应用层:** ```text 1. chunk 级去重(docId#chunkIndex) 2. maxChunksPerDocument 3. return-n 裁剪 4. relevanceLevel / isLowQuality 5. 可选轻规则精排(title/breadcrumb 命中) 6. context pack 与 Agent 投影 ``` **应降级或删除的:** ```text 把 keyword contains 大额加分当主排序器 在应用层重复实现一套 dense+bm25 分数硬加 ``` L0 仍只做: ```text - 是否启用 category filter - 轻量精排特征 - trace 解释 ``` --- ## 8. 配置建议(示意) ```properties # 检索模式:dense | hybrid retrieval.search.mode=hybrid # 旧 sdk 读路径默认关闭(后续删除) retrieval.legacy-sdk.enabled=false # 召回/返回分离 rag.retrieve-k=20 rag.return-n=5 rag.max-chunks-per-document=2 # 融合 retrieval.hybrid.ranker=rrf retrieval.hybrid.rrf-k=60 # 若用 weighted: # retrieval.hybrid.ranker=weighted # retrieval.hybrid.weight.dense=1.0 # retrieval.hybrid.weight.sparse=0.8 # filter 策略 retrieval.filter.retry-unfiltered-on-low-quality=true retrieval.normalization.reference-threshold=0.5 ``` 说明: - `rag.top-k` 应逐步废弃,避免“召回/返回/展示”一个参数打天下 - 阈值字段若 hybrid 后分数语义变化,需要重新校准,不能照搬旧 L2 经验值 --- ## 9. 分数与阈值:hybrid 后要重标定 当前后处理默认假设: ```text score ≈ 兼容 L2 距离 normalizeL2 后得到 0~1 ``` hybrid 后常见变化: | 来源 | 语义 | |---|---| | dense raw | L2 / cosine | | sparse/BM25 raw | 另一套 | | fused RRF | 名次融合分,不是相似度概率 | 因此: 1. `SearchHit` 要区分 `fusedScore` / `denseScore` / `sparseScore` 2. `isLowQuality` 不要直接拿 RRF 分当旧 L2 用 3. 过渡期可: - 用“是否有命中 + 规则完整性”判断 found - 或只对 dense 分做阈值,RRF 只负责排序 4. 重新用 15~30 条回归 query 标定 --- ## 10. 分阶段落地(推荐) > 与 **§1.1 交付 1 / 交付 2** 对齐。Phase 编号用于执行拆解;对外沟通优先用两个交付里程碑。 ### 交付 1 对应 Phase #### Phase 0:应用层前提(交付 1 核心) ```text [ ] chunk 级去重(不要 source 级吞 chunk) [ ] 候选暴露 docId / chunkIndex / evidenceKey [ ] retrieve-k / return-n 分离 [ ] maxChunksPerDocument [ ] Projector 按证据身份去重(不再 source 唯一) [ ] 明确 legacy-sdk 读路径仅兼容、默认关或冻结 [ ] 单测:同文档多 chunk / 真重复 / 单文档上限 ``` #### Phase 1:检索端口收敛(交付 1 建议同批或紧随) ```text [ ] 定义 KnowledgeSearchPort / SearchRequest / SearchHit [ ] VectorSearchService 适配该端口(先 dense-only 也可) [ ] KnowledgeDocumentRetriever 只依赖端口 [ ] 单测用 fake search port,不再绑 SDK 细节 ``` **交付 1 出口:** 不上 hybrid 也可合并;hybrid 所需 hit 身份与裁剪契约已稳定。 ### 交付 2 对应 Phase #### Phase 2:写入与 schema 支持 sparse/BM25 ```text [ ] 新 collection schema [ ] 写入 dense + sparse/BM25 文本 [ ] doc_id 级删除与重灌 [ ] 知识库全量重建脚本/任务 ``` #### Phase 3:打开 hybrid 读路径 ```text [ ] search.mode=hybrid [ ] ranker=rrf [ ] category filter 策略接入 [ ] low-quality 时 unfiltered 兜底 [ ] trace 记录 dense/sparse/fused 信息(内部) [ ] 确认 hybrid 多命中仍走 chunk 级去重,不被 source 误伤 ``` #### Phase 4:瘦身后处理 + 下线旧路径 ```text [ ] 规则 boost 降为轻精排或可关 [ ] 删除/隔离旧 SDK search 实现 [ ] 校准 found/relevance 阈值 [ ] 回归评测与延迟对比 ``` **交付 2 出口:** dense|hybrid 可切换;RRF 默认可用;旧 SDK 检索不再被新逻辑依赖。 --- ## 11. 类级改造对照表 | 类 | 优先级 | 动作 | 是否依赖旧 SDK | |---|---|---|---| | `KnowledgeDocumentRetriever` | P0 | 接新端口,透传 hybrid 选项,补 chunk 身份 | 否 | | `KnowledgeEvidencePostProcessor` | P0 | 去重改 chunk 级;融合职责外移 | 否 | | `LookupKnowledgeTool` | P1 | 使用 retrieve-k/return-n;保留 filter 降级策略 | 否 | | `VectorSearchService` | P0 | 增加 hybrid;冻结/移除 sdk 增长 | 实现可无 SDK | | `VectorIndexService` | P0 | dense+sparse 写入,doc_id 删除 | 实现可无 SDK | | `MilvusClientFactory` | P2 | 仅迁移期维护;新 schema 建议新模块 | 旧 | | `VectorEmbeddingService` | P1 | 继续 dense;不塞 hybrid 逻辑 | 否 | | `RagResultProjector` | P2 | 若 document_id 变 chunk 级,同步语义 | 否 | | Spring AI `VectorStore` | P2 | 可继续承载 dense;hybrid 需单独能力层 | 否 | --- ## 12. 测试清单 ### 单元 ```text [ ] RRF 融合结果顺序(可用 fixture,不连库) [ ] chunk 去重:同 doc 不同 chunk 都保留 [ ] 同 doc 超过 maxChunksPerDocument 被裁 [ ] category filter 低质时走 unfiltered [ ] SearchHit 字段映射:docId/chunkIndex/content ``` ### 集成 / 回归 ```text [ ] 专有名词/错误码 query:hybrid 应优于 pure dense [ ] 换说法语义 query:hybrid 不低于 pure dense [ ] 错误 domain filter:unfiltered 兜底仍能找回 [ ] 长文档多 chunk:返回不少于 2 个相关片段(若存在) [ ] 延迟:hybrid P95 可接受 [ ] 重灌后旧 doc 删除干净,无幽灵 chunk ``` ### 兼容 ```text [ ] mode=dense 仍可用(回滚开关) [ ] Agent 契约字段不破(evidence/document_id/excerpt) [ ] tool_invocation / trace 仍有 selectedAttempt 与基本检索信息 ``` --- ## 13. 明确不做的事 1. **继续在旧 SDK `search` 上叠 hybrid 细节当长期方案** 2. **应用层把 dense raw 分和 BM25 raw 分直接相加** 3. **用 L0 contains 大额加分替代库内 RRF** 4. **只改查询、不重灌 sparse 数据** 5. **hybrid 后仍拿旧 L2 阈值硬套 fused score** 6. **让 Agent 直接依赖内部 fused/raw score 字段**(除非契约明确升级) --- ## 14. 和前序讨论的对齐 | 讨论结论 | 在本清单中的落点 | |---|---| | K=3 不必先上复杂 rerank | `retrieve-k=20, return-n=5` | | filtered + unfiltered 有价值 | hybrid 之上的 filter 策略双路径 | | BM25 是跨维度召回 | schema sparse/BM25 + hybrid 路 | | 跨路优先 RRF | 库内 `RRFRanker` | | `w_i` 是路权 | `WeightedRanker` / 配置 weight.dense/sparse | | L0 只做导航 | 仅影响 filter 与轻精排,不负责主融合 | | 旧 SDK 后续废弃 | 新开发只走 search/write port,不绑 SDK | --- ## 15. 最小可交付定义(MVP) MVP 拆成两个可独立验收的里程碑(与 §1.1 一致)。 ### MVP-1:分块去重与身份契约(交付 1) ```text 1. 候选/证据具备 docId、chunkIndex、evidenceKey 2. 后处理与投影均按 chunk 身份去重 3. maxChunksPerDocument 生效 4. retrieve-k / return-n 分离且可测 5. 同文档多相关 chunk 在未超预算时可同时到达 Agent 6. 不新增对旧 SDK 的依赖 ``` ### MVP-2:Hybrid 接入(交付 2) ```text 1. 新 collection 可写入 dense + BM25/sparse 2. 知识库可全量重建 3. lookup_knowledge 可通过配置切换 dense/hybrid 4. hybrid 默认 RRF 融合 5. 应用层 chunk 去重与 return-n 在 hybrid 下仍正确 6. 旧 SDK 检索不再被新逻辑依赖 7. 至少一套回归 query 证明: - 术语类 query 不回退 - 语义类 query 不回退 - filter 误杀有兜底 ``` 只有 MVP-1 完成,才建议开始 MVP-2 的数据迁移与开关切换。 --- ## 16. 建议的下一步实现顺序(动手时) 实现讨论与排期默认按此顺序: ```text 1. 交付 1 / MVP-1 - chunk 身份 - chunk 去重 + maxChunksPerDocument - retrieve-k / return-n - Projector 对齐 - SearchPort 雏形(建议) 2. 交付 2 / MVP-2 - schema + 重灌 - hybrid RRF 读路径 - filter 兜底 - 阈值校准 - 下线旧 SDK 读路径 ``` **不要跳过交付 1 直接做 hybrid。** 交付 1 不依赖旧 SDK,不阻塞后续 hybrid,且单独合并就有质量收益。 --- ## 17. 后续实现讨论检查清单 开会或开 PR 前,用下面问题对齐范围: ```text [ ] 本次是交付 1、交付 2,还是仅其中子项? [ ] 是否改动了证据身份字段(docId/chunkIndex/evidenceKey)? [ ] 去重 key 是否仍存在 source 级路径? [ ] retrieve-k 与 return-n 是否仍混用 top-k? [ ] 是否把 schema 重灌/hybrid 开关误塞进交付 1? [ ] 是否有新增旧 SDK 依赖? [ ] 单测是否覆盖“同文档多 chunk”? [ ] 若已 hybrid:fused score 是否被误当成旧 L2 阈值? ```