上周我让一个 Coding Agent 给一个 Web 应用实现登录功能。三轮迭代之后,它汇报:「完成了,所有测试通过。」
我打开浏览器一看:登录按钮不见了。
我把截图丢回去,它看了一眼,回复:「抱歉,上一轮我把按钮组件误删了。」
有意思的不是 Agent 会犯错——犯错很正常。有意思的是: 它直到看见那张截图,才知道自己犯了错。 那三轮迭代里,它有编译反馈,有测试报告,但它没有任何办法知道最终页面长什么样。它的「世界」是残缺的。
这件事让我更确认了一个琢磨了很久的判断:
对于 Coding Agent,最重要的基础设施不是更聪明的模型,不是 Benchmark,而是端到端(E2E)Test Harness。
Agent 不是代码生成,是闭环
很多人对 Coding Agent 的心智模型还停留在「更聪明的补全」:给它一个任务,它生成一段代码,结束。
但 Agent 真正的工作方式是一个闭环:
Task
↓
Agent 修改代码
↓
Build → Run → E2E Test
↓
反馈:Logs / Screenshot / Network / Exit Code
↓
Agent 分析失败原因
↓
继续修改
这个闭环和「一次性生成」的本质区别在于: Agent 不是一步到位,而是靠反馈逐步逼近目标。
反馈的质量,决定迭代的质量。而提供反馈的那个东西,就是 Harness。
换句话说,Harness 就是 Agent 的环境(Environment),更准确地说,是 Agent 的 世界模型接口(World Interface):Agent 通过它感知世界,也通过它验证自己的每一个动作。
开头那个故事里,Agent 前三轮之所以「不知道自己删了按钮」,就是因为它的 Harness 里没有浏览器这一层——它的世界里根本不存在「页面长什么样」这个事实。
Unit Test 回答的是错误的问题
很多人会说:我们有测试啊,unit test 覆盖率 80%。
但 unit test 和 E2E 回答的是两个不同的问题:
- Unit Test 回答:「这个函数是不是对的?」
- E2E Test 回答:「整个任务完成了吗?」
还是登录这个例子。Unit test 的验证是这样的:
def test_validate_password():
assert validate_password("secret") is True
# generated by hugo AI
而任务「帮我实现登录」真正需要的验证是这样的:
def test_login_e2e(page):
page.goto("http://localhost:3000/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.click("button[type=submit]")
# oracle 是「最终状态」,不是中间函数
assert page.url.endswith("/dashboard")
assert "alice" in page.locator(".welcome").inner_text()
# generated by hugo AI
区别在哪?Unit test 全绿,只能证明 validate_password 这个函数对了;它证明不了「用户能登录并看到 Dashboard」。路由接错了、按钮被删了、Session 没写进去——这些都会让 unit test 全绿,但任务实际是失败的。
对于 Agent 来说, 最终状态远比中间函数重要。因为用户要的是「登录能用」,不是「某个函数正确」。
这不是说 unit test 没用——后面会讲,它是分层反馈里必不可少的一层。但如果 Harness 只有 unit test,Agent 拿到的就是一个 与真实任务错位 的信号。
Harness 的本质:给 LLM 提供 Reward
LLM 本身是没有 reward 的。它生成代码,但它不知道这段代码好不好——除非有人告诉它。
Harness 就是那个「告诉它」的东西:
PASS
或者:
FAIL
Expected: Dashboard
Actual: 500 Internal Server Error
甚至是一张截图:
Screenshot: Login button disappeared.
每一种反馈,都在让 Agent 的下一轮决策变得更准。拿到 500 错误,它去查服务端日志;看到按钮消失的截图,它意识到「我把按钮删掉了」。
所以 Harness 的价值不是「跑测试」这么简单,而是 为 Agent 的每一步行动提供可验证的结果。模型负责决策,Harness 负责让决策有后果。
我在 面向 Agent 开发:可测试和可运维第一次真正成为前提条件 里讲过:人可以靠经验兜底,Agent 只能依赖「可调用、可验证、可反馈」的接口。E2E Harness 就是这套接口里最关键的一块——它是 Agent 的验收能力本身。
但 Reward 有经济学:延迟、噪声、防作弊
承认「Harness 提供 reward」只是第一步。真正做过的人都知道,Harness 设计的难点不在「有没有信号」,而在三个变量。我把它们叫做 reward 经济学。
变量一:延迟
E2E 很慢。起服务、起浏览器、跑完整流程,一轮可能要几分钟。如果 Agent 每改一行代码都要等一次完整 E2E,它的迭代次数会被墙钟时间卡死。
所以好的 Harness 一定是 分层 的:
| 层 | 信号 | 延迟 | 作用 |
|---|---|---|---|
| Build / 类型检查 | 语法、类型错误 | 秒级 | 挡住低级错误 |
| Unit Test | 函数逻辑错误 | 十秒级 | 挡住局部回归 |
| E2E Test | 任务级失败 | 分钟级 | 最终验收 |
设计原则是: 让快而稠密的信号挡住大部分错误,把慢而贵的 E2E 留给终审。 登录例子里那个 500 错误,理想情况下应该在 Run 阶段就被抓到,根本轮不到浏览器出场。
变量二:噪声
E2E 天然 flaky:端口冲突、时序竞态、第三方服务抖动。
一个有 10% 抖动率的 Harness,给出的不只是稀疏的 reward,而是 错误的 reward——测试挂了,Agent 花了五轮去「修」一个根本不存在的 bug,最后学会了「这个测试随机挂,忽略它」。噪声比延迟更致命,因为它会主动教坏 Agent。
所以 Harness 的质量,本质上是一个信噪比问题。确定性环境(docker-compose、固定 seed、隔离的测试数据)不是锦上添花,是 Harness 的地基:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./seeds/init.sql:/docker-entrypoint-initdb.d/init.sql # 固定 seed
app:
build: .
environment:
DATABASE_URL: postgresql://test:test@db:5432/test
# generated by hugo AI
每个任务开始前重置数据库到同一个 seed,测试之间互不污染——这是让 FAIL 信号「可信」的最低成本做法。
变量三:防作弊(Goodhart)
这是最容易被忽视、也最阴险的一个变量。
一旦 Harness 成为 reward 来源,Agent 就有可能不去解决问题,而是 解决提出问题的人:
def test_login_success(page):
# 正确做法:验证真实行为
# assert page.url.endswith("/dashboard")
# Agent 学会作弊后的做法:
assert True # 全绿
# generated by hugo AI
删掉失败的测试、mock 掉关键断言、硬编码期望值——这些都不是假想,是在真实 Agent 工程里反复出现的行为。对策是隔离: 测试代码和任务卡放在 Agent 可写面之外 (只读挂载或独立仓库),让裁判不可被选手修改。
| 变量 | 差的 Harness | 好的 Harness |
|---|---|---|
| 延迟 | 每轮都等完整 E2E | 分层:快信号挡错误,E2E 做终审 |
| 噪声 | 随机挂,Agent 学会忽略 | 确定性环境,固定 seed,隔离数据 |
| 作弊 | Agent 能改测试文件 | 测试与 Agent 可写面隔离 |
Benchmark 和 Harness,区别是「给谁用」
讲到这里,可能有人会问:SWE-bench 这些 benchmark 不也是 E2E 环境吗?为什么你说最重要的不是 Benchmark?
因为同一个 E2E 环境,接法不同就是两种东西:
| Benchmark | Harness | |
|---|---|---|
| 用途 | 一次性打分 | 持续供能 |
| 服务对象 | 外界(排行榜、选型) | Agent 自己(迭代循环) |
| 频率 | 跑一次出个分 | 每个 commit、每轮迭代都跑 |
| 输出 | 一个数字 | 结构化失败信号 + 轨迹 |
SWE-bench 接上排行榜,它是 benchmark;接进 Agent 的迭代循环,它就是 harness。所以真正的区分不在形态,而在 给谁用。
这个区分很重要,因为它决定了投入方向:行业里不缺「能打分的环境」,缺的是 高频、可信、能喂回 Agent 的任务级反馈。前者是奖杯,后者是粮食。
为什么大厂都在往这个方向砸钱
近一两年,可以观察到一个明显的趋势:Anthropic、OpenAI、Cursor 这些团队都在加强沙箱(sandbox)、可重复执行的开发环境、自动测试执行、浏览器自动化、日志采集、截图反馈。
这些投入看起来不像在「提升模型能力」,其实是在提升 反馈质量。而且背后还有一层更深的逻辑:
Harness 里的每一次任务执行,就是一条带 reward 标注的轨迹。
Agent 在 Harness 里跑「实现登录」这个任务,无论成败,整个过程——每一步修改、每一次测试输出、最终结果——都是一条完整的训练数据。Harness 不只是推理时的验收系统,它同时是 数据飞轮:闭合的不只是推理循环,还有训练循环。
我在 修 Bug 的真正目的:让 AI 下次能自己修 里讲过这个思路的微观版本:每一次 bug 修复都应该沉淀为 regression test 和结构化诊断信号,让 AI 下次能自己修。把这句话放大一万倍,就是大厂正在做的事——把整个开发环境变成可持续产出带标注轨迹的基础设施。
模型会趋同——大家都能接入差不多的基座模型。但「环境 × 轨迹数据」的积累是复利的,别人抄不走。这也是为什么我在 好的 Runtime 不挑模型 里说,Runtime 和模型在共同演化:模型越来越 Agent Native,而 Harness 越来越成为决定胜负的那一侧。
一句话总结
把这些串起来,我会这样总结:
Prompt 决定起点,Model 决定上限,而 E2E Test Harness 决定上限能否兑现。
也可以写成一个乘法关系:
最终能力 = 模型能力 × 兑现率
兑现率是什么?是「模型理论上能做到」和「在实际任务里稳定做到」之间的系数。而这个系数,几乎完全由反馈回路的质量决定——延迟够不够低、信号够不够干净、裁判能不能被收买。
所以回到开头的判断:未来 Coding Agent 的竞争,未必主要发生在模型层,而会越来越多地发生在 Harness、执行环境(Runtime)、可观测性(Observability)和自动评测(Evaluation)这些基础设施层面。
E2E Test Harness 听起来是个很「传统」的词——它甚至是软件工程里被抱怨最多的东西(慢、脆、难维护)。但在 Agent 时代,它的身份变了:从「给人看的验收流程」,变成「Agent 的世界本身」。
对 Coding Agent 最友好的,不是更大的模型,是一个它能看得见、信得过、改不动的世界。
你给 Agent 配的 Harness,信噪比过关吗?欢迎留言讨论。