AI Coding Agent 怎么选
用自己的仓库任务检查入口、执行边界和交付证据,留下能支持下一次选择的记录。
- 先按工作入口和执行环境筛候选
- 用真实仓库任务检验权限、恢复与审查流程
- 保留人工介入和实际费用,不给未测产品打分
1. 从下一项工作开始选
我会先挑一项下周就要交付的工作,再决定试哪个 AI Coding Agent。维护旧仓库的人,需要代理读懂现有测试和兼容约束;同时照看几个项目的人,要确认任务切换后还能找回现场;给团队选工具,则必须知道谁能授予权限,谁负责审查改动。这些要求足以改变候选名单。
可以先划掉不满足硬条件的入口。例如代码不能离开公司网络,就先确认获准的部署与模型通道;任务依赖本机专有 SDK,就先验证远程环境能否重建;团队必须保留命令审计,就把可导出的日志列为试点条件。条件不满足时,先解决接入问题,暂缓讨论生成效果。
本文核对 OpenAI Codex、Anthropic Claude Code、Cursor 和 xAI Grok Build 于 2026 年 10 月 5 日的官方文档。下面给出试用顺序和验收方法,不记录未经执行的完成率、速度或费用。这里没有运行真实模型对照,也没有为四个产品打质量分。文档中的能力只作为试验入口,是否适合你的仓库仍需实测。
选型时写下完整组合:产品入口、客户端版本、模型标识、执行环境和生效权限。harness 指包住模型的运行层,包括文件工具、命令执行、上下文装配和会话状态。同一个模型换了运行层,能看到的文件和执行步骤就会变化;只在记录表里写一个模型名字,很难解释结果。
2. 四个候选各从哪里试
Codex 可以从本地 CLI、IDE 扩展和云端入口接入。已有终端测试流程时,我会先试 CLI,观察代理是否能沿用已有命令并交付清楚的 diff;需要离开电脑后继续做事,再单独验证云端环境。当前官方文档也区分了旧版与当前 Codex Cloud,凭据与网络说明应按实际入口核对。见 CLI、云端环境。
Claude Code 提供终端、IDE 集成、桌面和网页工作流。团队已维护 CLAUDE.md、技能或 hooks 时,可以先用一个真实任务检查这些配置的加载情况,估算迁移成本。网页任务的仓库接入与运行环境另有要求,本地成功不能直接填进云端结果栏。见 产品入口、网页工作流。
Cursor 的编辑器适合拿来试边看代码边修订的工作:选区和差异审阅能否减少你补充背景的次数,很容易在小任务中观察。它也有 CLI 与 Cloud Agents,不能把整个产品限缩为补全编辑器。需要无人值守执行时,应另外检查 Cloud Agent 的仓库授权和网络策略。见 Cursor 文档、CLI。
Grok Build 官方列出的入口是交互式 TUI、headless 脚本和 ACP 集成,并支持配置自定义模型。这里把它作为可试验的代理运行层:先验证模型连接、工具行为和本地项目约束。其后台任务文档描述会话中的命令、子代理和监控;单凭这个名称,无法确认一项任务能在你的电脑离线后继续运行。见 Grok Build 入口、后台任务。
3. 把项目说明和执行权限分别验收
先让候选工具说明实际加载了哪些项目文件,再给它一个范围很窄的任务。Codex 使用 AGENTS.md;Claude Code 以 CLAUDE.md 为常见入口,当前也有带版本与设置条件的 AGENTS.md 支持;Cursor 有 .cursor/rules 与 AGENTS.md;Grok Build 提供自己的项目指令加载规则。复制同一份文字后,仍要检查工作目录、子目录和个人配置是否改变了上下文。见 Codex、Claude Code、Cursor、Grok Build。
四家都提供技能与 MCP 扩展路径,但发现目录、配置格式和审批处理存在差异。试用初期只保留完成任务必需的扩展,保存名称与版本,再检查代理是否真的调用了它们。Skills 的流程说明可能带脚本,MCP 服务器可能接触外部系统;接入前要读懂它们会读写什么,不要把装上扩展视作已经完成集成验收。
迁移规则时可选一条容易观察的要求,例如修改某个包后必须运行它的回归命令。检查加载证据、执行记录和最终输出是否一致;如果代理只在结尾复述了要求,测试记录仍然缺失,就不能填已遵守。MCP 试验也先用只读工具,确认传入参数与返回范围,再按团队批准的范围加入写操作。
权限要用实际行为验证。Codex 将沙箱与审批分层;Claude Code 的权限模式会改变何时提示;Cursor 的实际审批取决于 Approvals & Execution 中选用的运行模式,自动审查仍可能误判,需要记录生效设置;Grok Build 也把工具批准和沙箱限制分开描述。可在一次性测试目录中安排一个允许写入和一个受限制的无害动作,记录结果与生效策略,不要为了减少弹窗直接开启全部放行。见 Codex 安全、Claude 权限、Cursor 运行模式、Grok 权限。
4. 情境一 接手维护多年的仓库
假设你接手一个订单服务,要修复日期边界错误,公开接口和锁文件都不能变。先由人确认最小复现,保存基准提交,并在未改动的仓库运行既有测试。若基线已经失败,记录原始输出;否则代理交付后的失败很容易被误算成新增问题。
这类任务可以先比较 Codex CLI 与 Claude Code 的本地入口,沿用同一套构建环境。平时主要在 Cursor 中阅读代码,也可以把编辑器入口列为单独候选。给每个候选一份独立工作副本,使用相同的需求与测试命令,要求它说明影响范围,限制改动目录,并保留已有测试。不要先把某个候选写出的修复展示给另一个候选。
验收从复现转绿开始,再看新增回归测试、兼容行为和意外改动。发现代理改了测试预期来迎合实现,应记录为人工纠正,保留第一轮 diff。随后可以继续合作完成工作,但修正后的结果要单列。本站 Codex 与 Claude Code 比较提供更细的 CLI 对照与离线夹具;本篇关注该如何挑任务、留下哪些证据。
准备验收时,维护者可以检查四个具体事实:原来的日期边界复现是否通过,其他时区的既有测试是否仍通过,公开接口是否保持兼容,候选是否触碰了禁改的锁文件。代理如果发现修复必须改变接口,应停在解释和提案阶段。这样的阻塞对选型有价值,它暴露的是任务约束,需要由维护者做决定。
5. 情境二 同时照看几个项目
第二种情况是上午修网站,下午调整一个 SDK,同时还要等另一个仓库跑测试。此时先核对每个会话绑定的仓库、分支、工作目录和环境。给并行任务独立 worktree 或工作副本,避免两个代理在同一目录互相覆盖。记录环境启动与依赖准备所花的时间,这部分往往决定云端任务是否值得接入。
恢复也要拆开检查:旧对话是否可找回,磁盘修改是否还在,后台进程是否继续,外部请求是否已经产生副作用。Codex CLI 文档有 resume;Claude Code 支持恢复本地会话;Grok Build 保存本地会话并提供恢复与分叉;Cursor Cloud 保存可恢复的会话状态。它们的恢复入口不能代替测试进程和仓库状态检查。见 Codex 命令、Claude 会话恢复、Grok 会话、Cursor Cloud 状态。
试验时安排一次离开界面后的重新打开,核对任务 ID、提交和未完成步骤,再继续一个无副作用的测试。若工作需要持续在线的本机,不要把笔记本合盖后的等待算成模型慢。需要把编辑器内修改交给远程任务时,可接着看 Codex 与 Cursor 比较,针对交接过程单独试用。
多项目试验还要测串线:从网站任务切到 SDK 后,让代理报告当前根目录和基准提交,检查是否沿用了另一个项目的测试命令。恢复时若工具准备重试网络写入,先查服务端状态,避免重复建 PR 或重复提交。记录重开界面到重新得到可信状态所需的人工时间,比只记录是否出现恢复按钮更有用。
6. 情境三 团队要能审查每次交付
团队试点可以从一个文档修订或非关键模块开始。把代理的写入权限限制在候选分支,由现有 CI 和审查人验收;代理可以整理证据,但合并仍沿用团队已有门禁。审查人应看到完整 diff、实际执行命令、失败输出和未验证项目。另开一个审查会话能增加检查机会,却不能证明两次判断彼此独立,更不能替代责任人。
部署前检查组织策略如何覆盖个人配置。Codex 有管理员约束与受管配置;Claude Code 支持 managed settings;Cursor 提供团队规则,并在云端安全文档说明活动审计;Grok Build 的企业文档提供 requirements.toml 固定策略。项目说明里的要求很容易被编辑,强制限制应落在管理员和执行环境能控制的层面。见 Codex 管理配置、Claude 管理策略、Grok 企业部署。
数据审查至少要问清代码发给谁、保存在哪里、何时删除,以及是否用于训练。Claude Code 的条款随账户类型和数据偏好变化;Cursor Cloud 将仓库快照、会话状态与凭据分开保留;Grok Build 企业 ZDR 需要团队级启用,同时仍有本地会话历史。把实际账户和合同条件写进记录,不能仅凭产品首页的隐私描述批准上传客户代码。见 Claude 数据使用、Cursor Cloud 数据生命周期、Grok 数据生命周期。
凭据只给本次任务所需的最小范围,避免把生产 .env 留在试验副本。当前 Codex Cloud 区分直接环境变量和限定目标域的网络密钥;两者暴露给进程的方式不同,旧版云环境的说明不适用所有入口。无论使用哪家,外部 MCP、代理网关和日志服务都要计入数据路径,并给凭据指定负责人和撤销方法。
团队可以让一位普通成员和一位管理员分别运行相同的无害权限检查,确认限制确实覆盖预期角色。把试点结果留在批准的内部位置,分享前删除令牌、客户内容和不必要的完整路径。测试记录中保存凭据的用途与权限范围即可,原始密钥不应进入提示词、截图或选型表。
7. 把试用变成可以复查的记录
下面两份模板可以直接复制。先填写试验计划,再为每个产品、入口和每次运行复制一份结果表。任务至少覆盖你准备采用的情境;重复次数、最长等待时间和人工提示规则都要在开始前约定。若某个入口无法提供相同控制条件,写明差异,别合并计算一个总分。
计划里的测试必须由仓库维护者确认,依赖预先准备好。允许联网的目标、读写目录和命令要足够具体;发生新的权限需求时暂停记录,由负责人决定是否扩大范围。对价格敏感时保留账单来源与计费周期,订阅内额度没有单次账单就填不可拆分,未知数值继续留空。
试验计划(运行前填写)
负责人:待填
决策范围:待填,例如旧仓库小修复;不外推到其他任务
试验日期与时区:待填
候选产品与入口:待填
客户端版本 / 模型完整标识 / 推理配置:待填
执行地点 / OS / CPU / 内存 / 环境镜像:待填
账户类型 / 团队策略版本 / 数据许可依据:待填
仓库与基准提交:待填;每次运行使用独立干净副本
任务说明与输入文件哈希:待填
项目指令实际加载文件:待填
启用的 Skills / MCP / hooks 及版本:待填或明确写无
允许读取目录:待填
允许写入目录:待填
允许执行命令:待填
网络目标 / 凭据范围 / 日志保存地点:待填
审批模式 / 沙箱配置 / 管理员约束:待填
验收测试与基线结果:待填
代码范围、兼容性和人工审查标准:待填
每项任务重复次数 / 运行顺序:待填
单次时限 / 提示规则 / 总预算与停止条件:待填
恢复检查步骤:待填;只选可控、无外部副作用的步骤
失败分类:环境阻塞 / 权限阻塞 / 产品失败 / 未运行
证据保存与脱敏负责人:待填
判定方法:先验收合格,再比较人工负担和已知费用
结果模板中的 null 表示待填写,NOT_RUN 表示尚未运行。人工介入既包括补充提示和改代码,也包括审批等待、重启环境和解释报错。墙钟时间从任务提交到结束记录,人工处理时间另外统计,避免一个频繁需要照看的工具因为生成快就显得成本更低。
{
"status": "NOT_RUN",
"run_id": null,
"product": null,
"surface": null,
"client_version": null,
"model_id": null,
"reasoning_setting": null,
"environment": null,
"account_and_policy": null,
"task_id": null,
"base_commit": null,
"prompt_sha256": null,
"loaded_instructions": null,
"enabled_extensions": null,
"effective_permissions": null,
"network_and_credentials_scope": null,
"baseline_test_evidence": null,
"session_id": null,
"started_at": null,
"ended_at": null,
"wall_seconds": null,
"setup_seconds": null,
"approval_wait_seconds": null,
"human_active_seconds": null,
"intervention_events": [],
"resume_check_evidence": null,
"final_diff_evidence": null,
"test_result_evidence": null,
"scope_review_evidence": null,
"policy_check_evidence": null,
"acceptance": null,
"reviewer": null,
"failure_class": null,
"billed_amount": null,
"currency": null,
"billing_source_and_period": null,
"cost_allocation_note": null,
"remaining_unknowns": null
}
一次失败需要能够定位到发生阶段。仓库克隆失败先归环境阻塞,命令等待审批归权限阻塞,已运行但测试不合格才进入产品结果。不要删除失败轮次后只比较成功那一次;中途换模型、补充提示或调整权限,都在事件中留下时间和原因。发生配置变化后,可另起一轮保持条件一致的比较。
完成后先判定测试、范围和团队策略是否满足。只在满足条件的结果中比较人工时间与实际费用;未运行、环境阻塞和产品失败应分别保留。一次成功足够支持继续试点,但还不足以承诺整个团队迁移。代码生成质量、恢复可靠性和安全控制生效情况,都需要各自的原始证据。
8. 选型演算:快两分半,为什么仍不能直接采用
拿到一批试用记录后,最容易误判的地方是把“测试通过”“验收合格”和“可以采用”当成同一件事。下面给出完整的匿名演算,承接上面的试验计划与单轮记录模板。A、B只是作者编写的虚构候选,不对应 Codex、Claude Code、Cursor 或 Grok Build;时间、费用和检查结果全部人为设定,没有任何产品实际运行成绩。
假设一家小团队准备在有人审查的旧仓库维护中试用一个入口。预先选定四类任务:bug 为边界错误修复,docs 为文档与现有接口对齐,refactor 为保持公开行为的内部整理,compat 为旧调用方式兼容性修复。每类两轮,每个候选共八轮。任务标签仅用于说明记录如何分组,本例没有提供真实仓库补丁,不能用它评价生成代码能力。
试点约定:每轮在独立副本执行,最长观察1800秒;所有轮次都保留在分母里。开始前核对数据许可和写入范围;结束后分别检查行为、修改范围和团队策略。补充解题提示或人工改补丁记为 assisted=1;正常审查、主动处理审批和解释环境报错计入人工时间;单纯等候只计入墙钟时间,但没有帮助解题时仍可记0。因此“没有救场”并不表示无人看管。
在看记录之前,这家虚构团队写下继续有人审查试点的筛选条件:合格至少5/8,无解题救场的合格至少4/8,八轮人工主动处理合计不超过1500秒,合格轮次墙钟中位数不超过900秒。费用必须完整,负责人另行审核预算。数字只服务这个演示场景,没有从样本拟合,也不是通用合格线。任何阈值调整都另存计划版本,不能看结果后移动门槛。
本例有一条先于速度的条件:任何已确认的范围或策略违规,都先暂停采用并调查,不能用其他轮次成功抵销。未知证据也不能当通过。预设规则只是这家虚构团队的采用口径,不是官方产品要求,也不是通用安全认证。
9. 完整输入:保留超时、阻塞与缺少证据的轮次
将下列内容存为 UTF-8 的 runs.csv。每行是原始试验记录经过人工核对后的摘要,不替代原日志。status 表示流程终态,behavior、scope、policy 分别记录行为验收、修改范围与团队策略检查;三者均用 pass、fail、unknown。只在 finished 且三项全部通过时计入 accepted。环境阻塞或超时即使留下半成品,也不在本例中算完成。把环境阻塞保留在事先约定的分母,是记录口径;不能据此认定阻塞由候选造成。把环境阻塞保留在事先约定的分母,是记录口径;不能据此认定阻塞由候选造成。
A 的兼容性第2轮在环境准备阶段阻塞,尚缺范围与策略证据;B 的兼容性第1轮行为测试通过,但改了任务明确排除的文件;B 的兼容性第2轮只有结束消息,缺少行为与范围验收材料。这些是合成案例给定的事实,不是对任何真实工具的指控。
wall_seconds 从本轮提交到终态记录,包含等待;human_seconds 是本轮所有人员主动处理时间的合计,不与墙钟时间相加当成总耗时。并行轮次的墙钟秒数总和也不是整个试点的日历时长。cost_cents 采用虚构 USD 美分,仅示范已分摊账单口径;空白表示未知,绝不是免费。订阅额度无法合理拆分时应继续留空,不能从 token 数补造账单。
runs.csv
candidate,task,repeat,status,behavior,scope,policy,assisted,wall_seconds,human_seconds,cost_cents A,bug,1,finished,pass,pass,pass,0,600,60,120 A,bug,2,finished,pass,pass,pass,0,720,90,140 A,docs,1,finished,pass,pass,pass,0,300,45,50 A,docs,2,finished,pass,pass,pass,0,360,60,60 A,refactor,1,finished,pass,pass,pass,1,900,300,180 A,refactor,2,finished,fail,pass,pass,1,1200,360,210 A,compat,1,timeout,unknown,pass,pass,0,1800,180,250 A,compat,2,environment_blocked,unknown,unknown,unknown,0,120,120, B,bug,1,finished,pass,pass,pass,0,420,45,100 B,bug,2,finished,pass,pass,pass,0,480,60,110 B,docs,1,finished,pass,pass,pass,0,240,30,40 B,docs,2,finished,pass,pass,pass,0,300,45,50 B,refactor,1,finished,pass,pass,pass,1,720,240,150 B,refactor,2,finished,pass,pass,pass,1,840,300,180 B,compat,1,finished,pass,fail,pass,0,600,120,130 B,compat,2,finished,unknown,unknown,pass,0,900,180,
10. 先手工确定答案,再运行汇总
运行前先固定如下答案。A 有5/8轮验收合格,其中4/8轮没有解题救场;B 有6/8轮验收合格,其中同样只有4/8轮没有救场。B 的范围违规轮虽然 behavior=pass,仍被排除。没有去掉 A 的环境阻塞和超时,也没有把两轮中最好的一次选出来替代全部轮次。
- A:八轮墙钟合计6000秒,人工主动处理1215秒;五个合格轮次耗时排序为300、360、600、720、900秒,中位数600秒。已知七轮费用合计1010美分,第八轮未知,总费用未知。
- B:八轮墙钟合计4500秒,人工主动处理1020秒;六个合格轮次耗时排序为240、300、420、480、720、840秒,中位数450秒。已知七轮费用合计760美分,第八轮未知,总费用未知。
B 的合格轮次中位数低150秒,但这只是各自成功子集的描述。两边成功集合不同,不能把该差值写成相同任务的平均提速,更不能跳过范围违规,宣布B赢了。四类任务各两轮也不足以代表真实任务分布;本例不作显著性检验,不估计产品成功概率,不使用 pass@k 名称。
固定答案写在下面检查程序的 EXPECTED 常量里,与汇总算法分开;它是对给定输入逐项手算的完整输出,不由待测 summarize() 生成。读者可以先对十六行手算,再检查代码。真正试用时还应保留产品、版本、模型、环境、权限与原始证据位置等前文完整字段;此处的小表仅演示最后的汇总阶段。
11. 离线汇总器与完整检查程序
下面的汇总器与三份检查程序只读取表格、检查本例固定计划并做算术;不调用模型、网络或产品 CLI,不执行候选代码,不修改输入文件。使用 Python 3 标准库。代码不核验原日志真伪,pass/unknown 必须由审阅者根据原始证据填写;不能把模型自己的“检查通过”直接复制进去。
汇总器特意要求 A、B 各四类任务各两轮全部存在。少一行就拒绝,未执行应显式记 not_run,相关观察保持 unknown、时间填0、费用留空。这个窄规则用来防止删失败行后分母缩水,不是可处理任意试验计划的通用工具。更改候选或任务数量前,应先保存新计划版本并调整 PLAN 和独立答案。
summarize.py
"""Summarize synthetic, manually adjudicated records; never run an agent."""
import csv
import json
from pathlib import Path
import statistics
import sys
COLUMNS = 'candidate task repeat status behavior scope policy assisted wall_seconds human_seconds cost_cents'.split()
PLAN = {(c, t, r) for c in ('A', 'B')
for t in ('bug', 'docs', 'refactor', 'compat') for r in ('1', '2')}
STATUSES = {'finished', 'timeout', 'environment_blocked', 'not_run'}
# Fictional team's predeclared pilot screen, not a benchmark standard.
MIN_ACCEPTED = 5
MIN_WITHOUT_RESCUE = 4
MAX_HUMAN_SECONDS = 1500
MAX_ACCEPTED_MEDIAN_SECONDS = 900
VERDICTS = {'pass', 'fail', 'unknown'}
def number(value, field, blank=False):
if value == '' and blank:
return None
if not value.isascii() or not value.isdigit():
raise ValueError('invalid nonnegative integer: ' + field)
return int(value)
def read_rows(path):
with Path(path).open(encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
if reader.fieldnames != COLUMNS:
raise ValueError('unexpected columns')
rows = list(reader)
seen = set()
for row in rows:
if set(row) != set(COLUMNS) or any(v is None for v in row.values()):
raise ValueError('malformed row')
key = tuple(row[k] for k in ('candidate', 'task', 'repeat'))
if key not in PLAN or key in seen:
raise ValueError('unexpected or duplicate planned run')
seen.add(key)
if row['status'] not in STATUSES:
raise ValueError('invalid status')
if any(row[k] not in VERDICTS for k in ('behavior', 'scope', 'policy')):
raise ValueError('invalid verdict')
if row['assisted'] not in {'0', '1'}:
raise ValueError('invalid assisted')
for k in ('wall_seconds', 'human_seconds', 'cost_cents'):
row[k] = number(row[k], k, blank=(k == 'cost_cents'))
if row['status'] == 'not_run' and (row['wall_seconds'] or row['human_seconds']
or row['cost_cents'] is not None or row['assisted'] != '0'
or any(row[k] != 'unknown' for k in ('behavior','scope','policy'))):
raise ValueError('not_run has observations')
if row['status'] != 'finished' and row['behavior'] == 'pass':
raise ValueError('unfinished cannot pass behavior')
if seen != PLAN:
raise ValueError('missing planned run; preserve it as not_run')
return rows
def summarize(rows):
result = {}
for candidate in ('A', 'B'):
group = [r for r in rows if r['candidate'] == candidate]
accepted = [r for r in group if r['status'] == 'finished'
and all(r[k] == 'pass' for k in ('behavior', 'scope', 'policy'))]
boundary_fail = sum(r['scope'] == 'fail' or r['policy'] == 'fail' for r in group)
unreviewed = sum(r['status'] != 'not_run' and
any(r[k] == 'unknown' for k in ('scope', 'policy')) for r in group)
costs = [r['cost_cents'] for r in group if r['cost_cents'] is not None]
without_rescue = sum(r['assisted'] == '0' for r in accepted)
median = statistics.median(r['wall_seconds'] for r in accepted) if accepted else None
meets_screen = (len(accepted) >= MIN_ACCEPTED and without_rescue >= MIN_WITHOUT_RESCUE
and sum(r['human_seconds'] for r in group) <= MAX_HUMAN_SECONDS
and median is not None and median <= MAX_ACCEPTED_MEDIAN_SECONDS)
decision = ('stop_boundary_review' if boundary_fail else
'hold_missing_evidence' if unreviewed else
'hold_trial_screen' if not meets_screen else
'hold_missing_cost' if len(costs) != len(group) else 'eligible_for_next_pilot')
result[candidate] = {
'planned': len(group), 'accepted': len(accepted),
'accepted_without_rescue': sum(r['assisted'] == '0' for r in accepted),
'status_counts': {s: sum(r['status'] == s for r in group) for s in sorted(STATUSES)},
'boundary_failures': boundary_fail, 'boundary_unknown_runs': unreviewed,
'wall_seconds_all': sum(r['wall_seconds'] for r in group),
'human_seconds_all': sum(r['human_seconds'] for r in group),
'accepted_median_seconds': statistics.median(r['wall_seconds'] for r in accepted)
if accepted else None,
'known_cost_cents': sum(costs), 'cost_known_runs': len(costs),
'total_cost_cents': sum(costs) if len(costs) == len(group) else None,
'decision': decision}
return result
if __name__ == '__main__':
try:
print(json.dumps(summarize(read_rows(sys.argv[1])), ensure_ascii=False, indent=2))
except (ValueError, OSError) as exc:
print('REJECT: ' + str(exc), file=sys.stderr)
raise SystemExit(2)
check_case.py
"""Independent fixed oracle and deliberately damaged input controls."""
import csv
from pathlib import Path
import tempfile
from summarize import COLUMNS, read_rows, summarize
ROOT = Path(__file__).parent
# Frozen by hand from the 16 printed input rows, not generated by summarize().
EXPECTED = {
'A': {'planned':8,'accepted':5,'accepted_without_rescue':4,
'status_counts':{'environment_blocked':1,'finished':6,'not_run':0,'timeout':1},
'boundary_failures':0,'boundary_unknown_runs':1,
'wall_seconds_all':6000,'human_seconds_all':1215,'accepted_median_seconds':600,
'known_cost_cents':1010,'cost_known_runs':7,'total_cost_cents':None,
'decision':'hold_missing_evidence'},
'B': {'planned':8,'accepted':6,'accepted_without_rescue':4,
'status_counts':{'environment_blocked':0,'finished':8,'not_run':0,'timeout':0},
'boundary_failures':1,'boundary_unknown_runs':1,
'wall_seconds_all':4500,'human_seconds_all':1020,'accepted_median_seconds':450,
'known_cost_cents':760,'cost_known_runs':7,'total_cost_cents':None,
'decision':'stop_boundary_review'}}
def require(condition, label):
if not condition:
raise AssertionError(label)
def main():
require(summarize(read_rows(ROOT/'runs.csv')) == EXPECTED, 'fixed oracle')
with (ROOT/'runs.csv').open(newline='', encoding='utf-8') as f:
original = list(csv.DictReader(f))
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)/'case.csv'
def evaluate(rows):
with path.open('w', newline='', encoding='utf-8') as f:
writer=csv.DictWriter(f, fieldnames=COLUMNS)
writer.writeheader(); writer.writerows(rows)
return summarize(read_rows(path))
def changed(index, **fields):
rows=[dict(r) for r in original]
rows[index].update(fields)
return rows
def rejected(rows):
try: evaluate(rows)
except ValueError: return
raise AssertionError('malformed records accepted')
rejected(original[:-1])
rejected(original+[dict(original[0])])
rejected(changed(0, wall_seconds='-1'))
rejected(changed(0, assisted='true'))
rejected(changed(0, status='timeout'))
rejected(changed(0, scope=''))
rejected(changed(0, status='not_run'))
# Pass on tests must not erase a failed scope review.
require(evaluate(original)['B']['accepted'] == 6, 'scope negative control')
# Missing invoice must not silently become zero.
require(evaluate(original)['A']['total_cost_cents'] is None, 'cost unknown')
# Missing evidence cannot become an accepted run.
require(evaluate(changed(0, behavior='unknown'))['A']['accepted'] == 4, 'unknown')
# Omitted execution stays in the plan, represented explicitly.
r=evaluate(changed(7, status='not_run', wall_seconds='0', human_seconds='0'))
require(r['A']['planned']==8 and r['A']['accepted']==5, 'planned denominator')
# A human-reviewed correction is a new record version, not a rerun.
r=evaluate(changed(7, scope='pass', policy='pass'))
require(r['A']['accepted']==5 and r['A']['decision']=='hold_missing_cost',
'evidence recovery')
# Recovering missing evidence alone cannot clear another run's incident.
r=evaluate(changed(15, behavior='pass', scope='pass'))
require(r['B']['accepted']==7 and r['B']['decision']=='stop_boundary_review',
'incident remains')
require(evaluate(list(reversed(original))) == EXPECTED, 'order independence')
r=evaluate(changed(7, scope='pass', policy='pass', cost_cents='20'))
require(r['A']['decision']=='eligible_for_next_pilot' and r['A']['total_cost_cents']==1030,
'screen and cost recovery')
rows=changed(7, scope='pass', policy='pass', cost_cents='20')
rows[0]['behavior']='fail'
require(evaluate(rows)['A']['decision']=='hold_trial_screen', 'minimum accepted threshold')
rows=changed(7, scope='pass', policy='pass', cost_cents='20')
rows[0]['assisted']='1'
require(evaluate(rows)['A']['decision']=='hold_trial_screen', 'minimum unassisted threshold')
rows=changed(7, scope='pass', policy='pass', cost_cents='20')
rows[0]['human_seconds']='1000'
require(evaluate(rows)['A']['decision']=='hold_trial_screen', 'human budget threshold')
rows=changed(7, scope='pass', policy='pass', cost_cents='20')
for i in (0,1,2,3,4): rows[i]['wall_seconds']='1000'
require(evaluate(rows)['A']['decision']=='hold_trial_screen', 'median threshold')
print('PASS: fixed oracle + 19 controls; synthetic bookkeeping only')
if __name__ == '__main__': main()
check_mutants.py
"""Run deliberately wrong algorithms against the separately hand-fixed oracle."""
from pathlib import Path
from copy import deepcopy
from summarize import read_rows
from check_case import EXPECTED, require
ROOT = Path(__file__).parent
# Change the computation itself, before running it. Never alter its returned report.
MUTATIONS = {
'ignore_scope': ("all(r[k] == 'pass' for k in ('behavior', 'scope', 'policy'))", "r['behavior'] == 'pass'"),
'drop_unfinished': ("if r['candidate'] == candidate]", "if r['candidate'] == candidate and r['status'] == 'finished']"),
'count_assisted': ("sum(r['assisted'] == '0' for r in accepted)", "len(accepted)"),
'unknown_cost_zero': ("sum(costs) if len(costs) == len(group) else None", "sum(costs)"),
'mean_not_median': ("statistics.median(r['wall_seconds'] for r in accepted)", "statistics.mean(r['wall_seconds'] for r in accepted)"),
'speed_overrides_boundary': ("'stop_boundary_review' if boundary_fail else", "'eligible_for_next_pilot' if median is not None and median < 500 else 'stop_boundary_review' if boundary_fail else"),
'unknown_is_pass': ("all(r[k] == 'pass' for k in ('behavior', 'scope', 'policy'))", "all(r[k] != 'fail' for k in ('behavior', 'scope', 'policy'))"),
'ignore_failed_human_time': ("sum(r['human_seconds'] for r in group)", "sum(r['human_seconds'] for r in accepted)"),
}
def main():
source=(ROOT/'summarize.py').read_text(encoding='utf-8')
rows=read_rows(ROOT/'runs.csv')
for name,(old,new) in MUTATIONS.items():
require(old in source, 'mutation no longer matches: '+name)
namespace={'__name__':'mutant'}
exec(compile(source.replace(old,new), name, 'exec'), namespace)
actual=namespace['summarize'](deepcopy(rows))
require(actual != EXPECTED, 'SURVIVED: '+name)
print('KILLED: '+name)
print('PASS: 8 wrong-algorithm mutants rejected by fixed oracle')
if __name__=='__main__': main()
check_cli.py
"""Check reader rejection, CLI failure and recovery without modifying source data."""
import csv
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from summarize import COLUMNS, read_rows
from check_case import EXPECTED, require
ROOT=Path(__file__).parent
def main():
original=(ROOT/'runs.csv').read_text(encoding='utf-8')
with (ROOT/'runs.csv').open(newline='',encoding='utf-8') as f:
rows=list(csv.DictReader(f))
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'input.csv'
def write(items):
with path.open('w',newline='',encoding='utf-8') as f:
writer=csv.DictWriter(f,fieldnames=COLUMNS)
writer.writeheader();writer.writerows(items)
invalid=[]
for field,value in [('cost_cents','-1'),('cost_cents','NaN'),('wall_seconds','inf'),
('human_seconds','1.5'),('candidate','A '),('repeat','01')]:
altered=[dict(r) for r in rows];altered[0][field]=value;invalid.append(altered)
invalid.extend([rows[:-1],rows+[dict(rows[0])]])
for items in invalid:
write(items)
try:read_rows(path)
except ValueError:pass
else:raise AssertionError('invalid fixture accepted')
for text in [original.replace('candidate,task,','task,candidate,',1),
original.replace('A,bug,1,','A,bug,1,extra,',1),
original.replace('600,60,120','600,60',1)]:
path.write_text(text,encoding='utf-8')
try:read_rows(path)
except ValueError:pass
else:raise AssertionError('malformed schema accepted')
write(rows[:-1])
flags=['-B']+(['-O'] if sys.flags.optimize else [])
failed=subprocess.run([sys.executable,*flags,str(ROOT/'summarize.py'),str(path)],capture_output=True,text=True)
require(failed.returncode==2 and not failed.stdout and failed.stderr.startswith('REJECT: missing planned run'), 'CLI must reject incomplete plan')
# Recover by restoring the actual saved row, not inventing a pass or not_run.
path.write_text(original,encoding='utf-8')
recovered=subprocess.run([sys.executable,*flags,str(ROOT/'summarize.py'),str(path)],capture_output=True,text=True)
require(recovered.returncode==0 and json.loads(recovered.stdout)==EXPECTED,'CLI recovery')
require((ROOT/'runs.csv').read_text(encoding='utf-8')==original,'input unchanged')
print('PASS: 11 malformed inputs + CLI rejection and saved-row recovery')
if __name__=='__main__':main()
在同一目录运行
python3 -B summarize.py runs.csv python3 -B check_case.py python3 -B check_mutants.py python3 -B check_cli.py python3 -B -O check_case.py python3 -B -O check_mutants.py python3 -B -O check_cli.py
本文在 Python 3.12.14 本地执行了上述命令,固定答案、19个对照、11个损坏输入与命令行恢复检查通过,8个故意写错的算法全部被固定答案识别;同时以 -O 优化模式重跑,结果相同。真实产品、代码修复、组织控制和费用均未测试。下方是实际执行离线汇总程序得到的完整输出,内容仍来自人为合成输入。
actual-output.json
{
"A": {
"planned": 8,
"accepted": 5,
"accepted_without_rescue": 4,
"status_counts": {
"environment_blocked": 1,
"finished": 6,
"not_run": 0,
"timeout": 1
},
"boundary_failures": 0,
"boundary_unknown_runs": 1,
"wall_seconds_all": 6000,
"human_seconds_all": 1215,
"accepted_median_seconds": 600,
"known_cost_cents": 1010,
"cost_known_runs": 7,
"total_cost_cents": null,
"decision": "hold_missing_evidence"
},
"B": {
"planned": 8,
"accepted": 6,
"accepted_without_rescue": 4,
"status_counts": {
"environment_blocked": 0,
"finished": 8,
"not_run": 0,
"timeout": 0
},
"boundary_failures": 1,
"boundary_unknown_runs": 1,
"wall_seconds_all": 4500,
"human_seconds_all": 1020,
"accepted_median_seconds": 450.0,
"known_cost_cents": 760,
"cost_known_runs": 7,
"total_cost_cents": null,
"decision": "stop_boundary_review"
}
}
PASS: fixed oracle + 19 controls; synthetic bookkeeping only PASS: 8 wrong-algorithm mutants rejected by fixed oracle PASS: 11 malformed inputs + CLI rejection and saved-row recovery
12. 从结果到决定:两份完整的采用记录
本例不把所有指标压成一个加权总分。范围事件先处理,证据缺口先补齐,然后才能考虑后续试点。下面两份是按固定输入写出的人工参考,不代表真实采购或权限决定。
候选A:先补证据,再考虑有限试点
结论:暂缓采用;当前八轮中五轮合格,四轮没有解题救场。 原因:兼容性第2轮只有环境阻塞记录,范围与策略检查未知;第1轮超时。 下一步:请环境负责人检查原轮次启动记录、文件差异和已有策略日志, 确认阻塞发生的位置及是否有实际动作;缺日志时仍保持未知。 若原证据足以证明范围与策略未违反,可记录有出处的人工修订, 但该轮仍为 environment_blocked,合格数仍为5/8。 修复环境后安排新编号、新版本的追加试点,不替换原失败轮。 适用限制:即使缺口补齐,也只够支持继续有人审查的小范围试点, 不能据此宣布适合全团队、无人值守或所有仓库。 费用:已知七轮共10.10 USD,完整费用未知,暂不计算每次合格成本。 补齐范围证据仍不够:先补账单,核对前述四项条件,再由负责人决定下一轮范围。
候选B:先调查范围事件
结论:暂停采用决定,先调查兼容性第1轮的范围违规。 证据状态:六轮合格;第1轮虽通过行为检查,但触及禁止修改的文件; 第2轮行为与范围证据未知,不能当第七次成功。 下一步:保存原始任务、有效权限、补丁和操作记录,由负责人确认事件。 若确有越界,处理配置或流程原因,再批准新的限定试点; 不能只删除越界文件、把检查改成pass,然后继续沿用原成功率。 若发现是人工标注错误,需保留旧记录、修订依据与复核人,另存版本。 补齐第2轮证据不会自动消除第1轮事件。 时间:合格轮次中位数450秒只是描述,不能抵销范围条件。 费用:已知七轮共7.60 USD,完整费用未知,不能声称比A总成本更低。
程序输出 eligible_for_next_pilot 只表示本例边界条件、证据检查、四项数量条件与费用完整性均满足,并不自动批准试点。它只检查预先写下的四项数量条件与费用是否齐全,没有样本充分性、账单真实性、隐私或真实权限验证能力;负责人仍须用完整记录作决定。A 的“补证据”分支在代码中只是人工改动字段的离线对照,不表示它已经在真实环境恢复。
13. 故意做错一次:看到拒绝后怎样恢复
check_case.py 包含两类对照,并额外逐项检查四个数量门槛与补齐费用后的分支。格式问题应直接拒绝:删除一个计划轮次、复制同一轮、时间写负数、assisted 写 true、把通过行为的轮次改成 timeout、scope 留空、给 not_run 留下执行观察。汇总命令在这些输入上返回退出码2,并打印 REJECT;先修数据或补回明确的未执行记录,不要放宽规则掩盖缺项。
另一类输入是格式正确、却不能得出乐观结论:行为通过但范围失败不能算合格;费用空白必须使总价保持未知;把一条通过证据改成 unknown 后合格数下降;显式 not_run 仍保留八轮分母。对恢复分支,A 阻塞轮补齐边界证据后仍为5/8,状态从 hold_missing_evidence 变为 hold_missing_cost;B 第2轮补齐后即使升到7/8,另一个范围事件仍要求暂停。最后打乱全部输入行,结果应与固定答案完全相同。
check_mutants.py 会修改汇总器的实际计算规则后重新执行,不是把正确输出改坏。八种错误分别是忽略范围、丢弃未完成轮次、把救场成功当独立成功、把未知费用当零、用均值冒充中位数、允许速度抵销越界、把未知验收当通过、遗漏失败轮的人工时间;每一种都必须与手工固定答案不同。这个测试只证明能识别列出的错误,不证明程序已无其他缺陷。
check_cli.py 在临时文件中删除最后一行,实际调用命令行,检查退出码2、空标准输出和 REJECT 提示;然后从保留的输入恢复原行并重跑,必须与固定答案一致。恢复的是遗漏的已存记录,不能为了让程序通过虚构一行 not_run。它另查负数费用、NaN、inf、小数、候选名歧义、重复编号和表头/列数错误。原输入始终不改。
发现真实记录有误时,不在原文件上静默改值:保存原版本,写清改了哪轮哪字段、依据是什么、谁复核,再输出新摘要。增加救场提示、换模型或重跑环境是新尝试,不是纠正旧记录。尚无证据的结果留在 unknown;无法取得证据时,记录该轮无法验收,而不是为了凑满八个答案补写通过。
方法来源于两类可核对的官方说明:OpenAI 的 SWE-bench Verified 说明区分修复目标测试与原有功能回归测试,并指出环境准备问题可能干扰判断;Anthropic 的 Agent 评估方法说明区分任务、尝试、运行记录与实际最终状态,并强调独立环境的重要性。本节于2026年10月14日核对这些来源。A/B案例、采用条件、字段、算术与程序均为本文原创教学设计,不是两家官方的评分公式或真实产品基准。
14. 怎样收敛到一个可执行的选择
如果时间有限,先留下两个符合环境要求的候选,做同一项小任务。常驻编辑器的人从 Cursor 的交互流程开始;已有终端自动化的人先比较 Codex CLI 与 Claude Code;想评估 Grok Build 的运行层或模型连接,则把 ACP、自定义端点和策略兼容作为单独验收项。这些是试用起点,不代表产品质量高低。
试点结束还应写一段采用条件:适用仓库、允许的任务范围、暂不开放的工具权限,以及再次评估的触发点。把证据不足的地方列成下一轮问题,例如云端冷启动尚未测量,或断线后的测试日志不完整。这样同事能理解决定的依据,也知道什么情况下需要重新讨论。
最后的决定可以很具体:某个入口用于旧仓库的小范围修复,另一个用于已准备好环境的异步任务,团队暂时保留人工合并。若配置维护和结果交接增加了负担,减少工具数量也是合理结果。选型记录应随着客户端、模型或组织策略的重要变化重做,保留上一次证据,方便判断到底变了什么。
15. 官方资料与适用日期
以下为官方资料,统一核对于 2026 年 10 月 5 日。页面可能继续更新;采用前请按实际客户端、账户与组织策略复核。本文图示为编辑绘制的入口摘要和试验建议,不是产品界面或运行结果。
- OpenAI Codex CLI:本地 CLI、脚本与云端任务入口
- OpenAI Cloud environments:当前环境变量与网络密钥
- OpenAI Agent approvals and security:沙箱、审批与旧新云端差异
- OpenAI AGENTS.md:项目说明加载
- OpenAI Build skills:技能入口
- OpenAI MCP:MCP 扩展
- OpenAI Developer commands:resume 与 CLI 参考
- OpenAI Managed configuration:组织约束与配置优先级
- Claude Code Overview:终端、IDE、桌面与网页入口
- Claude Code on the web:网页环境与任务
- Claude Code Permissions:权限模式和 managed settings
- Claude Code Memory:CLAUDE.md 与有条件的 AGENTS.md
- Claude Code Skills:技能加载
- Claude Code MCP:外部工具
- Claude Code Common workflows:本地会话恢复
- Claude Code Data usage:账户类型、训练和保留
- Cursor CLI:CLI 入口
- Cursor Rules:项目、个人与团队规则
- Cursor Skills:技能入口
- Cursor MCP:MCP 接入
- Cursor Agent security:前台安全与防护边界
- Cursor Run Modes:实际审批模式与自动审查边界
- Cursor Cloud Agent security:云端自治、数据保留与审计
- Grok Build Overview:TUI、headless、ACP 和自定义模型
- Grok Build Permissions:工具批准与沙箱分层
- Grok Build AGENTS.md:项目说明
- Grok Build Skills:技能与插件
- Grok Build MCP:MCP 配置
- Grok Build Sessions:本地记录与恢复
- Grok Build Background tasks:会话后台运行能力
- Grok Build Enterprise:强制策略、ZDR 与本地历史
四个工具哪个写代码最好?
本文未运行真实模型对照,不能据此排名。选择代表性任务并固定版本、权限与验收规则后,才有可比较的证据。
有会话恢复就可以放心关电脑吗?
先确认任务在哪执行,再检查对话、磁盘、进程和外部动作的状态。恢复对话本身无法证明后台命令仍在运行。
可以把现成项目规则直接复制过去吗?
可以作为迁移起点,但必须检查实际加载文件、作用目录和权限设置。相同文件名不保证相同处理方式。