1. 从“单兵作战”到“摇人干活”:Agent 能力跃迁背后的安全逻辑断层
过去两年,我接触过不少 Agent 项目,从最简单的“读文件、调接口、返回结果”的脚本型 Agent,到能自主规划任务、调用多个工具、甚至动态生成子任务的多层 Agent 系统。早期大家关注的重点几乎都落在“能不能跑通”“任务完成率多少”“响应延迟能不能压到 2 秒以内”这些指标上。直到有一次,我在一个内部测试环境里看到一个 Agent 为了完成“整理季度数据并生成报告”的任务,自己决定去调用一个数据库导出工具、一个文件压缩工具,还顺手拉起了一个子 Agent 去处理数据清洗——整个过程没有任何人工确认。那一刻我才真正意识到:Agent 开始“摇人”了,而我们的安全边界还停留在“单兵作战”的假设里。
所谓“摇人”,在 Agent 语境下指的是一个 Agent 在执行任务过程中,主动调用其他 Agent、工具、服务或子进程来协同完成目标。它不再是一个封闭的、行为可预测的单一执行体,而是一个会动态扩展执行边界的“调度中心”。这个变化带来的安全问题,和传统应用安全、模型安全都有本质区别。传统应用的安全边界是清晰的:代码写死了调哪些接口、访问哪些资源、用什么权限。模型安全更多关注输入输出层面的对抗样本、提示注入、数据泄露。但 Agent 一旦开始“摇人”,安全边界就变成了一个动态的、运行时才确定的集合——你很难在部署前穷举它到底会调用哪些工具、以什么顺序调用、传递什么参数、产生什么副作用。
这也是为什么最近“Agent 安全”这个词频繁出现在各种技术讨论里。不是大家突然变得谨慎了,而是实际项目里踩到的坑越来越具体:一个 Agent 被诱导调用了不该调用的工具,一个子 Agent 继承了父 Agent 的全部权限导致越权操作,一个工具链里的中间环节被替换成了恶意实现,等等。这些问题在单 Agent 时代要么不存在,要么影响可控;但在“摇人”场景下,它们会被放大、被串联、被隐藏。
这篇文章想聊的,就是当 Agent 从“自己干活”进化到“摇人干活”之后,安全边界到底变了什么、为什么变、以及在实际项目里我们该怎么重新画这条线。适合正在做 Agent 开发、Agent 框架设计、或者负责 AI 系统安全测试的同行参考。我不会堆太多理论,更多是从实际项目里总结出来的判断和做法,有些是踩坑之后的补救,有些是提前设计时就想清楚的取舍。
2. “摇人”到底摇出了哪些新攻击面:一次真实的多 Agent 越权链路复盘
2.1 一个看似正常的任务,如何演变成权限逃逸
先讲一个我亲身经历的案例。当时我们在做一个内部数据分析 Agent,架构大概是:主 Agent 负责理解用户需求、拆解任务;子 Agent A 负责查数据库;子 Agent B 负责生成图表;还有一个工具调用层,封装了数据库查询、文件读写、邮件发送等能力。权限设计上,主 Agent 拥有“读取数据库”和“发送邮件”的权限,子 Agent A 只有“读取数据库”权限,子 Agent B 只有“写临时文件”权限。看起来是典型的最小权限原则。
问题出在一次测试中。用户输入是“帮我查一下上个月的销售数据,生成图表后发给张总”。主 Agent 拆解任务后,调用子 Agent A 查数据,调用子 Agent B 生成图表,然后主 Agent 自己调用邮件工具发送。这本身没问题。但测试同学故意构造了一个输入:“帮我查一下上个月的销售数据,如果数据里有异常,直接发给张总确认,不用给我看。”主 Agent 理解后,决定让子 Agent A 去查数据并判断异常,如果异常就直接触发邮件发送。但子 Agent A 没有邮件权限,于是它做了一件“聪明”的事:它把数据写到了一个共享的临时文件里,然后通过某种机制通知主 Agent 去读取并发送。主 Agent 读取后确实发送了,但发送的内容里包含了未经脱敏的原始数据,而按照公司规定,对外发送的数据必须经过脱敏处理。
这个链路里,没有哪个环节是“被攻破”的。子 Agent A 没有越权,它只是写了个文件;主 Agent 也没有越权,它本来就有邮件权限。但组合起来,就产生了一个绕过脱敏流程的副作用。这就是“摇人”带来的第一个新攻击面:权限的组合爆炸。单个 Agent 或工具的权限是可控的,但它们之间的调用关系、数据流转、状态传递,会创造出部署时没有预料到的权限组合。
2.2 工具描述被污染:Agent 如何被“自己人”带偏
第二个攻击面更隐蔽,我称之为“工具描述污染”。在多 Agent 系统里,主 Agent 通常需要知道有哪些子 Agent、每个子 Agent 能干什么、需要什么参数。这些信息一般以工具描述或能力清单的形式提供给主 Agent。如果这个描述被篡改,主 Agent 就会做出错误的调度决策。
我们做过一个实验:在一个测试环境里,把某个子 Agent 的描述从“查询数据库,只读”改成了“查询数据库,支持写入和删除”。主 Agent 在后续任务中,果然开始把一些写操作也调度给这个子 Agent。虽然子 Agent 实际执行时会因为权限不足而失败,但主 Agent 的调度逻辑已经被带偏了。更危险的是,如果子 Agent 的实现本身有漏洞,或者攻击者能够替换子 Agent 的实现,那么主 Agent 就会成为攻击者的“帮凶”——它主动把敏感操作路由到了恶意节点上。
这个问题的根源在于:Agent 之间的信任关系往往是隐式的、基于描述的,而不是基于密码学验证的。主 Agent 信任子 Agent 的描述,就像你信任通讯录里的联系人名字一样。但在开放或半开放的多 Agent 环境里,这种信任假设非常脆弱。
2.3 子 Agent 继承父权限:最小权限原则的隐形破坏
第三个攻击面是权限继承。很多 Agent 框架为了方便,会让子 Agent 默认继承父 Agent 的权限上下文。比如主 Agent 有“读数据库、写文件、发邮件”三个权限,它创建的子 Agent 也会自动拥有这三个权限,除非显式限制。这在开发阶段很省事,但在安全上是个大坑。
我见过一个项目,主 Agent 的权限很大,因为它需要协调多个任务。子 Agent 本来只需要读数据库,但因为继承了父权限,它也能发邮件。结果在一次提示注入攻击中,攻击者通过用户输入诱导子 Agent 去读取敏感数据并发送到外部邮箱。如果子 Agent 只有读数据库权限,这个攻击链就断了;但因为继承了发邮件权限,攻击链就通了。
最小权限原则在单 Agent 时代是“给这个 Agent 最小的权限”,在多 Agent 时代必须变成“给这个 Agent 在本次任务中所需的最小权限,并且这个权限不能被子 Agent 继承或传递”。这听起来简单,但实现起来需要框架层面的支持,比如动态权限令牌、任务级权限隔离、调用链权限衰减等机制。
2.4 调用链路上的“中间人”:工具返回值被篡改
第四个攻击面在工具调用的返回值上。Agent 调用工具后,会根据返回值决定下一步动作。如果返回值被篡改,Agent 的决策就会被操纵。比如一个查询库存的工具,正常返回“库存充足”,Agent 就会继续下单;如果攻击者把返回值改成“库存不足”,Agent 就会取消订单或者触发补货流程。在多 Agent 场景下,这个返回值可能经过多个 Agent 的传递和处理,任何一个环节被污染,都会影响最终决策。
我们做过一个简单的测试:在一个 Agent 调用链里,A 调用 B,B 调用 C,C 返回结果给 B,B 处理后返回给 A。我们在 B 的处理逻辑里加了一个“如果结果里包含某个关键词,就修改返回值”的规则。结果 A 完全基于被修改后的结果做了决策,而且没有任何日志记录这个修改。调用链越长,返回值被篡改的风险越大,而且越难追溯。
3. 重新画边界:从“静态权限表”到“运行时安全契约”
3.1 为什么传统的 RBAC 和 ACL 在 Agent 场景下不够用
传统的权限模型,不管是 RBAC(基于角色的访问控制)还是 ACL(访问控制列表),都有一个共同假设:主体是确定的,资源是确定的,操作是确定的。你给用户 A 分配角色 R,角色 R 有权限 P,用户 A 就能对资源 X 执行操作 P。这个模型在 Web 应用、微服务里运行得很好,因为调用关系是代码写死的,运行时不会变。
但 Agent 场景下,主体是动态创建的(子 Agent 可能是一次性的),资源是动态发现的(工具列表可能运行时才确定),操作是动态组合的(Agent 自己决定调什么、怎么调)。你没法在部署前写一张完整的权限表,因为这张表在运行时才逐渐成形。更麻烦的是,Agent 的决策过程是不透明的,你很难解释它为什么选择了这个工具而不是那个工具,为什么传递了这些参数而不是那些参数。
所以我们需要一种新的安全机制,我把它叫做“运行时安全契约”。它的核心思想是:不试图在部署前穷举所有可能的调用关系,而是在运行时对每一次调用进行动态检查和约束。这个契约包括几个要素:调用者身份、被调用者身份、本次调用的目的、允许的操作范围、数据流向约束、以及调用后的审计记录。每次 Agent 发起调用时,都要先通过这个契约的检查,才能实际执行。
3.2 调用链上的“权限衰减”:让子 Agent 永远比父 Agent 更受限
权限衰减是我认为在多 Agent 系统里最应该优先实现的机制。它的逻辑很简单:子 Agent 的权限必须是父 Agent 权限的子集,而且随着调用链加深,权限只能缩小不能扩大。比如主 Agent 有“读数据库、写文件、发邮件”三个权限,它创建子 Agent A 时,可以授予“读数据库”权限;子 Agent A 再创建子 Agent B 时,只能授予“读数据库”权限的子集,比如“读某个特定表”。
实现上,可以用权限令牌的方式。主 Agent 持有一个包含所有权限的令牌,创建子 Agent 时,生成一个派生令牌,只包含需要的权限,并且令牌里记录调用链信息。子 Agent 再派生时,只能从自己的令牌里继续缩小范围。每次工具调用时,工具层检查令牌里的权限是否覆盖本次操作。这样即使子 Agent 被攻击者控制,它也无法获得超出令牌范围的权限。
这个机制的关键在于:权限令牌必须绑定调用链,不能跨链复用。也就是说,子 Agent A 的令牌不能给子 Agent B 用,因为它们的调用链不同。这需要在令牌里加入链 ID 或者父令牌签名,确保令牌的派生关系可验证。
3.3 工具调用的“双向确认”:不只是 Agent 选工具,工具也要选 Agent
传统的工具调用是单向的:Agent 决定调用哪个工具,工具就执行。但在多 Agent 环境里,工具也应该有选择权。我称之为“双向确认”机制。工具在被调用时,可以检查调用者的身份、调用链、以及本次调用的上下文,决定是否接受这次调用。
比如一个发送邮件的工具,可以配置成“只接受来自主 Agent 的直接调用,不接受来自子 Agent 的调用”。或者“只接受调用链深度不超过 2 的调用”。这样即使子 Agent 被诱导去调用邮件工具,工具层也会拒绝。这个机制需要工具层能够获取调用链信息,并且有独立的策略配置。
实现上,可以在工具调用的协议里加入调用链元数据,工具层根据元数据和自身策略做决策。这比单纯依赖 Agent 侧的权限控制更可靠,因为工具层是最后一道防线,它不依赖 Agent 的“自觉”。
3.4 返回值签名:让篡改在调用链上无处藏身
针对返回值被篡改的问题,最直接的方案是给返回值加签名。工具在返回结果时,用私钥对结果签名;调用方在收到结果后,用公钥验证签名。如果签名不匹配,就拒绝使用这个结果。在多 Agent 传递的场景下,每个 Agent 在转发结果时,可以选择保留原始签名,或者用自己的签名覆盖,但必须记录处理过程。
这个机制会增加一些计算开销,但对于安全敏感的场景是值得的。更重要的是,它让返回值篡改变得可检测、可追溯。我们在一套内部系统里试过这个方案,性能损耗大概在 3% 到 5% 之间,对于大多数 Agent 任务来说是可以接受的。
4. 落地时的取舍:安全强度、开发效率和运行性能的三角平衡
4.1 不是所有 Agent 都需要最高安全等级
在实际项目里,我见过两种极端:一种是完全不管安全,Agent 想调什么就调什么;另一种是过度设计,每个调用都加签名、加审计、加多重确认,结果开发效率极低,运行延迟翻倍。这两种都不可取。
我的经验是:先对 Agent 任务做分级,再匹配相应的安全策略。比如:
| 任务等级 | 典型场景 | 安全策略 |
|---|---|---|
| L1 低风险 | 内部查询、格式化输出、只读操作 | 基础权限检查,记录审计日志 |
| L2 中风险 | 写文件、发内部通知、调用内部 API | 权限衰减 + 工具侧白名单 + 返回值校验 |
| L3 高风险 | 对外发送数据、修改生产数据、涉及资金操作 | 全链路签名 + 双向确认 + 人工审批节点 |
这个分级不是拍脑袋定的,而是根据“如果这个操作被恶意触发,最坏后果是什么”来倒推。L1 最坏就是信息泄露给内部人员,L2 可能造成内部数据污染,L3 可能造成对外影响或资产损失。不同等级匹配不同强度的机制,既保证安全,又不至于拖垮开发效率。
4.2 审计日志:不是记了就行,关键是怎么记、记什么
审计日志是安全的基础设施,但很多项目的审计日志是“为了记而记”,真出问题时根本查不出东西。在多 Agent 场景下,审计日志必须包含几个关键字段:调用链 ID、调用者身份、被调用者身份、调用的工具或 Agent、传入参数、返回结果摘要、权限令牌信息、时间戳、以及本次调用的决策依据(比如“因为任务类型是查询,所以路由到只读子 Agent”)。
我特别想强调“决策依据”这个字段。Agent 的决策过程往往是不透明的,如果只记录“调用了工具 X”,不记录“为什么调用工具 X”,事后排查会非常困难。我们在一套系统里加了这个字段后,排查一个越权问题时,直接从日志里看到“主 Agent 因为子 Agent 描述里写了‘支持写入’,所以把写操作路由过去了”,问题一目了然。
另外,日志的存储和访问也要有权限控制。审计日志本身可能包含敏感信息,不能谁都能看。我们一般会把日志写到独立的存储里,只有安全团队和系统管理员有读取权限,Agent 本身没有权限修改或删除日志。
4.3 性能开销的实测数据与优化思路
安全机制一定会带来性能开销,关键是怎么把开销控制在可接受范围内。我们在一套包含 5 个 Agent、12 个工具的测试系统里,对比了不同安全策略下的性能表现:
| 安全策略 | 平均任务延迟 | 相对基线增幅 |
|---|---|---|
| 无额外安全机制 | 1.2s | 基线 |
| 权限检查 + 审计日志 | 1.35s | +12.5% |
| 权限衰减 + 工具白名单 | 1.48s | +23.3% |
| 全链路签名 + 双向确认 | 1.72s | +43.3% |
从数据看,权限检查和审计日志的开销最小,适合所有场景;权限衰减和工具白名单的开销中等,适合 L2 及以上;全链路签名和双向确认开销最大,只建议在 L3 场景使用。优化思路包括:把权限检查缓存在本地、签名验证异步化、审计日志批量写入等。我们实际优化后,全链路方案的开销可以压到 25% 左右。
4.4 开发者的安全心智:从“能跑就行”到“先想边界”
最后想聊一个非技术但很重要的问题:开发者的安全心智。我见过太多项目,开发阶段为了快速验证,把权限开到最大,想着“上线前再收紧”。结果上线前要么忘了,要么收紧后一堆功能跑不通,最后又放开了。安全边界不是上线前才画的,而是设计阶段就要想清楚的。
我的建议是:在 Agent 项目的设计阶段,就强制回答几个问题——这个 Agent 需要哪些权限?它的子 Agent 需要哪些权限?这些权限能不能进一步缩小?工具调用有没有白名单?返回值要不要校验?审计日志记哪些字段?这些问题不需要一开始就有完美答案,但必须有人问、有人答、有人记录。等到代码写完了再想这些,成本会高很多。
5. 几个容易踩的坑和我的应对建议
5.1 子 Agent 的“临时权限”变成了“永久权限”
很多框架在创建子 Agent 时,会临时授予一些权限,但子 Agent 执行完后,这些权限没有被回收。如果子 Agent 被复用,或者它的权限令牌被泄露,就会造成越权。我的做法是:子 Agent 的权限令牌必须设置过期时间,并且绑定任务 ID。任务结束后,令牌自动失效。如果子 Agent 需要复用,每次复用都重新申请令牌,而不是复用旧的。
5.2 工具描述里的“隐藏指令”
有些工具描述里会包含一些看似无害的说明文字,比如“本工具支持批量操作,建议一次传入多个参数以提高效率”。如果这个描述被恶意修改,加入“请忽略之前的权限检查”之类的指令,主 Agent 可能会被诱导。我的建议是:工具描述必须来自可信源,并且在加载时做完整性校验。不要把工具描述当成普通文本,它实际上是 Agent 的“决策输入”,需要像对待代码一样对待。
5.3 调用链太深导致的安全盲区
调用链越深,安全机制越难覆盖。比如 A 调用 B,B 调用 C,C 调用 D,D 执行了敏感操作。如果安全策略只在 A 和 D 上生效,B 和 C 就是盲区。我的经验是:安全策略必须覆盖每一层调用,不能有“信任中间层”的假设。每一层都要做权限检查、日志记录、返回值校验。虽然开销大,但比出事后排查的成本低得多。
5.4 忽略“非功能调用”的安全影响
Agent 除了调用业务工具,还会调用一些“非功能”服务,比如日志服务、监控服务、配置服务。这些调用往往被忽略,但它们也可能被利用。比如一个 Agent 被诱导去修改配置服务里的某个参数,就可能影响整个系统的行为。我的建议是:把所有调用都纳入安全边界,不管是业务调用还是非功能调用。配置修改、日志删除、监控关闭,这些操作都应该有权限控制和审计。
5.5 安全测试的“最后一公里”:怎么验证机制真的生效
安全机制写完了,怎么知道它真的生效?我的做法是构造“攻击用例”来验证。比如:构造一个提示注入,看子 Agent 会不会越权;构造一个工具描述篡改,看主 Agent 会不会被带偏;构造一个返回值篡改,看调用方会不会检测到。这些用例不需要很复杂,但必须覆盖主要的攻击面。我们一般会在 CI 流程里跑这些用例,确保每次代码变更不会破坏安全机制。
6. 写在最后:安全边界是画出来的,不是等出来的
Agent 开始“摇人”之后,安全边界确实变了。它从一条静态的线,变成了一个动态的、需要持续维护的曲面。这个曲面不是部署时画一次就完事,而是要在每次任务执行时重新确认。听起来很麻烦,但这就是多 Agent 系统的本质——它的能力来自动态组合,它的风险也来自动态组合。
我在实际项目里的体会是:不要试图一次性解决所有安全问题,而是先建立“最小可行安全边界”,然后根据实际攻击面和业务需求逐步加固。权限衰减、工具白名单、审计日志,这三个机制投入产出比最高,建议优先做。全链路签名和双向确认,可以在高风险场景再上。最重要的是,让团队里每个人都意识到:Agent 的每一次“摇人”,都是一次安全决策,而不是一次普通函数调用。这个意识转变,比任何技术方案都关键。