批量与回填运行。
当你第一次把一个能跑的智能体对准十万条历史记录时,你不是在把一个功能放大——你是在对「你只在 N=1 上验过的每一条性质」做第一次真正的测试,而它失败的方式正是最昂贵的那种:不是崩溃,而是十万次看着像样的错误写入,被你的完成率看板报告成一次干净的通扫。请把回填当成一个独立的系统来设计,有它自己的写入姿态、自己的停机条件,以及一份你在两百行上演练过的撤销——因为到了十万行,那就不再是撤销了。
先算账,再谈架构。
回填把交互式智能体的安全模型整个翻了过来。交互式运行默认有一个人在读输出,影响半径是一条记录;批量运行两者皆无,而约束它的,换成了一组没人在「单次运行」以上验过的性质。四行先算出来的账,决定这个项目到底可不可行:
records 120,000 cost per task $0.14 -> $16,800 total, vs a $4,000 monthly budget median task latency 40 s target wall clock 12 h -> 2.8 records/s -> ~111 concurrent runs third-party write cap 10 req/s -> the real ceiling; 2.8/s is fine, 28/s is not needs-human-review 2 % -> 2,400 reviews -> 5 reviewer-weeks
这四行里有三行经常直接把计划判死,而最常判死它的是最后一行。对任何需要人来接受其产出的东西来说,真正的速率上限是人工评审的产能,而不是模型吞吐——定价见人工评审的成本,运营见评审队列。在任何人动手写编排器之前,就把这一块贴进设计文档:它是整个项目里最便宜的产出物,也是那个会把它的糟糕版本当场取消的产出物。
那些在交互场景下好用的按任务成本上限,在这里会严重误导你,因为你量到的那个按任务数字是中位数,而账单是一条长尾上的求和。请按第 90 百分位的任务成本、而不是按中位数来给这次运行定价;在智能体负载上,两者之间常常差 3 倍以上,而推高它的正是那些在重试循环里打转的记录——回填里装的恰恰就是这种记录。
挑定写入姿态,并让第一趟产出一件产物、而不是一次改动。
姿态只有三种,而这个选择是整次运行里后果最重的那一个决定:
- 提议。智能体读取、判断,然后把一条「被提议的改动」写进一个独立的存储——每条记录一行,含旧值、新值与理由。对权威系统零副作用。每一次,这都是正确的第一趟,因为它是唯一一种让你在任何东西变得不可逆之前、就能审计完整产出分布的姿态。
- 暂存。写入落进一个影子列、一个草稿状态或一张并行表,再由一个独立的提升步骤把它们放到线上。适合那种「写入本身才是难点」的迁移,也是那种能让你按切片提升的姿态。
- 直写。智能体边跑边往权威系统里写。只有在「按行撤销很便宜」且你已在同一总体上跑过提议那一趟时,这才站得住。
把人工闸门放在提升这一步、而不是放在循环里,才是让批量运行付得起钱的关键:一位评审者按某个置信度或新颖度信号排好序、批准五百条提议,成本只是同一位评审者批准五百次单独运行的一个零头;而且分布层面的问题,只有在批量视图里才看得见。
爬一道试点阶梯,每一级的退出判据都在跑之前写好。
二十条、两百条、两千条,然后是全量——而这道阶梯的意义不在于「为谨慎而谨慎」,而在于:只有当你事先定下了退出判据,每一级才有可能把计划驳倒。「看着还行」不是退出判据;「二十条里需要修正的不超过一条,且产出类别分布与历史基准率相差不超过十个百分点」才是。
- 给前几级做分层,不要随机抽。从 120,000 条里随机抽二十条,里面一条会把你搞死的用例都不会有。请按你本就知道难的那些形态手挑:最老的记录、可选字段为空的记录、非拉丁文本、那个配置很别扭的租户、由某个早已下线的导入器写入的记录。
- 让一个金标准切片贯穿整次运行。一百条由人类确定了正确答案的记录,与真实工作交错混入、并打上不可见的标签,就能给你一个运行期间的准确率读数,而不是事后的复盘。单凭这一项控制,就能把一次十二小时的运行从一桩信念行为变成一次测量。
- 把一级干净的两千行当成对十二万行的弱证据。它确立的是常规路径走得通。它对速率限制的行为、缓存淘汰、或你的供应商降级的那一小时什么也没说——而那些正是负载测试与供应商容量要管的事。
在写「开始」之前,先把「恢复」和「停止」写好。
一次批量运行一定会被打断。把这当成前提接受下来,设计就自然掉出来了:一本每条记录一行的账本,加上一个由账本推导出来、而不是攥在某个进程里的游标。
- 每条记录一行账本——状态取
pending | claimed | done | failed | needs_review,一个带过期时间的租约(这样一个死掉的 worker 所认领的条目会回到池子里),尝试次数,以及运行 id。于此之后,恢复是一次查询,而不是一套抢救流程。这就是把持久状态与可恢复性用到批处理上。 - 幂等键源自记录,而不是源自那次尝试。如果重试时键变了,你造出来的就是一个没有去重的至少一次写入器——这正是幂等与重试里剖开的那种失败,也正是把一次续跑的回填变成双重写入的那一种。
- 三道彼此独立的停机闸,全部归编排器所有,一道也不归智能体。一个累计花费上限;一个基于滑动窗口而非累计均值的错误率熔断器,这样一个在第九小时才开始的故障仍然能把它跳开;以及一个你在试点里真按下过的人工急停开关。一个没被测过的急停开关是一句注释。
- 在整次运行的生命周期里把行为三元组钉死。一次十二小时的运行会横跨发布。如果模型、提示词或工具版本在运行中途变了,你就在同一个名字下产出了两份数据集,而且无法分辨哪些行来自哪一份——所以请按灰度发布与版本化钉到带日期的快照上,并把这个三元组盖在每一行账本上。
请压住「把并发拉满」的本能。有用的上限由你要写入的那个下游系统决定,而回填是一种极其高效地耗尽别人速率限制的办法——而且那个「别人」常常就是你自己那个用同一份配额服务线上用户的生产 API。请用带独立配额的单独凭据来跑批量工作,让回填无法把交互路径饿死;并且宁愿墙上时钟长一些,也不要一场重试风暴。
审计分布,因为那些汇总指标会好看得无懈可击。
这是团队会跳过的一步,也是批量运行会悄无声息地跑歪的原因。回填的典型失败不是某个错误率——而是一个很高的完成率,覆盖在一批自信、均匀地错着的产出之上。完成率、错误率与延迟会统统是绿的。
- 把产出分布与一个你信得过的基准率比一比。如果智能体把某一类贴给了 61% 的记录,而历史比率是 18%,那这次运行已经失败了,不管错误率怎么说。单这一道检查抓到的坏回填,比本页其余全部内容加起来还多,而它只要一次查询。
- 对一个持续样本做盲审。每千条抽十条,把源记录与被提议的产出给评审者看,但不要给他看智能体的理由,让他独立作答。一个看过推理过程的评审者会同意那个推理;这是已知效应,而它会把审计的价值毁掉。
- 留意向「可信默认值」的塌陷。一个判断不出答案而去猜的智能体,产出会比现实更均匀,而不是更多样——这就是失败隐瞒的聚合形态,也正是那种会被一份二十条的按条评审样本欢快放过的失败。
- 按 STEP 3 里的那些分层切开看。一个 94% 的准确率,如果在近期记录上是 99%、在 2019 年之前的记录上是 51%,那它不是一个 94% 的结果;它是两个结果,而其中只有一个该上线。
盖上运行 id,并在两百行上演练那次撤销。
这次运行碰过的每一行都带着运行 id。它是整个项目里杠杆率最高的一行代码,因为它把「我们觉得那些坏写入是在周二」变成了一个单一谓词,也因为没有它,回滚就成了考古。
从那里开始,你需要两条出口之一,而且要事先定好:
- 回退——为每一行被碰过的记录留下旧值,并按运行 id 还原。只要写入是字段更新就可用;而且即便你很有信心,也值得把旧值存下来,因为存储很便宜,而替代方案是从备份里恢复——那会把同一时间窗内所有其他人的工作一并退掉。
- 补偿——一趟做修正而非做还原的第二遍,这是当写入触发了某件外部事情时你仅剩的东西:一封已发出的邮件、一次已投递的 webhook、一张已开出的发票。请按修复智能体副作用所述,在设计正向动作的同时设计补偿动作,并弄清你的哪些工具压根没有补偿——那些工具就该永久地待在 STEP 2 的那道提升闸之后。
然后去演练它。把那一级两百行真回滚一次,核对这些行与运行前的值一致,并给它计时。一次在规模上、在事故当中、拿着一个你自己也不确定的游标才首次尝试的回滚,不是回滚——那是第二次故障。
一次最小可行的回填,按「让每一步都便宜」的顺序排好:算出那四行账并拿给人看;用提议姿态跑第一趟,使任何东西都不至于不可逆;在一份手工分层的样本上爬 20 → 200 → 2,000,退出判据先写;交错混入一个百条的金标准切片,让准确率是实时的;把「模型—提示词—工具」三元组钉死,并连同运行 id 盖在每一行上;在提升任何东西之前,把产出分布与一个历史基准率比一比。如果你只能做其中两件,就做提议姿态与分布检查——这两件合起来能抓住的那种失败,正是另外四件存在的理由。
延伸:重建索引与向量迁移讲同一问题在检索侧的形态,定时与触发式智能体讲周期性的那种情形,而大规模迁移智能体讲当那些记录是源文件时的情形。