向量检索召回很多但答案变差:按来源分组保留上下文多样性
知识库问答里有一种很隐蔽的退化:召回数量从 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_id、group_by_source 和 selected:候选先进入来源组,组内截断后再汇总,最终仍受 top_k 限制。示例输出会包含 deploy-guide、incident-2024-11 和 runbook-cache,但并不保证三个来源各占相同数量。

图中只画这段实验真正存在的关系:候选片段进入 group_by_source,按 source_id 分组,再汇入 selected。如果某来源只有一个高相关片段,它不会为了凑数被复制。
如何决定每个来源的配额
固定的 per_source = 2 适合做第一版实验,但线上通常需要把候选数量、来源数和上下文预算一起看。可以先设一个较宽的召回池,再用每组上限控制重复度:
- 知识库来源很多时,每组保留 1 段,优先观察覆盖面;
- 来源较少但每份文档内部有互补章节时,每组保留 2 段或 3 段;
- 某些来源是权威规范时,可以给它额外配额,但要在配置中显式记录,不要让长文天然获得特权。
别把“来源多”直接当成“答案可靠”。如果一组的最高 similarity_score 都明显低于另一组,仍应先检查查询改写、切块粒度和元数据过滤。分组是控制证据结构的手段,不是相关性校验的替代品。

运行检查:别只打印最终片段
小实验至少输出三个结果:最终片段数、来源集合、每个来源的分数范围。这样才能分辨是配额生效,还是候选本身就只有一个有效来源。
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、来源数量、每组保留数和最终引用来源。这样答案变差时,能判断问题发生在召回不足、来源过度集中,还是生成阶段没有使用已提供的证据。
Python 调用大模型时如何用结构化输出校验 JSON:从解析失败到可重试
- 上一篇
- Python 调用大模型时如何用结构化输出校验 JSON:从解析失败到可重试
- 下一篇
- Go slog.Group 如何组织结构化日志字段:嵌套键与输出核对
-
- 科技周边 · 人工智能 | 4小时前 |
- 大模型流式输出断在半句:增量 JSON 缓冲、finish_reason 与重连收口
- 348浏览 收藏
-
- 科技周边 · 人工智能 | 4小时前 | MySQL · 数据库 · JSON_TABLE · SQL教程 · mysql JSON_TABLE 嵌套数组 JSON路径 数据展开
- MySQL JSON_TABLE 如何拆分嵌套数组:路径映射、行扩展与空值边界
- 322浏览 收藏
-
- 科技周边 · 人工智能 | 5小时前 | 异步任务 · 人工智能 · openai · 工程实践 · Batch API · OpenAI Batch API 部分结果 cancelling cancelled output_file_id error_file_id
- OpenAI Batch API 取消任务后怎么收口:cancelling、cancelled 与部分结果处理
- 250浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5421次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4911次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4834次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 5097次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 5055次使用
-
- go格式“占位符”输入输出 类似python的input
- 2023-01-19 346浏览
-
- Golang如何调用Python代码详解
- 2023-01-07 235浏览
-
- HTTP 的 response 中的响应体和头部是分开发送的吗?
- 2023-01-28 387浏览
-
- B站等视频网站的弹幕用的是 websocket 还是轮询?
- 2023-02-16 447浏览
-
- Linux 下有什么命令行工具以时序显示 CPU 占用率?
- 2023-01-13 360浏览

