Persist provider reasoning and assistant text separately on agent_reasoning_audit (DeepSeekAssistantMessage path), extract diagnosis_run.conclusion, enrich RAG tool audit (step_id/query/qualityScore), gate empty mysql tools, drop devtools, and align MVP docs after live E2E verification.
28 KiB
Milvus Hybrid Search 接入对照清单
日期:2026-07-27
前提:旧 Milvus SDK 直连检索路径后续废弃,不作为长期实现基础
目标:在现有 lookup_knowledge pipeline 上接入 dense + sparse/BM25 混合检索,融合优先走服务端 RRF
关联文档:
docs/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 方法”:
- 新建 collection 或新版本 collection(推荐)
- 全量重灌知识库(dense + sparse)
- 双写一段时间(可选)
- 切换读路径到 hybrid
- 下线旧 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 | 名次融合分,不是相似度概率 |
因此:
SearchHit要区分fusedScore/denseScore/sparseScoreisLowQuality不要直接拿 RRF 分当旧 L2 用- 过渡期可:
- 用“是否有命中 + 规则完整性”判断 found
- 或只对 dense 分做阈值,RRF 只负责排序
- 重新用 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. 明确不做的事
- 继续在旧 SDK
search上叠 hybrid 细节当长期方案 - 应用层把 dense raw 分和 BM25 raw 分直接相加
- 用 L0 contains 大额加分替代库内 RRF
- 只改查询、不重灌 sparse 数据
- hybrid 后仍拿旧 L2 阈值硬套 fused score
- 让 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 阈值?