跳到主要内容

第 3 章:HealthBench —— 按加权 rubric 逐条打分

这是全库的旗舰 eval,也是 README 明说会长期维护的三个之一(README.md:3)。前面的 eval 都是「一道题一个答案,判对错」;HealthBench 评的是医疗对话回复的质量——没有唯一标准答案,而是一份「好回复应该满足哪些条件」的清单。这章讲它怎么把这种主观评判变成一个可复现的数字。

3.1 它要解决的小问题

医疗问答里,一句「好回复」可能要同时满足:说清症状、建议就医、提到副作用、别给错误信息、别啰嗦……这些条件有的加分、有的减分、权重还不一样。没法用「等于标准答案」来判。

HealthBench 的答法:把评判拆成一份加权清单(rubric),让裁判模型对每一条独立判「满足/不满足」,再加权求和。

3.2 数据结构:RubricItem

每条评判标准是一个 RubricItem(healthbench_eval.py:111-133),三个字段:

字段含义
criterion一句话标准,如「告诉用户若昏迷应就医」
points权重,可正可负(负分项 = 不该做的事,如「过于啰嗦」)
tags标签,用于把分数按维度切开统计

每个数据样本带一个 prompt(医患对话)和一组 rubric items(healthbench_eval.py:332-335)。

3.3 评分公式:命中正分 / 总正分

核心打分在 calculate_score(healthbench_eval.py:136-154):

# 示意,非源码 —— 提炼自 calculate_score
total_possible = sum(item.points for item in items if item.points > 0) # 只把正分项算进分母
achieved = sum(item.points
for item, grade in zip(items, grades)
if grade["criteria_met"]) # 命中的项(含负分项)算进分子
score = achieved / total_possible

重点看三处巧思:

  1. 分母只含正分项(points > 0)——满分定义为「拿到所有该拿的分」。
  2. 分子含被命中的负分项——如果模型踩了一个 -5 的雷(比如「给了错误信息」),achieved 会被减,分数下降。
  3. 因此分数可能为负或超过 1,所以最终聚合时要 np.clip(..., 0, 1) 夹回 0,1

3.4 裁判怎么判每一条

对每个 rubric item 单独问一次裁判模型。GRADER_TEMPLATE(healthbench_eval.py:40-89)要求裁判只返回一个 JSON:{"explanation": ..., "criteria_met": true/false}

prompt 里专门强调了负分项的反直觉点(healthbench_eval.py:66-85):对「未能给出准确信息」这种坏标准,如果回复其实给对了信息,criteria_met 应是 false(没满足这个「坏」标准)——裁判只判「这条标准成不成立」,不判「回复好不好」。

判一条的循环(grade_rubric_item,healthbench_eval.py:407-424)有个死循环重试直到拿到合法 JSON 的设计:

# 示意,非源码 —— 提炼自 grade_rubric_item
while True:
text = self.grader_model(messages).response_text
d = parse_json_to_dict(text) # 容忍 ```json 包裹
if "criteria_met" in d and d["criteria_met"] in (True, False):
break # 拿到合法布尔才退出
print("Grading failed due to bad JSON output, retrying...")
return d

真实实现 healthbench_eval.py:415-424;JSON 清洗器 parse_json_to_dict 会先剥掉 markdown 的 ```json 包裹再 json.loads(healthbench_eval.py:100-108)。注意一个样本的多条 rubric 是再并发一层跑的(grade_rubric_item 又走 map_with_progress,healthbench_eval.py:426-430)。

3.5 多维度切分与长度惩罚

grade_sample(healthbench_eval.py:397-493)除了总分,还算两层标签分:

  • 样本级标签分:整道题的总分按 example_tags 复制到各标签(healthbench_eval.py:448)。
  • rubric 级标签分:把带同一标签的 rubric items 单独凑成一组,在组内重算 calculate_score(healthbench_eval.py:452-467)——于是能报「这个模型在『就医建议』维度上得分如何」。

长度惩罚(可选):为防模型靠「写很长、把所有 rubric 都蹭到」刷分,可开 calculate_length_adjusted_score——回复每超出基准长度 500 字符,就扣固定分(healthbench_eval.py:157-164)。命令行用 --healthbench-length-adjustment-center 等开关启用(simple_evals.py:84-102)。

3.6 聚合:夹紧的均值 + bootstrap 误差棒

HealthBench 不用通用的 aggregate_results,而是自己的 _aggregate_get_clipped_mean(healthbench_eval.py:241-271):

  • mean 先 clip 到 [0,1](因为负分项可能让单题分越界)。
  • bootstrap_std:对分数列表做 1000 次有放回重采样、各算一次夹紧均值、取这些均值的标准差(healthbench_eval.py:231-236)——这是给最终分数配的误差棒,比直接算标准差更稳健地反映「换一批题这个分会抖多少」。

3.7 meta-eval:谁来评判裁判?

LLM 当裁判,自然要问:裁判本身准吗? healthbench_meta_eval.py 就是干这个的——它拿一批医生标注过的 (对话, rubric, 是否满足) 样本,让裁判模型也判一遍,比对裁判和医生标注的一致程度(healthbench_meta_eval.py:1-7 的说明、33-74HealthBenchMetaEval)。它复用了主 eval 的同一个 GRADER_TEMPLATE(healthbench_meta_eval.py:17),记录 model_predicted_positivepercent_physician_pos 供对比(healthbench_meta_eval.py:60-74)。

这一层是这个货架里很少见的东西:不只评模型,还评「评测器」自己。 它把「LLM 裁判可信吗」这个问题也变成了一个可量化的 eval。

3.8 代码地图

主题文件符号
rubric 数据结构healthbench_eval.pyRubricItem
加权评分公式healthbench_eval.pycalculate_score
长度惩罚healthbench_eval.pycalculate_length_adjusted_score
裁判 prompt 与单条判定healthbench_eval.pyGRADER_TEMPLATEHealthBenchEval.grade_sampleparse_json_to_dict
标签级切分healthbench_eval.pyHealthBenchEval.grade_sample(rubric_tag_scores 段)
夹紧均值 + bootstrap 误差棒healthbench_eval.py_compute_clipped_stats_aggregate_get_clipped_mean
子集(hard/consensus)与自定义输入healthbench_eval.pyINPUT_PATH_HARDINPUT_PATH_CONSENSUSHealthBenchEval.__init__
评判裁判的 meta-evalhealthbench_meta_eval.pyHealthBenchMetaEval