很多团队给 AI Agent 补安全,第一反应是多写几句提示词:谨慎一点、别乱调用、出手前三思。问题是,真正会把系统搞出事故的,通常不是“它说错了”,而是“它做对了调用、做错了对象”。
一个会发邮件、能写数据库、还能调第三方 API 的 Agent,白天看着像自动化,晚上看着像定时炸弹。最吓人的地方不是它答错话,而是它真的会动手。一旦权限给大了、日志没留、执行不能撤回,它犯的就不是“聊天错误”,而是业务事故。
前阵子帮老大整理一套 Agent 调用链路时,我原本以为最大的问题会出在模型判断不稳。结果翻日志翻到后面,发现更要命的是另一个事:工具层几乎没边界。模型其实没有“胡来”,它只是很认真地执行了一个本来就不该开放的动作。那一刻我才更确定,别先追求 Agent 更聪明,先把它改成“安全默认”。
AI Agent 的真正风险是什么?
不是模型幻觉本身,而是“幻觉 + 执行权”绑在一起。
你让它总结邮件,错了顶多尴尬;你让它直接发邮件、改用户状态、调用支付或 CRM API,错一次就是实打实的脏数据、误触发,甚至合规问题。Simon Willison 那篇写 Git 和 coding agents 的文章,提醒的其实也是同一件事:Agent 一旦开始改东西,你就必须默认它会改错,然后提前准备回放和回滚。
我帮老大整理这类系统时,最常见的坑不是模型差,而是工程默认值太松:
- 一个
tools=[...]把所有能力全开 - 工具参数只做“能不能跑”,不做“该不该跑”
- 执行结果只打一行文本日志,事后根本追不回现场
- 高风险操作没有演练态,模型一决定就真提交
这几个坑叠在一起,Agent 就会从“助手”变成“高权限实习生”。干活很快,出事也很快。
有次我在 Discord 里看到读者问:Agent 明明测得好好的,怎么一上生产就让人不敢开自动执行?这个问题问得很准。因为测试环境里你看到的是“它会不会做”,生产环境里你真正该问的是“它能做到哪一步、做错了我能不能拦住”。这两件事,差别很大。
怎么把“允许做什么/禁止做什么”写死在代码里?
答案很直接:不要相信提示词里的“请谨慎”,要相信代码里的白名单、校验器和隔离层。
你可以把 Agent 想成一个新来的运营同事。你不会第一天就给他老板邮箱、生产库写权限和支付后台。Agent 也一样,先给它一间能活动的小房间,而不是整栋楼的钥匙。
1)先按动作拆权限,不按“工具名”拆
很多人做工具白名单,停在“可以用 send_email,不可以用 shell”。这还不够。
真正该收口的是动作级别的 scope:
| 工具 | 允许的动作 | 禁止的动作 | 代码里怎么卡 |
|---|---|---|---|
| 邮件工具 | 发给测试名单、草稿箱、固定模板 | 群发真实客户、改收件人域名 | 收件人白名单 + 模板 ID 白名单 |
| 数据库工具 | 查订单、写临时表、改 sandbox 数据 | 直接写生产主表、删历史记录 | 只暴露受限 SQL/存储过程 |
| 第三方 API | 查状态、创建模拟请求 | 退款、扣费、正式 webhook 回调 | 区分 sandbox key 和 production key |
重点不是“有没有数据库工具”,而是“数据库工具能写哪几张表、能不能 delete、能不能跨租户”。
2)参数校验别偷懒
最小权限不只是“能不能调用”,还包括“参数进来后像不像人话”。
比如发邮件工具,至少校验这几层:
- 收件人域名是否在白名单
- 标题和正文是否命中特定模板
- 单次发送数量是否超阈值
- 是否带有必填的工单号、trace ID、审批人信息
数据库工具也一样。不要让 Agent 直接生成任意 SQL 去跑。更稳的做法是:只暴露几个动作函数,比如 create_refund_request()、mark_ticket_pending(),而不是裸给它一个“可执行 SQL”。
3)资源隔离要分环境,不要只分配置
很多事故不是模型想搞事,是你把测试和生产接成了同一套入口。
最低限度,你应该拆成三层:
dev:本地开发,允许更高观察权限staging:联调/演练环境,真实流程但假数据或沙箱 APIprod:生产环境,只开放最窄动作集
我自己现在会对“只靠配置隔离”的方案天然多留个心眼。它不是一定不行,但太脆,尤其是多人协作、临时脚本多、环境变量到处飘的时候。你可能今天还觉得没问题,明天排查事故时就会发现,真正缺的不是文档,而是代码里那道硬闸门。
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。 这样即使模型判断失误,损失也会被锁在可见、可拦、可回滚的边界里。
两段式执行,建议固定成这个流程
-
Plan / dry-run
Agent 只生成执行计划、参数预览、影响范围,不做真实写入。 -
Review
人审或规则审。检查收件人、变更条数、目标环境、金额阈值、租户边界。 -
Commit
拿着已批准的计划 ID,才允许真正调用高风险工具。 -
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. 所有写操作必须有回滚或补偿方案
FAQ
Q: Agent 安全是不是会把效率拖死?
A: 会增加一点流程,但比起一次误发邮件、误写生产库,这点摩擦很值。安全默认的目标不是让 Agent 变慢,而是让它只在对的轨道上快。
Q: 我只有一个内部小工具,也要做这么全吗?
A: 不用一步到位,但最少要有默认 deny、trace ID、dry-run。内部工具最容易因为”先凑合能跑”而拿到过大的权限。
Q: 可回滚是不是等于一定能撤销?
A: 不是。有些外部 API、真实邮件一旦发出,技术上只能补偿,不能真正撤回。所以回滚设计越早做,代价越低。
如果你这周只能改一处,我会先改高风险工具的执行方式:别让它直接 commit,先让它吐出一份 dry-run 计划。等你第一次靠这份计划拦下一次“差点就发出去”的操作,你大概就会重新定义自己心里那句“已经能用了”。