在上一篇(《SOAR 的记忆蓝图》)里,我拆解了检索式记忆系统的 Precision-Recall 矛盾:
- 提高召回率 → 必然引入噪声 → LLM 被无关信息干扰
- 提高精度 → 降低召回率 → 遗漏相关信息
- 加 Reranker/CRAG → 延迟和复杂度上升 → 本身也会失败
这不是"优化几个参数就能解决"的工程问题,这是高维空间的几何学、Transformer 的注意力机制、以及 LLM 的噪声敏感性共同决定的结构性约束。
但有一个问题还没回答:如果这个矛盾没法解决,那在实际干活的人是怎么做的?
答案是:他们不解决矛盾,他们管理矛盾。
经过 2023-2026 年的快速迭代,工业界已经收敛到一个标准的"七步流水线"。每一步都在做不同的权衡,每一步都承认矛盾没有被消除。但这个管道叠加起来,把 PR 矛盾压到了一个可接受的范围。
第一步:查询变换(Query Transformation)
原始的用户查询很少适合直接检索。
用户说"帮我看看那个张三上次提到的项目进度",直接拿去向量检索,八成找不到对应的文档。需要先把查询转换成更适合检索的形式。
常用方案
| 方法 | 做法 | 效果 |
|---|---|---|
| Multi-Query | 用 LLM 把一个问题改写为 3-5 个不同角度的检索词 | 增加召回率,减少遗漏 |
| HyDE (Gao, 2022) | 先生成一个"假设的理想文档",用它的嵌入去检索 | 对需要精确匹配的场景效果好 |
| Step-back Prompting | 先生成一个更抽象的宽泛问题,从宽到窄检索 | 适合需要先理解上下文再找细节的场景 |
| 查询分解 | 把复杂问题拆成多个子问题,分别检索 | 适合多跳问题 |
这一步的权衡
查询变换提高了召回率(覆盖了查询的不同侧面),但代价是:
- 可能偏离原始意图——改写后的查询抓住了相关的 false positive,漏掉了真正的目标
- LLM 调用成本——每次查询变换都多了一次 LLM 调用
- 延迟增加——Multi-Query 让一次检索变成 3-5 次
这是 PR 矛盾的第一层应对:用算力换召回率。
第二步:元数据预过滤(Metadata Pre-filtering)
纯向量检索在生产环境中已经没人用了。
你的知识库有各种属性——时间范围、文档来源、领域、权限级别。如果不在向量检索之前用这些属性做预过滤,你的向量搜索就会在一个巨大的、大部分不相关的空间里找相似度。
Pinecone、Weaviate、Qdrant 的生产指南里有一条共识:先过元数据,再过向量。
典型实现
查询: "2026年Q1的项目报告"
过滤: date >= 2026-01-01 AND date <= 2026-03-31
AND type = "project_report"
向量检索: 在过滤后的子集里做 top-k
这一步的权衡
预过滤大幅提高精度(把搜索空间缩小到真正相关的子集),但代价是:
- 如果过滤条件太激进,召回率会骤降——你过滤掉了你以为是无关、但实际上相关的内容
- 预过滤 vs 后过滤的永恒争议:先过滤再向量搜索(丢失潜在匹配)vs 先向量搜索再过滤(浪费算力在不相关的结果上)
- 两个过滤器之间的:元数据过滤掉的文档,向量搜索不会看到
这是 PR 矛盾的第二层应对:用先验知识减少搜索空间。
第三步:混合检索 + RRF(Hybrid Search + Reciprocal Rank Fusion)
纯稠密检索(Dense Retrieval)和纯稀疏检索(BM25)各有缺陷:
| 检索方式 | 强项 | 弱项 |
|---|---|---|
| 稠密检索(Embedding) | 语义匹配 | 丢失精确关键词匹配,受嵌入质量影响 |
| 稀疏检索(BM25) | 精确关键词匹配 | 无法处理同义、近义表达 |
工业界的答案是:两个都做,然后把结果融合。
Reciprocal Rank Fusion(RRF)
RRF 的公式极其简单:
score(d) = Σ 1 / (k + rank(d))
对每个文档,取它在两种检索方式中的排名,加起来算一个综合分数。k 是一个平滑常数,通常取 60。
这一步的权衡
混合检索在召回率上有明显的提升(5-15% 的 recall@10 改善),但代价是:
- 两个索引都要维护——稠密索引 + 稀疏索引,存储翻倍
- RRF 权重难调——k 值、稠密 vs 稀疏的权重比例,对不同的领域和查询类型有不同的最优值
- 两个引擎都可能出问题——一个引擎的失败结果会通过 RRF 污染最终排名
这是 PR 矛盾的第三层应对:用多个正交的检索信号互相补充。
第四步:小 Chunk 检索(Small Chunk Retrieval)
这是整个流水线里最微妙的一步。
你面临一个根本性的矛盾:
| Chunk 大小 | 嵌入精度 | 上下文完整性 |
|---|---|---|
| 小(~200 tokens) | 高——每段聚焦一个主题,嵌入向量干净 | 低——切碎了语义边界 |
| 大(~1000 tokens) | 低——一段包含多个主题,嵌入向量的平均化稀释了语义 | 高——保留了上下文 |
Anyscale 在 2023 年做了一个有影响力的实验:把 chunk_size 从 100 逐步增加到 900,观察检索分数和生成质量的变化。
结果是两条方向相反的曲线:
- 检索分数单调递增——更大的 chunk 更容易被命中(里面有更多关键词)
- 生成质量先升后降——在 300-500 tokens 达到峰值,之后更多的上下文开始引入噪声
工业界的共识
小 chunk 用于检索,大 chunk 用于阅读。
具体来说:嵌入和检索用 256-512 tokens 的小 chunk(保证精度),检索到之后把整个 parent document 或相邻 chunk 一起拉上来送给 LLM。
这被称为 “small-to-retrieve, big-to-read” 模式。
这一步的权衡
小 chunk 让检索精度大幅提升,但引入了一个新的复杂度:你需要额外维护 chunk ↔ parent 的映射关系,且 chunk 边界的切割永远会破坏一些语义边界。
这是 PR 矛盾的第四层应对:把"检索"和"阅读"解耦,让它们用不同的粒度。
第五步:上下文展开(Context Expansion)
第四步检索到的是小 chunk。但单独把小 chunk 扔给 LLM 是不够的——它只看到了切片,看不到上下文。
上下文展开的做法是:拿到命中的小 chunk 后,把它的邻居 chunk 或 parent 文档一起带上来。
实现方式
- SentenceWindowRetrieval:命中一个句子,拉它前后 N 个句子作为上下文窗口
- ParentDocumentRetriever:命中一个子 chunk,拉对应的父文档
- Sliding Window:把 chunk 边界滑动一下,重新拼接上下文
这一步的权衡
展开把精度↔完整性的天平往回拉了拉——你放弃了部分精度,换回了上下文。但展开范围越大,你重新引入了多少噪声就越不可控:
chunk 精度: 高
展开后: 上下文完整了,但噪声回来了
这是 PR 矛盾的第五层应对:用"检索精度换阅读完整性".
第六步:重排序(Re-ranking)
前五步产出的结果是一堆"roughly 相关"的候选。它们之间的相关性差异很小,而向量相似度在这个阶段已经丧失了分辨力。
Reranker(通常是一个交叉编码器)把 query 和每个候选文档配对,算一个精确的相关性分数。
效果
| 方案 | 精度提升 |
|---|---|
| 不加 Reranker | 基线 |
| 加 Cross-encoder Reranker | +5-15pp nDCG(在 BEIR 基准上) |
Cohere Rerank、BGE-Reranker 是生产中最常用的选择。
这一步的权衡
Reranker 是目前最有效的精度提升手段,但它有一个你不能忽视的限制:
它不能挽救没有被前面步骤命中到的文档。
Reranker 做的是"从候选里挑出最相关的",候选名单在上一步就已经定了。如果前面六步都没有命中那个真正相关的文档,Reranker 也帮不了你。
Reranker 提高精度,召回率天花板由前序步骤设定。
另外,交叉编码器比双编码器慢 2-5x。
这是 PR 矛盾的第六层应对:用更多的算力(交叉编码)换取精度,但承认召回率的天花板已经定了。
第七步:LLM 生成
最后一步,把前六步精挑细选出来的上下文送给 LLM,让它生成答案。
这一步本身也能做一些事情来管理 PR 矛盾:
- 系统提示约束:明确告诉 LLM"如果检索到的内容不足以回答,请说不知道"
- 引用锚定(Citation Anchoring):要求 LLM 在回答的每个关键事实后面注明来源 chunk
- 置信度声明:如果模型的回答基于不充分的检索结果,要求它声明不确定性
这一步的权衡
这一步的约束越严格,精度越高(更少幻觉),但用户可能得到更多"我不知道"的回复——这是用用户满意度换精度。
这是 PR 矛盾的第七层应对:用生成时的约束来兜底。
全景:七步的得失
把七步串起来,每一层的权衡一目了然:
| 步骤 | 主要 tradeoff | 状态 |
|---|---|---|
| 查询变换 | 召回率 ↑ / 意图偏离风险 ↑ | 生产标配 |
| 元数据过滤 | 精度 ↑ / 过滤过头则召回率骤降 | 生产标配 |
| 混合检索 + RRF | 召回率 ↑ / 维护成本 ↑ / 权重难调 | 生产标配 |
| 小 chunk 检索 | 精度 ↑ / 上下文完整性 ↓ | 生产标配 |
| 上下文展开 | 完整性 ↑ / 噪声回归 | 生产标配 |
| 重排序 | 精度 ↑ / 延迟 ↑ / 召回率天花板已定 | 生产标配 |
| LLM 生成约束 | 精度 ↑ / 用户满意度 ↓ | 按需采用 |
每一个步骤都引入了一个新的 tradeoff,下一个步骤又来部分弥补这个 tradeoff。整个流水线不是在解决问题,而是在不断转移问题。
局限:七步之后,矛盾仍在
必须坦诚地说:即使走了七步,PR 矛盾也没有被消灭。
- Query 理解是根本瓶颈——如果你的查询本身就是模糊的,整个管道的上限就定死了
- Chunk 边界永远在丢失信息——无论切多小,信息的连续性都会在切割点中断
- 递归检索没有好的停止条件——搜了一次觉得不够,再搜一次,什么时候该停?
- 新鲜度检测没人做好——系统无法可靠判断检索到的信息是否已经过时
工业界的七步流水线是一个"足够好"的方案,不是一个"解决了问题"的方案。
结论:管理矛盾,而不是解决矛盾
Precision-Recall 矛盾不是一个 bug,是一个 feature——它是检索式记忆系统在数学层面的固有属性。你不可能消除它,就像不可能消除重力。
但你可以在重力存在的情况下建房子。七步流水线就是那个房子。
每一步都在做权衡,每一步都在承认矛盾没有被消除。但这个管道叠加起来,把 PR 矛盾压到了一个工程上可接受的范围内,让生产系统能够稳定运行。
如果你在做 RAG 或 Agent 记忆系统,我的建议是:
- 先走完前六步,看看效果。大部分人走到第三步就觉得够了。
- 在每一步记录你的权衡。 清楚你在哪里丢了精度、哪里丢了召回率。
- 接受’足够好’。 追求完美的 PR 平衡会让你走进死胡同。
回到上一篇的核心论点:记忆系统有用,但它有天花板。七步流水线就是工业界在天花板之下能找到的最佳实践。
理解了这个框架,你就理解了为什么 Harness 工程(工具设计、反馈质量、状态控制)可能是比记忆系统更大的杠杆——因为你在七步之内能做的最大的改变,往往不在检索这一步。