题目:AI 测试平台需要对大模型的回答做批量自动化评估。给定一批样本,每条包含参考答案 reference 和模型输出 prediction,请用字符级 Jaccard 相似度(交集字符数 / 并集字符数,忽略所有空白字符)给每条打分,并输出统计结果。
要求:
- 相似度保留 4 位小数;
- 相似度
>= threshold视为通过,统计总条数、通过数、通过率、平均分; - 返回相似度低于阈值的 badcase(按分数升序,分数相同按 id 字符串升序),最多取
top_k条; - 空样本列表返回各项为 0 且
bad_cases为空;参考答案与输出都为空串视为完全一致(1.0)。
def batch_evaluate(samples: list[dict], threshold: float = 0.8, top_k: int = 3) -> dict:
"""批量评估大模型输出与参考答案的字级 Jaccard 相似度。
Args:
samples: [{"id": 1, "reference": "订单已发货", "prediction": "订单已经发货"}, ...]
threshold: 相似度通过阈值
top_k: badcase 最多返回条数
Returns:
{"total": int, "passed": int, "pass_rate": float,
"avg_score": float, "bad_cases": [{"id": ..., "score": ...}, ...]}
"""
pass
# 断言用例
S1 = [
{"id": 1, "reference": "订单已发货", "prediction": "订单已发货"},
{"id": 2, "reference": "订单已发货", "prediction": "订单已经发货"},
{"id": 3, "reference": "支持七天无理由退货", "prediction": "不支持退货"},
{"id": 4, "reference": "客服工作时间9点到18点", "prediction": ""},
]
r = batch_evaluate(S1)
assert r["total"] == 4
assert r["passed"] == 2
assert r["pass_rate"] == 0.5
assert r["avg_score"] == 0.5583
assert [b["id"] for b in r["bad_cases"]] == [4, 3]
assert batch_evaluate([]) == {"total": 0, "passed": 0, "pass_rate": 0.0,
"avg_score": 0.0, "bad_cases": []}
assert len(batch_evaluate(S1, threshold=1.0, top_k=1)["bad_cases"]) == 1
难度:中级
题型:笔试(编程题)
参考答案/答题要点:
思路一(暴力直译,面试先写这个保命)
def jaccard_similarity(a: str, b: str) -> float:
sa = set(a.replace(" ", "").replace("\n", ""))
sb = set(b.replace(" ", "").replace("\n", ""))
if not sa and not sb:
return 1.0
if not sa or not sb:
return 0.0
return len(sa & sb) / len(sa | sb)
def batch_evaluate(samples, threshold=0.8, top_k=3):
results = []
for s in samples:
score = round(jaccard_similarity(s.get("reference", ""), s.get("prediction", "")), 4)
results.append({"id": s.get("id"), "score": score})
total = len(results)
passed = sum(1 for r in results if r["score"] >= threshold)
bad = sorted([r for r in results if r["score"] < threshold],
key=lambda x: (x["score"], str(x["id"])))[:top_k]
return {
"total": total,
"passed": passed,
"pass_rate": round(passed / total, 4) if total else 0.0,
"avg_score": round(sum(r["score"] for r in results) / total, 4) if total else 0.0,
"bad_cases": bad,
}
思路二(优雅解,一次遍历 + 字典推导,工程上更好扩展)
def jac(a: str, b: str) -> float:
A = {c for c in a if not c.isspace()} # 用 isspace 统一处理空格/换行/Tab
B = {c for c in b if not c.isspace()}
if not A and not B:
return 1.0
if not A or not B:
return 0.0
return len(A & B) / len(A | B)
def batch_evaluate2(samples, threshold=0.8, top_k=3):
scores = {s.get("id"): round(jac(s.get("reference", ""), s.get("prediction", "")), 4)
for s in samples}
total = len(scores)
ok = [v for v in scores.values() if v >= threshold]
bad = sorted(((k, v) for k, v in scores.items() if v < threshold),
key=lambda kv: (kv[1], str(kv[0])))
return {"total": total, "passed": len(ok),
"pass_rate": round(len(ok) / total, 4) if total else 0.0,
"avg_score": round(sum(scores.values()) / total, 4) if total else 0.0,
"bad_cases": [{"id": k, "score": v} for k, v in bad[:top_k]]}
实测运行结果(Python 3.14,assert 全部通过)
样本各条得分: [(1, 1.0), (2, 0.8333), (3, 0.4), (4, 0.0)]
batch_evaluate(S1) ->
{'total': 4, 'passed': 2, 'pass_rate': 0.5, 'avg_score': 0.5583,
'bad_cases': [{'id': 4, 'score': 0.0}, {'id': 3, 'score': 0.4}]}
threshold=1.0, top_k=1 -> passed=1, bad_cases=[{'id': 4, 'score': 0.0}]
思路一与思路二在 threshold ∈ {0.0, 0.8, 0.84, 1.0} 下结果完全一致
ALL PASS
采分点
- 空值/边界处理:两个空串=1.0、一空一非空=0.0、空列表不抛
ZeroDivisionError(除法前判total)——这是最大的区分点。 - 空白归一化用
str.isspace()而不是只replace(" ", ""),换行、Tab 也要能处理。 - 排序用
key=lambda x: (x["score"], str(x["id"]))元组多关键字,体现工程素养。 - 除零保护、字段缺失用
.get()兜底(脏数据不能让评测任务整体挂掉)。 - 能主动说出字级 Jaccard 的局限:无法识别语义(“支持退货” vs “不支持退货” 相似度反而高),生产上应改用向量余弦相似度 / BLEU / ROUGE / LLM as Judge,Jaccard 只做第一版快速兜底。
- 加分:百万级样本时的优化(先按长度剪枝、
process_map多进程、结果落库分片统计,避免一次性list全量驻留内存)。