试运行与预览模式。
在大多数智能体产品里,用户批准的是模型关于它将要做什么的那句话,而不是真正要去做这件事的代码的输出——而这两者会静默地分叉;预览就是这样从一项安全控制,退化成那套教人一路点「是」的仪式。只有当预览与动作由同一条代码路径产出、并且批准被绑定在那份具体的预览上(而不是绑在它背后的意图上),这次预览才配得上一次批准。
步骤 1预览是一项声明;问题在于是谁作出的。
给用户显示「我会把 14 条超过 90 天的会话归档,并保留加星的那些」,你给出的是一项关于未来动作的声明。能写出这句声明的只有两者之一。要么是模型写的——它描述自己即将发出的那次工具调用——那么这就是一段摘要,受所有寻常生成错误的支配,而系统里没有任何东西拿它跟工具核对过。要么是工具写的——它被以某种模式调用,算出效果并返回、但不提交——那么这就是一次测量。
今天几乎每一个智能体界面交付的都是第一种,却用第二种的视觉语法把它呈现出来。问题不在于模型说谎,而在于模型根本拿不到那些信息。它不知道归档接口把「超过」当作严格大于、不知道这 14 条里已有 2 条被归档过、也不知道一个共享标签会级联扩散。它那句话是对自己意图的一次合理转述,而意图恰恰是用户早就明白的那件事,与他们真正需要看到的东西正好相反。
检验办法:把模型从画面里拿掉,再问这份预览还能不能被产出。如果能——因为是一个函数算出来的——那它就是一次试运行。如果不能,它就是一段描述;那么界面上就该把它标成描述,而不是摆出一副「事实已确认」的样子。
步骤 2两份预览,只有一份是由真实路径产出的。
基础设施工具链比智能体早二十年就想通了这件事,而它们落定的那个区分正是值得照抄的。kubectl apply --dry-run=client 把你这条命令本会发出的对象渲染出来并打印;它从不离开你的笔记本,所以它无从得知集群会拿它做什么。kubectl apply --dry-run=server 把请求提交给 API server,由后者跑完整条 admission 链——默认值填充、mutating 与 validating webhook、配额检查——然后拒绝落盘。同一个旗标、同一个动词,而两者之间是一道沟:客户端预览是对你输入的一次渲染,服务端预览是真正的那次裁决,只在最后一步把写入掐掉。
Terraform 用一个文件画出同一条线。光跑 terraform apply 会重新算一份计划、显示它、然后询问;而在显示与询问之间,世界可以动。terraform plan -out=tfplan 再接 terraform apply tfplan,应用的是那一份计划——批准被挂在一件产物上,而不是挂在一屏记住了的文字上。SQL 的 EXPLAIN 有意待在较弱的那一端:它返回的是查询规划器的意图,而 EXPLAIN ANALYZE 之所以存在,正因为意图与执行众所皆知地会不一样。
Stripe 的测试模式是那个有教益的反例。它是一个完整、忠实、独立记账的世界;而一次成功的测试扣款几乎不能告诉你真实那一次会怎样,因为你担心的那些东西——这位客户、这张卡、这个余额——压根不在里面。沙箱里的排练是一件开发工具。它不是你即将采取的那个动作的预览。
步骤 3把它做成一个参数,绝不要做成一个并列的函数。
实现规则由此直接得出:试运行是那条唯一代码路径上的一个参数,一路传到最后一次调用,在那里跳过提交、改为返回算出来的效果。你一旦写下第二个函数——在 archive() 旁边放一个 preview_archive()——你就有了同一套策略的两份实现,而它们会在任何人赶时间修的第一个边界情形上开始分叉。你的测试套件不会察觉,因为两个函数各自的测试都还是通过的。
# one path, one policy, two endings def archive_threads(query, *, commit: bool): targets = resolve(query) # the real selection logic effect = plan_effect(targets) # 12 archive, 2 no-op, 1 label cascade if not commit: return effect # this object IS the preview return apply(effect) # applies the object shown, not a re-plan # the approval binds to the effect, not to the request effect = archive_threads(q, commit=False) token = sign(effect) # hash of the exact planned effect # ... human approves, minutes later ... apply_approved(token) # re-plans; MISMATCH -> stop, re-ask
最后三行才是大家跳过的那部分。一次针对某份预览给出的批准,只在那份预览仍然为真时有效;而在点击与调用之间,邮箱收到了新邮件、分支多了新提交——这就是经典的检查时刻与使用时刻之间的缝隙。把算出来的效果签上名、并在提交时重新核对,就把一次过期的批准变成了第二次询问,而不是一个意外。Terraform 的计划文件就是这个想法,外加一个文件名。
步骤 4有些动作无法被预览——请诚实地给它们定价。
试运行要求「效果可以在效果尚未发生时被算出来」,而不少真实动作过不了这一关:发一封邮件、刷一张卡、调一个其响应取决于你读不到的状态的第三方 API,以及任何由一个不会提前告诉你的系统来裁决结果的动作。给这些东西伪造一份预览,比不给更糟,因为一份对不可预览动作的自信预览,是一次假测量穿着真测量的戏服。
对这些动作,请在预算的别处买安全:收窄任务作用域,让最坏情形变小;把动作做成可撤销、至少可修补的;加一个幂等键,让一次重试不等于第二次扣款;并留足记录,使修补是一套流程而不是一场调查。可撤销性与预览是同一笔预算里的替代品——便宜的撤销为你买来跳过预览的权利;而一个既不可预览又不可撤销的动作,是唯一真正需要由人来发起、而不是由人来批准的情形。