【每日一题】大模型输出质量批量评估(Python 编程题,附参考答案)

题目:AI 测试平台需要对大模型的回答做批量自动化评估。给定一批样本,每条包含参考答案 reference 和模型输出 prediction,请用字符级 Jaccard 相似度(交集字符数 / 并集字符数,忽略所有空白字符)给每条打分,并输出统计结果。

要求:

  1. 相似度保留 4 位小数;
  2. 相似度 >= threshold 视为通过,统计总条数、通过数、通过率、平均分;
  3. 返回相似度低于阈值的 badcase(按分数升序,分数相同按 id 字符串升序),最多取 top_k 条;
  4. 空样本列表返回各项为 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

难度:中级
题型:笔试(编程题)

:point_right:参考答案/答题要点:

思路一(暴力直译,面试先写这个保命)

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. 空值/边界处理:两个空串=1.0、一空一非空=0.0、空列表不抛 ZeroDivisionError(除法前判 total)——这是最大的区分点。
  2. 空白归一化用 str.isspace() 而不是只 replace(" ", ""),换行、Tab 也要能处理。
  3. 排序用 key=lambda x: (x["score"], str(x["id"])) 元组多关键字,体现工程素养。
  4. 除零保护、字段缺失用 .get() 兜底(脏数据不能让评测任务整体挂掉)。
  5. 能主动说出字级 Jaccard 的局限:无法识别语义(“支持退货” vs “不支持退货” 相似度反而高),生产上应改用向量余弦相似度 / BLEU / ROUGE / LLM as Judge,Jaccard 只做第一版快速兜底。
  6. 加分:百万级样本时的优化(先按长度剪枝、process_map 多进程、结果落库分片统计,避免一次性 list 全量驻留内存)。