试运行与预览模式

A48
概念 · 智能体 AI 详解

试运行与预览模式。

在大多数智能体产品里,用户批准的是模型关于它将要做什么的那句话,而不是真正要去做这件事的代码的输出——而这两者会静默地分叉;预览就是这样从一项安全控制,退化成那套教人一路点「是」的仪式。只有当预览与动作由同一条代码路径产出、并且批准被绑定在那份具体的预览上(而不是绑在它背后的意图上),这次预览才配得上一次批准。

步骤 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,以及任何由一个不会提前告诉你的系统来裁决结果的动作。给这些东西伪造一份预览,比不给更糟,因为一份对不可预览动作的自信预览,是一次假测量穿着真测量的戏服。

对这些动作,请在预算的别处买安全:收窄任务作用域,让最坏情形变小;把动作做成可撤销、至少可修补的;加一个幂等键,让一次重试不等于第二次扣款;并留足记录,使修补是一套流程而不是一场调查。可撤销性与预览是同一笔预算里的替代品——便宜的撤销为你买来跳过预览的权利;而一个既不可预览又不可撤销的动作,是唯一真正需要由人来发起、而不是由人来批准的情形。

该怎么做。挑出你的智能体后果最重的三个工具,逐个看它展示的是哪一种预览。如果批准对话框里那串文字是模型写的,就把它改成工具的输出:给那唯一一份实现加一个 commit 旗标、返回算出来的效果、然后把它渲染出来。接着把这份效果做哈希,并在提交时重新核对。如果某个工具不执行就算不出自己的效果,就在对话框里把这件事说明白——「此操作无法预览」是一句比任何转述都更有用的话——并把预算花在撤销上。对话框本身该怎么写,见审批与确认 UX;而当你干脆不再询问之后会发生什么,见自动批准与允许清单。