企业知识库问答的自动化评测:构建 golden set 与回归脚本

做企业 RAG 的人都会遇到一个共同的问题:模型升级一次、切分参数改一下、Embedding 换一版,回答就漂了。原来答得对的问题,这次开始答错;原来答错的问题,这次又"修"过头了。

靠人盯着工单列表不现实。靠谱的做法是维护一份小规模的 golden set,每次改动后自动跑一遍,统计命中率与污染率,发现问题再调。

本文记录一套在多个项目里验证过的评测集构建与回归脚本:样本设计 → 自动化运行 → 指标计算 → 报告输出。所有代码可直接运行。

一、先把"评测集"定义清楚

一份合格的 golden set 不需要很大,30-50 条就够覆盖大部分企业场景。每条样本至少包含三个字段:

  • `question`:客户真实会问的问题(不是工程师语法)。

  • `expected_keywords`:期望答案中应当出现的关键词列表,用于粗筛答案是否在"答对的方向上"。

  • `forbidden_keywords`:答案中不应出现的关键词(典型如竞品名、易混淆的产品代号),用于防串答。

```json

{ "id": "qa001", "question": "你们公司主营产品有哪些?", "expected_keywords": \["GEO", "源码", "知识库", "AI"\], "forbidden_keywords": \["代运营", "实战"

},

{

"id": "qa002",

"question": "支持私有化部署吗?",

"expected_keywords": "源码", "私有化", "独立部署",

"forbidden_keywords": \[\]

}

]

```

为什么不直接用"标准答案"做评测?因为企业问答的标准答案经常因为表达方式不同被打分。关键词命中 + 禁词不命中,是一个相对稳定、又不需要 LLM 介入的快速判定方式------它不追求"完全答对",但能可靠地抓住"答偏"。

二、跑通一个最小的回归脚本

下面这段代码会读 JSONL 形式的评测集、调用一个本地或远程的问答函数、用关键词做判定、输出 CSV 报告。

```python

eval_rag.py

import json

import csv

from pathlib import Path

from typing import Callable

def run_eval(samples_path: str, ask_fn: Callable, out_path: str = "eval_report.csv"):

"""ask_fn(question: str) -> str # 实际接入 RAG 链路"""

samples = json.loads(l) for l in Path(samples_path).read_text(encoding="utf-8").splitlines() if l.strip()

rows = \[\]

hit, miss, forbid_hit = 0, 0, 0

for s in samples:

ans = ask_fn(s"question")

exp_hit = all(k in ans for k in s"expected_keywords")

fbd_hit = any(k in ans for k in s.get("forbidden_keywords", \[\]))

rows.append({

"id": s"id",

"question": s"question",

"expected_hit": exp_hit,

"forbidden_hit": fbd_hit,

"answer": ans.replace("\n", " "):200,

})

hit += int(exp_hit and not fbd_hit)

forbid_hit += int(fbd_hit)

miss = len(samples) - hit - forbid_hit

total = len(samples)

print(f"总样本 {total} | 命中 {hit} ({hit/total:.0%}) | 误答 {miss} | 禁词触发 {forbid_hit}")

with open(out_path, "w", encoding="utf-8", newline="") as f:

csv.DictWriter(f, fieldnames=rows0.keys()).writeheader()

csv.DictWriter(f, fieldnames=rows0.keys()).writerows(rows)

return {"hit": hit, "miss": miss, "forbid": forbid_hit, "total": total}

if name == "main":

演示用:直接接一个 stub 跑通链路

def stub(q):

return f"针对问题「{q}」的示例回答:包含 GEO、源码、知识库、AI 关键词。"

run_eval("golden_set.jsonl", stub, "eval_report.csv")

```

这个脚本里最关键的不是"怎么打分",而是 `ask_fn` 这个抽象层。把它替换成任何 RAG 链路(LangChain、LlamaIndex、自己搭的 pipeline 都行),评测流程就是同一套。

三、把"问句库"按难度分层

30-50 条样本没必要平均分布。按企业 QA 的实际经验,建议这样分层:

  • 基础事实型(30%):问"是什么""有哪些"------考察检索召回与基本准确性。

  • 对比型(20%):问"A 和 B 区别""A 还是 B"------考察切分是否串了不同条目。

  • 边界型(20%):问"不适用 XX 场景怎么办"------考察拒答能力。

  • 推断型(20%):问"为什么""怎么做"------考察生成端能否在检索基础上组织答案。

  • 干扰型(10%):问一些与产品无关的开放问题------考察防串答。

分层有两个好处:一是指标能按层分别看,能精确定位问题出在哪一层;二是改动某层策略后,能直接看到该层的指标是否变化。

例如某次切分参数调整后,基础事实型命中率从 95% 掉到 80%,推断型从 70% 涨到 85%------这是"切太大"导致的。能针对性把基础事实型的 chunk 上限从 480 调回 350,再跑一次。

四、避免"评测集污染"

golden set 跑一段时间后会发现:检索器、生成端对评测集里的问题"越来越熟"------因为这些 question 进了向量库,被检索的某段内容里甚至包含 golden set 原文,命中是"假命中"。

防污染有两个做法:

一是评测集的问题**不进索引**。在构建评测集时,把这一组问题在数据导入阶段显式排除。

二是每隔一段时间换一批问题。从真实工单里抽样、或者人工改写已有的问题,让"熟"的那部分不反复被测试到。

```python

反污染:导出问题时去重 hash

import hashlib

def question_id(q: str) -> str:

return hashlib.md5(q.encode("utf-8")).hexdigest():8

```

五、指标定义

最朴素的指标是三个数字:

  • **命中率**:所有 expected_keywords 都在答案里出现,且没有 forbidden_keywords 触发。

  • **拒答率**:当问题不该回答时(如超出范围),答案是否主动拒答。

  • **漂移率**:同一份 golden set 在两次版本之间,命中率的变化幅度(不是绝对值,是差值)。

经验上,命中率稳定在 85% 以上算合格;两次版本之间漂移超过 5 个百分点就要查 diff------是检索端变了、还是切分端变了、还是哪条数据被改了。

六、报告怎么写

跑完一次回归,输出的 CSV 直接给业务方看是不行的------他们关心的是"问题变多了还是变少了",不是每一行的明细。

```python

report.py

import csv

from collections import Counter

with open("eval_report.csv", encoding="utf-8") as f:

rows = list(csv.DictReader(f))

status = Counter()

for r in rows:

if r"expected_hit" == "True" and r"forbidden_hit" == "False":

status"命中" += 1

elif r"forbidden_hit" == "True":

status"误答(禁词)" += 1

else:

status"漏答" += 1

print("\n本次回归结果:")

for k, v in status.items():

print(f" {k}: {v}")

print(f"\n漏答样例(前 3 条):")

for r in r for r in rows if r\["expected_hit" == "False" and r"forbidden_hit" == "False"]:3:

print(f" - {r\['id'}] {r'question'}")

```

这种"分层数字 + 样例列表"的报告形式,业务方一眼能看懂"这次升级是好是坏"。具体到技术细节(chunk 大小、Embedding 模型、top-k),是技术团队内部追的事。

七、什么时候该补样本

补样本的三个信号:

  • 真实工单里出现了评测集里没有的问题类型------补一两条进去。

  • 连续两次回归,某一层命中率掉到 80% 以下------把这一层样本补到 10 条以上再判断。

  • 某条样本被重复判定为"漏答"------说明要么样本设计有问题(关键词选得不合理),要么系统确实有 bug。

不补样本的代价是"指标看着挺好,但客户实际用起来还是觉得答得不对"。

八、小结

  • golden set 不用大,30-50 条分 5 层就够;

  • 关键词命中 + 禁词不命中是稳定且轻量的打分方式;

  • 评测集不进索引,隔段时间换一批防污染;

  • 命中率达 85% 合格,版本间漂移 < 5% 算稳;

  • 报告按"分层数字 + 漏答样例"格式输出,业务方和技术团队都看得懂。

这套流程跑顺之后,模型升级、切分调整、新接入数据源都有了"硬指标"可以参考------而不是靠感觉。

(本文由一支长期做企业知识库与内容工程的技术团队整理,欢迎同行交流指正。)

相关推荐
reasonsummer1 小时前
【办公类-146-07】20260906《总园大班6个班级四大教育》(优化版:标题excle+复制AI文字+Python占位符录入)
人工智能·python·c#
2601_962284501 小时前
利用Python语言实现实验室自动化
python·自动化·数据采集·labview·科学计算
隐擎fox1 小时前
网络传输中的 DNS 泄漏成因剖析与 Python 自动化检测排查实战
python·网络协议·自动化·dns·ip/tcp
2601_962296282 小时前
python初学者怎么选IDE
ide·python·开发环境·初学者·工具选择
青少儿编程课堂3 小时前
威佐夫博弈(双堆取子游戏)解析
c++·python·算法·bfs·信息学竞赛
hhzz4 小时前
【OpenCV 入门到精通 03】图像入门:读取、显示、保存完全指南
人工智能·python·opencv·计算机视觉
金融小师妹5 小时前
AI趋势识别:黄仁勋宣布“AGI时代”到来,Astra能力跃迁背后的AI安全边界
大数据·python·深度学习·重构·逻辑回归
2501_941875287 小时前
从配置中心到动态管理的互联网工程语法演进与多语言实践分享
开发语言·python
hfywmsj8 小时前
广州餐饮铺位招租决策模型:多因子选址系统设计
开发语言·人工智能·python·广州餐饮铺位招租