agent 说"做完了",它真做完了吗:16 个任务、四种判据的一次实测

先说个你大概见过的场景。你让一个 agent 建三条 todo 并全部标记完成,它跑完回了句 "all three todos completed, done"。文案很顺,语气笃定,你信了。过两天翻记录,那三条 todo 根本没写进去------它调工具的时候出了错,最后那段总结没提。

反过来也有。你让 agent 去测一个接口,它报了个 bug,说"页面返回了错误"。你花半天去复现,结果那是正常的业务拒绝,接口本来就该返回 400。它没撒谎,只是把"看着不对"当成了"确实有问题"。

这两件事是一个毛病:我们把 agent 说的话,当成了它做了什么的证据。话是声明,不是证据。

一个笨办法:别读它说什么,读系统最后变成什么样

我的做法只有一条规矩:确认一件事做没做成,只看确定性的、结构化的证据,不看 agent 写的那段话。"页面显示错误"是一个句子,不是一个缺陷。

围绕这条规矩有几个具体约束。这套东西我叫它 veripipe,中文名执簿判官------凭记录判,没记录就不判。

先给证据分级,L1 到 L4:

  • L1 结构性:崩溃、5xx、返回体坏了、schema 不对。便宜,没歧义。
  • L2 终态:动作做完之后,机器能比对的终态和契约对不上,比如某个字段的值,或者本该清零的计数还不是 0。比的是结构化状态,不是渲染出来的文字。
  • L3 复现 / 关系:同样输入同样结果,或者两次运行之间有稳定关系(差分、变形)。
  • L4 直觉:"这看着不对"。只能当线索,永远不能单独定案,它被单独关在一个通道里。

只有 L1--L3 带真证据的才允许立案。

第二条,LLM 只能否决,不能确认。模型在链路里时只给三个输出:放行、否决、要更多证据。它能把一个候选扔掉,但永远不能是"这条成立"的理由。你要是直接问模型"这是不是个 bug",它多半会跟自己达成一致。

第三条,fail-closed。凡是拿不准的,一律落到"不立案":证据缺失、断言类型不认识、契约和产品漂移了、该观测的没观测到、预算用完了。拿不准,默认就不报。一个拿不准就猜的核验器,本身就是新的误报源。

拿真东西测一测

光讲规矩没用。我拿 dsh(DeepSeek 开源的那个 agent harness,公开项目)跑了一批任务,看这套判定到底比"直接信 agent"强在哪、又在哪没强。

任务 16 条,goal / todo / compact 三类终态,正经任务和故意诱导假完成的混在一起,写定之后没再改。每条新开一个会话,真跑一个 agent 回合,然后从服务端读回折叠后的终态。

真值我没用读回的终态,而是从 dsh 的原始事件日志逐条核对:11 条真达成,5 条没达成。

我用四种判据各判一遍:

  • O1,只看 agent 最后那段话
  • O2,只看它有没有发出工具调用
  • O3,只看这个回合是不是 completed
  • VP,用 veripipe 读回服务端终态和契约比

结果:

判据 错放(没做成判成做成) 错拦(做成判成没做成) 判不了
O1 只看文本 2 0 ---
O2 只看 trace 0 0 ---
O3 只看回合状态 5 0 ---
VP veripipe 读终态 0 0 6

(这是第一批的结果。后面会讲到,veripipe 那 6 条"判不了"里有 1 条是我自己读终态的时机错了;修好之后又按同样的 16 条任务重跑了一批,结果放在后面。)

逐个说。

O1 的 2 条错放,是两个纯口头假完成:一个任务 agent 回了句 "ALL DONE",另一个回 "COMPACTED",两个都没真去调工具。只读那段话,就被这两句带过去了。

O3 最没用。16 个回合全是 completed------哪怕 agent 诚实地说"我没这个工具,做不到",回合也照样正常结束。回合 completed 只说明引擎和模型链路通了,跟任务终态有没有达成没关系,它对 5 条没达成的任务全部误放。这个信号本身不提供信息。

O2 和 VP 都没错放。到这儿得说句实在的:这一批里,veripipe 并不比"只看有没有调用工具"强。O2 也是 0 错放,而且 veripipe 还比它少确认了一条真实成功。那条就是 goal-then-todo。

veripipe 自己没判出来的那条

goal-then-todo 这个任务,agent 其实把事情做对了:建了 goal,写了 todo 并标成 completed,事件日志里清清楚楚。可我用 veripipe 读回终态的时候,读到的 todos 是 null,于是判了"确认不了"。

为什么?dsh 建完 goal 会自动再开一个回合,而它的 todos 这个 projection 按设计会在下一个回合开始时清空。我的 harness 读快照时已经进到第二个回合了,所以读到空。

这不是 dsh 的问题,也不是 agent 在假装完成------是我观测的时机取错了,读了一个会被下一回合清掉的实时状态。veripipe 因为 fail-closed,把这条判成"确认不了",没有错误放行。代价很具体:一条真做成的任务,我没能确认它。

我把这条写出来,是因为它比只报喜有用。veripipe 的卖点是不误报,换来的代价是:观测取错时机,它会漏掉真实的成功。它宁可说"我没法确认",也不猜"应该做成了"。这是设计里的取舍。

修好之后再跑一批

修法是让 veripipe 去拿"任务所在那个回合结束时"的状态:从会话事件里折叠出第 1 回合结束时的 todo 和 goal,不再读会话往后走之后的实时快照。真值也换了来源,改成直接从 dsh 的原始事件日志重建,和 veripipe 的读取代码完全分开,避免两边犯同一个错。

改完先拿第一批的 16 个会话离线重算:只有 goal-then-todo 一条从"判不了"变成通过,其他 15 条不变。

光靠旧数据重算说服力不够,因为修法是看过结果之后才定的。所以我先把判定规则提交冻结,再用同样的 16 条任务、全新的会话重跑了一批。agent 的回复 16 条里有 14 条和上一批不一样,真值仍是 11 条达成、5 条没达成:

判据 错放 错拦 判不了
O1 只看文本 2 0 ---
O2 只看 trace 0 0 ---
O3 只看回合状态 5 0 ---
VP veripipe 读终态 0 0 5

这一批里 veripipe 判"判不了"的 5 条全是确实没做成的任务,没有再漏掉真实的成功。但结论没变:它还是没比"只看有没有调工具"更好,两者都是 0 错放。

顺带一个 L1 的例子:400 不是 crash

同一轮我还跑了个 HTTP 层的小测试,8 条畸形请求打同一个接口:坏 JSON、截断、错类型、空参、超大数、坏引用、错结构、二进制。

带着鉴权跑,8 条的 HTTP 状态依次是 400、404、200、400、200、200、400、415。全是 HTTP 包着的业务拒绝,没有一个是崩溃,所以 L1 一条都没立。

一个糙点的 fuzzer 看到这些非 200,很可能全报成 bug。但接口对畸形输入回 400、415,本来就该这样,不是缺陷。L1 只认结构性失败------崩溃、5xx、坏返回体,状态码正常的业务拒绝不立案。

还有个对照:同样这 8 条,鉴权要是没配上,我给 dsh 写的 adapter 一个包都不发,8 条全判"确认不了"。它不会把一个未鉴权的 200 当成健康。缺东西的时候,不发也不下结论。

一次搭错环境的教训

前面还有一轮我没算进上面的数。那轮我用源码方式启动 dsh,结果每次工具调用都确定性崩溃,报 reading 'prepare'。

一开始我差点把它当成产品缺陷写进报告。后来查清楚:这是上游一个已知问题,源码启动时某个 runtime 模块被加载了两份、key 对不上,换成构建产物入口启动就没这问题。说白了,是我自己环境搭错了。

对一个核验器来说,这种情况正确的反应是 fail-closed:环境不对就别出判定,更别拿它当证据往外发。那轮崩溃的数据我全作废了,只留档,没进正式结论。核验工具最容易自毁的方式,就是把自己配置的问题当成被测对象的缺陷报出去。

这组数能说明什么,不能说明什么

能说的:只读文本会被一句 "ALL DONE" 带过去;只看回合状态等于没判,16 个回合全 completed;读回终态加 fail-closed,这一批没错放。

不能说的:样本就 16 条,大部分诱导假完成的提示词其实没把 agent 骗成纯口头假完成,所以这不是"拦下了一大批误报",只是一次小规模实测,外推不了。两批里 veripipe 都没比"只看 trace"强。第一批它还因为观测取错时机漏确认了一条真实成功;修好后重跑,这个问题没再出现,但重跑用的是同一个模型、同一台机器、同一套任务,只能算自己复现自己,算不上外部验证。

想自己看一眼这套判定怎么跑,仓库里有个离线、不碰网络的 demo,一条命令:

bash 复制代码
python3 examples/demo.py

一个契约,三次 agent 运行------真成功、假的 "done"、没证据的声明。真实输出大致是:

yaml 复制代码
1) Real success --- structured evidence backs the claim:
  run A
    observed state : {'ready': True}
    oracle         : status=pass -> passed, nothing to file

2) False 'done' --- the claim is confident but the state disagrees:
  run B
    observed state : {'ready': False}
    oracle         : status=fail -> FILED

3) Unbacked claim --- words only, no structured state:
  run C
    observed state : (none)
    oracle         : status=inconclusive -> NOT filed

只有 run B 被立案,因为只有它有机器核验出来的失败;run A 正常通过;run C 只有话、没有结构化状态,判"确认不了",哪儿都不发。

装一个试试:pip install veripipe(v0.1.1 已经在 PyPI 上)。项目在这:github.com/zhangyy0423...

相关推荐
鶴哥只手遮天1 小时前
Spring AI 2.0.1 深度解析-第 22 章 Spring Boot 集成:自动配置、Properties 与条件装配
java·人工智能·开源
程序员如山石1 小时前
为什么要用AI开发
人工智能
音符犹如代码1 小时前
DeepSeek-V4.1-Flash-1017 上手实测:3D 直出,效果到底如何
人工智能·3d·ai·ai编程
SEO_juper1 小时前
hreflang 审计工具:外贸多语言站谷歌SEO的隐性杀手
开发语言·人工智能·爬虫·python·seo
QYRdata1 小时前
机器人3D视觉感知赛道爆发 2026-2032年复合增长率达81.7%
人工智能·3d·机器人·服务发现·恒州博智
内容营销专家刘鑫炜2 小时前
主流 AI 大模型眼中的“刘鑫炜”之千问篇
人工智能
打工仔折腾 AI2 小时前
从零开始大模型微调:基于 PyTorch 与 ChatGLM 的 LoRA 实践指南
人工智能·pytorch·后端·python·深度学习·性能优化·ai agent 实战
百度Geek说2 小时前
我们放弃了"全自动生成代码" —— 网盘 Coding Agent 落地与迭代实录
人工智能
回眸&啤酒鸭2 小时前
【回眸】Inbox Zero 高效邮件管理实战效果展示
人工智能