核心思路是让 AI 提议而确定性代码提交,将模型输出作为采样而非指令,policy 始终留在代码层。
同一个功能有两种设计方案。第一种,给模型提供一组工具,让它处理客户的退款请求,由模型自行决定金额并调用退款工具。第二种,将请求和订单记录一并交给模型,让它返回一个结构化的提议——退款类型、金额、原因码——然后由普通代码根据退款策略进行评估,再决定是否执行。
模型在两种方案中做的工作相同。区别在于改变世界的权力在谁手里。第一种方案,糟糕的输出是一笔错误的退款。第二种方案,糟糕的输出是一条被拒绝的提议和一行日志,因为策略是代码,而代码不会哪天状态不好。
这就是整个模式,它的价值来源于依赖本身的特性,而非对模型的任何不信任:模型输出是从某个分布中采样的结果,让样本执行不可逆操作的系统,其不可约减的失败率等于模型本身的失败率。而样本只作为提议的系统,其失败率由策略代码决定,这一点你可以测试。
输出约束层(Output Rail) 是投入产出比最高的一层,却也是最常被"在 prompt 里客气请求"所替代的一层。如果答案必须是六个标签之一,那就让它只能是六个标签之一——通过 schema 中的 enum、通过提供商支持的约束解码,或者在不支持的情况下通过你自己的代码进行拒绝。一个 prompt 说"请恰好回答以下之一"是请求,而一个经过验证的 enum 才是保证。
某个调用应该有多大的自由度,可以从其输出所触及的事物的唯一一个属性来判断。问一下如果这个具体的输出以最坏的方式出错了会发生什么,然后根据答案来路由。
系统可逆,对任何人都不可见——内部标签、路由提示、缓存键。给模型宽阔的发挥空间;影响范围不过是一次重新计算。在这个层面加约束是浪费精力。
用户可逆,对用户可见——草稿、建议、预填充字段。只用输出约束层。用户是操作约束层,只要界面让复核变得真正容易而非名义上可行,这种设计就是合理的。
工作人员可逆,对客户可见——已发送的消息、已发布的摘要、状态变更。输出约束层加操作约束层,外加对提议内容和使用了哪个 prompt 版本的记录,因为后续你会需要重建这个过程。
不可逆或对外可见——资金转移、数据删除、发送给第三方的邮件、监管者能读到的任何内容。模型只能提议,永远不能提交。要么由代码决定,要么由人决定。如果两者都做不到,功能就没准备好——这是一个合理的结论,而非胆怯。
这条规则的价值在于它关注的是行动,而非模型。无论模型上个月表现多好或多多差,它给出相同的答案——在对一个每年替换两次的依赖的设计规则领域,这正是你对设计规则的期望。
// OUTPUT RAIL: 提议是一个闭合类型。不存在可以让未预料指令通过的自由文本字段
type Proposal = {
action: "refund_full" | "refund_partial" | "replace" | "decline" | "escalate";
amount_cents?: number;
reason_code: ReasonCode; // enum,有验证
evidence: string[]; // 订单事件的 id 列表,不是叙述性文本
};
// ACTION RAIL: 纯函数、确定性、可单元测试,且是唯一被允许说"是"的东西。
// 注意它没有做什么:不查询模型,不信任任何交给它的字段。
function authorise(p: Proposal, order: Order, actor: Actor): Authorised | Denial {
if (!actor.can("issue_refund")) return deny("permission");
if (p.action === "refund_partial") {
if (p.amount_cents == null) return deny("malformed");
if (p.amount_cents > order.paid_cents) return deny("exceeds_paid");
if (p.amount_cents > POLICY.auto_refund_ceiling_cents) return deny("needs_human");
}
if (order.age_days > POLICY.refund_window_days) return deny("outside_window");
if (!p.evidence.every((id) => order.hasEvent(id))) return deny("bad_evidence");
return authorised(p);
}
// BLAST-RADIUS RAIL: 即使是已授权的操作,在总量上也有上限。
await withinDailyCap("refunds", p.amount_cents, () => execute(p));
证据检查是值得抄过去的细节。要求提议引用必须存在于订单记录中的标识符,将"辩护"——模型可以为任何结论生成辩护——转化为机械上可证伪的主张。它很便宜,直接捕获了捏造,且其有效的原理与提取任务中 span 有效的原理相同:指向真实数据的指针可以被检查,而散文不能。
还要注意 authorise 返回的是拒绝原因而非布尔值。拒绝原因的分布随时间变化,是 AI 功能产生的最有信息的指标之一:exceeds_paid 比率上升意味着模型读取订单的方式发生了变化,而且这在客户投诉之前很久就可见了。
有四样东西常被部署为约束手段,但它们并不约束。
Prompt 中的指令。"永远不要退款超过订单总额"是一条有力的提示,而非不变量。它值得包含——能提高提议的通过率——但它不是一条约束,把当约束用是这个错误最常见的版本。
客户端验证。 在组装请求的界面中应用的限制根本没有被应用,原因是它从未在任何其他系统中真正起过作用。
用模型检查模型。 守卫模型可以降低错误率,但无法界定错误率的上限。在没有确定性检查的地方用它——作为验证而非授权——且永远不要把它放在样本和不可逆操作之间的最后一环。
以输出过滤作为安全边界。 扫描生成文本中的不良内容是一层有用的卫生手段,而非一个好的边界。有效的边界是操作约束层:如果危险的事情需要组件不具备的能力,任何输出都无法调用它。这与能力范围限定的工具调用背后的推理相同,而且这是在注入攻击下唯一站得住脚的版本。
反模式:不经验证信任输出
反模式:无界限 Agent
模式:渐进式自主