文件落盘不等于子代理写完了
这是我自己那条批量写作流水线上出的一次事故,不是听来的案例。那条线一次跑一批选题,每篇配一个实现者子代理去写,写完把文件落到内容目录里,我在外面盯着整批的进度。我当时的习惯是边跑边收:看到哪几篇的文件出现在目录里,就把它们提交进版本库。这样即便中途断掉,也不至于把一整批的成果全丢在内存里。
问题就出在「看到文件出现」这一步上。
我看到的:提交里混进了半成品
事后回头看,那次收进版本库的几篇里,有一篇的内容和它最终的样子并不一致。它读起来是通顺的,格式是对的,字数看着也不离谱——就是比最终稿多了一些后来被作者自己删掉的段落。如果我不去比对,光看提交记录,完全看不出有什么不对。
这类错误最难受的地方在于它不报警。写作失败会留下空文件,那种失败一眼能看见;而半成品是一个看起来完全正常的结果。它不触发任何检查:字数够,结构齐,标签闭合,质量门脚本一条都不会响。
先说是怎么发现的
这一段我觉得比问题本身更值钱,所以放在前面讲。
我不是因为读到了不对劲的文章才发现的,我是因为提交动作本身报了错。
那次我用的是目录级的批量添加——把整个内容目录一次性收进去。它中途报了个错停下来,大意是读取某个文件时拿到的长度和它预期的对不上。我第一反应是环境或者磁盘的问题,就直接重试了一次,重试就过了。
「重试一次就好了」这件事,才是真正的线索。
如果是权限或者路径的问题,重试不会好;重试能好,说明那一瞬间那个文件正在被改写,读到一半内容就变了。顺着这条线我才回头去比对已经提交的内容和最后停下来的文件,确认了那段窗口的存在:文件出现的时刻,和文件内容定稿的时刻,中间隔着一段时间,而我的提交正好落在这段时间里。
如果那次批量添加没报错,我大概率到今天都不知道有这回事。所以这次真正被我记下来的不是「别提交半成品」,而是一个偶发的、看起来像环境抖动的报错,可能是唯一一个把结构性问题捅出来的口子。后来我对这种「重试一次就好了」的报错都会多问一句:它为什么重试就好了。
落盘只是过程中的一个状态
想明白之后,原因其实很朴素。
子代理写一篇长文,不是一次性把成品吐到磁盘上。它更接近人的写法:先落一版,然后自己回读,删掉重复的段落,把话说得更紧一点,可能还会调整段落顺序。这个过程里它会反复写同一个文件。首次落盘只是过程中的一个状态,不是终点。
而我在外面用的判据是「文件出现了」——这是一个产物侧的观察。我拿一个我能看见的现象,去推断一个我看不见的进程的状态。这个推断在同步执行里成立,在异步任务里不成立。
两件事叠在一起,就是那段窗口。窗口有多长我控制不了,也不该去猜——它取决于那一篇要精简多少,跟我没关系。
顺带一提,目录级的批量添加把伤害放大了一层。它扫的是整个目录,包括那些我这一轮压根没打算收的文件。只要目录里有任何一个文件正在被写,整个提交动作就可能撞上去中止。也就是说,我不但会收到半成品,还会因为别人的半成品而做不成自己的事。
改成了两条硬规矩
第一条:完成信号只认任务系统给的那一条通知,不认文件出现。
我这条线上,每个子代理跑完会有一个结构化的返回(写作阶段要返回 slug、是否写入、字数这几项)。任务真正结束的标志是这个返回到手,不是目录里多了个文件。文件在不在,是我拿到通知之后去核对的东西,不是我判断有没有完成的依据。
这个顺序不能颠倒。我这条线的验收口径本来就是「回磁盘数文件,不看汇总」——因为编排层的汇总两个方向都骗过我:报零完成的时候磁盘上可能已经有东西了,报完成的时候也可能什么都没写。这两条不矛盾:数文件是验收手段,任务通知是完成判据。先等通知,再数文件,两步都不能省,顺序也不能换。
第二条:提交时只列显式的文件名,不用目录级批量添加。
# 不用
add <整个内容目录>
# 改成
add <这一批我确认要收的那几个文件,一个一个列出来>
这条改完之后,还带来一个结构上的变化:显式列文件名,等于要求我在提交之前先把「这一轮应该产出哪几个文件」写清楚。多出来的文件当场就看得见,少掉的也当场就看得见。用目录级添加的时候,这两种情况都会被悄悄吞掉——多的被一起收走,少的没人问。这跟我这条线的验收口径是一致的:验收不看汇总,回磁盘一个一个数文件;提交也别看目录,一个一个列文件名。两件事其实是同一个动作。
能带走的那句话
异步任务的完成信号必须是任务系统给的,不能是产物侧的观察。
我自己在用的时候,会把它拆成一个方向性的提醒:产物存在推不出任务完成,但产物不存在能推出任务没完成。这个推理只有一个方向是成立的,我以前反着用了。
这也是为什么我这条线上的评审员被要求先查文件在不在,再查写得好不好——文件存在只是一个必要条件,它把「压根没做」这一类失败挡在门外,但它挡不住「做了一半」。这两类失败在外部看起来长得一模一样:都是「返回了一个看起来正常的结果」。
再往上收一层,这件事其实是同一个前提的又一个实例:单个子代理的成功率明显低于 1,编排、回查、独立评审这些东西都是在这个前提下的补偿措施。而「文件落盘就算写完」这种判据,本质上是在假设子代理的行为是原子的、一步到位的——一旦按这个假设去设计,偶尔的失败就会被当成基本不会失败来处理,直到某天一个像环境抖动的报错把它捅出来。
这篇写的是我自己那套流程里的一次实际情况,不是通行做法。你的场景、工具和团队规模不同,结论未必适用,判断方式可能比结论更值得拿走。
延伸阅读
- 上一篇(跑闭环):Agent 干活里的静默失败:退出码 0 是怎么骗过所有人的
- 下一篇(跑闭环):Agent 质量门的误报比漏报更危险
- 这个专题的主论点:Agent 调用没报错,不等于这件事办成了
- 专题导读与七个阶段的地图:把 Agent 当成一个要上岗的员工来带:这个专题讲什么