Files
SuperBizAgent-java/mvp/engineering/rag/Milvus-Hybrid接入清单.md
zhuyongxin 473bb5d004 docs(mvp): update RAG docs for py-rag extraction
- rewrite RAG architecture doc for py-rag service contract mapping, ingest and rebuild ops
- refresh observability/trace doc for post-L0 single-attempt semantics
- add py-rag chain exploration note; mark superseded Milvus/L0 notes with status banners
- close ISS-017 (superseded by L0 sinking); mark knowledge_domain table orphaned
- refresh architecture/mvp/engineering indexes
2026-09-30 17:03:50 +08:00

29 KiB
Raw Permalink Blame History

Milvus Hybrid Search 接入对照清单

已过时(2026-09-29):RAG 模块已抽离为独立 py-rag 知识服务,进程内 Milvus (MilvusHybridKnowledgeStore / VectorSearchService)与本文描述的接入路径已整体删除。 当前检索架构与契约映射见 ../../architecture/RAG知识检索架构.md。 本文仅作历史决策追溯。

日期:2026-07-27
前提:旧 Milvus SDK 直连检索路径后续废弃,不作为长期实现基础
目标:在现有 lookup_knowledge pipeline 上接入 dense + sparse/BM25 混合检索,融合优先走服务端 RRF
关联文档:

  • RAG排序-多路召回与RRF.md(排序与多路召回判断框架)
  • 本文后续实现讨论以本节 「交付拆分:分块去重 + Hybrid 同规划」 为基线

1. 结论先说

可以接,而且和前面讨论的多路召回 / RRF 高度一致。
但(写作当时)项目 还不具备 hybrid 运行条件,缺的不是“再调一次 search”,而是:

1. schema 只有 dense,没有 sparse/BM25 字段
2. 写入只产 dense embedding
3. 检索抽象仍以单路 similaritySearch 为中心
4. 后处理仍承担了过多“伪融合”职责
5. 证据去重粒度偏文档/source 级,同文档多 chunk 会被吞掉

接入原则:

- 不继续加厚旧 SDK search 分支
- 以“检索端口 + 写入端口”抽象为准
- hybrid 融合尽量下沉到向量库(RRFRanker)
- 应用层保留:filter 策略、chunk 去重、return-n、阈值、投影
- 分块去重与 hybrid 同规划、分里程碑交付(先共用地基,再开 hybrid)
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
flowchart LR
    D1[交付1<br/>chunk 身份/去重] --> D2a[交付2a<br/>app RRF]
    D2a --> D2b[交付2b<br/>真 BM25 hybrid]
    D2b --> NOW[生产:V2 store + hybrid mode]

当前生产知识路径:

VectorIndexService / VectorSearchService
  -> MilvusHybridKnowledgeStore (MilvusClientV2 only)
  collection: milvus.collection (default biz)
  mode: retrieval.search.mode = dense | hybrid
  hybrid: dense ANN + BM25 sparse ANN + RRFRanker
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<br/>dense + BM25 schema)]

运维必做: 全量重灌知识库到 hybrid schema collection(配置名以 milvus.collection 为准,常见 biz);旧纯 dense collection 不能直接当 hybrid 用。


1.1 交付拆分:分块去重 + Hybrid 同规划

实现讨论基线。后续排期、拆 PR、评审范围,默认按本节两个交付理解。

判断

可以一起做,而且应该绑在同一条改造主线上;
但不要理解成「一个 PR 把 hybrid 全做完」。

更准确的表述:

同一条演进线,两层交付:
  交付 1:共用地基(证据身份 + chunk 去重 + 检索裁剪 + 端口雏形)
  交付 2:hybrid(schema/写入/查询/RRF + 阈值校准)

为什么必须同规划

两边改的是同一条链上的相邻环节:

检索命中
  -> 候选身份(docId / chunkIndex / evidenceKey)   ← hybrid 要,去重也要
  -> 去重 / 单文档 chunk 上限                       ← 分块去重
  -> 排序融合(现在规则 / 以后库内 RRF)             ← hybrid
  -> return-n / Agent 投影
flowchart TB
    HIT[检索命中] --> ID[候选身份<br/>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 雏形 检索调用面先稳定,后续只换实现

可称为:

「检索结果身份与裁剪契约」

不上 hybrid 也有独立价值,并且为交付 2 铺路。

不要硬塞进交付 1 的同一 PR

可同规划、建议第二波(交付 2):

项 原因
新 collection + sparse/BM25 schema 数据迁移/重灌,风险独立
全量重索引 耗时长,需单独验证
打开 search.mode=hybrid 依赖 sparse 数据已就绪
fused score 阈值重标定 要 hybrid 跑起来后有样本
删除旧 SDK 读路径 最后做,降低回滚成本

否则单个交付会同时碰:业务排序逻辑 + 数据迁移 + 基础设施,评审、回滚、评测都困难。

交付 1:chunk 级证据身份 + 去重 + 检索裁剪

主题: 让同一次检索内,同文档多个相关 chunk 能作为独立证据存活,并为 hybrid 统一 hit 模型。

范围(实现讨论默认包含):

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

明确不包含:

- sparse/BM25 schema
- 全量重灌
- hybridSearch 开关
- 旧 SDK 删除

完成定义(交付 1 Done):

[ ] 同文档多相关 chunk 可同时出现在内部 evidenceBlocks
[ ] Agent 投影后仍能看到多于 1 条同文档片段(未超预算时)
[ ] retrieve-k / return-n 可配置且行为可测
[ ] 候选身份字段稳定,足够支撑后续 hybrid hit 映射
[ ] 不依赖旧 SDK 新增逻辑

交付 2:Hybrid 写入 + 查询

主题: 在交付 1 的身份/裁剪契约稳定后,打开 dense + sparse/BM25 与库内 RRF。

范围:

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):

[ ] 可配置 dense | hybrid 切换
[ ] hybrid 默认 RRF,术语类与语义类回归不回退
[ ] filter 误杀有兜底
[ ] 应用层仍按 chunk 身份去重,hybrid 多命中不会被 source 级逻辑误伤
[ ] 新逻辑不再依赖旧 SDK search

节奏与评审方式

规划:一件事(检索质量主线)
设计评审:按交付 1 + 交付 2 两章看
开发:
  先合并交付 1(可独立上线/验证)
  再做交付 2(数据迁移 + hybrid 开关)
验收:
  交付 1 用“同文档多 chunk”用例
  交付 2 用“术语/语义/filter 兜底/延迟”用例

反模式(实现讨论时直接否决)

❌ 一个大 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 基本不动

当前关键文件:

写入:
  VectorIndexService
  DocumentChunkService
  VectorEmbeddingService
  MilvusClientFactory          # schema/index 创建(旧)

读取:
  VectorSearchService          # 门面,含 sdk/spring 路由
  KnowledgeDocumentRetriever
  KnowledgeEvidencePostProcessor
  LookupKnowledgeTool

配置:
  retrieval.vector-store.mode
  retrieval.kb-scope
  rag.top-k

3. 目标架构(不绑旧 SDK)

                    ┌─────────────────────────┐
  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
flowchart TB
    subgraph write_port["写入边界"]
        W[KnowledgeWritePort<br/>upsert / deleteByDocId]
    end

    subgraph search_port["检索边界"]
        S[KnowledgeSearchPort<br/>mode DENSE / HYBRID]
    end

    W --> VDB[(Vector DB<br/>dense + BM25 sparse<br/>metadata filter)]
    S --> VDB

    UP[upload/init] --> W
    LK[lookup_knowledge] --> S
    S --> RET[DocumentRetriever]
    RET --> POST[PostProcess<br/>dedup / cap / threshold / pack]
    POST --> PROJ[LookupResult / Projector]
    PROJ --> AG[Agent]

说明:

  • Port 是应用边界,实现可换成 Spring AI、Milvus 新客户端、或其他封装。
  • 旧 MilvusServiceClient 检索实现可以暂时留着,但 新功能不要往里堆。
  • Hybrid 是 KnowledgeSearchPort 的一种 mode,不是再开一套平行 tool。

4. Schema 改造清单

4.1 建议逻辑模型

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 索引

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 必须具备

- dense_vector: embed(buildEmbeddingText(chunk))
- sparse 输入: 建议用“可检索文本”
    title + breadcrumb + content
  而不是只丢 raw content
- doc_id / chunk_index / category / kb_scope
- 稳定 chunk id(doc_id + chunk_index 派生)
flowchart LR
    CHUNK[DocumentChunk] --> EMB[dense embed]
    CHUNK --> ST[search_text<br/>title+path+content]
    CHUNK --> META[docId/chunkIndex<br/>category/kb_scope]
    EMB --> ROW[upsert row]
    ST --> ROW
    META --> ROW
    ROW --> FN[BM25 Function<br/>search_text → sparse]
    ROW --> COL[(collection)]
    FN --> COL

5.3 注意

  • embedding 文本可以继续拼 Title/Path/Content
  • 返回给 Agent 的 content 仍应是原文 chunk,不要返回 embedding 拼接串
  • BM25 文本建议包含 title/breadcrumb,否则专有名词在标题里时字面路会弱

6. 检索路径改造清单

6.1 新检索端口(建议)

不要继续扩:

searchSimilarDocuments(query, topK, category)

建议收敛成:

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
}
flowchart TB
    REQ[SearchRequest<br/>query · retrieveK · mode<br/>filter · rrfK] --> PORT[KnowledgeSearchPort]
    PORT -->|DENSE| D[dense ANN only]
    PORT -->|HYBRID| H[dense + BM25 + RRF]
    D --> HIT[SearchHit 列表<br/>id/docId/chunk · content · ranks]
    H --> HIT
    HIT --> APP[后处理 / 投影]

6.2 VectorSearchService 怎么演进

短期:

保留门面类名也可
但内部:
  - 不再把 sdk 当长期分支
  - 增加 hybridSearch(...) 能力
  - mode 配置改为:
      dense | hybrid
      (spring 仅作 dense 兼容实现)

中期:

VectorSearchService 实现 KnowledgeSearchPort
旧 sdk 分支删除或仅 test/fallback 开关默认关

6.3 Hybrid 查询语义

路 A: dense(query_embedding)  limit=retrieveK
路 B: bm25/sparse(query_text) limit=retrieveK
可选过滤: category / kb_scope
融合: RRFRanker(k=60) 或 WeightedRanker
输出: top retrieveK/returnN

对应我们之前的公式:

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 应用后处理

推荐策略:

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

留给应用层:

1. chunk 级去重(docId#chunkIndex)
2. maxChunksPerDocument
3. return-n 裁剪
4. relevanceLevel / isLowQuality
5. 可选轻规则精排(title/breadcrumb 命中)
6. context pack 与 Agent 投影

应降级或删除的:

把 keyword contains 大额加分当主排序器
在应用层重复实现一套 dense+bm25 分数硬加

L0 仍只做:

- 是否启用 category filter
- 轻量精排特征
- trace 解释

8. 配置建议(示意)

# 检索模式: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 后要重标定

当前后处理默认假设:

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 核心)

[ ] chunk 级去重(不要 source 级吞 chunk)
[ ] 候选暴露 docId / chunkIndex / evidenceKey
[ ] retrieve-k / return-n 分离
[ ] maxChunksPerDocument
[ ] Projector 按证据身份去重(不再 source 唯一)
[ ] 明确 legacy-sdk 读路径仅兼容、默认关或冻结
[ ] 单测:同文档多 chunk / 真重复 / 单文档上限

Phase 1:检索端口收敛(交付 1 建议同批或紧随)

[ ] 定义 KnowledgeSearchPort / SearchRequest / SearchHit
[ ] VectorSearchService 适配该端口(先 dense-only 也可)
[ ] KnowledgeDocumentRetriever 只依赖端口
[ ] 单测用 fake search port,不再绑 SDK 细节

交付 1 出口: 不上 hybrid 也可合并;hybrid 所需 hit 身份与裁剪契约已稳定。

交付 2 对应 Phase

Phase 2:写入与 schema 支持 sparse/BM25

[ ] 新 collection schema
[ ] 写入 dense + sparse/BM25 文本
[ ] doc_id 级删除与重灌
[ ] 知识库全量重建脚本/任务

Phase 3:打开 hybrid 读路径

[ ] search.mode=hybrid
[ ] ranker=rrf
[ ] category filter 策略接入
[ ] low-quality 时 unfiltered 兜底
[ ] trace 记录 dense/sparse/fused 信息(内部)
[ ] 确认 hybrid 多命中仍走 chunk 级去重,不被 source 误伤

Phase 4:瘦身后处理 + 下线旧路径

[ ] 规则 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. 测试清单

单元

[ ] RRF 融合结果顺序(可用 fixture,不连库)
[ ] chunk 去重:同 doc 不同 chunk 都保留
[ ] 同 doc 超过 maxChunksPerDocument 被裁
[ ] category filter 低质时走 unfiltered
[ ] SearchHit 字段映射:docId/chunkIndex/content

集成 / 回归

[ ] 专有名词/错误码 query:hybrid 应优于 pure dense
[ ] 换说法语义 query:hybrid 不低于 pure dense
[ ] 错误 domain filter:unfiltered 兜底仍能找回
[ ] 长文档多 chunk:返回不少于 2 个相关片段(若存在)
[ ] 延迟:hybrid P95 可接受
[ ] 重灌后旧 doc 删除干净,无幽灵 chunk

兼容

[ ] 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)

1. 候选/证据具备 docId、chunkIndex、evidenceKey
2. 后处理与投影均按 chunk 身份去重
3. maxChunksPerDocument 生效
4. retrieve-k / return-n 分离且可测
5. 同文档多相关 chunk 在未超预算时可同时到达 Agent
6. 不新增对旧 SDK 的依赖

MVP-2:Hybrid 接入(交付 2)

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. 建议的下一步实现顺序(动手时)

实现讨论与排期默认按此顺序:

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 前,用下面问题对齐范围:

[ ] 本次是交付 1、交付 2,还是仅其中子项?
[ ] 是否改动了证据身份字段(docId/chunkIndex/evidenceKey)?
[ ] 去重 key 是否仍存在 source 级路径?
[ ] retrieve-k 与 return-n 是否仍混用 top-k?
[ ] 是否把 schema 重灌/hybrid 开关误塞进交付 1?
[ ] 是否有新增旧 SDK 依赖?
[ ] 单测是否覆盖“同文档多 chunk”?
[ ] 若已 hybrid:fused score 是否被误当成旧 L2 阈值?