AI

初探 Coding Agent Benchmark

Posted by Piasy on March 30, 2026
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2026/03/30/Coding-Agent-Benchmark/

背景

随着大模型从“代码补全”逐步走向“自主完成开发任务”,Coding Agent 的评测对象也发生了明显变化。传统代码基准更多关注函数级生成正确率,例如是否能根据题目写出通过单元测试的代码;但对于今天的 coding agent 来说,这类评测已经不足以反映真实能力。一个可用的 agent 往往需要理解 issue、浏览代码仓库、修改多个文件、调用工具、执行命令、运行测试,并在长链路交互中持续纠错。因此,Coding Agent Benchmark 的核心已经从“会不会写代码”,转向“能不能在真实软件工程环境中完成任务”。

在这一背景下,近年来出现了一批更贴近实际开发场景的 benchmark。以 SWE-Bench 为代表的软件工程类基准,强调 agent 在真实仓库中根据 issue 生成正确 patch 的能力;以 Terminal-Bench 为代表的终端交互类基准,则进一步把评测环境扩展到沙箱化命令行,考察 agent 是否真的能够像开发者一样使用 shell、编辑文件和运行测试。与此同时,WebArenaOSWorldτ-Bench 等 benchmark 也从网页操作、操作系统交互、工具调用等角度补充了 agent 能力画像。这说明,单一 benchmark 已很难全面衡量 coding agent,评测体系正在向“多环境、多步骤、多能力维度”演进。

除了 benchmark 本身,另一个值得关注的趋势是评测框架(evaluation harness)平台化。过去很多 benchmark 更像是单独发布的数据集和脚本,而现在越来越多项目开始提供可复用的开源框架,用于构建新任务、组织运行环境、记录轨迹并执行标准化打分。例如,Harbor 提供了面向 agent benchmark 的通用任务与执行框架,适合构建终端和容器环境中的真实任务;OpenHands Evaluation Harness 支持将自定义 benchmark 接入 agent runtime;Inspect AI 则提供了较通用的 dataset-task-solver-scorer 抽象,便于快速搭建多轮交互和工具调用类评测流程。这意味着,benchmark 的建设正在从“做一套题”升级为“搭一套可持续迭代的评测基础设施”。

因此,在讨论 Coding Agent Benchmark 时,需要同时区分两个层面:一类是用于定义“考什么”的 benchmark 数据集与任务集,另一类是用于解决“怎么考”的评测框架与执行系统。前者决定了评测覆盖的软件工程场景是否真实、是否具有代表性;后者决定了评测是否可复现、可扩展、可自动化。对于研究者和产品团队而言,未来的重点不只是复现几个公开分数,而是构建一套能够持续反映真实开发效率、工具使用能力和复杂任务完成度的 benchmark 体系。

今天,我们就来探究一下 Terminal-Bench 和它背后的 Harbor 框架。

Terminal-Bench 2.0

Terminal-Bench 2.0 可以理解为“把 coding agent 放进真实终端环境里做事”的评测集,目前包含 89 个测试任务,完全开源。它不只看模型会不会写一个函数,而是看 agent 能否在容器中完成完整工程流程:理解指令、编辑文件、处理运行时行为、通过验证脚本并拿到最终 reward。每道题通常由 instruction.md(任务说明)、task.toml(元数据与资源限制)、environment(运行镜像)、tests(验证逻辑)和 solution(oracle 参考解)组成,任务组织方式高度标准化,便于复现和扩展。

这里我们看三个典型任务:

  • cancel-async-tasks:这是一个“题面小但语义深”的异步并发题,要求在实现并发上限控制的同时正确处理 SIGINT/KeyboardInterrupt 取消传播,确保任务 finally 清理逻辑仍能执行。
  • mailman:这是一个典型的系统集成题,需要把 postfix + mailman3 + 本地邮箱投递链路打通,并通过真实的“入会确认→群发→退订确认”邮件流程完成端到端验证。
  • fix-ocaml-gc:这是一个重工程 Debug 题,要求在真实 OCaml 源码仓库中修复 GC 改动导致的编译器构建问题并跑通基础 testsuite,考察大型代码库定位与修复能力。

cancel-async-tasks

表面上它只要求实现一个 Python 函数 run_tasks(tasks, max_concurrent),但真正考点不是语法,而是异步系统语义:既要控制并发上限,又要在 SIGINT/KeyboardInterrupt 发生时确保任务的清理逻辑(finally)仍被执行。这种“短指令 + 深语义”的设计很有代表性。

这个任务的验证逻辑也体现了 Terminal-Bench 的风格:测试并不直接调用你的函数断言返回值,而是通过子进程运行脚本、测量执行时长、注入中断信号并统计标准输出,来检查并发行为和取消传播是否正确。例如:

  • n_tasks=2, max_concurrent=2 时,任务应并发完成;
  • n_tasks=2, max_concurrent=1 时,总耗时应接近串行(至少 6 秒);
  • 在不同并发关系(低于/等于/高于上限)下发送 SIGINT,应看到已启动任务进入清理阶段。

这类测试能有效筛掉“看起来能跑、但取消语义错误”的实现(例如错误使用线程池或不当处理任务队列取消)。因此,cancel-async-tasks 很适合用来展示 Terminal-Bench 2.0 的核心价值:它评估的不是“代码片段正确率”,而是 agent 在真实终端环境中的工程可靠性与运行时鲁棒性。

对于这个任务我两个小槽点:作者估计用时居然资深工程师都要 2 小时,初级要 10 小时,我觉得 stackoverflow 工程师都能一小时搞定;还有就是测试用例不是很严谨,cancel 的 case 应该验证没打印 Task finished。

mailman

这个任务看起来像“配个邮件服务”,但真正难点是端到端链路联调:不仅要让 postfix 能收发本地邮件,还要把 mailman3 的列表地址(reading-group@local.edureading-group-join@local.edureading-group-leave@local.edu)正确接进投递路径,并满足“免管理员审批但需用户确认”的策略约束。换句话说,考的不是单个配置项,而是 MTA、LMTP、列表策略和本地邮箱落盘之间的系统一致性。

它的验证逻辑也很有代表性:不是检查某个配置文件字符串,而是直接跑完整邮件行为流。测试会通过 SMTP localhost:25 发信、用 mailbox.mbox("/var/mail/<user>") 读收件箱,并在运行时创建用户验证真实投递。例如:

  • 先验证普通本地邮件是否能送达 /var/mail/<username>
  • 验证 reading-group@local.edu 列表对象是否确实存在;
  • 走完“发 join 请求 → 收到确认邮件并回复 → 成功入会 → 发群发公告 → 发 leave 请求并确认 → 成功退订”的全流程,并检查成员状态变化与邮件数量是否符合预期。

这类任务能很好地区分“会写命令”与“会做系统集成”的 agent:哪怕只漏掉一个环节(比如 postfix 映射、mailman 别名生成、服务启动时序、确认邮件回执),最终端到端行为都会失败。因此,mailman 体现了 Terminal-Bench 2.0 在基础设施类任务上的评测价值:它真正考察的是复杂服务栈的可运行性与操作可靠性,而不是静态配置拼写能力。

fix-ocaml-gc

fix-ocaml-gc 是一个非常典型的“大仓库真实 Debug”任务:题面只说“改 GC 后把编译器 bootstrap 搞崩了”,但实际要求是在 OCaml 源码中定位并修复运行时 bug,然后完成从 ./configuremake -j4 的构建,并跑通 tests/basic。这类题目几乎没有“模板解法”,核心是阅读代码、复现失败、缩小问题范围并给出最小修复。

它的 verifier 设计也很硬核,明显在防“投机过测”:测试阶段会重新 clone 同一 broken tag 的原仓库,把原始 testsuite 覆盖回工作目录,避免模型提前改测试;同时删除 tests.txt,防止伪造结果文件。最终判定不是看 patch,而是看真实执行输出里是否出现 40 tests passed。例如:

  • 必须能把编译器完整构建起来,而不是只修一个局部目标;
  • 必须成功执行 make -C testsuite one DIR=tests/basic
  • 必须在真实测试日志中产出通过标记,才能拿到 reward。

这让它成为 Terminal-Bench 2.0 里非常“工程化”的题:它评估的是 agent 在大型代码库中的故障定位和修复闭环能力,而非小范围函数补丁能力。再结合题目给出的时间预估(资深 24 小时、初级 240 小时),也能看出它刻意强调的是长链路软件工程韧性,而不是快速刷题速度。

Harbor 框架详细介绍

Harbor 是一个开源的、用于评估和优化 AI 代理(agents)和语言模型的框架,由 Terminal-Bench 的创建者(Laude Institute)开发。它旨在为 agentic evaluations 和 rollouts 提供标准化的基础设施,特别适合在容器化环境中运行真实世界的终端任务。

核心概念

Harbor 的架构围绕四个核心组件构建:

  1. 任务(Tasks):评估的基本单元,包含一个指令、一个容器环境和一个测试脚本。任务以 Harbor 任务格式 的目录结构实现。
  2. 代理(Agents):被评估的 AI 系统,例如 Claude Code、OpenHands、Codex CLI 等。
  3. 环境(Environments):容器化的执行上下文,通常由 Docker 镜像定义,支持 Docker、Daytona、Modal、E2B 和 Runloop 等多种云容器运行时。
  4. 验证器(Verifiers):用于测量代理成功与否的测试套件,通过运行测试脚本并计算奖励分数来评估代理表现。

此外,还有两个重要的组织概念:

  • 数据集(Dataset):任务的集合,通常对应一个基准测试(如 Terminal-Bench、SWE-Bench Verified)。
  • 作业(Job):一次评估运行,可以包含多个数据集、代理、任务和模型,通过 JobConfig 类或 job.yaml 文件配置。

核心工作流程

运行一次 Harbor 评估包含以下标准步骤:

  1. 环境设置:Harbor 根据任务中定义的 Dockerfile 创建一个隔离的容器环境。
  2. 代理执行:代理接收任务指令(来自 instruction.md)并在该环境中执行操作。
  3. 验证:验证器运行测试脚本(tests/test.sh)来检查代理的解决方案,并计算奖励分数(通常通过向 /logs/verifier/reward.txt 写入 1 或 0 来表示成功或失败)。
  4. 结果收集:收集指标、日志和代理轨迹(trajectory)以供分析。每次运行会产生一个试验(trial)目录,包含配置、结果和日志文件。

核心原理与设计

1. 任务格式标准化

Harbor 定义了严格的任务目录结构,确保评估的可复现性和可移植性。一个标准任务目录必须包含以下文件:

  • instruction.md:纯文本指令,展示给代理。
  • task.toml:任务配置和元数据。
  • environment/:运行时的构建上下文(如 Dockerfile)。
  • solution/solve.sh:由 Oracle 代理执行的参考解决方案脚本。
  • tests/test.sh:验证的入口脚本。

容器内部遵循特定约定:日志挂载在 /logs/agent/logs/verifier,代理产生的文件放在 /output,解决方案和测试目录分别复制到 /solution/tests

2. 适配器机制

Harbor 通过适配器(Adapters) 集成外部基准测试。适配器将原始基准的任务格式转换为 Harbor 任务格式,确保评估的忠实性和可靠性。开发适配器需要深入理解原始基准的四个关键要素:任务指令、环境设置、验证逻辑和解决方案。

3. 并行与扩展性

框架设计支持大规模并行实验。用户可以通过 --n-concurrent 参数指定并发数,并利用 Daytona、Modal 等云提供商在数千个环境中并行运行评估。

4. 环境抽象

通过 BaseEnvironment 接口,Harbor 提供了与不同容器运行时交互的统一接口,使得支持新的云环境或本地配置变得相对简单。

优缺点分析

优点

  1. 降低维护负担:正如 CompileBench 团队迁移到 Harbor 的经验所示,使用 Harbor 可以将开发者从维护自定义执行 harness(通常是 Go、Rust、Python 和 shell 脚本的混合体)的负担中解放出来,让他们能专注于基准任务本身。
  2. 确保可复现性:标准化的任务格式和容器化环境保证了评估结果在不同运行和不同机器上的一致性和可复现性,这对科学研究和工程实践都至关重要。
  3. 提升敏捷性:框架使得切换执行环境(如从本地 Docker 切换到云环境)、测试新模型或利用评估设置进行强化学习微调变得更加容易和快速。
  4. 生态集成:作为 Terminal-Bench 2.0 的官方 harness,并支持 20 多个流行基准测试(通过适配器),Harbor 处于 agent 评估生态的核心位置。
  5. 开源与社区驱动:完全开源,拥有活跃的 Discord 社区,鼓励贡献和协作。

缺点与考量

  1. 学习曲线:需要理解 Harbor 特定的任务格式、配置文件和运行命令,对于新手有一定入门成本。
  2. 容器依赖:重度依赖 Docker 或其他容器运行时,这在某些受限环境(如无特权容器、严格安全策略的环境)中可能带来挑战。
  3. 适配器开发成本:要将一个现有基准完全集成到 Harbor,需要开发适配器,这需要深入理解原始基准和 Harbor 框架,并投入一定的开发时间。
  4. 相对较新:作为一个较新的框架(2026年初活跃开发),其 API 和功能可能仍在演进中,长期稳定性有待更多生产使用验证。

总结

Harbor 框架通过提供标准化的任务格式、模块化的架构(任务、代理、环境、验证器)和对大规模并行运行的支持,解决了 AI 代理评估中的关键痛点:维护复杂性、可复现性差和扩展困难。它特别适合需要在真实、沙箱化的终端/容器环境中评估 coding agent 能力的团队。虽然需要一定的学习成本,但其降低的长期维护负担、提升的评估一致性以及活跃的社区支持,使其成为构建可持续、可扩展 agent benchmark 基础设施的强有力候选。

测试运行 Harbor benchmark task

我们以 claude-code agent 为例,试着运行一下 cancel-async-tasks。

这个过程最主要的问题是,我不知道怎么传 agent 和 model 参数(具体取值),以及怎么让 harbor 能用到我的 model provider api key。不过经过一番摸索,好歹是摸索出来了。

还有个小坑,在 linux 上安装 docker 之后,需要 sudo 才能运行,而 harbor 不会去 sudo,所以一直报错 docker 服务没运行。直接切换到 root 用户去运行 harbor 就好了。

sonnet 4.6

要用 Anthropic 官方的模型,首先得创建 api key,之前使用都是命令行启动 claude,然后 /login。现在我们需要命令行执行 claude setup-token,然后和 login 一样,浏览器打开认证网页,同意授权,然后把 code copy paste 到命令行,然后 claude 会打印出形如 sk-ant-XXXX 的 api key 了。我们 export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-XXXX" 后,就能不 login 直接使用 claude 了,也就能让 harbor 用上 Anthropic 的模型了。

claude setup-token
# follow the interactive guide

export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-XXXX"
harbor run \
  -d terminal-bench@2.0 \
  -a claude-code \
  -m anthropic/claude-sonnet-4-6 \
  --task-name cancel-async-tasks

这样就能成功用 claude code + sonnet 4.6 去跑 cancel-async-tasks 了,不过出乎意料的是,居然失败了,claude 写的版本是:

import asyncio
from collections.abc import Awaitable, Callable


async def run_tasks(tasks: list[Callable[[], Awaitable[None]]], max_concurrent: int) -> None:
    semaphore = asyncio.Semaphore(max_concurrent)

    async def run_with_semaphore(task: Callable[[], Awaitable[None]]) -> None:
        async with semaphore:
            await task()

    futures = [asyncio.ensure_future(run_with_semaphore(t)) for t in tasks]

    try:
        await asyncio.gather(*futures)
    except BaseException:
        for f in futures:
            f.cancel()
        # Await cancelled tasks so their finally blocks can run before we propagate
        await asyncio.gather(*futures, return_exceptions=True)
        raise

果然命中了作者预判的问题:asyncio.gather doesn’t properly cancel existing tasks if there are still tasks in the queue.

=================================== FAILURES ===================================
____________________ test_tasks_cancel_above_max_concurrent ____________________

    def test_tasks_cancel_above_max_concurrent():
        """
        Test that cancellation occurs and only the first two tasks are started and
        cancelled.
    
        This is a common gotcha in Python because asyncio.gather doesn't properly cancel
        existing tasks if there are still tasks in the queue.
        """
        ...
    
        assert stdout.count("Task started.") == 2
>       assert stdout.count("Cleaned up.") == 2
E       AssertionError: assert 0 == 2
E        +  where 0 = <built-in method count of str object at 0x7fe14d915c50>('Cleaned up.')
E        +    where <built-in method count of str object at 0x7fe14d915c50> = 'Task started.\nTask started.\n'.count

其他模型

我的目标是只测试 claude code 这一个 agent,但要测不同模型,调研了一番,我最终采用了 openrouter 来作为 provider。

创建了 openrouter 账号、充值了 credit、创建了 api key 之后,export 几个环境变量就可以了(这里我先试了个免费模型):

export ANTHROPIC_BASE_URL="https://openrouter.ai/api" && \
export ANTHROPIC_API_KEY="sk-or-v1-XXXXX" && \
export ANTHROPIC_MODEL="stepfun/step-3.5-flash:free"

harbor run \
  -d terminal-bench@2.0 \
  -a claude-code \
  -m stepfun/step-3.5-flash:free \
  --task-name cancel-async-tasks

运行结果和 sonnet 4.6 基本一样,也是踩了 asyncio.gather 的坑。

codex + gpt-5.3-codex high reasoning

GPT plus 会员应该是确实没有 API Key 可用的,所以又充了个 pincc,比 OpenRouter 是便宜多了。

export OPENAI_BASE_URL="https://v2.pincc.ai"
export OPENAI_API_KEY="sk-XXXX"
harbor run \
  -d terminal-bench@2.0 \
  -a codex \
  -m gpt-5.3-codex \
  --task-name cancel-async-tasks

还能通过 --agent-kwarg reasoning_effort=xhigh 参数,设置使用 xhign reasoning。

然后他就水灵灵的通过了!看来比 sonnet 4.6 强。

添加新的 Harbor benchmark task

本章所用的 AI 工具都是 Codex CLI + gpt 5.3 codex high reasoning。

task 选择

和 gpt 讨论了几轮构建 coding agent 的核心点是什么之后,我决定添加一个在复杂项目里做一个中等难度需求的任务,不是做个算法题、不是解个 bug,而是做个需求。

项目就是 HackWebRTC/kmp-socketio(kotlin multiplatform 的 socket-io 客户端实现),但我一开始自己琢磨的几个需求点都有点太简单了,于是我决定问问 AI。

AI 分析完代码库,给出了如下可做点:

功能点 为什么适合 benchmark 关键代码位置 难度
连接超时语义修正(timeout=-1 真正禁用) 现在注释写 -1 disable,但实现会走 delay(-1) 立即超时,属于“真实 bug + 明确验收” Manager.kt
WebSocket 发送失败错误传播 当前 doSend catch 后只打日志,官方还留了 TODO,适合做鲁棒性增强题 WebSocket.kt
Emitter 重入/并发安全 当前明确“不线程安全,回调里 on/off 可能 CME”,很适合考并发正确性 Emitter.kt
动态 auth 刷新(重连前重新取 token) 现在 auth 在 Socket 构造后基本固定,重连场景难支持 token 轮换 Socket.kt 中高
支持嵌套 Binary payload README 明确限制“binary 不能嵌套”,做完后价值高、区分度强 Socket.kt

并建议我优先选这 3 个作为 Harbor task 梯度:

  1. timeout=-1 语义修复(低难)
  2. WebSocket 发送失败传播(中难)
  3. 嵌套 Binary payload(高难)

我决定选择 2,并且还要求 agent 也补充 UT。主要用来考察 agent 的以下能力:

  • 中等复杂度项目的理解能力;
  • kotlin (kotlin multiplatform, KMP) 的开发能力;
  • 中等复杂度需求开发的能力,多文件修改;

测试点设计(AI)

这个任务的核心不是“把一行 TODO 改掉”,而是要验证发送失败能不能从 WebSocket transport 层正确冒泡到上层状态机,并且不破坏正常发送路径。所以在测试设计上,我采用“分层 + 回归保护”的思路:既测 transport 自身行为,也测 EngineSocket/Socket 的级联行为,最后再加一组成功路径防回归。

先把验收目标写清楚:

  1. WebSocket.doSend 里的发送异常不能只打日志,必须触发错误事件。
  2. 发送失败不能伪装成成功发送(不能错误触发 EVENT_DRAIN)。
  3. 上层(EngineSocket/Socket)要能感知失败并进入可恢复状态(如 close、ack 超时)。
  4. 正常发送路径不能被修复逻辑破坏。
  5. agent 不能只改业务逻辑,还必须补充/更新 JVM 单元测试,并保证全量行覆盖率不下降。

基于这个目标,我设计了 7 个测试点:

  1. 文本发送异常传播(Transport 层)
    模拟 ws.send(String) 抛异常,断言触发 Transport.EVENT_ERROR,且不触发 Transport.EVENT_DRAIN
  2. 二进制发送异常传播(Transport 层)
    模拟 ws.send(ByteArray) 抛异常,覆盖 binary 分支,断言同上。
  3. 批量发送首包失败(Transport 层)
    一次发送多个包,第一个包发送失败时应中止后续发送,且不能触发 drain 假成功。
  4. EngineSocket 级联错误感知(Engine 层)
    在连接已建立后触发底层发送失败,断言 EngineSocket.EVENT_ERROREVENT_CLOSE 能被上抛。
  5. 发送失败下 Ack 超时语义(Socket 层)
    发送带 AckWithTimeout 的事件并触发底层发送失败,断言 ack 不会悬挂,超时回调仅执行一次。
  6. 成功路径回归保护(正向路径)
    正常发送时必须仍然有 EVENT_DRAIN,并且不应出现 EVENT_ERROR
  7. 测试与覆盖率门禁(工程约束)
    agent 完成逻辑改动后,必须同时提交 JVM UT;评测侧先跑完整 :kmp-socketio:jvmTest,再生成 kover 报告,校验全量行覆盖率不低于基线值。

这 7 个点的覆盖关系是:1~3 保证 transport 修复是“真修复”,4~5 保证上层语义没有断,6 保证没有引入副作用,7 保证任务结果具备工程可维护性(不是一次性 patch)。这样就能避免 agent 通过“补一行 emit(error)”这种表面修复来投机过测。

此外,在 Harbor 任务落地时我会把测试分成公开与隐藏两组:公开测试给出最基础目标(例如文本/二进制异常传播 + 正常路径),隐藏测试重点卡住投机实现(例如批量首包失败、Engine 级联、Ack 超时 + 覆盖率门禁)。这套分层能明显提高任务区分度,也更符合真实工程中的验收方式。

关于覆盖率门禁:先在任务初始 commit 记录一次 kover 行覆盖率,评测时要求 new_coverage >= baseline_coverage。这样既能鼓励 agent 补测试,又不会因为绝对阈值设置不合理而误伤任务通过率。

Harbor 任务的验收(也就是上面的 7 个测试点),1~5 都是需要我们写好对应的单元测试,在 test 的时候,copy 到 kmp-socketio 项目里去,带着我们写的单元测试去运行的。7 则是在没有带着我们的单元测试时(只带着原有的 + agent 新写的),验收覆盖率。

task 实现(AI)

实现部分就不多说了,codex cli + gpt-5.3-codex high 一两个小时就搞定了。

打分部分,我不是简单的给 0/1 分,而是累计得分:

  • full_jvm_suite: 0.25
  • kover_xml_report: 0.10
  • coverage_gate: 0.20
  • jvm_test_changes: 0.10
  • 隐藏语义 5 个子校验:每个 0.07,共 0.35

总分 1.00,全部通过 -> 1.0,全部不通过 -> 0.0,中间按子项得分给部分分。

模型测评

运行本地的 task,可以使用 -p 参数:

export ANTHROPIC_BASE_URL="https://openrouter.ai/api" && \
export ANTHROPIC_API_KEY="sk-or-v1-XXXX" && \
export ANTHROPIC_MODEL="moonshotai/kimi-k2.5"

harbor run \
  -p MissionImpossible/MissionImpossible \
  -a claude-code \
  -m $ANTHROPIC_MODEL

保持使用 claude-code agent,我分别测了以下几个模型:kimi-k2.5,glm-5-turbo,sonnet 4.6。

oracle,耗时 6:04。

kimi-k2.5, 耗时 12:58,score breakdown:

 - [PASS] full_jvm_suite: +0.25
 - [PASS] kover_xml_report: +0.10
 - [PASS] coverage_gate: +0.20
 - [PASS] jvm_test_changes: +0.10
 - [PASS] hidden::textSendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::binarySendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::firstPacketFailureStopsBatchAndNoDrain: +0.07
 - [PASS] hidden::engineSocketShouldCascadeSendFailureAsErrorAndClose: +0.07
 - [PASS] hidden::ackWithTimeoutShouldFireOnceWhenUnderlyingSendFails: +0.07
REWARD=1.000000
final reward: 1.000000

glm-5-turbo,耗时 14:06,score breakdown:

 - [PASS] full_jvm_suite: +0.25
 - [PASS] kover_xml_report: +0.10
 - [PASS] coverage_gate: +0.20
 - [PASS] jvm_test_changes: +0.10
 - [PASS] hidden::textSendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::binarySendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::firstPacketFailureStopsBatchAndNoDrain: +0.07
 - [PASS] hidden::engineSocketShouldCascadeSendFailureAsErrorAndClose: +0.07
 - [PASS] hidden::ackWithTimeoutShouldFireOnceWhenUnderlyingSendFails: +0.07
REWARD=1.000000
final reward: 1.000000

sonnet 4.6,耗时 9:38,score breakdown:

 - [PASS] full_jvm_suite: +0.25
 - [PASS] kover_xml_report: +0.10
 - [PASS] coverage_gate: +0.20
 - [PASS] jvm_test_changes: +0.10
 - [PASS] hidden::textSendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::binarySendFailurePropagatesErrorWithoutDrain: +0.07
 - [PASS] hidden::firstPacketFailureStopsBatchAndNoDrain: +0.07
 - [PASS] hidden::engineSocketShouldCascadeSendFailureAsErrorAndClose: +0.07
 - [PASS] hidden::ackWithTimeoutShouldFireOnceWhenUnderlyingSendFails: +0.07
REWARD=1.000000
final reward: 1.000000

都能通过测评,说明难度还是不够大,不过速度和 token 消耗上,还是有些差异:

  • sonnet 4.6 最快,kimi k2.5 较慢,glm-5-turbo 更慢一点;
  • kimi k2.5 token 消耗最低,glm-5-turbo 大约是 kimi 的 2.5 倍,sonnet 4.6 则只有 kimi 的 1/3 左右(粗略推算,不精确);

出于好奇,我又用前面试过的 OpenRouter 上的免费模型 stepfun/step-3.5-flash:free 试了下,也能拿到满分,不过耗时 24:25,用了 kimi 3 倍多的 token,这个任务也还是能看出点区别的。

上难度

没有区分度,怎么能叫 Mission Impossible 呢?

更难的任务和测试点设计

和 AI 再次交流一番之后,我采纳了下面的任务。

在不破坏旧接口语义的前提下,做出一套可用的可靠发送能力。重点是:

  • 新能力只允许出现在 emitReliable
  • emit(带/不带 ack)语义必须保持原样

先把验收目标写清楚:

  1. 新增 emitReliable API,并支持基于 ack timeout 的重试。
  2. emitReliable 具备可配置的重试参数(retries/ackTimeout/retryDelay/retryDelayMax/retryJitter)。
  3. emitReliable 的重试流程有终态保证:成功或耗尽都只能结束一次。
  4. emitReliable 队列必须保持顺序(前一条未终态,后一条不能抢跑)。
  5. emit 行为不变:不能隐式获得 reliable retry,不应触发 reliable 专属事件。
  6. agent 必须补充/更新 JVM 单元/集成测试(至少改动 src/jvmTest);强制验收仍以“全量 JVM 套件 + 覆盖率门禁 + 隐藏语义测试”为准。

基于这个目标,我设计了 6 个隐藏语义检查点:

  1. emitReliable unknown event 重试并耗尽
    触发无 ack 事件,断言 retry_attempt 次数与 retries + 1 对齐,且 retry_exhausted 触发一次。
  2. emitReliable ack 成功路径无多余重试
    正常 ack 场景下只能有一次 attempt,不应触发 exhausted。
  3. emitReliable 队列顺序保证
    前一个 reliable 消息先超时耗尽,后一个再成功,事件顺序必须稳定。
  4. legacy emit 在 reliable 配置开启下仍不自动重试
    emit 的 unknown event 只走原有 ack timeout,不触发 retry_attempt
  5. legacy emit ack 正常兼容且无 reliable 事件污染
    emit("ack", ...) 仍成功,且不触发 reliable 专属事件。
  6. emitReliable pre-open 缓冲与连接后 flush
    socket.open() 前调用 emitReliable,连接后仍可按预期发送并成功。

公开测试建议放更基础的两类:

  • emitReliable 最小可用(重试 + 成功)
  • emit 回归兼容(无行为变化)

隐藏测试重点放在:

  • 队列时序、终态唯一性、旧接口反向验证
  • 通过“兼容性反例”防止模型把旧 emit 一起改成可靠重试

关于覆盖率门禁:

  • 先在任务初始 commit 记录 kover 行覆盖率 baseline
  • 评测时要求 new_coverage >= baseline_coverage

Harbor 验收方式:隐藏语义检查会临时注入到 kmp-socketio/src/jvmTest/ 执行;覆盖率门禁在不包含隐藏注入测试时计算(只看仓库原测试 + agent 新增测试)。 此外加入 hard gate:emitReliable 相关隐藏测试通过数必须至少通过一半(2 个),否则 reward 直接为 0。

上难度后的测评结果

这下区分度就出来了:

  • stepfun/step-3.5-flash:free:耗时 56:42,消耗 10MB token,agent 自己写的 UT 都不通过、覆盖率也不达标,hidden test 也都没通过,得分 0;
  • sonnet 4.6:耗时 36:28,消耗约 1.9MB token,agent 自己写的 UT 通过了、覆盖率达标了,hidden test 也都没通过,所以得分还是 0;
  • kimi-k2.5:耗时 1:03:32,消耗 5.7MB token,实际上 agent 执行 1h 没完成,超时了,所以都没跑完,编译也不过,得分 0;(刚好也快把我可怜的 $5 OpenRouter credit 耗尽了,不测了
  • opus 4.6:可怜的 Pro 用户,30% 的 5h 限额都不够发出第一个请求,就算凑两个 5h 窗口也不够跑的。。。 凑两个窗口,总算是跑成功了,耗时 24:29,消耗 token 就不好估算了,不知道第一个窗口用了多少,第二个窗口用了 69%,结果和 sonnet 4.6 一样,agent 自己写的 UT 通过了、覆盖率达标了,hidden test 也都没通过,所以得分还是 0;

我还补测了其他 agent:

  • codex cli + gpt-5.3-codex high reasoning: 耗时 22:31,消耗 2.1MB token,agent 自己写的 UT 通过了、覆盖率达标了,hidden test 都没通过,得分是 0;
  • codex cli + gpt-5.3-codex xhigh reasoning: 耗时 18:13,消耗 4MB token,结果和 high 一样,也是 0 分;
  • codex cli + gpt-5.4 xhigh reasoning: 耗时 16:58,消耗 2.3MB token,结果和上面一样,也是 0 分;
  • ~~forgecode + sonnet 4.6: ~~
  • ~~forgecode + gpt-5.3-codex xhigh reasoning: ~~

本来想试试号称世界第一 harness 的,不过他们控制 provider 似乎有些麻烦(不支持从环境变量读取了),不搞了不搞了。

sonnet 实现问题分析

针对 sonnet 模型的失败情况,我让 AI 分析了一下它的实现,结论如下:

  1. 没有发出 hidden 依赖的重试事件
  • 你的实现只定义了 reliable_timeout,没有 retry_attempt / retry_exhausted。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T120954Z.patch:254
  • 所以 emitReliableDropsThenSuccess 里 retryAttempt 一直是 0(期望 3)。
  1. 重试耗尽时没有回调用户传入的 AckWithTimeout.onTimeout()
  • 你的 ReliableItem.successAck 是 Ack?,耗尽时只 fireSocketEvent(EVENT_RELIABLE_TIMEOUT, …),没调用用户 ack 的 timeout。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T120954Z.patch:65、/home/linke.r/downloads/kmp-socketio-diff-20260329T120954Z.patch:156
  • 所以 emitReliableLateAckExhaustsOnce 的 timeoutDone.await(…) 超时。
  1. 断线场景会出现“重复超时链路”,导致重连恢复失败
  • 断线后 clearAck() 会触发一次 onTimeout,但原先定时器超时也会再触发,缺少 token/attempt 失效保护,出现并行重试链路、重复耗尽。
  • 结果 emitReliableReconnectAndRecover 卡死超时(我日志里能看到 attempt=1/2/3 重复出现并两次 exhausted)。
opus 实现问题分析

针对 opus 模型的失败情况,我让 AI 分析了一下它的实现,结论如下:

  1. 没有发出 hidden 依赖的重试事件
  • 你的实现在超时时只有 Logging.info(...),没有 super.emit("retry_attempt", ...) / super.emit("retry_exhausted", ...)
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:144、/home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:153(只有日志);整个 patch 里也没有 retry_attempt / retry_exhausted 事件定义。
  • 所以 emitReliableDropsThenSuccessemitReliableLateAckExhaustsOnce 都会在 retryAttempt 断言处失败(期望分别是 3 和 2,实际都是 0)。
  1. 断线时直接清空 reliable 队列并触发 timeout,破坏了“重连后恢复发送”
  • onClose() 里直接调用 clearReliableQueue();而 clearReliableQueue() 会把未完成项标记完成并立即 userAck.onTimeout()
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:190、/home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:170、/home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:175。
  • 这会导致消息在断线瞬间被终止,不会等重连继续发送,所以 emitReliableReconnectAndRecover 里 success 期望 1、实际 0。
  1. 没有处理“晚到 ACK”窗口,queue-order 场景会把 first 打成 timeout
  • 这版实现的 internal ack 仍是普通 AckWithTimeout,超时后直接按 backoff 调度下一次 sendNextReliable(),没有 attempt/token 保护,也没有“保留前一轮 ack 接收窗口”的机制。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:129、/home/linke.r/downloads/kmp-socketio-diff-20260329T210819Z.patch:145。
  • 在 hidden 的 msg6 drop=1 delay=200ms 场景下,第二次尝试的 ACK 会晚于 reliableAckTimeout,最终变成 first-timeout,所以 emitReliableQueueOrder 断言失败(期望 "first",实际 "first-timeout")。
gpt-5.3-codex high reasoning 实现问题分析

针对 gpt-5.3-codex high 模型的失败情况,我让 AI 分析了一下它的实现,结论如下:

  1. emitReliable 没有把尾参 Ack 拆出来,导致协议参数错位
  • 你的实现把 args 原样 copyOf() 进 reliable 包(包含 AckWithTimeout 对象本身)。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T232454Z.patch:112、/home/linke.r/downloads/kmp-socketio-diff-20260329T232454Z.patch:115。
  • harbor_ack_ctl(id, cb) 这类 server handler 来说,第二个位置参数被 AckWithTimeout.toString() 占掉,cb 不再是函数,服务端直接 return,所以 hidden 里 msg6/msg1/msg2 尝试计数起不来(emitReliableQueueOrder 首个 gate 就报 msg6 should have started before timeout)。
  1. reliable 成功/失败都没有回调用户传入的 AckWithTimeout
  • onReliableAck(...) 只做队列推进,不会把 ack 参数透传给用户回调;onReliableFinalTimeout(...) 也只发 EVENT_ERROR,不触发用户 onTimeout()
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260329T232454Z.patch:199、/home/linke.r/downloads/kmp-socketio-diff-20260329T232454Z.patch:250。
  • hidden 里多个用例用 CountDownLatch 等待用户 ack 回调(success/timeout)来放行,因此 emitReliableDropsThenSuccessemitReliableLateAckExhaustsOnceemitReliableReconnectAndRecover 都会卡到超时失败。
  1. 缺少 hidden 依赖的重试观测事件
  • 实现里没有发出 retry_attempt / retry_exhausted(只在内部计数重试)。
  • patch 全文无这两个事件定义与 emit,与 hidden 的断言契约不一致。
  • 即便把前两点修好,emitReliableDropsThenSuccess / emitReliableLateAckExhaustsOnce 仍会在 retryAttempt/exhausted 断言上失败(hidden 明确要求可观测的重试事件流)。
gpt-5.3-codex xhigh reasoning 实现问题分析

针对 gpt-5.3-codex xhigh 模型的失败情况,我让 AI 分析了一下它的实现,结论如下:

  1. 仍然缺少 hidden 依赖的重试观测事件
  • 这版实现虽然补了 emitReliable 队列与重试流程,但在重试与耗尽分支里仍没有 retry_attempt / retry_exhausted 事件发射。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T001524Z.patch:173-190(只有 retries++scheduleReliableRetry / completeReliable,没有 emit 观测事件)。
  • 因此 emitReliableDropsThenSuccessemitReliableLateAckExhaustsOnce 这类 hidden case 依然会在 retryAttempt/exhausted 断言上失败。
  1. 断线场景没有“暂停 reliable 重试”,会在离线期间提前耗尽
  • sendReliable / scheduleReliableRetry 路径没有 connected 判定,重试定时器到点就继续发下一轮(即便 socket 已断开)。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T001524Z.patch:156-163、/home/linke.r/downloads/kmp-socketio-diff-20260330T001524Z.patch:195-202。
  • 同时 patch 没有对断线清理链路做联动改造(onClose -> clearAck 仍是原实现),会触发当前 ack 的 timeout,从而把 reliable 逻辑推进到离线重试/耗尽;在 hidden 的重连恢复场景里,消息可能在重连前就被判定失败(emitReliableReconnectAndRecover 失败)。
  1. “晚到 ACK”仍按严格 attempt 丢弃,queue-order 边界下会误判 timeout
  • 这版通过 item.attempt + isActiveReliableAttempt(...) 做强校验:只接受“当前 attempt”的 ack;一旦 timeout 进入下一轮,前一轮 ack 直接无效。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T001524Z.patch:161-166、/home/linke.r/downloads/kmp-socketio-diff-20260330T001524Z.patch:224-225。
  • 在 hidden 的 delayed-ack 边界(ack 晚于 reliableAckTimeout)下,这种策略会把本可恢复的 first 消息打成 first-timeout,导致 emitReliableQueueOrder 断言失败。
gpt-5.4 xhigh reasoning 实现问题分析

针对 gpt-5.4 xhigh 模型的失败情况,我让 AI 分析了一下它的实现,结论如下:

  1. 新增 Options 字段缺少 @JvmField,有 Java 侧兼容性风险
  • 这版把 reliableRetries/reliableAckTimeout/... 定义成了普通 Kotlin var(带 setter),但没有像既有 timeout 一样标注 @JvmField
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:22-25、/home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:33。
  • 对 Java 侧调用方(或 hidden 注入测试)如果沿用字段直写风格(opts.reliableRetries = 2)会直接踩兼容性问题,这类问题通常在 hidden 阶段才暴露。
  1. 仍未满足 hidden 依赖的重试观测事件契约
  • onReliableAckTimeout(...)completeReliableTimeout(...) 只有内部状态推进和日志,没有发出 retry_attempt / retry_exhausted
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:332-350、/home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:377-395。
  • 因此即便重试本身能跑通,emitReliableDropsThenSuccessemitReliableLateAckExhaustsOnce 这类 hidden case 依然会在观测断言上失败。
  1. 断线终态处理与测试覆盖都偏“理想路径”,hidden 边界仍容易挂
  • 实现在 onClose 里只做 pauseReliableDelivery()(取消 job)而不做 reliable 终态决策;如果后续不发生重连,in-flight reliable 可能长期悬挂、用户回调不落地。
  • 见 /home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:255、/home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:303。
  • 同时新增 UT + mock server 主要验证 reliableAck(name, succeedOnAttempt) 这种顺滑路径,没有覆盖 drop/delay/reconnect 控制场景(/home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:438-536、/home/linke.r/downloads/kmp-socketio-diff-20260330T020852Z.patch:648-655),因此对 hidden 的 queue-order / reconnect 边界没有形成有效防护。
失败共性
  1. 重试观测事件缺失或不完整;
  2. 断线恢复与终态唯一性处理不稳;
  3. 队列顺序在 delayed-ack 边界处理不稳;
  4. 兼容性回归(legacy emit)常被连带破坏。

全都扑街,也可能是任务描述不够充分,agent 都没注意到或偷懒了。于是我又让 AI 细化了一版 instruction,结果测 gpt-5.4 xhigh,还是零蛋。

测评总结

  • 同样的项目里,中等难度的任务(修复 bug + 编写单元测试),用 claude code,各个模型都能得满分,虽然速度和 token 消耗量有点差异,但也不很明显,国模在中等难度任务里也堪重用;
  • 加大难度后,能用到的 agent 和模型全部扑街,我一开始怀疑是任务描述不够充分,agent 都没注意到或偷懒了,结果加强描述后也还是不行,看来单次交互的测评形式,要求还是比较高;
  • 测评是单次交互,没有计划、澄清环节,这也会放大“自然语言模糊性“问题,「我以为说清楚了、agent 以为理解清楚了,结果做出来的都是零蛋」,实际使用时通过计划、澄清环节,可以很大程度缓解这个问题,此外即便单轮失败了,还可以继续反馈让 agent 继续改,测评结果能体现 agent 的单轮能力,也不能完全体现实际使用感受(比如测评榜单落后,但其实也基本够用);
  • 我尝试手动让 codex cli + gpt-5.3-codex high 做了一版,结果和测评当然一样,不过我进一步把隐藏用例直接加到仓库里,然后再让 AI 修,它也能一次修好(一方面再次说明测评不行、实际也挺好用,另一方面也说明用 AI coding 时系统的软件工程实践也能明显提升效果);
  • 相同模型、不同 harness 的测评,就先不搞了,后面有需求再搞;