数据截至 (上游 commit d28711d0da26)
从终端输出到分数:解析、判定、指标、续跑
30 秒导读: Agent 跑完之后,屏幕上只是一屏乱糟糟的终端文字。这一章讲清楚三件事—— 怎么把那屏文字解析成"哪个测试过了"、怎么判定这个任务算不算解决、怎么把成百上千 次尝试聚合成一个 benchmark 分数(accuracy / pass@k),以及怎么用锁文件让一次中断的 大跑能安全续上、不被偷偷改过的配置污染。
这一章接在 02-harness-loop(一次 trial 的主线)之后。02 讲"怎么把 agent 跑起来、
怎么跑测试";本章讲"跑完之后那半段"——从终端 pane 文字到 results.json 到排行榜。
1. 先看全景:一次 trial 的"后半段"
一次 trial 到了尾声,harness 手里只有一样东西:测试脚本跑完后终端窗口的整屏文字
(post_test_pane)。从这坨文字到最终分数,要过四道关:
终端整屏文字 post_test_pane
│
▼
① 解析 Parser.parse(pane) ─── ──► {测试名: PASSED/FAILED} (抽状态)
│ │
│ 解析抛异常 ▼
│ ② 判定 _is_resolved
└──► failure_mode=PARSE_ERROR (全 PASSED 才 True)
│
▼
③ 落盘 TrialResults → results.json
(每个 trial 一份,单一真相源)
│
many trials ▼
④ 聚合 BenchmarkResults
accuracy / n_resolved / pass@k
│
▼
⑤ 上传 S3 整目录 + Postgres 结构化
怎么读这张图: 从上到下是一次结果的生命周期。左边那条岔路是"解析失败"——
测试根本没跑出预期格式时,直接记一个失败原因(FailureMode),不进后面的判定。
各部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
Parser(如 PytestParser) | 把终端文字抽成 {测试名: 状态} | parsers/pytest_parser.py |
Harness._is_resolved | "全部测试 PASSED"才算解决 | harness/harness.py:536 |
TrialResults | 一次尝试的完整档案(含失败原因、token、各阶段时间戳) | harness/models.py:43 |
BenchmarkResults | 把 N 次 trial 聚合成 accuracy / pass@k | harness/models.py:62 |
RunLock 一族 | 锁住配置,保证续跑不漂移 | utils/run_lock.py:185 |
results.json(每 trial 一份) | 单一真相源,续跑时重建全局 | harness/harness.py:1013 |
2. 解析层:从一屏文字里抠出"谁过了"
2.1 统一契约:BaseParser 与 UnitTestStatus
所有解析器都实现同一个抽象方法,产出同一种字典。这样后面的判定、聚合完全不关心 底层用的是 pytest 还是别的东西。
# 示意,非源码:所有解析器共同的契约
class BaseParser(ABC):
@abstractmethod
def parse(self, content: str) -> dict[str, UnitTestStatus]: ...
真实定义在 parsers/base_parser.py:12 的 BaseParser.parse。关键在于输出类型:
UnitTestStatus 只有两个值——PASSED / FAILED(base_parser.py:7-9)。注意这里没有
skipped、没有 xfail:外面世界只有"过"和"不过"两种颜色。测试框架里那些花样,是在解析器
内部就被折叠掉的(见 2.3)。
2.2 PytestParser:只信"short test summary info"那一段
pytest 跑完会输出一大坨,但结尾有一段固定的汇总,长这样:
=========== short test summary info ============
PASSED tests/test_foo.py::test_a
FAILED tests/test_foo.py::test_b - assert 1 == 2
SKIPPED tests/test_foo.py::test_c
====================== 1 failed, 1 passed, 1 skipped ======================
PytestParser.parse(pytest_parser.py:82)的策略很干脆:用正则把整屏文字从这一行汇总标题
处劈成两半,只吃后半段。
# 示意,非源码:解析的主干思路
parts = re.split(r"=+\s*short test summary info\s*=+", content, maxsplit=1)
if len(parts) < 2:
raise ValueError("No short test summary info found") # 没这段 → 解析失败
summary = parts[1] # 只要标题之后的部分
return self._parse_test_results(summary.splitlines()) # 逐行抽
对应真实符号:切割正则是 SHORT_TEST_SUMMARY_INFO_PATTERN(pytest_parser.py:39),
劈不出两段就 raise ValueError(pytest_parser.py:90-93)——这个异常后面会被 harness 接住、
记成 PARSE_ERROR(见 §4)。
为什么只吃汇总段? pytest 正文里也会出现 "PASSED"、"FAILED" 字样(进度点、traceback),
连正文一起扫会误判。汇总段格式最稳:一行一个测试,状态 路径::测试名。
逐行抽取时还有两处细节:
_clean_line(pytest_parser.py:42):FAILED行尾常挂一句- assert ...的原因说明, 用分隔符" - "把它切掉,免得污染测试名。_parse_result_line(pytest_parser.py:50):按空白切成"状态 + 路径",状态词若不在 已知枚举里就记UNKNOWN(跳过),测试名取::之后的部分。
2.3 最反直觉的一格:skipped / xfail 记成 PASS
pytest 的原始状态比两值丰富,PytestTestStatus(pytest_parser.py:7-17)列了 7 种。落回
UnitTestStatus 时的映射,是这一章最容易踩的坑——skipped 和 xfail 都算过:
| pytest 原始状态 | 折叠成 | 直觉 |
|---|---|---|
PASSED | PASSED | 正常通过 |
SKIPPED | PASSED | 被跳过 = 不拦路,视作过 |
XFAIL(预期失败,果然失败了) | PASSED | 符合预期 = 过 |
FAILED | FAILED | 真失败 |
XPASS(预期失败,却过了) | FAILED | 意外 = 当失败处理 |
ERROR | FAILED | 测试自身报错 |
UNKNOWN | FAILED | 认不出的状态,保守判失败 |
真实实现是 PytestTestStatus.to_test_status(pytest_parser.py:19-35)的两个 match 分支。
把 SKIPPED/XFAIL 归到 PASS 这件事,直接影响 §3 的"全 PASSED 才算解决"——一个任务里
如果有测试被合理地 skip 掉,不会因此判成没解决。
2.4 为什么还有 swebench / swelancer / mlebench / sweperf 四个解析器
PytestParser 只认得 pytest 的汇总格式。但 terminal-bench 通过 adapter 移植进来的外部
benchmark(见 06-datasets-adapters),它们的测试脚本输出格式各不相同,
pytest 的正则对它们无效。所以每种外部 benchmark 配一个专属解析器:
| 解析器 | 它认的输出长啥样 | 依据 |
|---|---|---|
SWEBenchParser | 夹在 SWEBench results starts/ends here 之间,整块 == PASSED 才过 | parsers/swebench_parser.py:9-35 |
MLEBenchParser | 夹在 marker 之间,整块 == ALL TESTS PASSED 才过 | parsers/mlebench_parser.py:8-25 |
SWEPerfParser | 同 marker 套路,性能 benchmark 的结果块 | parsers/sweperf_parser.py:11- |
SWELancerParser | 扫 swe lancer success / failure 关键词 | parsers/swelancer_parser.py:6- |
这些解析器共同的暗线:外部 benchmark 的测试脚本自己在输出里打了起止 marker,解析器
只负责在两个 marker 之间取那一块、跟一个"成功字符串"做比对。找不到 marker 就 raise
(例如 swebench_parser.py:13-21,注释里说这通常是服务端拉不到仓库,属于环境问题不是 agent 的错)。
2.5 用哪个解析器,由任务自己声明
选解析器不是全局设定,而是每个任务在配置里挑。任务配置有个字段
parser_name: ParserName,默认 PYTEST(handlers/trial_handler.py:53-56)。TrialHandler
在初始化时用工厂把它实例化:
# 示意,非源码
self.parser = ParserFactory.get_parser(self.task.parser_name) # trial_handler.py:250
ParserFactory(parsers/parser_factory.py:19)就是一张 ParserName → 解析器类 的查表
(PARSER_NAME_TO_CLASS,parser_factory.py:20-26),认不出的名字直接 raise ValueError。
ParserName 枚举(parser_factory.py:11-16)列全了上面五种。这样加一个新 benchmark,
只要"写个解析器 + 注册进枚举和查表",判定/聚合那半段一行都不用动。
3. 判定层:一票否决的"全 PASSED 才算解决"
解析出 {测试名: 状态} 之后,判定极其简单,也极其严格:
def _is_resolved(self, parser_results): # harness.py:536
if parser_results is None:
return False # 没结果 = 没解决
return all(r == UnitTestStatus.PASSED for r in parser_results.values())
三条语义要记住:
- 一票否决:只要有一个测试是
FAILED,整个任务判is_resolved=False。没有"部分得分"。 - 空字典的坑:
all([])在 Python 里是True。也就是说,如果解析器抽出了一个空结果 (一个测试都没匹配到),会被判成"解决"。所以解析器"抽到东西"这件事本身很重要—— 抽不到通常应该走raise(§2.2/2.4)而不是返回空字典。 None与空字典不同:解析异常时 harness 传的是失败原因、parser_results保持None,_is_resolved(None)明确返回False。
判定的调用点在 _run_trial 尾部(harness.py:819):先把 parser_results 存进 TrialResults,
再算 is_resolved。
4. 失败原因:FailureMode 十一态,以及它们在哪被贴上
一次 trial 不是只有"解决/没解决",还要记为什么没解决——这对 debug 和统计极重要。
FailureMode(agents/failure_mode.py:4-15)是个 11 值枚举。下面按"在流水线哪一步被贴上"排:
| FailureMode | 含义 | 在哪被设置(依据:) |
|---|---|---|
UNSET | 初始占位,还没跑到判定 | TrialResults 默认值 models.py:49 |
NONE | 这一步没出错(成功路径) | agent 成功返回 base_agent.py:23;测试/解析成功 harness.py:595,603 |
AGENT_TIMEOUT | agent 超时(仍会继续跑测试) | harness.py:673(_run_agent) |
CONTEXT_LENGTH_EXCEEDED | 上下文超长 | harness.py:682 |
OUTPUT_LENGTH_EXCEEDED | 输出超长 | harness.py:688 |
FATAL_LLM_PARSE_ERROR | 解析不了 LLM 自己的回复 | harness.py:694 |
UNKNOWN_AGENT_ERROR | agent 返回 None / 兜底异常 | harness.py:658,695,701;trial 顶层兜底 harness.py:1028 |
TEST_TIMEOUT | 测试脚本超时 | harness.py:593(_run_tests) |
PARSE_ERROR | 解析器抛异常(如没找到汇总段) | harness.py:611(_parse_results) |
AGENT_INSTALLATION_FAILED | 装 agent 就失败了 | agents/installed_agents/abstract_installed_agent.py:166(见 04-agents) |
UNKNOWN | 枚举里留了,但源码中未见显式赋值 (inferred) | failure_mode.py:7 |
这些贴标签的时机,主线在 02-harness-loop 里。这里补两处判定优先级的细节
(都在 _run_trial,harness.py:703):
- agent 超时不短路测试:
AGENT_TIMEOUT只记下失败原因,仍然往下跑测试 (harness.py:752-757)——因为 agent 可能超时前已经把活干完了,值得验一下。 - 失败原因不覆盖:测试超时要写进结果,前提是
failure_mode还是UNSET(harness.py:800-806);agent 阶段已经贴过标签的,不被测试阶段盖掉。谁先出错记谁。
5. 结果模型:三张表
5.1 TrialResults —— 一次尝试的完整档案
TrialResults(models.py:43-59)是最小的落盘单位。除了 is_resolved、failure_mode、
parser_results,它还记了:
- token 计量:
total_input_tokens/total_output_tokens(从 agent 结果回填,harness.py:762-764)。 - 六个时间戳:trial / agent / test 各自的 started_at、ended_at(
models.py:54-59)—— 能拆出"agent 想了多久、测试跑了多久"。 recording_path:asciinema 录像的相对路径,用来回放 agent 到底敲了什么。
5.2 BenchmarkResults —— 把一堆 trial 聚合成分数
BenchmarkResults(models.py:62)就是 list[TrialResults] 加一组 @computed_field。最基础的
两个:
accuracy(models.py:134-139)=n_resolved / 总 trial 数。空结果时返回0.0。n_resolved(models.py:114-117)=is_resolved为真的 trial 数。
注意 accuracy 的分母是所有 trial,不是任务数。如果 n_attempts=3、10 个任务,那分母是 30。
所以当每个任务多跑几次时,accuracy 更像"平均单次成功率",而不是"解决了几个任务"——后者要看 pass@k。
5.3 RunMetadata —— 一次大跑的封面
RunMetadata(models.py:14-40)记的是整场 run 的元信息:run_id、数据集名/版本、agent 名、
model 名、并发数、n_attempts、commit hash、用户名、起止时间,以及跑完回填的 accuracy 和
pass_at_k。它在开跑时写一次(_write_run_metadata,harness.py:934),跑完再更新终值
(_update_metadata_on_end,harness.py:971)。resumed_at 字段专门标记这是不是一次续跑。
6. pass@k:多跑几次,"至少中一次"的无偏估计
6.1 要解决的问题
Agent 有随机性。同一个任务跑 1 次可能失手,跑 10 次里中 3 次。pass@k 回答的是: "如果我从这些尝试里随机抽 k 次,至少有一次成功的概率是多少?"这比单次 accuracy 更能反映 "这个 agent 在预算 k 次时的真实能力"。
6.2 为什么不能"抽 k 次数数",而要用公式
最朴素的做法是真的去随机抽 k 次、看中没中。但那有采样噪声。terminal-bench 用的是 HumanEval
那套无偏估计:某任务总共跑了 n 次、其中 c 次成功,那么"抽 k 次全都落在失败堆里"的
概率是 C(n-c, k) / C(n, k),于是至少中一次 = 1 − C(n-c, k)/C(n, k)。
真实实现 _pass_at_k_estimator(models.py:74-78)没直接算组合数(会溢出),而是用连乘的
等价形式:
def _pass_at_k_estimator(self, n, c, k): # models.py:74
if n - c < k:
return 1.0 # 失败数还不够抽满 k 个 → 必中
return float(1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1)))
那个 if n - c < k: return 1.0 是边界:失败的次数比 k 还少,随便抽 k 个都不可能全是失败,
所以必中,概率 1。
6.3 k 取哪些值:2^i,外加 5、10
不是每个 k 都算,只算有意义的几档。pass_at_k(models.py:90-112)先求所有任务里最少
跑了几次(min_attempts),k 的候选是 2, 4, 8, … 一路到 min_attempts(models.py:105,
{2**i for i in range(1, …)}),再补上 5 和 10(如果跑得够多)。
两个要点:
- 从 2 起步:
range从i=1开始,最小的 k 是2^1=2,不算 pass@1(那基本就是 accuracy)。 len(success) < k的任务被跳过(_calculate_pass_at_k,models.py:80-88):某任务跑的次数 不够 k,就不参与这一档 pass@k 的平均,避免拿不足样本硬算。
每一档 pass@k 是"各任务估计值的平均"(np.mean(passes),models.py:88)。
7. 落盘:每个 trial 的 results.json 是单一真相源
这是整个续跑机制的地基,值得单独强调。
harness 有两级 results.json:
output/<run_id>/
├── tb.lock ← 配置锁(§8)
├── run_metadata.json ← 封面
├── results.json ← 全局聚合(可随时重建,不是真相源)
└── <task_id>/
└── <task>.<i>-of-<N>.<run_id>/ ← 一次 trial 的目录
└── results.json ← 【单一真相源】每 trial 一份
每次 trial 一跑完就立刻把自己的 TrialResults 写进自己目录
(_execute_single_trial,harness.py:1013)。全局那份 results.json 只是把它们汇总
(_write_results,harness.py:830),每完成一个 trial 就重写一遍。
关键设计(_load_previous_results 的文档串,harness.py:1039-1043 明说):
"个别 trial 的 results.json 是单一真相源,全局那份只是聚合、可以重建"。 续跑时正是
遍历每个 trial 目录、把它们的 results.json 读回来重建 BenchmarkResults
(harness.py:1063-1090)。所以哪怕全局文件坏了,只要 trial 级的还在,历史成绩就不丢。
8. 续跑与防漂移:锁文件把配置钉死
8.1 什么算"完成",续跑跳过谁
重开一个已存在的 output/<run_id>/ 目录就触发续跑(_is_resuming,harness.py:154)。
判断一个任务"做完了"的标准很简单:它的每一次 attempt 目录里都有 results.json
(_filter_completed_and_cleanup_incomplete_tasks,harness.py:296-353)。
- 全 attempt 都有结果 → 完成,从待跑列表里剔除。
- 有 attempt 缺结果 → 未完成;而且把那个任务的半成品目录整个删掉
(
_clean_incomplete_task_artifacts,harness.py:365),避免残留 pane/日志污染重跑。
8.2 tb.lock:五段配置指纹
开跑时 harness 写一份 tb.lock(_create_run_lock,harness.py:505)。它就是 RunLock
(run_lock.py:185)序列化的 JSON,把这次跑的"身份"钉死成五段:
| 锁段 | 记什么 | 依据 |
|---|---|---|
HarnessLock | terminal-bench 包版本、是否 editable 安装 | run_lock.py:52-57 |
AgentLock | agent 名、module:class 导入路径、model 名、额外 kwargs | run_lock.py:127-145 |
RunConfigLock | 日志级别、并发数、n_attempts、超时倍率与各超时 | run_lock.py:163-175 |
DatasetLock | 数据集名/版本 或 本地路径、task_ids、registry | run_lock.py:60-124 |
LocalConfig | 输出路径、run_id、是否上传 | run_lock.py:177-183 |
8.3 续跑先比锁:配置漂移一律拒绝
续跑时不直接信任你这次的命令行参数,而是拿当前配置重新造一份 RunLock,跟磁盘上那份
比对(_validate_resume_configuration,harness.py:200-247)。不一致就 raise ValueError
拒绝续跑(harness.py:243-247)。这防的是"你上次用模型 A 跑了一半,这次手滑改成模型 B 想续"
这种会把成绩搅浑的操作。
比对逻辑有几处讲究(RunLock.__eq__,run_lock.py:197-213):
- 忽略时间戳和调用命令:
created_at、invocation(命令行原文)不参与比较——同一份配置 换个时间、换种敲法重跑,仍算"同一次配置"。 - task_ids 按集合比:
DatasetLock.__eq__(run_lock.py:105-124)把 task_ids 当集合比, 顺序不影响。 - editable 安装也算指纹:
HarnessLock里记了is_editable_installation(run_lock.py:57,由_is_editable_installation读direct_url.json判定,run_lock.py:29-49)。 它进RunLock.__eq__的比较——所以从"pip 装的版本"切到"本地 editable 开发版"再想续跑, 会被这一格拦下。
另外 RunLock.from_json(run_lock.py:218-226)读锁时还会顺手校验目录结构:run 目录名要等于
run_id、每个 trial 目录名要符合 <task>.<i>-of-<N>.<run_id> 命名、attempt 编号要从 1 连续
(_validate_directory_structure,run_lock.py:228-319)。结构对不上也拒绝——防的是被手工
挪乱过的目录。
9. 上传:S3 存全量、Postgres 存结构化
跑完且 upload_results=True 时走 _handle_results_upload(harness.py:1199),两条并行去处:
- S3 存整目录:
_upload_results_to_s3(harness.py:873)把output/<run_id>/下所有文件 (含录像、pane、日志、每个 trial 的 results.json)逐个upload_file到s3://<bucket>/<run_id>/, 单个文件失败不中断、只记进failed_uploads。这是"全量原始产物",用于事后复盘。 - Postgres 存结构化:
upload_results_to_db(db.py:205-234)把RunMetadata+ 每个TrialResults转成 SQLAlchemy 行写库。它对每条TrialResults调DBTaskResult.from_pydantic(db.py:107-131,调用点在db.py:225)——这里才是实际跑的那个转换方法:把parser_results里的UnitTestStatus枚举转成字符串、failure_mode转成它的.value。这是"可查询的结构化成绩", 喂排行榜。(db.py里另有一个DBTrialResult.from_pydantic(db.py:158-185)逻辑一模一样, 但没被调用,属遗留并行实现,见 §11。)
两者失败都不炸主流程:S3 整体异常只 log(harness.py:931-932),DB 异常也被兜住
(harness.py:1223-1224)——成绩已经落在本地 results.json 了,上传只是搬运。
10. 巧妙之处(可借鉴)
- 单一真相源在最细粒度:真相是每个 trial 自己的
results.json,全局聚合永远可重建 (harness.py:1039-1043)。这让"跑到一半崩了"变成小事——重建即可,不依赖任何全局状态没坏。 - 状态折叠只发生在解析器内部:pytest 的 7 种状态在
to_test_status(pytest_parser.py:19) 就被压成两值,判定/聚合层永远只面对 PASSED/FAILED。加新 benchmark 不碰下游。 - pass@k 用连乘避溢出:
1 − ∏(1 − k/i)(models.py:78)等价于组合数比值,却不会算爆 大阶乘。这是 HumanEval 传下来的经典写法。 - 续跑先比锁再动手:把"配置指纹"物化成
tb.lock、续跑前先 diff(harness.py:243), 用一个__eq__就挡住了所有"半路改配置"的成绩污染。忽略时间戳/命令行、task_ids 按集合比, 是"比该比的、放过不该比的"的好范例。
11. 边 界与局限(诚实)
- 空
parser_results会被判成解决:all([]) == True(§3)。解析器返回空字典而非raise时,会误判成功。防线在"抽不到就抛异常"的约定,而非判定层。 FailureMode.UNKNOWN是死枚举:枚举里有,全代码库未见显式赋值 (inferred)。真正的兜底 用的是UNKNOWN_AGENT_ERROR。- pass@k 需要
n_attempts够大:默认n_attempts=1时,min_attempts=1、k 候选从 2 起 (§6.3),pass_at_k基本为空。要看 pass@k 必须显式多跑几次。 - accuracy 的分母是 trial 不是任务(§5.2):多 attempt 时别把它当"解决了几个任务"读。
- DB 模型有历史包袱:
db.py里既有DBTaskResult(db.py:85,写task_results表) 又有DBTrialResult(db.py:133,写trial_results表),两者的from_pydantic转换逻辑 一字不差。但upload_results_to_db只调DBTaskResult.from_pydantic(db.py:224-225), 实际写的是task_results表;DBTrialResult那套是命名从 "task" 迁到 "trial" 时留下的 未被调用的并行实现(§9 已据此纠正为DBTaskResult.from_pydantic)。
12. 代码地图(导航索引)
| 主题 | 文件 | 符号 |
|---|---|---|
| 解析器契约 + 两值状态 | terminal_bench/parsers/base_parser.py | BaseParser.parse · UnitTestStatus |
| pytest 解析主干 | terminal_bench/parsers/pytest_parser.py | PytestParser.parse · SHORT_TEST_SUMMARY_INFO_PATTERN |
| 状态折叠(skip/xfail→PASS) | terminal_bench/parsers/pytest_parser.py | PytestTestStatus.to_test_status |
| 逐行抽取 / 清洗 | terminal_bench/parsers/pytest_parser.py | _parse_result_line · _clean_line |
| 解析器工厂与枚举 | terminal_bench/parsers/parser_factory.py | ParserFactory.get_parser · ParserName |
| 外部 benchmark 解析器 | terminal_bench/parsers/swebench_parser.py 等 | SWEBenchParser · MLEBenchParser · SWEPerfParser · SWELancerParser |
| 任务声明用哪个解析器 | terminal_bench/handlers/trial_handler.py | parser_name(默认 PYTEST)· self.parser |
| 判定:全 PASSED 才解决 | terminal_bench/harness/harness.py | _is_resolved |
| 失败原因枚举 | terminal_bench/agents/failure_mode.py | FailureMode |
| 贴失败原因的位置 | terminal_bench/harness/harness.py | _run_tests · _parse_results · _run_agent · _run_trial |
| trial 档案模型 | terminal_bench/harness/models.py | TrialResults |
| 聚合与 pass@k | terminal_bench/harness/models.py | BenchmarkResults · _pass_at_k_estimator · accuracy · n_resolved |
| run 封面 | terminal_bench/harness/models.py | RunMetadata |
| trial 落盘(单一真相源) | terminal_bench/harness/harness.py | _execute_single_trial · _write_results · _load_previous_results |
| 配置锁 | terminal_bench/utils/run_lock.py | RunLock · DatasetLock · AgentLock · RunConfigLock · HarnessLock |
| 写锁 / 续跑比锁 | terminal_bench/harness/harness.py | _create_run_lock · _validate_resume_configuration |
| 目录结构校验 | terminal_bench/utils/run_lock.py | _validate_directory_structure |
| 上传 S3 / DB | terminal_bench/harness/harness.py · terminal_bench/db.py | _upload_results_to_s3 · upload_results_to_db(调 DBTaskResult.from_pydantic) |
上一章 04-agents(被评测的主角:Agent 抽象与三大流派)· 下一章 06-datasets-adapters(数据集加载、版本注册与把外部 benchmark 移植进来)· 回 index