给编码 Agent 接上工具的那一刻,它就从"会说话"变成了"会动手"——能删文件、能调外部 API、能往群里发消息。能力越大,越需要一层清醒的安全设计:哪些动作可以放手让它自己跑,哪些必须停下来等人点头,以及它到底在一个多隔离的环境里执行。本文讨论编码 Agent 工程里的三件事:工具门控、人工审批、沙箱隔离。
不是所有工具都该自动执行
先把工具按"出错后能不能撤回"分两类:
- 可逆/只读:读文件、跑测试、查日志。错了重来即可,代价低。
- 不可逆/外发:删除、写生产配置、调外部 API、发消息、转账。一旦执行就泼出去了,收不回。
服务端工具循环里,对每个工具可以配一条权限策略:
always_allow:自动执行,不打断。适合可逆/只读工具。always_ask:执行前暂停并等待人工确认,收到批准事件后才继续。适合不可逆/外发动作。
1 | TOOL_POLICY = { |
注意一个边界:这套权限策略是针对服务端工具的。对于纯客户端自定义工具——也就是执行逻辑本来就握在你自己 orchestrator 手里的那种——门控由你的代码直接决定,不需要走这套 allow/ask 机制,因为你本来就掌控它的执行。
审批:把人放回环路的正确姿势
always_ask 的语义是:模型在这一轮产生了工具调用,但循环在执行前挂起,向上抛出一个"待审批"信号,把决定权交回人。人批准后,循环带着批准事件续跑,模型才看到工具结果继续推进。
伪代码层面,一个带审批的 manual loop 大致是这样:
1 | while True: |
两个工程细节值得强调:
- 拒绝要回灌成有用信息。别让审批被拒后静默失败。把"用户拒绝了该操作"作为
is_error: true的工具结果回灌,模型才会理解此路不通、改走别的方案,而不是原地重试。 - 审批粒度别太碎。如果每读一个文件都弹窗,人很快就会无脑点"同意",门控形同虚设。把
always_ask留给真正不可逆/外发的动作,可逆操作放行。
沙箱:假设模型会做错事
门控管的是"要不要执行",沙箱管的是"执行时能造成多大破坏"。核心心态是:假设模型可能被诱导、可能出错,把爆炸半径限制住。
代码执行类工具尤其要隔离:
- 隔离容器:代码跑在一次性容器里,与宿主、与其他任务隔离,跑完即焚。
- 无外网:默认切断出网,杜绝"在沙箱里偷偷把数据 POST 出去"或拉取恶意依赖。需要联网的能力单独白名单。
- 最小文件系统:只挂载任务必需的目录,限制对宿主路径的可见性。
路径穿越是个高频坑。比如一个"按 URL 下载到工作目录"的工具,模型若把文件名拼成 ../../etc/cron.d/x,就能写出工作目录。清洗文件名是必做项:
1 | import os |
密钥:永远不要让它进上下文
最后是凭据。一条铁律:密钥不要进系统提示,也不要进消息历史。原因有二:消息历史会被持久化(落进日志、落进记忆层),等于把密钥写进了多份存储;而且历史是可被 prompt injection 读取的攻击面,一段恶意输入就可能诱导模型把"上文里的那个 token"原样吐出来。
正确做法是把鉴权留在 orchestrator 侧。需要密钥的第三方调用,做成客户端自定义工具:真正的 API key 由你的编排层在执行时注入,沙箱和模型上下文里只看到占位符。
1 | # 沙箱/模型侧只见占位符 |
提示词也是安全的一部分
容易被忽略的一点:过激的系统提示会放大风险。满屏 “CRITICAL!你必须立刻调用工具完成"会让新一代模型过度触发工具,把本该谨慎的外发动作也抢着执行。把措辞改成条件式——“在确认目标文件存在后再调用删除工具”——既减少误触发,也让门控和审批更有意义。安全不只是事后拦截,也包括从一开始就别把模型逼成"先开枪后瞄准”。
小结
编码 Agent 的安全是三层纵深:用 always_allow/always_ask 做工具门控、把不可逆动作交人审批并把拒绝回灌成可恢复的错误、用无外网的隔离沙箱加路径清洗限制爆炸半径,再用客户端自定义工具把密钥牢牢留在上下文之外——能力放手之前,先把缰绳系好。