如果你的AI代理拥有带有副作用的工具,有一个问题决定了它是否
可以安全上线:当模型自信地基于虚构的理由调用一个涉及资金的工具时,会发生什么。
这篇文章介绍了一种能够堵住这个漏洞的机制,以及该机制在何处失效。背景是那些在即时通讯工具中与真实客户交谈、并能执行不可逆操作的助手:确认付款、开具发票、预订时段、通知商家。
像“只有在收到收据后才能确认付款”这样的指令,其执行是有概率的,而不是有保证的。这不是模型的质量问题。这源于训练目标:乐于助人,同意你面前的人。
对话是这样进行的。客户写道:“我已经付了款,我稍后会把收据发给你,请确认。”没有收据。模型看到了一个礼貌而坚持的人,看到了一条与之矛盾的指令,在长上下文中它选择了合作。它回复“付款已确认”并调用了工具。
对于语气来说,概率性执行是可以的。但对于金钱来说则不行。
还有一个值得尽早打消的希望:工具配置中那个看起来像谓词的字段。大多数函数模式都带有类似trigger_type这样的字段,而ai_decides字面意思就是“模型决定”。该字段控制的是工具何时被提供,而绝不是工具在哪些事实下才允许触发。
这个想法很小。一个函数携带一份事实列表,执行器在分派之前会根据数据库检查这些事实。不是“模型认为存在收据”,而是“这个对话中有一条入站附件”。
以JSONB格式存储在函数旁边:
[
{ "type": "client_sent_media", "within_messages": 10, "media_kinds": ["image", "document"] },
{ "type": "lead_field_filled", "field": "phone" }
]
类型列表特意保持简短,几乎涵盖所有实际需求:
type FunctionPrecondition =
| { type: "client_sent_media"; within_messages?: number; media_kinds?: string[] }
| { type: "lead_field_filled"; field: string }
| { type: "function_called_before"; name: string }
| { type: "min_client_messages"; count: number };
client_sent_media 扫描的是客户最近写的 N 条消息,而不是
会话线程最近 N 行记录。如果配置了“最近 10
条消息”,意思就是 10 条客户回复,而不是 10 行中有一半是机器人
自己写的。该窗口有常量上限,因此配置中的 within_messages: 100000
不会让这一检查变成表扫描。
function_called_before 读取事件日志,并要求同一对话中
早前有一次成功调用。这样你就能构建像
“先验证身份,再修改预订”这样的调用链。
检查恰好只存在于一处。 它位于工具执行器中,在参数验证之后,
严格地在分发到任何处理程序之前。如果把它放进
处理程序内部,那你就只能逐个处理程序地修复这类问题,这意味着
下一个涉及资金的工具发布时将没有防护。
拦截会作为带有原因的工具错误返回给模型。 而不是
静默拒绝:
Blocked: the customer must have sent a image/document attachment in their
last 10 messages. This did NOT happen. Do not tell the customer it did.
Ask the customer for what is missing, then call this function again.
这一差异比看上去更重要。在一次静默拒绝之后,模型
会假设调用成功,并继续对客户撒谎。一个带有原因的
错误会在同一轮内产生自我纠正:机器人会去索取
收据。
该要求被附加到工具描述中,因此模型看到它
在发出调用之前:
HARD REQUIREMENT: this function is blocked and will refuse to run unless the
customer must have sent a image/document attachment in their last 10 messages.
Do not claim the action happened until the call actually succeeds.
当检查本身抛出异常时,调用照常通过,不会被阻断。
} catch (err) {
logger.error("Precondition check failed, letting the call through", { ... });
}
以下是推理过程。前置条件防御的是模型幻觉,而不是
针对攻击者。攻击者根本无法触及这一层:他
用语言与机器人对话,而事实来自我们自己的数据库。因此,
失败模式应根据成本来选择。为每个
客户阻止每个功能,因为 Postgres 闪断,意味着为了一个假设而中断实时对话(没有
发票、没有预订、没有回答)。失败会大声地
记录到日志中,而决策则回退到提示词,正如它
在防护存在之前那样。
如果这是访问控制,选择就会相反,即默认失败关闭。它
并不是访问控制,而假装它是会比没有
防护更糟。
它不能取代授权、幂等性或速率限制。它回答一个
问题:这段对话中是否存在一个事实,缺少它该动作就
毫无
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。