搭建知识库以后,回答不准确不一定是模型不会写。有时真正需要的资料根本没被检索出来,有时前三条塞满了相似却无关的内容。只看最终回答“像不像正确答案”,很难知道应该改资料、改检索,还是改生成提示词。
本文用一个六份虚构售后文档、三道问题的小练习,解释 Precision@3 与 Recall@3。你会得到一个无需密钥、无需联网的 Python 检查脚本,能够比较两组手工填写的检索排名。它不是完整知识库,也没有调用向量模型;重点是把“找得好不好”变成可复查的记录。
一、先区分检索质量和回答质量
知识库问答通常先找资料,再根据资料组织回答。检索评测关心哪些文档进入候选列表,回答评测关心结论是否正确、有没有依据、引用是否匹配。两者有联系,却不能互相替代。
如果退货问题只拿到了发票说明,模型写得再流畅也缺少合适证据。如果检索已经包含退货条件,最终回答却漏掉期限,就要继续检查生成环节。先观察失败发生在哪一步,再决定改哪里,可以减少反复换模型的试错。
本练习只评估文档是否相关,不评价最终回答。真实系统还应分别检查引用真实性、资料有效期、拒答和响应速度,不能用一个检索分数宣布整个系统已经可靠。
二、两个分数分别回答什么
Precision 的基本含义是:找出来的内容里,有多少真正相关。Recall 的基本含义是:所有应该找到的相关内容里,实际找到了多少。官方 scikit-learn 文档给出了查准率与查全率的基本定义;下面是把相关文档当作正例、截取前几条结果后的教学约定。
本例每次最多看前三条,固定 k 为三。Precision@3 等于前三条里相关文档数除以三;返回不足三条时,空缺位置不算命中,分母仍为三。这个约定必须在报告里写明,不能有时按三算、有时按实际返回数算。
Recall@3 等于前三条命中的相关文档数,除以该问题全部相关文档数。一个问题若只有一份相关文档,只要它出现在前三条,Recall@3 就是一;此时 Precision@3 可能仍只有三分之一。
这两个结果并不矛盾:你找全了需要的资料,但也给用户带来两份无关资料。反过来,前三条全部相关也不保证找全,因为可能还有更多关键文档落在后面。分数反映的是指定语料、标注和截断位置下的表现。
三、准备可核对的小语料
建立一个练习文件夹,准备六份虚构材料,分别使用稳定编号。编号只代表文档身份,不包含排序含义。
- D1:A 型设备电池更换前的断电要求。
- D2:A 型设备电池更换步骤与复装检查。
- D3:普通商品退货条件与所需凭证。
- D4:维修寄送包装和地址填写说明。
- D5:个人电子发票申请材料。
- D6:电子发票补发与邮箱更正说明。
问题一是“A 型设备更换电池前后要做什么”,相关集合标为 D1、D2。问题二是“普通商品退货需要满足什么条件”,相关集合标为 D3。问题三是“申请和补发个人电子发票要准备什么”,相关集合标为 D5、D6。
这些标注来自本例明确给定的文档说明,并不代表真实维修或售后政策。生产环境要阅读完整原文,核对设备型号、适用人群和规则版本,不能只根据标题猜相关性。存在分歧的文档可以先记录理由,再由熟悉业务的人复核。
尤其注意,“维修寄送”和“退货”都可能包含地址、快递等词,但不是同一业务。相关标注的价值,正是在这里把表面词语接近与真正能回答问题区分开。
四、用两组排名演练计算
方案 A 为三个问题分别返回 D1、D3、D4;D3、D1、D6;D4、D5、D3。方案 B 分别返回 D2、D1、D3;D3、D4、D6;D5、D6、D4。
这两组是人为构造的排名,不是某款模型或数据库的实测结果,不能据此宣传方案 B 比某产品更强。保留手工排名的目的,是让任何读者都能直接用纸笔检查程序。
对问题一,方案 A 命中 D1,所以 Precision@3 为三分之一,Recall@3 为二分之一。方案 B 命中 D1、D2,所以分别为三分之二和一。对问题二,两组都命中唯一的 D3,所以都是三分之一和一。
对问题三,方案 A 只找到 D5,两个分数分别为三分之一和二分之一;方案 B 找到 D5、D6,分别为三分之二和一。逐题列出命中编号,比只展示一个平均数字更能解释差异。
五、保存并运行离线脚本
安装官方 Python 三,确认终端能显示版本。下面只用标准库,不需要安装向量数据库或机器学习库。保存为 retrieval_check.py,在文件所在目录运行 python retrieval_check.py。
from statistics import mean
CORPUS = {f'D{i}' for i in range(1, 7)}
LABELS = {'q1': {'D1', 'D2'}, 'q2': {'D3'}, 'q3': {'D5', 'D6'}}
A = {'q1': ['D1', 'D3', 'D4'], 'q2': ['D3', 'D1', 'D6'],
'q3': ['D4', 'D5', 'D3']}
B = {'q1': ['D2', 'D1', 'D3'], 'q2': ['D3', 'D4', 'D6'],
'q3': ['D5', 'D6', 'D4']}
def score(ranked, relevant, k=3):
if type(k) is not int or k <= 0:
raise ValueError('k must be a positive integer')
if not isinstance(ranked, list) or not isinstance(relevant, set):
raise ValueError('expected list and set')
if any(not isinstance(x, str) for x in ranked):
raise ValueError('document IDs must be strings')
if len(ranked) != len(set(ranked)):
raise ValueError('duplicate document IDs')
if not set(ranked) <= CORPUS or not relevant <= CORPUS:
raise ValueError('unknown document ID')
hits = set(ranked[:k]) & relevant
return len(hits) / k, len(hits) / len(relevant) if relevant else None
for name, ranking in [('A', A), ('B', B)]:
if ranking.keys() != LABELS.keys():
raise ValueError('query set mismatch')
rows = [score(ranking[q], LABELS[q]) for q in LABELS]
for q, (precision, recall) in zip(LABELS, rows):
print(name, q, f'P@3={precision:.3f}', f'R@3={recall:.3f}')
print(name, 'macro', round(mean(x[0] for x in rows), 3),
round(mean(x[1] for x in rows if x[1] is not None), 3))按给定数据,方案 A 的逐题平均 Precision@3 约为零点三三三,Recall@3 约为零点六六七;方案 B 分别约为零点五五六和一。这里是先逐题计算再平均,每道题权重相同,并非把所有文档判断合并后计算。
不要只检查最后一行。先用问题一的命中编号手算,再检查程序中问题编号与排名是否对应,最后核对平均值。小数显示做了四舍五入,不要把显示位数当成评测精度或置信范围。
六、把异常输入也列入验收
把某道题的排名改成空列表,应得到零命中,不能让程序崩溃。把排名写成 D1、D1、D2,程序应报重复编号,不能靠同一份资料重复出现来增加命中数。把编号写成 D99,应提示未知文档。
试着把 k 设成零、负数或布尔值,都应该拒绝。布尔值在 Python 中与整数存在类型关系,所以代码使用严格类型检查,避免 True 被误当成合法截断长度。
当相关集合为空时,Recall 的分母为零。本例返回 None,不直接写成一,也不把它混入可回答问题的平均 Recall。无答案问题应单独评估是否正确拒答、是否返回误导资料;它与“已经找到全部相关文档”是不同情况。
排名少于三条时,Precision@3 的空位惩罚来自本例约定。若团队使用另一种口径,应改指标名称和说明,并固定历史比较方式,不能把口径变化解释成质量提升。
七、迁移到真正的知识库
先固定一份可公开或获准使用的语料,记录每份文档的编号和版本。再收集来自真实任务的问题,人工给出相关集合,并留出没有用于调参的一组题。只反复优化同三道练习题,很容易得到好看的分数,却不能说明新问题也能回答。
从真实检索日志导出问题编号与排序后的文档编号,替换脚本中的 A 和 B。一次只改变一个环节,例如分块方式或是否增加重排,保持语料、问题、标注和 k 一致,再比较逐题变化。没有比较条件一致的记录,就不应说新版本提升了多少。
分块系统还要确定评估单位。一个文档拆成十块,如果标注仍按整篇文档,可能需要先把块映射回文档并去重;如果任务确实依赖某一段,则应另设段落级标注。不要把十个同源块当十份独立证据。
八、不要漏掉排序位置的差别
本例两个指标只检查前三条里的相关集合,不区分相关资料排在第一还是第三。把 D1、D3、D4 改成 D3、D4、D1,两个分数保持不变,但用户第一眼看到的内容已经不同。如果业务很看重第一条,应另外记录首条是否命中或相关资料首次出现的位置,不要期望一个指标解释所有体验。
另外,检索到了正确编号也不代表内容可以直接采用。假设 D1 对应旧型号,而文档编号映射没有随更新维护,分数可能看起来正常,实际证据却不适用。因此评测记录还应保存正文版本、索引构建时间和标注依据,发现资料变更后重新核对相关集合,而不只是复用旧答案。
九、发现分数不好后怎么改
Recall 较低时,先查看漏掉哪些相关资料:是否没入库、解析失败、编号错误,或者问题表达与资料差异太大。确认资料完整以后,再检查检索与分块设置,避免用扩大返回数量掩盖缺失文档。
Precision 较低时,逐条阅读无关候选,检查它们为什么进入前三条。可能是同名产品、过期政策或相似关键词,也可能是标注本身不够明确。提高 k 常能增加召回,但也可能带来更多干扰和上下文成本,不存在适用于所有任务的固定最优值。
完成这一轮练习后,应留下语料说明、问题与标注、两组排名、指标口径和失败记录。检索评测的作用是帮助定位改进方向;把证据保存下来,才有可能在下一次修改后判断变化是否真实。

还没有公开评论,欢迎分享你的想法。