让碰钱的 AI Agent 受治理:agent 只提议,永不执行
2026年7月15日
整套设计靠一条铁律撑起来:agent 可以提议,但永远不能对任何有物质后果的东西动手。每一个写操作——一笔付款、一条记账、一次账户变更——都离开模型可及范围,过一个模型无法编写、也无法绕过的授权服务,再过一个有对应角色的人。治理是当架构来建,不是靠一段措辞讲究的提示词。
为什么"把提示词写仔细点"注定失败
如果模型手里握着一个能动钱的工具,那你钱的安全就取决于模型在每一次调用上的判断——包括那些被检索到的邮件、被投毒的文档、或一句有歧义的指令带偏的调用。提示词是一种"请你表现良好"的请求,它不是一道强制边界。一旦某个有物质后果的动作能从模型的工具面里够得着,你就把模型变成了最后一道防线——而模型恰恰是你没法完全预测的那部分。
所以解法不是更好的提示词,而是让危险动作从模型所在的地方根本够不着。
那一条铁律,以及每条保证怎么被强制
agent 从最小权限、只读的数据源读取,起草一个"提议的动作"。它能吐出的只有这个提议——绝不是一次直接写入。所有要紧的事,都强制在一个 agent 没有任何工具能触达的层里:
- 没有未授权的付款或记账。写工具压根不在 agent 的工具面里。只有授权服务能触达它们,且必须先过一个按角色核验的人工审批。
- 职责分离。请求某个动作的身份,永远不能是批准它的身份——这由授权服务强制,不靠约定俗成。
- 每个数字可溯源。每个数值都带一个溯源指针——来源文档、页码、检索引用。没有溯源的,就标成"无来源",绝不当作事实呈现。
- 提示注入动不了手。检索来的邮件和文档是不可信的数据,不是指令;工具调用走白名单;输出经 schema 校验。一份恶意文档可以骗过模型,但它够不到任何写操作。
- 爆炸半径有界。每个集成跑在自己独立的最小权限、只读凭据上——一处被攻破,就是一个只读面,没法横向移动到其余部分。
一个实例:钱到底从一条托管支付流的哪里溜走
同一种直觉——假设那条危险路径一定会被走到,并把它设计成可控——正是"守得住的托管支付流"和"悄悄漏钱的托管支付流"之间的分界。两方谈判、达成一致、卡上放一笔预授权。把它端到端画出来,以下是钱真正溜走的四个地方,以及每个的设计对策:
- "零 UI"撞上强认证。对一张 3DS 卡做离线扣款,返回"需要认证"——可根本没有界面去认证,于是那笔预授权悄无声息地没发生。对策:在谈判之前就把授权确立好(一个保存离线支付方式的初始化步骤),再加一条针对仍要挑战的卡的明确兜底。
- 预授权过期。一笔没扣的授权大约一周就失效。如果你的扣款发生在预约之后,而预约又订得够远,那笔预授权早就死了。对策:限制一个时段最多能提前多久预订,或在临近预约时重新授权——并且把"因到店而释放"和"因我们太晚而过期"当成两个不同的事件。
- Webhook 重试。支付 webhook 是至少投递一次的——同一个事件一定会来两遍,没有去重的话,一次重试就把押金扣两次。更糟的是,一个没验签的端点就是一个公开的"扣这笔预授权"按钮。对策:对原始报文体验签、把每个事件 id 落库并在数据库层丢掉重复、在扣款和释放时都带幂等键。
- 哈希过的手机号并不匿名。手机号的取值空间极小;对它做一次普通哈希,离线几秒就被暴力破解,于是"哈希后的键"会把身份泄露给任何能读到这张表的人。对策:用一个服务端持有密钥的带密钥哈希——没有那个密钥就不可逆。
贯穿其中的那条线
无论是一个记账 agent 还是一条支付流,纪律都一样:把不可逆的动作,放在你没法完全信任的那部分够不着的地方;让每个有后果的数字都带着它的来源;只给每个集成它真正需要的那点权限;并假设最丑的那条路一定会被走到,于是提前为它做设计。这和我在工业侧用的是同一条规则——安全关键的限值在控制器侧重新核验,绝不从上层拿来就信。能被绕过的治理,不叫治理。
这就是我设计这类系统的方式——这套模式来自多年后端工作,以及构建那些"一次写错就有真实代价"的生产级 LLM 流水线与 MCP 工具调用系统的经历。
这套论证是可运行的。我把它做成了公开仓库:一个 agent 无法调用的授权服务、绑定内容且一次性的审批、职责分离、必须带溯源的金额、只可追加的哈希链审计日志,以及一个复现上面全部四种托管支付故障模式的 mock 提供方。然后十二次攻击试图让钱动起来——提示注入、自我审批、审批后偷换金额、重放的 webhook,以及一个几千次就被暴力破解的"匿名"手机号哈希。十二次全部失败,约一秒跑完、零依赖——harness 每次推送都在 CI 里跑。 代码 →
如果你造的机器需要一套界面、一个设备连接,或者数据必须落到别处去——告诉我它现在正让你付出什么代价。你会得到一个诚实的判断:能不能解;通常还会收到一个能跑的东西。从这里开始 →