03 · 打分内核:任务、奖励门与 pass^k
这是全库最该弄懂的一章。tau2-bench 的可信度全压在「凭什么算对」上,而它的答案和多数人的直觉不一样。
3.1 一个任务的答案卷长什么样
任务 Task(tasks.py:560)里管打分的是 evaluation_criteria(EvaluationCriteria,tasks.py:366)。它有四种「期望」,外加一个总开关:
| 字段 | 期望什么 | 对应 RewardType |
|---|---|---|
actions | 一条能解题的参考工具调用序列 | ACTION |
env_assertions | 在终态环境上跑的布尔断言 | ENV_ASSERTION |
communicate_info | Agent 必须对用户说出的字符串 | COMMUNICATE |
nl_assertions | 交给 LLM 主观判真假的自然语言断言 | NL_ASSERTION(实验性) |
reward_basis | 总开关:上面哪几项真正参与算分 | — |
reward_basis 默认是 [DB, COMMUNICATE](tasks.py:435),和最初的 τ-bench 对齐。
最反直觉的一点(必须记住):
actions不是「Agent 必须逐个调对的动作清单」。它是一条参考轨迹,评测器把它重放出一个「标准终态」用来比对。只要 Agent 用别的路径达到等价终态,DB 分照样满分。actions只有在reward_basis里显式包含ACTION时才变成硬性逐调用要求——标准的 airline/retail/telecom 任务都不用 ACTION(tasks.py:237-260 的注释、domains/README.md:38-47 反复强调这点)。
3.2 打分总流程:一个乘法门
入口 evaluate_simulation(evaluator.py:88)。先过两道闸,再算分:
① 这次 模拟是「正常结束」吗(AGENT_STOP / USER_STOP)?
否 ──▶ reward = 0.0,附终止原因,结束 (evaluator.py:113-123)
是 ▼
② 有 evaluation_criteria 吗? 没有 ──▶ reward = 1.0
有 ▼
③ 分别算 env / action / communicate (/ nl) 四个分量
▼
④ reward = 1.0;对 reward_basis 里的每一类,reward *= 该类分量
(只要有一类是 0,总分就是 0) (evaluator.py:215-248)
第 ④ 步是乘法不是加权平均,含义很硬:任何一个被选中的维度不达标,整题 0 分。客服场景要的就是这种「木桶效应」——改错了库、或漏说了必须说的话,都算失败。
每个分量各自也是 0/1:action/communicate 都是「所有期望全满足才 1,否则 0」(evaluator_action.py:101-103、evaluator_communicate.py:39-41)。
3.3 DB 分:整场评测的皇冠
RewardType.DB 这一分量,是 tau2「只看终态不看过程」哲学的落点。看 EnvironmentEvaluator.calculate_reward(evaluator_env.py:22)。它同时搭两个环境:
预测环境 predicted 标准环境 gold
──────────────── ──────────────
set_state( 开局 + 整段真实轨迹 ) set_state( 开局对话历史 )
│ 重放 Agent 实际调的工具 │
│ └─ 再逐个执行 answer 的 actions
▼ ▼
predicted DB hash gold DB hash
└──────────── 相等? ───────────┘
是→db_reward=1 否→0
关键点:
- 比的是 Agent 库哈希 AND User 库哈希都相等(evaluator_env.py:116-118)——双控领域里用户侧状态也要对。
- 标准终态是现算的:把
actions逐个make_tool_call打进 gold 环境(evaluator_env.py:99-105),而不是硬编码一个期望 DB。所以出题人只要给「一种解法」,框架自动推导出「正确终态」。 - 这就是为什么 3.1 说
actions是「参考轨迹」——它的作用是生成 gold 终态,不是逐条卡 Agent。
env_assertions 是补充:在预测环境上跑布尔函数(如「这条线现在是停用状态吗」),全过才给 ENV_ASSERTION 分(evaluator_env.py:128-142)。
3.4 另外两个分量(轻量但有坑)
COMMUNICATE:子串匹配。 检查 communicate_info 里每个字符串是否原样出现在 Agent 某条文字消息里——小写化、去逗号后做子串包含(evaluator_communicate.py:69)。够用但脆:源码自己标了 TODO: This could be improved!。典型用途是「Agent 有没有把退款金额 $150 报给用户」。
ACTION:逐调用匹配。 只在 reward_basis 含 ACTION 时才真正卡分。判定靠 Action.compare_with_tool_call(tasks.py:178):工具名要一致,参数按 compare_args 指定的子集比对(不指定就全比)。主要用在少数 banking_knowledge 任务上。
NL_ASSERTION:LLM 当裁判。 把自然语言断言丢给一个评判 LLM 判真假,实验性质,默认不进 reward_basis。
3.5 pass^k:一次过不算过
单次 0/1 分噪声很大。tau2 沿用 τ-bench 的 pass^k(agent_metrics.py:113)衡量稳定性——同一题跑 num_trials 次,随机抽 k 次全部成功的概率:
C(success_count, k)
pass^k = ───────────────────── (组合数之比)
C(num_trials, k)
直觉:k=1 就是普通成功率;k 越大越苛刻——pass^4 高,说明这题 Agent 不是碰运气过的,而是四次抽签次次都对。上层 compute_metrics(agent_metrics.py:202)对所有任务的 pass^k 求平均,连同 avg_reward、平均成本、DB 命中、鉴权成功率、终止原因分布一起汇总成 AgentMetrics。
细节:
INFRASTRUCTURE_ERROR(API 断连等基础设施故障,非 Agent 的错)会被剔除出指标,不算失败(agent_metrics.py:138-145)——避免把网络抖动记到模型头上。
3.6 EvaluationType:想测哪几维自己挑
EvaluationType(evaluator.py:24)是命令层的开关,决定跑哪些评测器:ENV / COMMUNICATE / ACTION 各测一维;ALL(默认)按 reward_basis 组合;带 IGNORE_BASIS 的变体则无视 reward_basis 把所有维度都乘进去(调试/全面体检用)。有个安全检查:如果任务的 reward_basis 要某维、但这次没评它,会直接抛错而不是悄悄少乘一项(evaluator.py:225-230)。
3.7 代码地图
| 主题 | 文件 | 符号名 |
|---|---|---|
| 打分总入口/乘法门 | src/tau2/evaluator/evaluator.py | evaluate_simulation |
| 评测维度开关 | src/tau2/evaluator/evaluator.py | EvaluationType |
| DB 终态比对 | src/tau2/evaluator/evaluator_env.py | EnvironmentEvaluator.calculate_reward |
| 动作匹配 | src/tau2/evaluator/evaluator_action.py | ActionEvaluator.calculate_reward |
| 动作比对规则 | src/tau2/data_model/tasks.py | Action.compare_with_tool_call |
| 沟通子串匹配 | src/tau2/evaluator/evaluator_communicate.py | CommunicateEvaluator.evaluate_communicate_info |
| 奖励类型语义 | src/tau2/data_model/tasks.py | RewardType |
| 评测标准结构 | src/tau2/data_model/tasks.py | EvaluationCriteria |
| pass^k 公式 | src/tau2/metrics/agent_metrics.py | pass_hat_k |
| 指标汇总 | src/tau2/metrics/agent_metrics.py | compute_metrics |