登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

向量检索召回很多但答案变差:按来源分组保留上下文多样性

来源:17golang原创

时间:2026-08-29 13:03:07 261浏览 收藏

知识库问答里有一种很隐蔽的退化:召回数量从 5 调到 20,检索指标看起来更宽了,回答却开始围着同一份长文打转。常见原因不是向量模型突然失效,而是相似片段集中来自同一个 source_id,有限的上下文窗口被重复证据占满。

先按来源分组,再在每组中保留最高分片段,最后用一个明确的 top_k 配额合并候选,通常比盲目增大召回数更容易保住上下文多样性。

要点速览

  • top_k 只是最终送入模型的数量,不等于有效证据的来源数量。
  • source_id 分组能先识别“同文档霸榜”,再决定每组最多保留几段。
  • similarity_score 仍负责组内排序,分组不会把低相关片段硬塞进上下文。
  • 小实验应同时比较来源数、分数分布和最终回答引用,不能只看召回条数。

先复现同一来源占满 top_k 的问题

假设检索器返回 8 个候选片段,分数已经按降序排列。前 5 段来自一份部署手册,后 3 段来自两个故障复盘。若直接取前 5 段,模型看到的是同一来源的连续解释,另两份材料即使包含反例,也进不了上下文。

candidates = [
    {"chunk_id": "a-01", "source_id": "deploy-guide", "similarity_score": 0.92},
    {"chunk_id": "a-02", "source_id": "deploy-guide", "similarity_score": 0.91},
    {"chunk_id": "a-03", "source_id": "deploy-guide", "similarity_score": 0.90},
    {"chunk_id": "a-04", "source_id": "deploy-guide", "similarity_score": 0.89},
    {"chunk_id": "b-01", "source_id": "incident-2024-11", "similarity_score": 0.88},
    {"chunk_id": "c-01", "source_id": "runbook-cache", "similarity_score": 0.87},
]

top_k = 5
selected = candidates[:top_k]
print(len(selected), {item["source_id"] for item in selected})

这段代码会打印 5 和只有一个来源。它没有违反相似度排序规则,却把“证据覆盖面”交给了文档长度和切块方式。这里先别急着调大 top_k,因为上下文窗口、重复内容和模型注意力都会一起增加。

用 source_id 分组,再做组内保留

一个可控的后处理步骤是先执行 group_by_source。它不重新计算向量,只把候选片段按真实来源归档;每一组内部仍按 similarity_score 从高到低排列。

from collections import defaultdict

def group_by_source(candidates):
    groups = defaultdict(list)
    for item in candidates:
        groups[item["source_id"]].append(item)
    for items in groups.values():
        items.sort(key=lambda item: item["similarity_score"], reverse=True)
    return groups

groups = group_by_source(candidates)
selected = []
per_source = 2
for source_id, items in groups.items():
    selected.extend(items[:per_source])
selected.sort(key=lambda item: item["similarity_score"], reverse=True)
selected = selected[:top_k]
print([item["source_id"] for item in selected])

这里的关键节点是 source_idgroup_by_sourceselected:候选先进入来源组,组内截断后再汇总,最终仍受 top_k 限制。示例输出会包含 deploy-guideincident-2024-11runbook-cache,但并不保证三个来源各占相同数量。

向量候选片段经过 group_by_source 按 source_id 分组,再汇总到 selected 的数据流示意图

图中只画这段实验真正存在的关系:候选片段进入 group_by_source,按 source_id 分组,再汇入 selected。如果某来源只有一个高相关片段,它不会为了凑数被复制。

如何决定每个来源的配额

固定的 per_source = 2 适合做第一版实验,但线上通常需要把候选数量、来源数和上下文预算一起看。可以先设一个较宽的召回池,再用每组上限控制重复度:

  • 知识库来源很多时,每组保留 1 段,优先观察覆盖面;
  • 来源较少但每份文档内部有互补章节时,每组保留 2 段或 3 段;
  • 某些来源是权威规范时,可以给它额外配额,但要在配置中显式记录,不要让长文天然获得特权。

别把“来源多”直接当成“答案可靠”。如果一组的最高 similarity_score 都明显低于另一组,仍应先检查查询改写、切块粒度和元数据过滤。分组是控制证据结构的手段,不是相关性校验的替代品。

similarity_score 组内排序后按来源配额汇入 top_k 的上下文选择示意图

运行检查:别只打印最终片段

小实验至少输出三个结果:最终片段数、来源集合、每个来源的分数范围。这样才能分辨是配额生效,还是候选本身就只有一个有效来源。

source_stats = {}
for item in selected:
    source_stats.setdefault(item["source_id"], []).append(item["similarity_score"])

print("selected=", len(selected))
print("sources=", sorted(source_stats))
for source_id, scores in sorted(source_stats.items()):
    print(source_id, min(scores), max(scores))

检查时重点看两种异常:第一,selected 数量不足,说明召回池或每组上限过小;第二,来源集合仍只有一个,说明候选池里没有其他来源,继续调后处理没有意义。只有把来源集合扩展开,才值得比较最终回答是否减少了重复解释。

常见问题:来源多样性和相关性怎么取舍

按来源分组会不会把最高分片段丢掉?

会有这个可能,所以应保留组内最高分,并用离线问答集比较答案正确率,而不是追求来源数量最大化。

top_k 应该先调大还是先做分组?

建议先固定一个能放进上下文的 top_k,完成分组实验后再调大;否则重复片段和窗口压力会同时变化。

只有一个 source_id 时怎么办?

先查元数据过滤、切块和召回池是否只覆盖一份文档。后处理无法凭空制造第二个来源。

如何判断多样性真的改善了回答?

把来源集合、引用覆盖和答案评测放在同一次实验里记录;仅看去重后的条数,不能证明回答更准确。

把实验收敛成一条可观测规则

在 RAG 链路中,向量召回负责找到候选,来源分组负责控制证据结构,selected 再把有限候选交给生成模型。上线前给这三个阶段分别打日志,至少记录 top_k、来源数量、每组保留数和最终引用来源。这样答案变差时,能判断问题发生在召回不足、来源过度集中,还是生成阶段没有使用已提供的证据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>