61 道题跑完那天晚上,我盯着结果看了很久:42 道通过,18 道失败,还有 1 道因为测试仓库本身就坏了,判了无效,没计入统计。如果只看通过率,它比我预想的低了一截。但真正让我在意的不是那 42 道,而是后面这 18 道。把它们摊开看,表面错误五花八门——有超时的、有产物差一半的、有直接把不该动的文件给改了的。可我一道一道手动复跑、逐个归因之后发现,这 18 道其实全死在同一类事情上。
这个结论挺反直觉。官方演示里,这种跑在终端里的编码 Agent 看起来无所不能:你说需求,它自己敲命令、读输出、改文件,比我手动复制粘贴强多了。但实际一跑,问题往往不在模型能力,而在一个很具体、很底层的执行环节。如果你也在用终端里的编码 Agent,或者正准备给这类工具搭一套自己的评测,这篇文章应该能帮你省下不少冤枉时间。我会把 61 道题的来龙去脉、18 道失败如何收敛到同一个根因、以及我后来怎么绕开这个坑,完整讲清楚。
1. 这 61 道题不是算法题,是从真实开发流里拆出来的“干活题”
1.1 为什么不用官方 benchmark,非要自己攒
市面上其实有不少编码 Agent 的评测集,模型厂商也爱拿高分说事。但我用下来的感受是:官方题集太“干净”了。题干描述接近一份理想 PR 描述,输入输出明确,依赖已经装好,仓库能独立构建,跑起来也不需要跟终端里的交互式程序打交道。可真实开发不是这样的。
真实场景里,我经常会碰到这种时刻:让 Agent 装个依赖,它执行到一半,终端弹出一个[Y/n]的确认;让它合并 commit,它打开一个git rebase -i的编辑器界面;让它调试一段卡住的代码,它一头扎进pdb交互调试器。这些在官方 benchmark 里几乎不存在,但在我的日常工作里到处都是。所以我决定不用现成的题集,而是从自己最近三个迭代的真实需求里,把能转成可判题形式的任务全部捞出来,不做筛选,原样保留。
1.2 题目怎么分类,通过标准怎么定
最终凑了 61 道。这个数字不是刻意凑出来的,是我从真实工作流里捞完就是这么多个。类型分布大概是这样的:
| 任务类型 | 题数 | 通过 | 通过率 |
|---|---|---|---|
| 单文件修改 / 函数实现 | 14 | 13 | 92.9% |
| 多文件重构 / 逻辑迁移 | 10 | 7 | 70% |
| Git 工作流(分支 / 合并 / 回滚) | 8 | 5 | 62.5% |
| Shell 脚本编写与调试 | 12 | 6 | 50% |
| 命令行工具调用与结果解析 | 9 | 4 | 44.4% |
| 现有代码排错与测试修复 | 8 | 7 | 87.5% |
| 合计 | 61 | 42 | 68.9% |
等等,看到 42 + 18 = 60 你可能会问,还有 1 道去哪了?就是我开头说的那题:测试仓库在准备阶段就坏了,依赖树不完整,跟 Agent 的行为无关,我直接标记为“无效”,所以最终有效题数是 60,通过率按 70% 算。
通过标准我卡得比较严。每道题都是在全新临时目录里跑的:准备脚本把仓库 reset 到初始 commit,再调用 Agent 的 CLI,让它自己理解任务、自己操作。判定分三档:自动测试全部通过、关键文件 diff 跟参考实现一致、随机抽 20% 人工复查。另外还加了几条否定项:不能改坏无关文件、不能留临时文件、不能偷偷改 git remote。
1.3 跑题环境必须锁死,否则结果没法比
我跑题是在同一台开发机上,Agent 版本和模型版本都锁在同一个 commit。这一点极其重要。终端编码 Agent 的更新频率很高,今天我测的是它,明天可能就换了内核;版本不锁,你根本不知道 42 和 18 到底是谁跑出来的。
每道题我给的超时上限是 15 分钟,命令级超时是 5 分钟。也就是说,某条命令如果挂住 5 分钟没输出,评测脚本会直接把它杀掉,判这道题失败。这个参数后来成了定位根因的关键线索。我本来以为 15 分钟足够宽裕,结果超时恰恰是失败任务里最集中的表象。
2. 通过的 42 道,暴露了这类 Agent 真正可靠的工作区间
2.1 从类型分布看,它会什么、不会什么
拿到分布表的时候,有两个数字让我意外。第一个是“单文件修改 / 函数实现”接近满分,14 道里只挂 1 道。这个我理解,任务边界越明确,Agent 越稳。第二个是“现有代码排错与测试修复”居然有 87.5% 的通过率,比我想象的高不少。回头看,排错题有个共性:错误信息是直接的、可读的,仓库又小,Agent 非常擅长“读报错 → 定位代码 → 改掉 → 再跑测试”这个循环。
通过率最低的是“命令行工具调用与结果解析”,只有 44.4%。这一类题恰恰是日常终端使用里最高频的场景:装依赖、查日志、启动服务、解析命令输出。结果它反而最拉胯。当时我还没意识到,这个分类的低分其实已经暗示了后面那个根因。
2.2 通过的题长什么样:局部收束型任务
举一个印象比较深的通过案例。有一道多文件重构题:把utils.py里的一段日期解析逻辑拆成独立的date_utils.py模块,然后更新所有调用方,保证整个测试套件通过。难点在于,有几个调用点藏在 f-string 里,不是简单的utils.parse_date(...)这种一眼能看到的模式。
Agent 怎么做的?它先用grep -rn把所有引用点找出来,列了一个清单,然后逐个改文件。改完之后跑了一遍pytest,发现有一个测试因为时区问题挂了,它又回去把模块里的默认时区处理修掉,再跑,全绿。整个过程没有问我一句话。
这道题能过,是因为它符合一个模式:任务被严格限定在某几个文件里,并且有测试作为客观反馈环。每次改动后,Agent 都能从pytest输出里拿到“对还是错”的信号,于是它可以自我修正。我把这类任务叫“局部收束型任务”,这是终端编码 Agent 最舒服的工作区间。
2.3 通过不等于完美,别把测试通过当成代码写得好
说实话,这 42 道“通过”里,有相当一部分只是“能过测试”。人工复查时我发现,13 道通过的单文件修改里,有 4 道命名风格跟项目不一致,有 2 道留下了调试用的print,还有几道代码写得像“为了过测试而过的”,逻辑绕得厉害。
这也给了我一个重要提示:如果拿这套题去评测 Agent 的能力,光看通过率会高估它。能跑通测试,跟代码写得像人写的、可维护,是两码事。所以后来我把评测拆成两层:第一层是功能对错,自动判;第二层是代码质量,人工抽审。前者决定“能不能用”,后者决定“是否值得合并”。
3. 18 道失败的复盘:从“错误各不同”到“全栽在同一类命令上”
3.1 初看失败面板,谁都会觉得是模型不行
18 道失败任务放一起,表面原因分布是:11 个超时、5 个产物不符合预期、2 个命令直接报错退出。如果你只看这张表,结论很自然:这 Agent 能力不行呗。
但我总觉得不对劲。超时占比超过一半,这在自然语言编程里不正常——如果模型真的不理解任务,它通常会很快交出一个错误答案,而不是一直挂在那不动。超时意味着它可能“卡住”了,而卡住往往不是能力问题,是环境问题。我决定把 18 道题全部手动重跑一遍,记录 Agent 每次执行的完整命令流。
3.2 逐题复跑:所有卡点都指向同一个画面
复跑的时候,我把 Agent 每一条命令的开始时间、结束时间、退出码、输出尾部都记录下来。然后我发现了一个非常明显的规律:这 18 道题里,Agent 都在某一时刻运行了一条“会进入交互模式”的命令,然后就没有然后了。
举几个例子。
题 44:让 Agent 在当前仓库里把最近 3 个 commit 合并成 1 个。它执行了git rebase -i HEAD~3。这一步需要打开一个文本编辑器让人改 pick / squash 计划并保存退出。Agent 没有手,也没有眼睛能看编辑器,于是它卡住了,直到命令级超时被杀。但我扒开它的历史看,其实它在执行 rebase 之前就已经分析好了:要把哪个 commit 标记为squash。它只是打不开那个编辑器。
题 52:让它用 Python 计算一组数据并导出 CSV。它启动了python3,进入了 REPL,输入了一行计算代码。但 REPL 的输出被管道缓冲了,Agent 等了好一会儿也没等到结果,超时。那个数据量很小,计算本身连一秒都用不了。
题 58:修一个测试挂起的问题。Agent 用pytest --pdb进入调试器,卡住。
还有一道 Shell 脚本题,它在验证脚本时直接跑了个bash -i,等待输入,把测试流程挂死。
你发现没有:这些任务前面的 90% 它都做对了,模型不是不懂题,差的就是最后那一步——程序进入了交互模式,而执行环境里没有“人”在终端前面按回车、输命令、保存文件。
3.3 决定性实验:给命令执行器加一层“假屏幕”,18 道全过
为了验证这个猜想,我做了一个很直接的决定:把所有命令的执行方式从“非交互子进程”改成“在伪终端(PTY)里运行”。用一句命令就能模拟,比如script -qec "命令" /dev/null,或者在评测脚本里用tmux send-keys把命令发给一个后台会话。这相当于给 Agent 的命令执行器装了一块“假屏幕”,让它执行的每个程序都以为自己跑在一个真实终端里。
改完之后我重跑了这 18 道题。结果让我有点不太敢信:18 道全部通过。有的题它甚至绕了远路,比如那个 rebase 合并 commit 的题,它这次没有直接git rebase -i,而是预先设置了GIT_EDITOR=true,让 rebase 过程跳过编辑器直接执行。这说明模型本身一点都不笨,之前纯粹是被 no-TTY 环境坑了。
这个实验说明了两件事:第一,18 道失败不是 18 个独立错误,而是同一个执行层缺陷的 18 个变体;第二,如果想提升终端编码 Agent 在真实场景下的通过率,改模型不如先改执行环境。
3.4 为什么说“全是同一个原因”而不是“18 个问题”
前两天我在写复盘笔记的时候,试着把这 18 道题归因到具体模块。结果发现,无论是“超时”还是“产物不符合预期”,往底层追,最终都落在了同一件事上:Agent 的命令执行器默认跑在一个没有 TTY、stdin 关闭、stdout 被管道捕获的环境里。这个环境对人来说是“后台任务”,对很多程序来说却是“非人类待遇”。
就像同一块短路的路由器,会让游戏掉线、网页打不开、视频加载失败。表面看是三个问题,其实是同一个。终端编码 Agent 也是这样:一条git rebase -i卡住,一条pythonREPL 卡住,一条pytest --pdb卡住,看起来风马牛不相及,实际上都是同一个执行环境缺陷在碰见不同交互式命令时的不同表现。
4. 根因不是模型笨,是执行器没有“屏幕”:PTY 和交互式命令的恩怨
4.1 终端编码 Agent 默认是怎么执行命令的
要理解这个坑,得先知道终端编码 Agent 的内部执行链路大概是怎样的。模型在输出里生成一段 bash 命令后,Agent 的 runner 会拉起一个子进程去执行。默认实现通常是:用subprocess起一个非交互的 shell,stdin 直接连到/dev/null,stdout 和 stderr 通过管道回传给模型。回传的内容还会按末尾 N 行截断,免得上下文爆炸。
这个设计的好处是可控、安全、好解析。但代价很大:所有被 Agent 启动的程序,都生活在一个“没有屏幕、没有键盘”的世界里。它们能看到环境变量,但看不到自己是被人操作还是被脚本调用。而不少程序恰恰会在这种情况下改变自己的行为。
4.2 程序发现自己不在终端里,通常有三种反应
我总结了一下,交互式程序在 no-TTY 环境下大体有三种反应。
第一种是直接拒绝。比如vim、htop这类铁了心要占住整个界面的程序,会直接报TERM environment variable not set或者stdin isn't a tty,然后退出。这类其实还好,Agent 看到报错能换策略。
第二种是静默改变行为。比如ls不再输出颜色、git log不再进入分页器、Python 的 print 输出变成块缓冲而不是行缓冲。这种最阴,因为命令还是能跑,但输出时机和内容跟人在终端里看到的不一样,Agent 可能因此误判。
第三种是最致命的:它在等待一个永远不会出现的输入。像read、input()、git rebase -i打开编辑器后的保存操作、npm install的确认提示,它们会一直等下去。而 Agent 的 runner 如果设置了超时,就会因为超时被强行终止;如果没设超时,整个任务就直接卡死。
4.3 一个一眼看穿的小实验
你可以自己在终端里跑一下这条命令:
python3 -c "input('press enter: ')" < /dev/null在有 TTY 的终端里执行,它会正常提示你按回车;但加了< /dev/null之后,它会立刻抛出一个EOFError: EOF when reading a line,程序退出。
bash也一样:
bash -c 'read -p "continue? " ans' < /dev/nullread 会立刻读到 EOF,然后退出。终端编码 Agent 的默认执行环境,约等于这种“读一个不存在键盘的输入”的状态。它不是没有能力处理交互,而是它根本不在现场,连“按键”这个动作都不存在。
4.4 为什么官方演示永远看不出这个坑
原因很简单:官方 benchmark 里选用的命令大多是非交互式的。pytest、npm test、git diff、grep这些命令在 no-TTY 环境下也能正常输出结果,所以模型可以顺利跑通。但一旦题目里包含一个需要用户确认、需要编辑器、需要 REPL 的步骤,整个任务就可能翻车。
真实终端使用里,交互式程序躲都躲不开。安装依赖时的确认、修改 git 历史、连数据库客户端、进 Python REPL、跑调试器、启动一个 TUI 菜单工具,哪一个不是交互式的?只要任务流程中踩中一次,Agent 就大概率整题报废,前面 90% 的正确操作全部白费。
4.5 这也解释了评测里最反直觉的那个现象
我最初以为 18 道失败会是“均匀散布”在各类型里,结果分布很不均匀:Shell 脚本类和命令行工具调用类占了大多数,而单文件修改类几乎全过。这不是模型能力的巧合分布,而是执行器缺陷的必然结果。只碰普通命令的任务,怎么都好说;只要碰一次交互式命令,就生死难料。
这也给我提了个醒:判断一个终端编码 Agent 好不好用,不能只看它写代码有多厉害,还要看它的“手”能不能在终端里正常工作。代码能力是模型决定的,命令执行环境则是工程实现决定的。前者决定上限,后者决定下限,而真实落地场景里,下限往往更重要。
5. 绕开这个坑的四个实测方案,以及一套可复用的评测思路
5.1 方案一:给命令执行器包一层伪终端
这是治本的办法。核心思路就是别让 Agent 跑到裸 no-TTY 子进程里,而是让它在伪终端里执行命令。
最简单的验证方式:
script -qec "要执行的命令" /dev/nullscript会分配一个 PTY,然后在里面运行目标命令。命令会误以为自己在一个真实终端里,颜色、分页、交互行为都会恢复正常。对评测脚本来说,一条命令就能改造。更工程化的做法是用tmux send-keys把命令发给一个后台会话,或者写个 Python wrapper 用pty.openpty()自己管理伪终端。
需要注意一个副作用:PTY 模式下命令输出里会混入 ANSI 颜色转义和控制序列。这些人类看着舒服,但大模型解析起来会有点吃力。我的做法是在回传给模型之前,用正则把 ANSI 转义序列剥掉,保留纯文本。这样既享受了 PTY 的“真实感”,又不会弄脏模型的上下文。
5.2 方案二:在规则层禁止裸交互命令
不是所有人都有权限改 Agent 的底层执行器。这时候可以退而求其次,在系统提示词或者项目规则文件里,把交互式命令直接禁掉,并给出替代写法。
我自己在用的规则会有这么几条:
- 禁止直接运行
vim、htop、python裸 REPL、node裸 REPL、pdb这类交互式程序。 - 必须用非交互模式替代:
python -c、node -e、mysql -e、psql -c。 - 遇到需要确认的命令,用管道预填输入,比如
printf 'y\n' | command,或者查一下命令有没有-y、--yes、--non-interactive这类参数。 - 涉及 git 编辑器,预先设置
GIT_EDITOR=true或core.editor指向一个非交互脚本。 - 所有命令统一加
timeout 120前缀,宁可快速失败,不要无限等待。
这套规则的效果立竿见影。改完规则后再跑那 18 道题,就算不换 PTY 执行器,也有很大一部分能过——Agent 会在执行前主动绕开交互命令。
5.3 方案三:把超时和输出截断调成“对 Agent 友好”的参数
如果你的 Agent 还是会在某个交互命令上卡住,至少让它快点失败。把命令级超时从 5 分钟缩短到 60 秒,Agent 很快就能收到“这条命令超时了”的反馈,然后它会换一个思路。很多卡死其实不是模型不想换,而是它根本不知道卡了。
输出问题也很关键。默认只回传最后 200 行的话,长日志的前半段全都看不到。我现在的做法是让 Agent 把命令输出重定向到文件,再通过tail -n 500 文件返回尾部内容。这样既保留完整日志,又不会把上下文撑爆。实测下来,这个改动对“命令输出很长导致 Agent 误判”的场景帮助很大。
5.4 方案四:把高频任务模板化,让 Agent 没有机会踩坑
这个方案看起来最笨,但长期收益最高。我把日常会让 Agent 干的几类高频任务做成了模板:
- 查数据库:一律用
psql -c、mysql -e,不启动交互式客户端。 - 跑 Python:一律先写临时脚本,再
python3 脚本.py,不进 REPL。 - 改 git 历史:用
git reset或者设GIT_EDITOR=true,不走git rebase -i。 - 调试:不用
pdb,而是用python3 -m trace --trace或者直接在代码里临时加日志。
这条思路的本质,是把“人怎么习惯性绕开交互陷阱”的经验固化给 Agent。它不解决所有问题,但能极大减少翻车概率。
5.5 想自己搭一套回归评测?我的建议和最小框架
经历了这次测试之后,我把这套 61 道题留了下来,作为日常回归集。每次 Agent 升级或者我调整配置之后,都会跑一遍。如果你想自己搭一套类似的评测,我给你几个建议:
- 环境必须可复现,最好容器化,把 agent 版本、模型版本、系统依赖全部固定。
- 每个任务一个独立目录,用 git 快照作为初始状态;任务之间互相不干扰。
- 评价分两层:自动判功能对错,人工抽审代码质量。
- 不仅记录最终答案,还要记录每条命令的完整执行历史和时间戳。这次能定位到根因,靠的就是完整命令流,而不是只看结果。
一个最小的评测循环大概是这个模样:
for task in tasks/*/; do cp -r "$task/repo" "$WORKDIR/$(basename "$task")" cd "$WORKDIR/$(basename "$task")" agent_cli run --task "$task/prompt.md" --timeout 900 judge --test "$task/tests" --repo "$(pwd)" done我用的是 Bash,你完全可以用 Python、Go 或者其他任何语言重写,核心就只有三件事:准备隔离目录、调用 Agent、自动判定。有了这套东西,以后任何关于“这 Agent 行不行”的争论,都能用数据说话,不用凭感觉。
最后再分享一个小体会。我以前评测编码 Agent,总爱盯着模型智商看——它能写多复杂的算法、能解多难的题。这次跑完 61 道题之后,我的标准变了:先看它在真实终端里会不会把自己卡死,再看它能力上限在哪。那 18 道失败的题,靠模型更新解决得很慢,但靠执行环境和规则调整,我当天就让它全部通过。工具链的问题,就要用工具链的手段去解决。