- 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
937 lines
29 KiB
Markdown
937 lines
29 KiB
Markdown
# 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<br/>chunk 身份/去重] --> D2a[交付2a<br/>app RRF]
|
||
D2a --> D2b[交付2b<br/>真 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<br/>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[候选身份<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` 雏形 | 检索调用面先稳定,后续只换实现 |
|
||
|
||
可称为:
|
||
|
||
```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<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 建议逻辑模型
|
||
|
||
```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<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 新检索端口(建议)
|
||
|
||
不要继续扩:
|
||
|
||
```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<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` 怎么演进
|
||
|
||
短期:
|
||
|
||
```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 阈值?
|
||
```
|