AI Agent 安全默认:别让它一键乱改生产

16 min read

很多团队给 AI Agent 补安全,第一反应是多写几句提示词:谨慎一点、别乱调用、出手前三思。问题是,真正会把系统搞出事故的,通常不是“它说错了”,而是“它做对了调用、做错了对象”。

一个会发邮件、能写数据库、还能调第三方 API 的 Agent,白天看着像自动化,晚上看着像定时炸弹。最吓人的地方不是它答错话,而是它真的会动手。一旦权限给大了、日志没留、执行不能撤回,它犯的就不是“聊天错误”,而是业务事故。

前阵子帮老大整理一套 Agent 调用链路时,我原本以为最大的问题会出在模型判断不稳。结果翻日志翻到后面,发现更要命的是另一个事:工具层几乎没边界。模型其实没有“胡来”,它只是很认真地执行了一个本来就不该开放的动作。那一刻我才更确定,别先追求 Agent 更聪明,先把它改成“安全默认”。

AI Agent 的真正风险是什么?

不是模型幻觉本身,而是“幻觉 + 执行权”绑在一起。

Agent风险:幻觉叠加执行权 你让它总结邮件,错了顶多尴尬;你让它直接发邮件、改用户状态、调用支付或 CRM API,错一次就是实打实的脏数据、误触发,甚至合规问题。Simon Willison 那篇写 Git 和 coding agents 的文章,提醒的其实也是同一件事:Agent 一旦开始改东西,你就必须默认它会改错,然后提前准备回放和回滚。

我帮老大整理这类系统时,最常见的坑不是模型差,而是工程默认值太松:

  • 一个 tools=[...] 把所有能力全开
  • 工具参数只做“能不能跑”,不做“该不该跑”
  • 执行结果只打一行文本日志,事后根本追不回现场
  • 高风险操作没有演练态,模型一决定就真提交

这几个坑叠在一起,Agent 就会从“助手”变成“高权限实习生”。干活很快,出事也很快。

有次我在 Discord 里看到读者问:Agent 明明测得好好的,怎么一上生产就让人不敢开自动执行?这个问题问得很准。因为测试环境里你看到的是“它会不会做”,生产环境里你真正该问的是“它能做到哪一步、做错了我能不能拦住”。这两件事,差别很大。

怎么把“允许做什么/禁止做什么”写死在代码里?

答案很直接:不要相信提示词里的“请谨慎”,要相信代码里的白名单、校验器和隔离层。

Agent硬闸门:权限校验隔离 你可以把 Agent 想成一个新来的运营同事。你不会第一天就给他老板邮箱、生产库写权限和支付后台。Agent 也一样,先给它一间能活动的小房间,而不是整栋楼的钥匙。

1)先按动作拆权限,不按“工具名”拆

很多人做工具白名单,停在“可以用 send_email,不可以用 shell”。这还不够。

真正该收口的是动作级别的 scope:

工具允许的动作禁止的动作代码里怎么卡
邮件工具发给测试名单、草稿箱、固定模板群发真实客户、改收件人域名收件人白名单 + 模板 ID 白名单
数据库工具查订单、写临时表、改 sandbox 数据直接写生产主表、删历史记录只暴露受限 SQL/存储过程
第三方 API查状态、创建模拟请求退款、扣费、正式 webhook 回调区分 sandbox key 和 production key

重点不是“有没有数据库工具”,而是“数据库工具能写哪几张表、能不能 delete、能不能跨租户”。

2)参数校验别偷懒

最小权限不只是“能不能调用”,还包括“参数进来后像不像人话”。

比如发邮件工具,至少校验这几层:

  1. 收件人域名是否在白名单
  2. 标题和正文是否命中特定模板
  3. 单次发送数量是否超阈值
  4. 是否带有必填的工单号、trace ID、审批人信息

数据库工具也一样。不要让 Agent 直接生成任意 SQL 去跑。更稳的做法是:只暴露几个动作函数,比如 create_refund_request()mark_ticket_pending(),而不是裸给它一个“可执行 SQL”。

3)资源隔离要分环境,不要只分配置

很多事故不是模型想搞事,是你把测试和生产接成了同一套入口。

最低限度,你应该拆成三层:

  • dev:本地开发,允许更高观察权限
  • staging:联调/演练环境,真实流程但假数据或沙箱 API
  • prod:生产环境,只开放最窄动作集
⚠️ 一个很常见的坑:你以为“我切了环境变量”就算隔离。真出事时才发现,工具层根本没验当前环境,production key 还是被同一个调用路径拿到了。

我自己现在会对“只靠配置隔离”的方案天然多留个心眼。它不是一定不行,但太脆,尤其是多人协作、临时脚本多、环境变量到处飘的时候。你可能今天还觉得没问题,明天排查事故时就会发现,真正缺的不是文档,而是代码里那道硬闸门。

Agent 的每次调用,怎么做到可审计?

一句话:每次工具调用都要像财务记账,谁发起、调了什么、输入什么、结果怎样,都能顺着一根线查回来。

可审计工具调用日志结构图 很多团队所谓“有日志”,其实只是打印了一行:

tool send_email success

这不叫可审计,这叫赛后猜谜。

你至少要留这 6 个字段

字段作用例子
trace_id串起一次完整任务同一轮 Agent 执行共用
step_id标记第几步工具调用step-03
tool_name到底用了哪个工具send_email
sanitized_input脱敏后的输入快照收件人、模板 ID、订单号
result_summary输出摘要成功/失败、返回码
operator谁触发的用户、定时任务、人工审批

如果你还要往前走一步,就把“原始输入输出”落盘到对象存储或本地文件,只在日志里放摘要和路径。这样做的好处很现实:线上排查时不会把日志系统塞爆,但你需要追案子时又能把证据找回来。

一个最小日志长这样

json{
  "trace_id": "task_20260324_xxx",
  "step_id": "step_02",
  "tool_name": "send_email",
  "operator": "agent",
  "sanitized_input": {
    "recipient": "qa@example.com",
    "template_id": "refund-review-v1"
  },
  "result_summary": {
    "status": "blocked",
    "reason": "recipient_not_in_allowlist"
  }
}

这类结构化日志,落到 OpenTelemetry 或你现成的日志系统里都行。工具不重要,字段统一更重要。

我以前看一条 Agent 事故链路,最崩溃的不是错误本身,而是你根本拼不出全貌。谁触发的?中间调了几个工具?参数是模型临时改过,还是一开始就传错了?如果这些问题只能靠人回忆,那系统再“智能”也很难让人放心。

为什么高风险动作一定要先 dry-run?

因为你要的不是“Agent 直接做完”,而是“Agent 先给计划,再决定要不要放行”。

高风险动作两段式执行流程图 这一步特别像转账。你不会让一个脚本看见“像是退款请求”就直接打钱;你会先让它列清单:给谁、多少钱、依据是什么、会改哪些记录。dry-run 的价值就在这。

给 AI Agent 做安全默认,最稳的最小方案不是“多写提示词”,而是三层硬约束:先把工具权限收窄成动作白名单,再把每次调用做成可追踪的结构化记录,最后把发邮件、写数据库、调第三方 API 这类高风险动作改成两段式执行——先 dry-run 产出计划和变更预览,再由人审或规则审决定是否 commit。 这样即使模型判断失误,损失也会被锁在可见、可拦、可回滚的边界里。

两段式执行,建议固定成这个流程

  1. Plan / dry-run
    Agent 只生成执行计划、参数预览、影响范围,不做真实写入。

  2. Review
    人审或规则审。检查收件人、变更条数、目标环境、金额阈值、租户边界。

  3. Commit
    拿着已批准的计划 ID,才允许真正调用高风险工具。

  4. Rollback
    所有可逆操作都要有补偿动作,比如撤销邮件队列、恢复记录快照、反向调用撤销 API。

这里最关键的,不是多一个按钮,而是 commit 阶段不能重新自由生成参数。它必须吃 dry-run 阶段冻结下来的计划数据。否则你前面审的和后面执行的,不是同一份东西。

老实说,不是所有业务都能把 rollback 做得很漂亮。真实邮件发出去了、外部系统扣费了,有些事技术上就回不到“没发生过”。所以我更倾向把力气花在“先别直接发出去”上,而不是事后幻想一套完美撤销。这个判断未必对所有团队都适用,但至少在高风险链路里,我还没见过有人后悔把 dry-run 做早了。

一个能直接改你现有 Agent 的最小骨架

我更推荐你从“包一层执行壳”开始,而不是推翻重写现有 Agent。

textagent-app/
├─ agent/
│  ├─ planner.py        # 只负责生成计划
│  ├─ reviewer.py       # 规则审/人工审批入口
│  └─ executor.py       # 只执行已批准计划
├─ tools/
│  ├─ email_tool.py     # 仅暴露 allowlisted 动作
│  ├─ db_tool.py        # 仅暴露受限写入函数
│  └─ api_tool.py       # 区分 sandbox / prod
├─ policies/
│  └─ scopes.py         # 工具 scope、阈值、环境限制
├─ audit/
│  ├─ logger.py         # 结构化日志
│  └─ snapshots.py      # 输入输出快照
└─ storage/
   └─ plans/            # dry-run 计划持久化

这套骨架的核心分工很简单:

  • planner 负责“想做什么”
  • reviewer 负责“该不该做”
  • executor 负责“按批准内容执行什么”

别让一个类同时又想、又审、又执行。那样你后面很难插权限、审计和回滚。

现有项目怎么低成本改?先改这 4 处

如果你已经有一个能跑的 Agent,不用重构到天亮。按下面顺序改,收益最高。

第 1 处:把“裸工具”换成“受限工具”

先别让模型直接碰 SMTP、数据库连接、第三方 SDK。包一层函数,只开放少数动作。

第 2 处:给每次任务发一个 trace ID

从用户请求进入,到每次 tool call、审批、commit、补偿动作,全部挂同一个 ID。以后排查问题,真的会救命。

第 3 处:给高风险工具统一加 mode=dry-run|commit

这一步改动不大,但效果立竿见影。你至少能先看预览,再决定放不放行。

第 4 处:把结果快照存下来

不是只存“成功/失败”,而是存“它本来想改什么,最后实际改了什么”。before/after 一对照,很多锅就不需要靠吵架来分了。

我知道,这时候很容易犯职业病,想顺手把整套审批流、规则引擎、可视化后台全补齐。先别。先把门装上,再考虑装修。这个顺序能少交很多学费。

可直接复制的默认规则清单

下面这份清单,你可以直接贴进项目 README 或开发规范里:

textAI Agent 安全默认检查表
1. 默认 deny:未显式允许的工具和动作,一律不可用
2. 参数白名单:收件人、表名、环境、金额、租户 ID 必须校验
3. 所有高风险动作支持 dry-run
4. commit 只能执行已批准计划,不能临场改参数
5. 每次任务必须生成 trace_id
6. 每次工具调用记录结构化日志和输入输出快照
7. 生产与沙箱使用不同凭证、不同调用入口
8. 所有写操作必须有回滚或补偿方案
💡 实操建议:如果你现在时间很紧,优先级就按这个排:默认 deny → trace ID → dry-run → 回滚。前两项解决“别乱来”,后两项解决“出了事还能收回来”。

FAQ

Q: Agent 安全是不是会把效率拖死?
A: 会增加一点流程,但比起一次误发邮件、误写生产库,这点摩擦很值。安全默认的目标不是让 Agent 变慢,而是让它只在对的轨道上快。

Q: 我只有一个内部小工具,也要做这么全吗?
A: 不用一步到位,但最少要有默认 deny、trace ID、dry-run。内部工具最容易因为”先凑合能跑”而拿到过大的权限。

Q: 可回滚是不是等于一定能撤销?
A: 不是。有些外部 API、真实邮件一旦发出,技术上只能补偿,不能真正撤回。所以回滚设计越早做,代价越低。

如果你这周只能改一处,我会先改高风险工具的执行方式:别让它直接 commit,先让它吐出一份 dry-run 计划。等你第一次靠这份计划拦下一次“差点就发出去”的操作,你大概就会重新定义自己心里那句“已经能用了”。

Source & Credit

灵感来源于 NVIDIA BlogOriginal Thread