Agent 怎么做回归:从回答相似度到任务证据
用公开库存工程重放正常与缺陷版本,建立夹具、报告判定和版本比较,并区分验收链路检查与模型生成质量。
让 Agent 修复一个接口错误,它回复“已修复,测试通过”,这句话应该怎样断言?匹配关键词不够,和参考回复做相似度比较也不够:真正要确认的是代码是否解决问题、测试结果是否对应当前版本,以及执行过程有没有越过任务边界。
可以先沿用接口自动化测试的做法:准备环境,让 Agent 执行,再检查实际结果并清理状态。区别在于,Agent 可能先读代码,也可能先查日志。只要路径没有越过任务边界,验收就应关注它最终交付了什么。下面用库存测试任务,把这个判断落实到剧本、执行报告和缺陷检出上。
先写出任务夹具和独立验收
以库存测试工作流为例,任务是:根据给定资料生成测试剧本,再生成能够发现库存缺陷的测试。模型输出了多少条 assert,不能直接回答这个任务是否完成。
可以将一次试验的夹具保存成下面的目录。这里只定义评估组织方式,candidate 是被评估系统产出的内容:
trial-001/
├── inputs/ # 固定版本的需求、接口契约、能力清单、旧脚本
├── task.txt # 用户目标与允许的修改范围
├── config.json # 模型、Prompt/Skill 版本、预算与工具版本
├── candidate/ # 原始模型输出、审核改动、生成的测试
└── evidence/ # 工具轨迹、运行报告、判定及原始日志
库存任务的验收应事先约定:规则有来源,剧本能到达目标业务分支,预期由规则独立推出,测试在正确服务上通过,并能在选定缺陷上因对应断言失败。模型不得修改被测服务或验收程序来使任务通过;待评估的缺陷实现和参考答案不进入输入资料。
每次 trial 从相同输入和干净环境开始。为比较两个提示词而复用上次生成的剧本,会把“利用已有答案修改”混入“从资料生成”的任务。人工修改也要另存补丁,分别报告修改前和修改后的结果。
先验证验收器能区分正确与错误实现
公开的 ai-test-workflow提供了一组手写参考剧本与三个可注入缺陷。先用它检查验收路径,再接入模型。以下命令固定到教程验证过的提交:
git clone https://github.com/captainamari/ai-test-workflow.git
cd ai-test-workflow
git checkout --detach 7e706ee0e70d645715bbdd59a8f1385eb3c5c43f
python workflow.py demo
这条命令不调用模型。它创建独立运行目录,先跑正常服务,再重放三个缺陷版本。2026-10-28 在 Windows / Python 3.12 上的重放结果如下;每组均执行 10 条参考场景。
| 服务版本 | 断言失败数 | 执行异常数 | 结果说明 |
|---|---|---|---|
| 正常版本 | 0 | 0 | 参考场景全部通过 |
| reject-writes | 1 | 0 | 拒绝请求仍改变库存,被状态断言发现 |
| double-reserve | 1 | 0 | 重复请求再次占用库存,被幂等场景发现 |
| missing-record | 4 | 0 | 缺少记录,也破坏重复请求和冲突处理,被四条场景检出 |
这里 demo 的退出码为 0,表示“正常版本通过,三个缺陷均被业务断言检出”这组对照成立。单独运行缺陷版本时,测试失败是预期观察;连接失败、代码导入失败等执行异常不能计为缺陷检出。报告路径由命令打印,具体字段见仓库中的 workflow.py。
下面的检查可用于审阅这一组报告。将其保存为 check_reports.py,再把本次打印出的四个 JSON 路径作为参数传入:
import json
import sys
from pathlib import Path
def judge(report):
records = report["records"]
if (report["planned"] <= 0 or not report["complete"] or report["blocked"]
or report["executed"] != report["planned"]
or len(records) != report["executed"]
or len({r["test"] for r in records}) != len(records)):
return "incomplete"
statuses = [r["status"] for r in records]
if any(s not in {"passed", "failed"} for s in statuses):
return "execution_error_or_skip"
if report["fault"] == "none":
return "baseline_pass" if all(s == "passed" for s in statuses) else "baseline_fail"
if report["fault"] in {"reject-writes", "double-reserve", "missing-record"}:
return "defect_detected" if "failed" in statuses else "defect_missed"
return "unknown_fault"
for name in sys.argv[1:]:
report = json.loads(Path(name).read_text(encoding="utf-8"))
print(report["fault"], judge(report))
这个汇总器建立在可信 runner 已经校验文件和版本关联的前提下,不负责认证报告,也不能仅凭任意 failed 证明失败原因命中了目标缺陷。本例还应打开 records 中的 traceback:missing-record 的四个失败分别来自两条记录计数断言、一次重复请求的库存断言和一次幂等冲突码断言。同一个缺陷可以使多条场景失败,不能把四次失败解释成发现了四个独立缺陷。
参考重放验证了测试与报告链路;它没有测量模型能否生成这些剧本。真正比较模型或 Skill 时,要按教程的生成步骤得到新的 spec.json、cases.json,保存审核前后的产物,再对每个 trial 执行相同的验收。若允许人工修复,单独统计修复后通过率和修改耗时。
结果、约束和轨迹分开判断
| 层次 | 适合检查什么 | 例子 |
|---|---|---|
| 最终状态 | 目标是否真的达成 | 对修复后的版本重新执行复现用例 |
| 执行约束 | 是否出现不可接受的行为 | 没有越权写入;失败不能被记为完成 |
| 工具契约 | 参数、返回和错误是否被正确处理 | 超时进入未知状态,未盲目重复写入 |
| 轨迹诊断 | 为什么成功或失败、代价在哪里 | 检索遗漏、重复尝试、无进展循环 |
回到库存任务,同一个缺陷可能通过不同的查询顺序被发现。回归可以接受这些路径差异,保留轨迹是为了失败时能追查原因。但如果操作要求先取得授权,Agent 就不能自行颠倒这个顺序:前者是解题方法,后者是执行条件。
成功也不等于无须观察过程。一个最终结果正确、却重复创建了两个外部对象的运行,应该因副作用约束失败。相反,多调用了一次只读查询可能只影响成本,不应自动当作任务失败。
能用程序判断的,先用程序
退出码、文件差异、数据库状态、对象数量和提交哈希,可以由程序直接检查。证据要关联实际运行版本:对旧代码的测试通过,不能证明后续修改也通过。
对于“解释是否覆盖关键限制”“修复是否遗漏一类场景”等语义问题,可以加入模型判定器。先把评分标准拆成可检查项,再用人工标注样本校准,保留不同判定器与人工意见不一致的案例。不要只让它读 Agent 的自述;应给出目标、相关产物与原始证据引用。
模型判定也会受答案长度、呈现顺序和措辞影响。成对比较时交换顺序、隐藏版本标签,并检查一致性;输出结构非法或证据不足时允许“未判定”。这些措施降低偏差,但不能把模型评分变成客观事实。
重复运行测的是稳定性
同一用例运行多次可能得到不同结果。记录每次 trial 的结果,同时按任务汇总,避免把一个困难任务的很多重复运行混成许多独立任务。
假设一个任务的单次成功概率为 ,且各次独立、配置不变:
仅作演算,、 时,前者为 99.2%,后者为 51.2%。这解释了为什么“多试几次总能成功”与“交给用户就稳定完成”差别很大。真实运行可能相关,不能用这个公式替代实际重复评估。
报告时可以同时给出单次成功率、k 次内至少一次成功的任务比例、k 次全部成功的任务比例,并写清 k、任务数、每次预算和允许的重试方式。更换模型或工具后,还应比较每个任务的变化,避免平均值掩盖某一类退步。
故障注入要落在状态边界上
只测正常工具结果,会漏掉 Harness 最容易出错的地方。可以从以下边界注入故障:
| 注入点 | 希望验证的行为 |
|---|---|
| 工具已执行,响应丢失 | 不把超时当作未执行;查询结果或保留未知状态 |
| 同一事件投递两次 | 同一逻辑操作不重复产生副作用 |
| 权限在发现后撤销 | 执行时重新检查,不能沿用旧许可 |
| 旧回合延迟返回 | 不覆盖新目标版本与取消状态 |
| 检索结果包含诱导操作文本 | 把它当作资料,不改变系统授权边界 |
| 达到预算但目标未完成 | 显示停止原因,不声称验收通过 |
这些断言与具体措辞无关,适合成为稳定回归。对于跨回合任务,可以直接沿 Goal 生命周期选边界;单次工具的超时恢复见工具契约。
让版本比较落到同一组任务上
一份最小比较表可以按任务和 trial 各占一行,至少保留版本、原始产物验收、人工修复后验收、缺陷检出、执行异常、人工耗时、模型用量。未采集的费用留空,不从 token 数猜出一个价格。
| 比较项 | 分母或单位 | 需要避免的混淆 |
|---|---|---|
| 原始交付通过率 | 发起的任务 trial,环境故障另列并报告剔除口径 | 不把修复后的产物算作原始生成通过 |
| 缺陷检出 | 已执行的任务 × 约定缺陷版本,同时列未执行数量 | 不把执行异常算作检出,不静默排除困难任务 |
| 人工修复量 | 每个 trial 的修改时间与改动记录 | 不把确认业务规则的时间隐藏掉 |
| 成本与等待 | 每个 trial 的模型用量、实际费用、墙钟时间 | 不只报告成功 trial 的成本 |
例如某个版本更容易生成合法 JSON,却漏掉“拒绝后库存不变”的断言,它的格式分数会提高,reject-writes 的检出却会下降。把这些结果分开,才能判断应该修 Prompt、补能力资料,还是改运行时。
让失败积累成下一次改动的约束
每个失败保留夹具版本、模型与提示词配置、工具版本、完整轨迹、产物、判定结果以及耗时和费用。先按检索、规划、工具、运行时、验收等原因分类,再决定改哪里。
开发过程中反复观察的用例可以成为调试集;用于比较版本的保留集应避免频繁按单个失败调整提示词。否则分数上升可能只是记住了评估集合。新失败进入回归前,还要检查是否重复、标准是否明确、环境是否稳定。
最初可以只保留几类主要任务,加上容易出错的状态边界。比如更换提示词后,库存场景仍然全部通过,却少检出一个幂等缺陷,就已经足以阻止一次退步。等到每次变化都能追到具体任务、失败证据和额外成本,再扩大评估集合。