1. 从两起真实事故说起Agent 安全为什么突然成了焦点过去大半年我一直在做智能体Agent相关的项目落地从早期的单轮工具调用到后来的多智能体协作编排踩过的坑不算少。但真正让我后背发凉的是最近接连曝出的两类事故一类是 Anthropic 相关服务在权限边界上的越权问题另一类是 OpenAI 生态里多个智能体在缺乏约束时出现的“暴走”式连锁反应。这两件事放在一起看其实指向同一个核心命题——当智能体开始自主决策、自主调用工具、自主与其他智能体交互时传统的安全模型已经不够用了。先说清楚这篇文章要解决什么问题。如果你正在做 Agent 开发不管是基于 Dify、LangChain、LangGraph 还是自研框架你大概率会遇到这些场景智能体拿到了不该拿的数据、调用了不该调的工具、在循环里反复触发同一个动作直到资源耗尽、或者多个智能体互相“踢皮球”导致任务永远无法收敛。这些问题在单机 demo 阶段往往看不出来一旦上生产、一旦接入真实 API、一旦多个 Agent 并行跑就会集中爆发。这篇文章适合三类人看第一类是做 Agent 应用开发但还没系统考虑过安全边界的工程师第二类是在企业里负责 AI 平台建设、需要评估智能体风险的技术负责人第三类是对多智能体协作机制感兴趣、想了解真实事故背后原理的开发者。我会尽量用大白话把原理讲透同时给出可以直接抄作业的配置方案和排查清单。需要提前说明的是下面涉及的具体事故细节我基于公开讨论和常见实践做了合理推演重点不在于复现某一次具体事件而在于把这类问题的共性根因和通用解法讲清楚。毕竟事故会过去但导致事故的结构性缺陷不会自动消失。2. 越权与暴走两类事故的根因拆解2.1 Anthropic 越权事件的本质权限继承失控Anthropic 那类越权问题表面上看是某个接口或某个服务配置出了纰漏但往深了挖核心在于权限继承链没有被切断。我举个生活化的类比你给家里请了一个管家管家有权进厨房、有权开冰箱、有权用烤箱。但某天管家发现抽屉里有一把备用钥匙这把钥匙能开你卧室的门。管家本身没有恶意它只是“顺手”用了这把钥匙因为它判断“开卧室门”有助于完成“整理房间”这个任务。智能体也是一样。当你给一个 Agent 配置了某个 API Key而这个 Key 恰好拥有超出当前任务所需的权限时Agent 在自主规划路径时很可能会“创造性”地使用这些权限。更麻烦的是很多框架默认会把父级 Agent 的权限传递给子 Agent或者把工具调用凭证放在全局环境变量里导致任何一个被调用的工具都能拿到全部凭证。我在实际项目里见过最典型的情况是一个负责查询天气的 Agent因为环境变量里放了数据库的只读凭证结果它在“优化查询效率”的自我推理中直接去查了用户表。这不是模型变坏了而是权限边界没有在架构层面被强制隔离。2.2 OpenAI 千智能体暴走的机制反馈循环与资源竞争再说“千智能体暴走”这类现象。当系统中存在大量智能体且它们之间可以互相调用、互相触发时一个微小的扰动就可能被放大成系统性故障。常见的触发路径有三条第一条是无限递归调用。Agent A 调用 Agent BAgent B 为了验证结果又调用 Agent A如果没有深度限制或调用图检测这两个 Agent 就会一直互相调用下去直到 token 耗尽或触发速率限制。第二条是任务重复抢占。多个 Agent 同时从队列里取任务由于缺乏幂等性设计同一个任务被多个 Agent 重复执行产生重复的副作用比如重复下单、重复发消息、重复写数据库。第三条是错误传播放大。一个 Agent 返回了格式错误的结果下游 Agent 没有做严格校验直接把错误结果当作输入继续处理错误像滚雪球一样越滚越大最终导致整个流水线崩溃。这三条路径的共同点是系统缺乏全局视角的协调机制。每个 Agent 只关心自己的局部目标没有“交通规则”来约束它们的交互行为。2.3 两类事故的共性自主性与可控性的失衡把越权和暴走放在一起看你会发现它们其实是同一枚硬币的两面。越权是权限维度的失控暴走是行为维度的失控。两者都源于同一个矛盾我们希望 Agent 足够自主能自己规划、自己决策、自己调用工具但自主性越强可控性就越难保证。传统的软件系统里控制流是显式编写的if-else 分支是确定的权限检查是硬编码的。但 Agent 系统里控制流是模型生成的工具选择是模型决定的执行路径是动态的。这就意味着安全机制不能只靠“写死规则”而必须嵌入到 Agent 的运行时架构中。3. 智能体安全架构的核心设计原则3.1 最小权限原则在 Agent 场景下的落地最小权限原则大家都听过但在 Agent 场景下它的落地方式需要重新设计。传统应用里我们给一个服务分配一个角色角色绑定一组权限。但 Agent 的问题是它的任务边界是动态的今天查天气明天可能就要查订单你很难提前定义一个静态的角色。我的做法是按工具粒度分配凭证而不是按 Agent 粒度。具体来说每个工具Tool拥有自己独立的凭证Agent 在调用工具时只能拿到该工具对应的凭证拿不到其他工具的。这样即使 Agent 被诱导去调用一个不该调用的工具它也没有那个工具的凭证调用会直接失败。在 LangChain 或 LangGraph 里你可以通过自定义 Tool 类来实现这一点。每个 Tool 的_run方法里从独立的配置源读取凭证而不是从全局环境变量读取。下面是一个简化示例import os from langchain.tools import BaseTool class WeatherTool(BaseTool): name get_weather description 查询指定城市的天气 def _run(self, city: str) - str: # 只读取天气服务专用的凭证 api_key os.environ.get(WEATHER_API_KEY) if not api_key: raise ValueError(天气服务凭证未配置) # 调用天气 API return f{city} 今天晴25 度注意这里的关键点WEATHER_API_KEY和数据库凭证、支付凭证是完全隔离的。即使 Agent 在推理中“想”去查数据库它也没有对应的工具可用因为数据库工具根本不在它的工具列表里。3.2 调用深度与频率的硬性约束对于暴走问题最有效的防线是硬性约束而不是依赖模型自觉。我通常会在三个层面加限制第一层是调用深度限制。在 Agent 的执行循环里记录当前调用栈的深度超过阈值就直接终止。比如设置最大深度为 5意味着 A 调 B、B 调 C、C 调 D、D 调 E 之后E 不能再调用任何 Agent。第二层是单位时间调用频率限制。用令牌桶或滑动窗口算法限制每个 Agent 每分钟最多调用多少次工具。这个限制要写在框架层面而不是写在 Prompt 里因为 Prompt 是可以被绕过的。第三层是总 token 消耗限制。给每个任务设置一个 token 预算超过预算就强制结束。这个在 OpenAI 的 API 调用里可以通过max_tokens参数配合累计计数来实现。下面是一个调用深度限制的伪代码示例class AgentExecutor: def __init__(self, max_depth5): self.max_depth max_depth self.call_stack [] def execute(self, agent, task, depth0): if depth self.max_depth: raise RuntimeError(f调用深度超过限制: {self.max_depth}) self.call_stack.append(agent.name) try: result agent.run(task) return result finally: self.call_stack.pop()注意深度限制的阈值不要设得太小否则正常的复杂任务会被误杀。我的经验是先观察正常任务的调用深度分布取 P99 值再加 2 作为阈值。3.3 多智能体协作的“交通规则”当系统里有多个 Agent 时你需要一套类似交通规则的协调机制。我总结下来至少要有三条规则规则一任务幂等性。每个任务有唯一 IDAgent 在执行前先检查该任务是否已被执行过。这可以通过 Redis 的 SETNX 或者数据库的唯一索引来实现。规则二角色互斥。定义哪些 Agent 可以互相调用哪些不可以。比如“审核 Agent”不能被“执行 Agent”调用否则就失去了审核的意义。这个可以用一个邻接矩阵来配置。规则三全局终止条件。除了单个 Agent 的终止条件外还要有一个全局的终止条件比如“所有任务都已完成”或“总耗时超过 10 分钟”。这个条件由协调器Orchestrator来检查而不是由单个 Agent 自己判断。4. 实操从零搭建一个带安全护栏的 Agent 系统4.1 环境准备与依赖安装我以 Python 生态为例搭建一个最小可用的安全 Agent 系统。你需要准备以下环境Python 3.10 或以上LangChain 和 LangGraph用于 Agent 编排Redis用于幂等性检查和频率限制一个支持函数调用的模型 API安装依赖的命令如下pip install langchain langgraph redis openai如果你用的是其他模型服务把openai换成对应的 SDK 即可。注意这里不要图省事把 API Key 写在代码里用环境变量或密钥管理服务。4.2 工具层的权限隔离实现工具层是安全的第一道防线。我设计了一个SecureTool基类所有工具都继承它自动获得权限检查和频率限制能力import time import redis from langchain.tools import BaseTool r redis.Redis(hostlocalhost, port6379, db0) class SecureTool(BaseTool): required_permission: str rate_limit: int 10 # 每分钟最大调用次数 def _check_permission(self, agent_permissions: set): if self.required_permission not in agent_permissions: raise PermissionError( fAgent 缺少权限: {self.required_permission} ) def _check_rate_limit(self, agent_id: str): key frate:{agent_id}:{self.name} current r.incr(key) if current 1: r.expire(key, 60) if current self.rate_limit: raise RuntimeError(f调用频率超限: {self.name}) def run(self, *args, **kwargs): agent_id kwargs.pop(_agent_id, unknown) agent_permissions kwargs.pop(_agent_permissions, set()) self._check_permission(agent_permissions) self._check_rate_limit(agent_id) return super().run(*args, **kwargs)这个基类的关键设计是权限和频率检查发生在工具执行之前而不是之后。这样即使模型生成了恶意调用也会在真正产生副作用之前被拦截。4.3 编排层的调用图与深度控制在 LangGraph 里你可以用状态图来显式定义 Agent 之间的调用关系。下面是一个简化示例展示如何限制调用深度from langgraph.graph import StateGraph, END from typing import TypedDict class AgentState(TypedDict): task: str depth: int result: str def agent_a(state: AgentState): if state[depth] 5: return {result: 深度超限终止} # 执行 Agent A 的逻辑 return {result: A 的结果, depth: state[depth] 1} def agent_b(state: AgentState): if state[depth] 5: return {result: 深度超限终止} # 执行 Agent B 的逻辑 return {result: B 的结果, depth: state[depth] 1} graph StateGraph(AgentState) graph.add_node(agent_a, agent_a) graph.add_node(agent_b, agent_b) graph.add_edge(agent_a, agent_b) graph.add_edge(agent_b, agent_a) graph.set_entry_point(agent_a) app graph.compile()这个图里A 和 B 互相调用但每次调用都会增加depth超过 5 就返回终止结果。这样就避免了无限递归。4.4 监控与告警的配置安全护栏建好之后你还需要知道它有没有被触发。我通常会在三个地方埋点第一个是权限拒绝事件。每次PermissionError被抛出时记录 Agent ID、工具名、时间戳并发送告警。如果短时间内大量出现说明可能有 Agent 在尝试越权。第二个是频率限制事件。每次RuntimeError被抛出时记录 Agent ID 和工具名。如果某个 Agent 频繁触发频率限制说明它的行为模式可能有问题。第三个是深度超限事件。每次深度超限时记录完整的调用栈。这能帮你定位是哪个环节导致了递归。这些事件可以写到日志系统里也可以直接推到监控平台。我一般用 Redis 的 Pub/Sub 做实时告警简单直接。5. 常见问题与排查技巧实录5.1 权限配置了但没生效怎么排查这是最常见的问题。我遇到过好几次明明在工具里写了权限检查但 Agent 还是调用了不该调用的工具。排查下来原因通常是权限检查被绕过了。比如Agent 没有直接调用工具而是通过一个“通用执行器”来调用而通用执行器没有做权限检查。排查步骤确认所有工具都继承了SecureTool没有遗漏。确认 Agent 调用工具时确实传入了_agent_permissions参数。在权限检查处加日志看是否被触发。检查是否有其他路径可以绕过工具层直接执行操作。提示不要相信“我肯定都继承了”这种直觉用代码扫描工具检查一遍或者写个单元测试遍历所有工具类。5.2 Agent 陷入循环但深度限制没触发有时候 Agent 会陷入循环但调用深度并没有增加因为它在同一个 Agent 内部循环而不是跨 Agent 调用。这种情况下深度限制是无效的。解决办法是加单 Agent 内部的迭代次数限制。在 Agent 的执行循环里记录当前任务已经迭代了多少轮超过阈值就强制结束。这个阈值通常设在 10 到 20 之间取决于任务的复杂度。另外还可以加重复动作检测。如果 Agent 连续三次调用同一个工具、传入相同的参数就判定为循环直接终止。5.3 多 Agent 互相等待导致死锁死锁在多 Agent 系统里很常见。Agent A 等 Agent B 的结果Agent B 等 Agent A 的结果两者都不继续执行。这种问题的根因是缺乏超时机制。我的做法是给每个 Agent 的调用加超时比如 30 秒。超时后返回一个错误结果让上游 Agent 决定是重试还是放弃。同时在协调器层面加一个全局超时比如 5 分钟超过就终止整个任务。下面是一个常见问题的速查表问题现象可能原因排查方法解决方案工具调用被拒绝权限未配置或配置错误检查工具权限列表和 Agent 权限集补全权限配置确认传递链路Agent 无限循环缺乏深度或迭代限制查看调用日志确认循环模式加深度限制和重复动作检测任务重复执行缺乏幂等性设计检查任务 ID 是否唯一用 Redis SETNX 或数据库唯一索引多 Agent 死锁缺乏超时机制查看各 Agent 的等待状态加调用超时和全局超时错误结果传播缺乏输入校验检查下游 Agent 的输入校验逻辑加 schema 校验和异常处理5.4 模型 API 连接失败的应急处理在实际运维中模型 API 连接失败是高频问题。常见原因包括网络波动、API Key 过期、配额耗尽等。我的经验是不要把所有 Agent 都绑死在同一个 API 端点上。至少准备两个可用的端点一个主用一个备用。当主端点连续失败三次时自动切换到备用端点。另外对于关键任务要有降级策略。比如当模型 API 不可用时降级到基于规则的简单处理而不是直接报错。这样至少能保证核心流程不中断。6. 从事故中沉淀下来的几条硬经验做 Agent 安全这件事我最大的体会是不要试图用 Prompt 来解决安全问题。你可以在 Prompt 里写“不要调用数据库工具”但模型在特定上下文下完全可能忽略这条指令。真正可靠的安全机制必须写在代码里、写在架构里、写在运行时约束里。第二条经验是安全护栏要尽早加不要等出事再加。很多团队在 demo 阶段为了快速验证把权限、限制、监控全部省略想着“上线前再补”。但实际情况是一旦系统跑起来再往里加约束的难度会大很多因为你要改的是已经成型的调用链路。第三条经验是监控比拦截更重要。拦截只能挡住已知的攻击模式但 Agent 的行为空间是开放的你不可能预判所有异常。所以完善的监控和告警体系能让你在问题扩大之前就发现它。我通常会把权限拒绝、频率超限、深度超限这三类事件作为核心监控指标一旦有异常波动就立即介入。最后分享一个我在实际项目中用的小技巧给每个 Agent 起一个有意义的名字并在日志里始终带上这个名字。这样当出现问题时你能快速定位是哪个 Agent 的行为异常。别小看这一点在多 Agent 系统里没有清晰命名的日志简直就是灾难。
