Agent 任务执行路径与性能分析
retort 项目分析 agent 构建的任务类型及多样化执行路径,展示最快/最慢通过方案的差异。对优化 agent 能力有参考价值。
retort 项目分析 agent 构建的任务类型及多样化执行路径,展示最快/最慢通过方案的差异。对优化 agent 能力有参考价值。
这是来自 https://github.com/adrianco/retort/blob/main/tasks-blog.md 的一份自动生成分析报告快照。
发布于 2026-07-30 · 更新于 2026-07-30 — Adrian Cockcroft
Retort 实际要求 Agent 构建什么?对于每项任务,完整通过的运行中,最快和最慢的分别是什么样?这里的最快/最慢,指在 requirement_coverage == 1.0 的运行中,duration_seconds 最短/最长的记录;同时仅限 Agent 日志已归档的运行,因为没有日志的记录无法展示。
慢的一端与快的一端同样值得关注。这里没有任何失败:下面的每次运行都拿到了完美的 1.00 分。只是它们抵达终点所花的时间相差 4~61 倍,而且每项任务背后的原因各不相同——一个是过度工程化,一个是编程语言本身,第三个则是某种工具拖慢了速度。
任务定义位于 tasks/,索引记录在 tasks/registry.yaml 中。已注册的七项任务里,有三项进行过大规模运行;另外四项(react-dashboard、cli-data-pipeline、brazil-bench-neutral、funkygibbon-port)虽然已有定义,但只有很少或完全没有经过评分的运行。
关于记录,还有一点需要说明。master.db 中还存在两次更快的运行,但没有列在这里:一次是 2.49 分钟的 brazil(exp-2),另一次是 0.71 分钟的 rest-api(exp-1)。它们都早于 Agent 日志归档机制和机械式 gate;同属 exp-2 的另一次“通过”记录,test_coverage=0.0,按今天的规则会自动失败。因此,它们无法与下面的运行直接比较。
来源:tasks/rest-api-crud/ · 712 次运行 · “简单”任务
为图书集合构建一个 CRUD REST API:POST /books、GET /books(支持 ?author= 筛选)、GET/PUT/DELETE /books/{id},以及 GET /health。数据存储在 SQLite 或相应语言自带的嵌入式替代方案中;返回 JSON 和正确的状态码;包含输入验证、README,以及至少 3 项测试。该任务在全部 13 种语言中都进行过评分。
这是整个项目的主力任务。它被刻意设计得平平无奇——关键在于,一个靠谱的技术栈每次都应该在这项任务上拿到 1.00 分,因此它衡量的是可靠性,而不是能力上限。
整个运行过程浓缩如下:
[Read] TASK.md
[TEXT] "I'll build a Flask + SQLite book API with tests."
[Write] app.py
[Write] test_app.py
[Write] README.md
[Write] requirements.txt
[Bash] python3 -m venv venv && ./venv/bin/pip install -q flask pytest && ./venv/bin/python -m pytest -q
-> ...... [100%] 6 passed in 0.19s
[TEXT] "Done — all 6 tests pass."
一次读取、四次写入、一条验证命令,然后完成。没有探索,没有迭代,也没有失败的尝试。这就是模型直接知道答案时,常规任务应有的样子——也正因如此,常规任务已经无法通过可靠性区分 frontier model,只能通过成本区分。
代码共 2,351 行,而 Fable 5 的版本只有 194 行。它构建了一个包含七个模块的 package,未经要求就用 ruff 对自己执行 lint,启动了一个 subagent,还运行了 mutation testing——而这只不过是一个图书 CRUD API。Judge 的五项发现全部都是 info,指出实现范围超出了规格:PATCH endpoint、筛选和分页、一份手写的 304 行 OpenAPI 文档、WAL journaling,以及 NUL-byte 验证。任务要求的仅仅是五个 endpoint、一个健康检查,以及至少三项测试。
这并不是 slop——它在可维护性上的得分更高(0.85 对 0.27),idiomaticity 也更好。但 gate 无法区分这两次运行:它们都是 1.00。完整分析,包括 thinking dial 带来了多少方差,记录在 experiments-blog.md 中。
来源:github://brazil-bench/benchmark-template · 284 次运行
基于六份真实的 Kaggle 巴西足球 CSV 构建一个 MCP server:其中五个文件共包含 23,954 场比赛,使用三种不同 schema;另有 18,207 名 FIFA 球员。REQUIREMENTS.json 中固定了十二项要求:按球队、日期范围、赛事和赛季查询比赛;统计球队胜-平-负记录;搜索和筛选球员;根据比赛结果计算赛季积分榜;汇总统计;交锋记录;以及自动化测试。
它的难点与算法无关:球队名称带有州后缀和重音符号(São Paulo-SP 与 Sao Paulo);其中一个文件使用葡萄牙语列名和 DD/MM/YYYY 日期格式;而且数据集之间相互重叠——同一场现实中的比赛可能同时出现在两到三个文件里。
现代 harness 此前的最佳成绩是 5.67 分钟(Opus 5,Clojure);此前最快的 Python brazil 通过记录是 5.02 分钟。这是一次真正的跨越,并非四舍五入造成的小差异。
[CMD] ls && sed -n '1,240p' TASK.md && rg --files # read spec + repo
[CMD] sed -n '241,520p' TASK.md ... head data/kaggle/*.csv # read rest of spec + data shapes
[CMD] sed -n '1,240p' prompts.txt; wc -l data/kaggle/*.csv # count rows
[MSG] "building a dependency-free MCP stdio server with an indexed CSV data
layer and BDD-style tests"
[EDIT] README.md, brazilian_soccer_mcp.py, server.py, test_*.py # ALL files, one patch
[CMD] python -m pytest -q -> exit 127: command not found: python
[MSG] "The environment provides python3 rather than python"
[CMD] python3 -m pytest -q -> 7 FAILED
"Extra data: line 2 column 1"
[EDIT] brazilian_soccer_mcp.py # fix the extended-file parser
[CMD] python3 -m pytest -q && python3 server.py <<< '{jsonrpc initialize}...'
-> 7 passed in 3.19s
-> loaded 23954 matches and 18207 players
-> top 2019: [Flamengo 38pl 90pts, Palmeiras 74, Santos 74]
-> {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05", ...}}
[CMD] python3 -c "Counter by source file" # investigate dataset overlap
[EDIT] brazilian_soccer_mcp.py # add standings source-dedup guard
[CMD] python3 -m pytest -q && py_compile && git diff --stat
[MSG] "all seven BDD-style tests pass, including the known 2019 Brasileirão
standings result, and the MCP stdio handshake/tool call succeeds"
十六个步骤,两次真正的失败,而且都得到了诊断和修复。这不是走运的一次成功。
解决方案的形态如下:3 个文件,共 342 行代码,零依赖——只使用 Python 标准库:
brazilian_soccer_mcp.py(291 行)——一个 SoccerData service,加上 MCP 层。
server.py(5 行)——entrypoint,调用 serve()。
test_brazilian_soccer_mcp.py(46 行)——7 项以 BDD 风格命名的测试。
Kaggle 数据真的被混合并加载了吗?是的。全部六个文件都已在运行期间得到验证:共加载 23,954 场比赛和 18,207 名球员,正好是 10296 + 4180 + 1337 + 1255 + 6886。五个比赛文件使用三种不同的行结构,通过一个分派式 row mapper,被标准化为统一且不可变的 Match dataclass:
球队名称通过 normalized() 函数进行匹配:移除重音符号、转换为小写,并通过列举巴西全部 27 个 UF 以及南美国家代码的正则表达式删除州后缀——因此 São Paulo-SP 和 Sao Paulo 会被视为相同名称。日期则通过支持多种格式的 parse_date 处理。这两项设计都直接回应了规格中的数据质量说明。
数据库 backend 是什么?没有。这里既没有数据库,也没有 graph store——没有 SQLite、Neo4j、networkx,甚至没有任何形式的索引。SoccerData 保存着 self.matches: list[Match] 和 self.players: list[dict],每次查询都会对全部 23,954 场比赛执行线性扫描,并重新进行筛选。Agent 自己在总结中称其为“indexed CSV data layer”,这并不准确——代码导入了 defaultdict,但从未使用。面对这种规模的数据,扫描已经足够快,因此没有机制会发现这个问题。
关于“knowledge graph”:规格的概述要求提供“a knowledge graph interface for Brazilian soccer data”,但其中的 Required Capabilities 部分——固定的 REQUIREMENTS.json 正是据此编写——规定的是查询类别,而非存储方式。REQUIREMENTS.json 中一次都没有出现“graph”这个词。因此,这个解决方案虽然拿到了 12/12,却完全没有 graph:没有 node、没有 edge,也没有 traversal。它只是一张带筛选功能的扁平表格。本项目中的所有 brazil 运行都采用同样的评分方式,因此各次运行之间的比较是公平的——但这份 checklist 并没有检验规格标题中强调的核心定位。
由于源文件的覆盖范围存在重叠(BR-Football 覆盖 2014~2023 年、Brasileirao_Matches 覆盖 2012~2022 年、novo_campeonato 覆盖 2003~2019 年),而 load() 只是把数据连接起来,没有去重,因此同一场现实比赛会被统计两次甚至三次。Agent 部分注意到了这个问题——它增加了一道 guard,让 standings() 只使用专用文件,但这仅适用于 Brasileirão,而且仅限 2003~2019 赛季。因此,它输出的 2019 年积分榜在历史上是正确的:Flamengo、比赛 38 场、积 90 分。
其他所有功能仍然存在重复统计。该次运行自己的 MCP handshake 就证明了这一点:
{"team": "Corinthians", "season": 2022, "venue": "home",
"matches": 44, "wins": 28, "draws": 11, "losses": 5, "goals_for": 62}
一支 Brasileirão 俱乐部每个赛季只有 19 场主场联赛;即使加上杯赛和 Libertadores 的主场淘汰赛,约 25 场也已是上限。44 场实际上是联赛赛程被统计了两遍,再加上杯赛。规格自带的示例指出,Corinthians 在 2022 年的主场记录应为 19 场比赛、11 胜、5 平、3 负。
测试没有发现这个问题,因为它们只断言会计恒等式——matches == wins + draws + losses、points == wins*3 + draws——即使每场比赛都被重复统计,这些等式依然成立。Judge 把数据重叠标记为 low/enhancement,而且认为它只影响 standings();实际上范围要广得多,这是 team_statistics、head_to_head、search_matches 和 aggregate_statistics 中的正确性缺陷。
这一发现针对的是任务的 checklist,并不意味着撤回这次运行的成绩。实测的 3 分 19 秒和 12/12 依然成立。但“通过固定要求”和“返回正确答案”并不是一回事,而在这里,两者出现了分歧。
这是一种完全不同的失败模式:它不是镀金式的过度设计,问题在于编程语言本身。代码由 9,000 行 .m 和 .h 组成——包括手写的 CSV parser、比赛/球员 model、查询引擎和 MCP server——因为 Objective-C 在这里没有任何生态系统可供依赖。工具使用情况说明了一切:59 次 Edit、42 次 Write、54 次 Bash,却只有 2 次 Read。它不是在探索,而是在打字。
看清这项成本最直接的方法,是固定模型和任务,只改变编程语言。Opus 5 使用全部 13 种语言运行了这项任务,而且每一种都拿到 1.00 分:
相同的模型、相同的规格、相同的 1.00 分——wall clock 相差 11 倍,成本相差 15 倍,而这一切完全由编程语言决定。(TypeScript 未列入:它在 59.9 分钟后通过,但记录的成本为 $0.00;这是 telemetry 缺失,并不意味着免费运行。)这是对 Retort 核心理念最有力的一次说明:衡量单位是 stack,而不是模型。
来源:tasks/py-catalog-reservations/ · 18 次运行
前两项任务都从空目录开始构建。这项任务则自带一个可以正常工作的、共 111 行的 catalog/ package(包括 model、store、loan 和 service facade)及其现有测试套件,然后要求添加预约功能:仅在可用副本数为零时允许预约;采用 FIFO 顺序;归还副本时自动完成预约;支持取消预约;所有功能均通过 facade 暴露——同时不能破坏已经通过的六项测试(no_regression gate)。
它是为 graphify tooling factor 设计的:Agent 需要理解不是由自己编写的代码。
[Read] TASK.md
[Read] catalog/models.py, catalog/store.py, catalog/loans.py,
catalog/service.py, tests/test_catalog.py, conftest.py # read EVERYTHING first
[TEXT] "Now I'll implement the reservations capability."
[Edit] catalog/models.py # add the Reservation type
[Write] catalog/reservations.py # new service
[Edit] catalog/service.py (×2) # wire into the facade
[TEXT] "Now the tests for the new behavior:"
[Write] tests/test_reservations.py
[Bash] python -m pytest -q -> command not found: python
[Bash] python3 -m pytest -q -> .............. [100%] 14 passed in 0.01s
[TEXT] "All 14 tests pass — the 6 existing tests plus 8 new reservation tests."
请注意,它的行为模式与 greenfield 任务不同:第一次编辑前先进行了六次读取。在修改现有项目的任务中,Agent 会先集中理解代码,然后进行外科手术式修改——models.py 和 service.py 是被编辑,而非重写。这种差异正是该任务存在的全部意义。
第三种失败模式其实根本不算失败。这次运行比 cloud 记录慢 3.7 倍,却没有成本——因为它是在 laptop 上运行的。如果任务只运行一次,花费 0.43 美元、用时 73 秒的方案胜出;如果任务需要循环运行,免费选项就会改变成本计算方式。这是最能说明为什么要把成本列和耗时放在一起看的案例。
这项实验中还有一个值得注意的模式:最慢的三次通过记录,tooling 都是 beads(分别为 4.5、4.3、4.2 分钟);而所有 none 和 graphify 运行都更快(2.6~3.3 分钟)。这与 README 中的 factor analysis 一致:beads 会显著增加成本。每个实验组只有 n=3,所以这只能算一致的排序结果,还不能视为已经证明的影响——但这正是设置 tooling factor 想要检测的方向。
python 与 python3 的磕绊——在这里发现,现已修复三项纪录保持者——来自两个不同 vendor、使用三个不同模型——都先运行了 python,遇到 command not found,然后改用 python3 重试。每次都为此多付出了一个步骤。
这是这台机器的属性(macOS 只自带 python3),却被计入了模型的 Agent 工作量。编写这个页面时,这个问题才显现出来,因为只有在这里,三次运行会被并排展示。
顺着这条线索继续追查,又发现了背后两个更大的问题。pip install 的成功与否如同抛硬币:面对 Homebrew interpreter,它会因为 externally-managed-environment 而失败,因此 Agent 能否安装依赖,完全取决于它是否碰巧先创建了 venv——有些 Agent 创建了(比如上面的 Fable 5),另一些则改写为只使用标准库的代码(比如上面的 Terra)。这是 stack 之间的差异,却被悄悄归因于模型。而且 scorer 使用的 interpreter 与 Agent 不同:如果 Agent 提交了 venv,它就复用;否则便创建一个临时环境——结果可能是,测试套件基于一个 interpreter 编写,却在另一个 interpreter 上接受评分。
Retort 现在会在 Agent 启动前,提前为每个 Python workspace 配置好 venv,并把 python、pip 和 pytest 预先放到 PATH 中;scorer 也会复用同一个 venv。配置发生在计时窗口之外,因此这既能消除一次额外的 turn,又不会增加运行时间。此次变更前后的 Python 运行,无法直接比较 turn 数量——相关信息已与其他会改变数据的 harness 变更一起记录在 experiments-blog.md 中。
这个项目反复学到的一般性教训是:任何 benchmark 数字,都包含了一部分 bench 本身。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。