RETRIEVAL · 检索与证据链路

检索与证据从一次提问到可溯源的引用

这条链路是整个系统技术含量最高的部分。朴素 RAG 是「embed → 单路召回 → 拼提示词 → 让模型自由发挥」;这里在召回、排序、权限、证据四个位置做了硬化,其中最关键的一条是:把「答不出来」当作合法结果

2
dense + sparse · RRF 融合
60
RRF k 值 · 硬编码
0.3
rerank 阈值 · 唯一生效阈值
0
给 LLM 的 score · 刻意剥离
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 通道
字段
sparse_vector
度量
BM25
输入
直接传原始文本,由 Milvus 服务端 BM25 Function 计算 sparse 向量
价值
补精确术语与编号。临床文档中充斥 D8221C00001 这类研究方案编号,纯向量召回容易漏
dense 通道
字段
dense_vector
度量
COSINE
模型
默认 bge-m3,实际取 kb.EmbeddingModel,按知识库可配
价值
语义召回,覆盖问法与原文表述不一致的情况

参数落点

RRF k
60,硬编码RerankStrategy.Rrf(60))· 融合在 Milvus 服务端完成
top_k
默认 15,clamp 1–50
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_enabledhits.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]referencesdocumentId 必须为 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 依赖深度评估。