RETRIEVAL · 检索与证据链路
检索与证据从一次提问到可溯源的引用
这条链路是整个系统技术含量最高的部分。朴素 RAG 是「embed → 单路召回 → 拼提示词 → 让模型自由发挥」;这里在召回、排序、权限、证据四个位置做了硬化,其中最关键的一条是:把「答不出来」当作合法结果。
2路
dense + sparse · RRF 融合
01 — 调用链
两条链路,能力不同
Agent 走的主链路带完整的范围校验、rerank 与术语扩展;调试 / API 链路是简化版,二者不可混为一谈。
STEP 01
KbContextPreProcessor
把 KB / 文档范围解析成 knowledge_scopes,做存在性校验、dup→owner 归一、filter→assetId 交集
STEP 02
KbSearchTool
Agent 实际调用的工具。参数校验、构造 filter、逐 scope 检索
STEP 03
MilvusVectorStore
发起 dense 与 sparse 双路查询
STEP 04
Milvus 服务端
/v2/vectordb/entities/hybrid_search · RRF 融合
STEP 05
GatewayRerankService
cross-encoder 精排 + 阈值过滤,再截断 topK
注意区分第二条链路
SearchVectorStoreQueryHandler 是调试 / API 用的独立链路,不过 reranker、不过术语扩展,其 document_order 也是简化版(仅 DocumentId→ChunkId)。排查检索效果时若走错链路,会得出错误结论。
02 — 召回层
双路召回与 RRF 融合
sparse 通道
输入
直接传原始文本,由 Milvus 服务端 BM25 Function 计算 sparse 向量
价值
补精确术语与编号。临床文档中充斥 D8221C00001 这类研究方案编号,纯向量召回容易漏
dense 通道
模型
默认 bge-m3,实际取 kb.EmbeddingModel,按知识库可配
参数落点
RRF k
60,硬编码(RerankStrategy.Rrf(60))· 融合在 Milvus 服务端完成
initial_recall
默认 0,最大 100 —— 即默认不做扩召回,需显式配置才启用粗召回 + 精排两段式
ef / nprobe / nlist
没有显式设置。索引统一 AUTOINDEX,搜索请求的 Params 当前调用方从不传,全依赖 Milvus 默认
已修复的隐蔽缺陷 · 值得记住
Milvus 的 hybrid REST 接口没有顶层 filter 字段。若按直觉在顶层传 filter,它会被静默忽略——表现为:明明限定了只查某几个文档,实际却全库召回,且不报错。正确做法是把 filter 下推到每个子 search。这类「不报错但结果错」的缺陷最难发现。
03 — 排序层
阈值只在一个地方生效
这里有个容易讲错的细节:不是「不设阈值」,而是分场景设。RRF 融合分与相似度分不可比,因此 hybrid 通道下不施加分数阈值;但 reranker 产出的分数是设阈值的。
生效中
rerank_threshold = 0.3
在 KbSearchTool 中于 rerank 之后硬过滤。这是主链路唯一真正生效的阈值。
配置存在但未使用
score_threshold = 0.2
工具 config 里有此项,但 KbSearchTool 中并未使用。真正读取它的是调试链路,且显式判断 channel is not "hybrid" 才生效。
设计原则
为何 hybrid 不设阈值
RRF 输出的是排名倒数融合分,量纲与余弦相似度完全不同。对其套用相似度阈值会无差别误伤高质量结果——这是很多 RAG 实现会踩的坑。
reranker
介入位置
Milvus RRF 之后、截断 topK 之前
触发条件
仅当 rerank_enabled 且 hits.Count > topK —— 召回不足 topK 时不做无谓精排
服务
经 AI 网关池 RERANK_POOL_ID(默认池名 reranker),POST /v1/rerank
截断
query 截 200 字,每个 doc 截 360 字
容错
无 backend / HTTP 失败 / 异常时降级为原顺序,并造伪分数 1.0 - i*0.01
降级的副作用
rerank 降级时产出的是伪分数(按原顺序递减),此时 0.3 阈值实际上失去过滤意义——伪分数从 1.0 起步,前 70 条都会通过。也就是说 reranker 不可用时,系统会静默退化为「RRF 顺序直接截断」。功能不中断,但相关性质量下降且无告警。
document_order · 还原阅读顺序
除按相关度排序外,另提供一种按原文位置还原的排序策略。
排序键
DirStructure + Filename(忽略大小写)→ 首页码 → chunkId
无页码处理
排为 int.MaxValue,沉到该文件末尾
用途
长文档问答需要连续上下文。若只给几个孤立高分块,模型无法理解章节脉络——写 MVR 报告尤其依赖这一点
04 — 证据层
每个 [N] 都能回溯到具体 chunk
这是整条链路的闭环所在,也是临床合规场景的硬要求:引用必须可核对,无证据必须承认。
双通道输出 · 刻意不给模型看分数
results → 前端
- 包含 score
- 包含完整 metadata
- 供用户查看检索质量
LlmData → 模型
- chunkId / documentId / text
- filename / pageNumList / heading
- 故意不含 score
不让模型看到分数,是为了避免它被「这条 0.92、那条 0.61」带偏——模型应当读内容判断相关性,而不是替系统做分数运算。许多实现会把 score 一并塞进提示词,这里刻意没有。
引用生成与落地
STEP 01
LLM 生成 [N]
按段落输入顺序编号,并在结尾输出 references JSON 块
STEP 02
流式过滤
ReferencesBlockStreamFilter 状态机吞掉该块,用户看不到裸 JSON;未闭合围栏则整段丢弃
STEP 03
正则抽取
CitationPostProcessor 抠出块并解析 JSON
STEP 04
结构化落地
产出 AgentReference(documentId, chunkId, excerpt, position);JSON 畸形则静默跳过
忠实性约束 · 写在 Skill 里的硬规则
无证据
不得输出 [N],references 与 documentId 必须为 null
有证据
必须输出 references;正文无证据处不得出现 [N]
字段来源
引用字段只能从工具结果复制:kb_search 用 chunkId,kb_read 用 section.locator,严禁拼造页号或 locator
未找到
输出「⚠️ 知识库中未找到相关信息」,而非编造答案
自检清单
无悬空引用 · 无多余来源 · 不得引入段落外事实
05 — 权限与范围
过滤发生在检索之前
权限不是检索完再筛,而是在发起查询前就收敛范围。这个顺序对多租户是必要的。
个人库隔离
fail-closed
owner 只从服务端 context 注入,LLM 不可覆盖。拿不到身份时该 scope 直接返空——不放行,也不阻断其他知识库。schema 层强制 Personal collection 必须有 owner 字段,缺失直接抛异常。
属性过滤
两段式,不落向量库
先在 SQL 侧把业务 filter 解析成 assetId 集合,再与 documentIds 取交集,最后才转成 Milvus 的 document_id in [...]。属性不存向量库标量字段,因此属性变更无需重建索引。
注入防护
ID 白名单校验
kbId / docId 均经 ^[a-zA-Z0-9_\-]+$ 正则校验后才拼入表达式。
属性重写不重新向量化
修改文档业务属性时,从 S3 复用已存的 chunks JSONL 与 embeddings.bin,只重建 metadata_json 后按主键 Upsert。这使得画像与属性可以反复调整而不付出重复嵌入成本。
跨知识库检索的已知取舍
Milvus 不支持跨 collection 检索,实现方式是应用层逐 scope 循环 + 结果拼接。关键限制:topK 与 rerank 都是每个 scope 内部各自完成的,跨库没有全局重排。因此同时查多个知识库时,库 A 的第 15 名可能实际比库 B 的第 1 名更相关,却排在其后。设计文档 306-milvus-cross-collection-search.md 明确记录本期不引入跨 collection rerank。
06 — 小结
相对朴素 RAG 的四处硬化
a · 召回层
双路 + RRF
服务端 BM25 sparse 与 COSINE dense 双路,RRF(k=60) 融合。并正确识别出「RRF 分与相似度不可比」,因而 hybrid 下禁用分数阈值。
b · 排序层
两段式 + 阅读序
recall → cross-encoder rerank(0.3 阈值、含降级 fallback)→ topK。额外提供 document_order 按「文件名 → 页码 → chunkId」还原阅读顺序。
c · 权限层
前置而非后筛
属性 filter 先在 SQL 侧解析再下推;个人库 owner 服务端注入且 LLM 不可覆盖,无身份即 fail-closed 返空。
d · 证据层
真正的闭环
给 LLM 的数据剥掉 score 只留可复制标识;提示词强制「引用只能从工具结果复制、无证据不得输出 [N]」;references 块流式阶段被过滤、结束后解析成结构化引用。每个 [N] 都能回溯到具体 chunk,而不是让模型自己编来源。
核心原则
「答不出来」是合法结果
临床合规场景宁可留空并标注【待补充】,也不能编造。这与 MVR 报告中「数值必须经 data_mart 查询、大模型不得自行计算」是同一原则的两处落地。
继续阅读
检索结果如何被 Agent 使用、十六章如何组织、MAF 依赖深度评估。