作者结合自用智能体系统Vantage,主张通过工具暴露范围与独立服务约束执行权限,而不能仅靠系统提示词要求模型守规矩。权限边界应能脱离模型单独测试,让智能体的建议能力与实际执行能力分离。
在工具层落实 Agent 安全,而不是依赖 system prompt。
本文源自我亲手构建、每天都在使用的个人 Agent 系统 Vantage。
大多数 Agent 的“护栏”都只是几句话。在某段 system prompt 里,总有这样一行:“未经询问,绝不删除文件”,或者“不要代表用户发送邮件”。这句话很客气,出发点也很好,但它不是控制机制。它只是向一个概率文本生成器提出的请求,每一轮都要重新接受评估,还要与上下文窗口中的其他所有内容竞争。等到它输掉竞争的那一天,你才会发现,自己的安全模型原来只是一条建议。
我想讨论的观点,比“prompt 不可靠”更具体,也更有用,毕竟这一点大家早就知道。我的观点是:Agent 可以建议什么、可以执行什么,两者之间的边界应该由你提供给它的工具结构来强制落实——更理想的方式,是通过不向它提供某些工具来落实。而且,这种约束应该放在一个无需模型参与就能测试的服务中。prompt 是请求,缺失的工具才是保证。
下面用一个我实际运行的系统来说明。
Vantage 是一个个人 Agent 系统。Agent 通过 MCP 连接到一个 Spring Boot 服务,目前用的是 Claude Code,但底层 Agent 的设计目标是可以替换。所有需要持久保存的内容都归这个服务管理:MySQL 中只允许追加的事件日志、一组读取采集器,以及少量写入工具。服务使用普通 Java 编写,通过 JUnit 测试,测试中完全没有 LLM。这种分工就是整个设计的核心:模型充当 controller,服务承担 service layer,而安全模型也直接建立在这种分工之上。
最基本的规则只有三句话:
读取、总结和起草可以自主执行。发送、花钱、删除,或任何公开可见的操作,都需要明确确认。这条规则由服务强制执行,而不是依赖 prompt。
值得琢磨的是最后一句,更值得琢磨的是,你在落实它时会犯哪些错误。
这套分级机制首先做对的一件事,是没有按照读取和写入来分级,而是按照操作对现实世界的影响来分级。
这个区别很重要,因为最直观的实现方式——“读取不受限,写入需要批准”——会立刻把系统卡死。Agent 最有价值的自主行为,就是记录东西:存下一个想法、记下一项观察、起草一份提案。这些都是写入操作,但也完全安全,因为它们只涉及系统自身的元数据:没有发送任何东西,没有花钱,没有删除任何东西,也没有让用户之外的任何人看到任何东西。我们把它称为零级:影响范围为零的写入,仍然可以自主执行。
所以,一个新工具需要回答的问题,不是“这会执行写入吗”,而是“这项操作的影响能否被系统外部看到或感受到,以及它能否撤销”。起草邮件:零级。发送邮件:需要通过审批。真正需要人参与的是不可逆或面向外部的影响。把这类操作与“写入”混为一谈,最终就会造出这样一个 Agent:记笔记需要许可,转账却不需要。
下面这一点,我认为最值得借鉴。
系统中只有一个工具可以主动开口,也就是产出用户没有要求的内容:它允许 Agent 提交提案,为某个项目建议下一步行动,供用户接受或拒绝。Agent 可以提交提案,但不能接受提案。原因不是 prompt 告诉它不可以,而是根本没有接受提案的工具。接受提案的入口只存在于面向用户的控制台中,必须由人点击。
源码里的这段注释,就是完整的安全论证:
/**
* There is no accept_proposal and there must not be. Vantage suggests and Jason decides.
* The service enforces the budget, but nothing except the absence of this tool stops an
* agent accepting its own suggestion into Jason's declared next actions -- which would make
* the accept/reject history a record of Vantage agreeing with itself, and that history is
* the only feedback signal this feature has.
*/
请仔细读最后一个分句,因为这个具体功能里藏着一条普遍原则。一个既能提出建议、又能批准建议的 Agent,算不上有人监督的助手;它只是一个自己填写日志的 Agent。审批记录会变成系统自我赞同的记录,这比没有记录更糟,因为它看起来像是经过了监督。要让人的判断保持真实,唯一的办法就是从结构上让 Agent 无法伪造它。而成本最低的结构性限制,就是根本不编写那个函数。
这个原则很容易推广。每当你想在 prompt 里写下“Agent 不应该自行执行 X”时,先问问:X 是否对应一个 Agent 已经持有的工具?如果是,你构建的就不是护栏,而是在一件已经装填好的工具上贴了张标签。通常,解决办法是把工具拆开:一个负责提出执行 X 的建议,另一个负责真正执行 X;后者必须放在 Agent 无法触及的地方。
同一个提案工具还说明了第二个道理:即使一个操作完全安全,它依然有成本,而这个成本就是人的注意力。
最初的设计是为每次运行设置额度:Agent 审查一个项目时,最多可以提交三条提案。这是最直观的上限,但它不起作用。运行两次就会产生六条提案,运行五次就会产生十五条;每次运行都没有超额,审阅面板却照样会被淹没,只是慢了一点。真正需要提防的,是第二次审查——那个没人留意的第二次。
有效的上限统计的是尚未处理的提案,而不是每次运行提交了多少条:任何一个项目,在任何时刻都最多只能有三条尚未决定的提案。这看起来只是细微调整,实际上却是性质上的变化。单次运行的上限,限制的是生成方的热情;未处理事项的上限,限制的是人的待办积压——在你对前三条中的至少一条作出决定之前,系统不能再交给你第四个决定。这个限制以真正稀缺的资源为计量单位:稀缺的不是 Agent 的输出,而是用户继续阅读这些输出的意愿。护栏如果限制错了数量,只会让你觉得有了限制,却得不到任何保护。
而且,因为这个限制位于服务中,工具自身的描述还可以向模型解释原因:“每一条提案都会让用户多作一个原本没要求作的决定,而一条不值得处理的提案,会让他更倾向于忽略下一条。”这样,模型就会提交一条有分量的提案,而不是为了填满额度提交三条平庸的提案。prompt 层面的指令仍然存在,只是不再承担核心保障职责。即使模型忽略了它,服务依然会拒绝第四条提案。服务提供腰带,prompt 提供背带——主次绝不能颠倒。
还有两个较小的设计决定补全了这套模式,它们都关乎同一件事:事后仍能清楚地看出人作了什么判断。
接受也可以包含修改。用户接受一条提案时,可以修改措辞后再接受。系统会同时保存原始文本和最终实际接受的文本,这样,Agent 提出的建议与人愿意认可的内容之间的差距,就始终可以衡量。如果系统在提案被接受的那一刻就覆盖原始建议,就等于丢掉了衡量建议质量的唯一信号。
决定只能追加。已经作出决定的提案,不能再次决定。如果答案变了,就提交一条新提案;两条带有日期、彼此不同的记录,共同构成历史。审批日志是一份判断记录,而一份可以悄悄修改的判断记录,算不上记录。这与整个系统底层事件日志的设计思路一致:只追加,绝不重写。因此,当你在凌晨三点需要弄清系统究竟做了什么、何时做的,就可以重放并审计它的完整运行历史。
这种做法不是没有成本,也不是在所有情况下都正确。
它把工作从 prompt 转移到了有类型约束的服务代码中,因此修改起来更慢:你要交付的是一个 Java 方法和一次数据库迁移,而不是改一句话。对于一个用完即弃的 Agent,这种额外成本并不划算;对于玩具项目,prompt 层面的一句“请不要这样做”,作为控制手段就够了。当 Agent 犯错确实会造成损失,而且系统准备长期运行时,结构性方案才值得投入——也就是生产环境、企业内部,以及任何能够发送内容、花钱或让外界看到结果的系统。
它还要求你真正列出所有危险操作,并为每一项明确划定权限等级。这需要做功课,而这恰恰就是关键的功课:“我们的哪些工具会影响外部世界,其中哪些工具目前握在 Agent 手里?”大多数正在交付 Agent 的团队,都没有把这个问题的答案写下来。工具层方案的价值在于,它迫使你把答案写下来,因为一个工具要么存在,要么不存在。
如果你只记住一件事,那就是:Agent 的真实权限,是它能够调用的那组工具,而不是你为它写下的那组指令。请审计前一份清单。后一份清单只是你对自身意图的说明,而你的意图不是安全边界。
本文是我基于真实系统撰写的系列文章之一。如果你的团队正在把 Agent 引入一个错误操作会造成实际后果的环境——企业、受监管环境或本地部署环境——同样可以采用这套结构性方法。我也提供这方面的咨询服务,具体包括 MCP 工具设计、分级权限审批,以及 Java/Spring 环境中可审计的 Agent 运行时。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。