ELL3N HSU

Available in Chinese only.

← 文章

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 条参考场景。

服务版本断言失败数执行异常数结果说明
正常版本00参考场景全部通过
reject-writes10拒绝请求仍改变库存,被状态断言发现
double-reserve10重复请求再次占用库存,被幂等场景发现
missing-record40缺少记录,也破坏重复请求和冲突处理,被四条场景检出

这里 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 的结果,同时按任务汇总,避免把一个困难任务的很多重复运行混成许多独立任务。

假设一个任务的单次成功概率为 pp,且各次独立、配置不变:

P(k 次至少成功一次)=1−(1−p)kP(\text{k 次至少成功一次})=1-(1-p)^k P(k 次全部成功)=pkP(\text{k 次全部成功})=p^k

仅作演算,p=0.8p=0.8、k=3k=3 时,前者为 99.2%,后者为 51.2%。这解释了为什么“多试几次总能成功”与“交给用户就稳定完成”差别很大。真实运行可能相关,不能用这个公式替代实际重复评估。

报告时可以同时给出单次成功率、k 次内至少一次成功的任务比例、k 次全部成功的任务比例,并写清 k、任务数、每次预算和允许的重试方式。更换模型或工具后,还应比较每个任务的变化,避免平均值掩盖某一类退步。

故障注入要落在状态边界上

只测正常工具结果,会漏掉 Harness 最容易出错的地方。可以从以下边界注入故障:

注入点希望验证的行为
工具已执行,响应丢失不把超时当作未执行;查询结果或保留未知状态
同一事件投递两次同一逻辑操作不重复产生副作用
权限在发现后撤销执行时重新检查,不能沿用旧许可
旧回合延迟返回不覆盖新目标版本与取消状态
检索结果包含诱导操作文本把它当作资料,不改变系统授权边界
达到预算但目标未完成显示停止原因,不声称验收通过

这些断言与具体措辞无关,适合成为稳定回归。对于跨回合任务,可以直接沿 Goal 生命周期选边界;单次工具的超时恢复见工具契约。

让版本比较落到同一组任务上

一份最小比较表可以按任务和 trial 各占一行,至少保留版本、原始产物验收、人工修复后验收、缺陷检出、执行异常、人工耗时、模型用量。未采集的费用留空,不从 token 数猜出一个价格。

比较项分母或单位需要避免的混淆
原始交付通过率发起的任务 trial,环境故障另列并报告剔除口径不把修复后的产物算作原始生成通过
缺陷检出已执行的任务 × 约定缺陷版本,同时列未执行数量不把执行异常算作检出,不静默排除困难任务
人工修复量每个 trial 的修改时间与改动记录不把确认业务规则的时间隐藏掉
成本与等待每个 trial 的模型用量、实际费用、墙钟时间不只报告成功 trial 的成本

例如某个版本更容易生成合法 JSON,却漏掉“拒绝后库存不变”的断言,它的格式分数会提高,reject-writes 的检出却会下降。把这些结果分开,才能判断应该修 Prompt、补能力资料,还是改运行时。

让失败积累成下一次改动的约束

每个失败保留夹具版本、模型与提示词配置、工具版本、完整轨迹、产物、判定结果以及耗时和费用。先按检索、规划、工具、运行时、验收等原因分类,再决定改哪里。

开发过程中反复观察的用例可以成为调试集;用于比较版本的保留集应避免频繁按单个失败调整提示词。否则分数上升可能只是记住了评估集合。新失败进入回归前,还要检查是否重复、标准是否明确、环境是否稳定。

最初可以只保留几类主要任务,加上容易出错的状态边界。比如更换提示词后,库存场景仍然全部通过,却少检出一个幂等缺陷,就已经足以阻止一次退步。等到每次变化都能追到具体任务、失败证据和额外成本,再扩大评估集合。