1. 从一次线上事故说起:多 Agent 为什么会“失控”
2026 年开年到现在,我参与过的多 Agent 项目里,至少有四个出过“看起来像灵异事件”的故障。最典型的一次:一个负责自动处理客户工单的 Agent 集群,在凌晨两点突然开始互相派发任务,A 把任务转给 B,B 判断“这不该我管”又转回 A,两个 Agent 在 40 分钟里生成了 1.2 万条内部消息,把消息队列打满,连带把下游的库存查询接口拖垮。事后复盘,根因不是模型变笨了,而是没有任何一层机制去约束“谁有权把任务交给谁、交出去之后谁负责收尾”。
这就是我理解的“多 Agent 走向失控”:单个 Agent 的能力越来越强,编排框架越来越成熟,但治理层几乎是空白。大家把精力全花在“怎么让 Agent 更聪明”上,却没人认真回答“怎么让一群 Agent 不互相添乱”。这篇内容我想把这件事拆开讲清楚——多 Agent 治理到底要治什么、FCoP 这类协议思路能解决哪部分问题、以及落到工程上你该怎么搭一套最小可用的治理骨架。适合已经在做 Agent 开发、或者正准备把单 Agent 扩成多 Agent 协作的工程师,也适合被“Agent 执行到一半报错终止”折磨过的同学。
先把结论摆前面:多 Agent 治理的本质,是把分布式系统里早就成熟的那套东西——身份、权限、契约、可观测性、熔断——重新翻译到 Agent 语境里。它不是什么全新的玄学,而是你熟悉的那些工程原则换了个皮。下面我按“问题从哪来 → 协议层怎么设计 → 工程上怎么落地 → 怎么观测和止损”这条线展开。
2. 多 Agent 失控的四种典型形态与根因定位
在讲治理方案之前,得先能识别“失控”长什么样。我踩过的坑基本能归到下面四类,每一类的排查思路完全不同,混在一起看只会越查越乱。
2.1 任务乒乓:两个 Agent 互相甩锅
前面说的工单案例就是典型。A 判断“需要退款处理”,转给 B;B 判断“退款前提是订单状态已确认”,而订单状态查询失败,于是它认为“这单该 A 重新核实”,转回 A。两边都“逻辑正确”,但组合起来就是死循环。
根因通常有三个:一是任务所有权没有唯一归属,谁都能转交,谁都不负责终结;二是转交次数没有上限,没有 hop count 之类的计数器;三是转交的判定条件依赖了不稳定状态(比如查询失败的接口),失败被误读成“该换人”。
排查这类问题的第一步,是给每条任务打上 trace id,把转交链路完整打出来。我一般会看两个指标:单条任务的平均转交次数,以及转交次数的 P99。正常业务里 P99 超过 3 就该警惕了,超过 5 基本可以确定有乒乓。
2.2 上下文污染:Agent 记忆互相串味
多 Agent 共享记忆(agent memory)是个很诱人的设计,但也是事故高发区。我见过一个场景:客服 Agent 和营销 Agent 共用一套向量记忆库,营销 Agent 为了“个性化推荐”写入了一条“该用户对价格极度敏感”,结果客服 Agent 在处理退款时读到这条,直接给出了超出政策的补偿方案。
这类问题的根因是记忆的读写权限没有按 Agent 角色隔离。共享记忆不等于无差别共享,写入方和读取方应该有明确的 scope。A-MemGuard 这类主动防御框架之所以被提出来,就是因为大家发现“记忆投毒”不只是安全问题,更是业务正确性问题。
2.3 资源争抢:并发一上来就雪崩
单 Agent 时你只需要考虑一个执行体的资源占用。多 Agent 之后,10 个 Agent 同时去调同一个下游接口,QPS 直接翻十倍。我遇到过一次:三个 Agent 同时触发全量数据同步,把数据库连接池占满,导致整个服务不可用。
根因是缺少全局的并发预算和限流。每个 Agent 自己看自己都很“克制”,但全局视角下就是过载。这跟微服务里的“惊群”是一个道理,只是 Agent 的触发时机更不可预测。
2.4 静默失败:Agent 执行到一半终止,没人知道
热词里那个 “agent execution terminated due to error” 我太熟了。Agent 在执行多步任务时中途报错退出,如果没有补偿机制,任务就永远卡在“进行中”。更糟的是,如果这个 Agent 是某个协作链路的中间环节,整条链路都会挂起,而上下游可能还在傻等。
根因是缺少任务状态机和超时兜底。Agent 的执行不是原子的,必须把长任务拆成可检查的步骤,每一步都有状态、有超时、有重试或转人工的出口。
把这四类问题放一起看,你会发现它们指向同一件事:多 Agent 系统缺少一个“治理平面”,就像微服务缺少服务网格一样。下面这张表是我自己用来快速定位的对照表:
| 失控形态 | 核心症状 | 首要排查点 | 典型根因 |
|---|---|---|---|
| 任务乒乓 | 转交次数异常高 | 转交链路 trace | 所有权不唯一、无 hop 上限 |
| 上下文污染 | 输出与角色不符 | 记忆读写日志 | 记忆 scope 未隔离 |
| 资源争抢 | 下游 QPS 突增 | 全局并发指标 | 无全局限流预算 |
| 静默失败 | 任务长期挂起 | 任务状态机 | 无超时与补偿 |
3. FCoP 协议思路:把“协作契约”显式化
热词里出现了 FCoP 和“协议”这个词,我觉得这是多 Agent 治理里最值得认真对待的方向。FCoP 我理解为一类面向 Agent 协作的契约协议思路——核心不是规定 Agent 怎么思考,而是规定 Agent 之间怎么交互。这跟网络协议的分层思想是一脉相承的:TCP 不关心你传的是 HTTP 还是 MQTT,它只保证可靠传输;同理,协作协议不关心 Agent 内部用什么模型,它只保证交互有规可循。
3.1 为什么“协议”比“框架”更适合做治理
很多人第一反应是“我用某个 Agent 框架不就行了吗”。但框架解决的是编排问题——谁先跑、谁后跑、数据怎么传。治理解决的是约束问题——谁能做什么、做到什么程度必须停、出错了谁负责。这两件事的抽象层级不一样。
框架是“怎么做”,协议是“允许怎么做”。举个例子:LangGraph 这类框架能帮你把 Agent 连成图,但它不会阻止两个节点无限互相调用。而一个协作协议会在设计层面就规定“转交必须有次数上限、必须有唯一 owner”。这就是为什么我说治理要靠协议思路,而不是靠换一个更强的框架。
3.2 一份最小协作契约应该包含什么
我实践下来,一份能用的 Agent 协作契约至少要有五个字段。这不是标准答案,是我自己项目里反复调整后觉得最顺手的结构:
- 角色标识(role id):这个 Agent 是谁,全局唯一。不要用“客服 Agent”这种模糊名字,要用
cs-refund-v2这种带版本和职责的标识。 - 能力声明(capability):它能处理哪些类型的任务,输入输出格式是什么。这相当于 Agent 的“接口定义”。
- 权限边界(scope):它能读哪些记忆、能调哪些工具、能写哪些数据。这是防污染的关键。
- 转交规则(handoff policy):允许转给谁、最多转几次、什么条件下必须终止并转人工。
- 超时与补偿(timeout & compensation):单步超时多久、超时后是重试还是回滚还是升级。
把这五个字段显式写出来,你会发现很多“失控”在设计阶段就能避免。比如转交规则里写死“最多转 2 次”,乒乓问题直接消失。
3.3 协议落地时最容易忽略的:版本与兼容
协议一旦定下来,Agent 就会依赖它。这时候版本管理就成了大坑。我吃过一次亏:给转交规则加了一个新字段,结果老版本 Agent 读到不认识的字段直接报错退出,整条链路静默失败。
后来我的做法是:协议字段只增不删,新增字段必须有默认值,Agent 解析时对未知字段一律忽略而不是报错。这跟 protobuf 的兼容性设计是一个思路。别小看这一条,多 Agent 系统里 Agent 的发布节奏很难统一,协议兼容性直接决定了你能不能灰度升级。
4. 工程落地:用三个层次搭起治理骨架
协议是设计层面的东西,落到代码里得有具体的承载。我一般把治理分成三层来搭,从下到上分别是执行层、协调层、观测层。这三层不需要一次性全做完,但顺序不能乱——先有执行层的约束,再谈协调,最后才是观测。
4.1 执行层:给每个 Agent 套上“紧箍咒”
执行层是最贴近 Agent 本身的一层,核心目标是让单个 Agent 的行为可预测。我通常会在 Agent 外面包一个轻量的 wrapper,统一处理几件事:
第一是输入校验。Agent 收到的任务必须符合它声明的 capability,不符合直接拒绝,而不是“尽力而为”。很多失控源于 Agent 接了自己不该接的活。
第二是步数上限。Agent 内部的多步推理必须有硬性上限,比如最多 15 步。超过就强制终止并上报。这能挡住大部分“Agent 自己绕进死胡同”的情况。
第三是工具调用白名单。Agent 能调哪些工具,在 wrapper 里写死。不要指望模型自己“守规矩”,模型在压力下什么都敢调。
class GovernedAgent: def __init__(self, role_id, capability, allowed_tools, max_steps=15): self.role_id = role_id self.capability = capability self.allowed_tools = set(allowed_tools) self.max_steps = max_steps def execute(self, task): if task.type not in self.capability: raise CapabilityMismatch(f"{self.role_id} cannot handle {task.type}") steps = 0 while steps < self.max_steps: action = self.plan(task) if action.tool not in self.allowed_tools: raise ToolNotAllowed(action.tool) steps += 1 # ... 执行与状态更新 raise StepLimitExceeded(self.role_id)这段代码很朴素,但它是治理的基石。没有执行层的约束,上层的协调和观测都是空中楼阁。
4.2 协调层:任务所有权与转交的硬约束
协调层解决的是 Agent 之间的事。我的核心设计原则只有一条:任何时刻,一条任务有且只有一个 owner。owner 可以转交,但转交是一个显式的、被记录的动作,不是“我处理不了就丢出去”。
具体实现上,我会维护一个任务状态表,每条任务记录当前 owner、转交历史、剩余转交次数。转交时做原子更新,剩余次数减到 0 就强制进入“人工介入”队列。这个设计直接掐死了乒乓问题。
-- 任务状态表的核心字段 CREATE TABLE agent_task ( task_id VARCHAR(64) PRIMARY KEY, current_owner VARCHAR(64) NOT NULL, handoff_count INT DEFAULT 0, max_handoff INT DEFAULT 2, status VARCHAR(16), -- pending/running/handoff/done/failed updated_at TIMESTAMP );转交逻辑用一条带条件的 UPDATE 就能保证原子性:
UPDATE agent_task SET current_owner = :new_owner, handoff_count = handoff_count + 1, status = 'handoff' WHERE task_id = :task_id AND handoff_count < max_handoff AND status = 'running';如果这条 UPDATE 影响行数为 0,说明要么次数用尽,要么状态不对,转交直接失败并触发升级。用数据库的条件更新来做治理约束,比在应用层写一堆 if-else 可靠得多,因为它天然并发安全。
4.3 观测层:让失控在发生前就被看见
观测层是我认为最被低估的一层。很多团队等到出事故才想起来加监控,但多 Agent 系统的失控往往是渐进的——转交次数慢慢变多、记忆写入慢慢变乱、并发慢慢变高。如果你有观测,这些都能在爆掉之前发现。
我必看的几个指标:
- 单任务转交次数分布:P50 应该接近 0,P99 超过 3 就要查。
- Agent 间消息量:按 role 对统计,突增往往意味着乒乓或广播风暴。
- 记忆写入速率与来源:某个 Agent 突然大量写记忆,可能是逻辑出了问题。
- 任务状态停留时长:某状态停留超过阈值,说明有静默失败。
- 工具调用失败率:按 Agent 维度看,某个 Agent 失败率飙升可能是它接了不该接的活。
这些指标不需要多高级的工具,一张时序表加几个告警规则就能覆盖。关键是你要在系统设计之初就把这些埋点留出来,事后补埋点成本高得多。
5. 记忆治理:多 Agent 协作里最隐蔽的雷区
单独把记忆治理拎出来讲,是因为它太容易被忽略,又太容易出事。多 Agent 协作里,记忆既是资产也是负债——共享得好能提升整体智能,共享得乱就是互相投毒。
5.1 记忆的三种隔离级别
我实践下来会把记忆分成三个隔离级别,按敏感度和用途区分:
| 隔离级别 | 可见范围 | 典型内容 | 写入权限 |
|---|---|---|---|
| 私有记忆 | 仅本 Agent | 本角色的临时推理状态 | 仅自己 |
| 角色共享记忆 | 同角色所有实例 | 该角色的经验、话术 | 同角色 |
| 全局记忆 | 所有 Agent | 用户画像、业务事实 | 受控写入 |
关键在第三级。全局记忆不能谁都能写,必须有写入审核。我的做法是全局记忆的写入走一个独立的“记忆管家”Agent,它负责校验写入内容是否符合规范、是否与已有事实冲突。这听起来多了一层,但能挡住绝大多数污染。
5.2 记忆投毒的两个真实案例
第一个案例是前面说的价格敏感标签。营销 Agent 写入的推断被客服 Agent 当成事实使用。修复方式是给记忆打上来源和置信度标签,客服 Agent 只采信置信度高于阈值且来源为“事实类”的记忆。
第二个案例更隐蔽:一个 Agent 在重试时反复写入同一条“任务失败”记忆,导致后续所有 Agent 读到这条都倾向于放弃该任务。这是典型的负面记忆放大。修复方式是记忆写入去重,并且对“失败类”记忆设置更短的过期时间。
提示:记忆治理里最实用的一个原则是“推断不共享,事实才共享”。Agent 自己推断出来的东西,默认只留在私有记忆里,除非经过审核升级为事实。
5.3 记忆的过期与清理
记忆不是越多越好。我见过一个系统跑了半年,向量库里堆了几百万条记忆,检索质量断崖式下降。后来加了分级过期策略:临时推理状态保留小时级,角色经验保留周级,业务事实长期保留但定期做一致性校验。
清理这件事最好做成自动化的定时任务,而不是靠人想起来。因为记忆膨胀是渐进的,等人发现时往往已经影响业务了。
6. 当治理本身成为瓶颈:性能与灵活性的平衡
讲到这里可能有人会担心:加这么多约束,Agent 还跑得动吗?会不会治理层自己成了瓶颈?这个担心很合理,我自己也踩过“治理过度”的坑。
6.1 治理开销的量化
我在一个中等规模的项目里做过对比:加治理层之前,单任务平均耗时 2.3 秒;加了执行层校验、协调层状态更新、观测层埋点之后,平均耗时 2.6 秒,增加约 13%。这个开销我认为完全值得,因为它换来的是可预测性和可排查性。
但如果你的治理层设计得不好,开销可能翻倍。最常见的错误是把治理逻辑放在同步关键路径上做重操作,比如每次转交都去查一次全量记忆、每次都做一次复杂的权限计算。我的做法是:权限和 capability 在 Agent 启动时加载到内存,转交只做一次轻量的条件更新,观测埋点异步上报。
6.2 什么时候可以放宽治理
治理不是越严越好。对于低风险、高吞吐的场景,比如批量内容分类,我通常只保留执行层的步数上限和观测层,协调层的严格所有权可以放宽。因为这类任务即使出错,代价也小,严格治理反而拖慢吞吐。
判断标准很简单:问自己“这个 Agent 出错的最坏后果是什么”。如果最坏后果是“多花点钱重跑”,那就轻治理;如果是“给用户发错钱”或“污染共享数据”,那就重治理。治理强度应该跟风险等级匹配,而不是一刀切。
6.3 灰度与回滚
治理规则本身也需要灰度。我一般会先把新规则设成“只告警不拦截”模式跑一周,观察会拦下多少请求、这些请求里有多少是真问题。确认误伤率可接受后再切成拦截模式。这样能避免“治理规则上线当天把正常业务全拦了”的惨剧。
回滚同样重要。治理规则要有版本号,出问题能一键回退到上一版。我吃过一次亏:改了一条转交规则,结果把一条正常的长链路业务给拦了,因为没有版本管理,回滚花了两个小时手工改配置。
7. 一套可复用的多 Agent 治理检查清单
最后把我自己项目里用的检查清单整理出来,你可以直接拿去对照。这不是理论,是踩坑踩出来的。
执行层
- 每个 Agent 是否有唯一 role id 和明确的 capability 声明?
- 是否有步数上限和工具白名单?
- 输入不符合 capability 时是否直接拒绝而非硬扛?
协调层
- 每条任务是否任何时刻只有一个 owner?
- 转交是否有次数上限和原子性保证?
- 转交次数用尽后是否有明确的升级出口?
记忆层
- 记忆是否按私有/角色/全局三级隔离?
- 全局记忆写入是否经过审核?
- 是否有记忆过期和清理机制?
观测层
- 转交次数、消息量、记忆写入、状态停留时长是否都有埋点?
- 告警阈值是否基于实际基线设定而非拍脑袋?
- 是否有 trace id 能串起完整协作链路?
运维层
- 治理规则是否有版本管理和灰度机制?
- 是否有回滚预案?
- 治理开销是否量化过,是否在可接受范围?
这份清单我每次开新项目都会过一遍,能省掉大量事后救火的时间。多 Agent 治理这件事,说到底就是把分布式系统几十年的工程经验,认真地搬到 Agent 语境里。协议、所有权、隔离、观测、熔断,这些词你都不陌生,难的是在 Agent 这个新场景里把它们重新想一遍、落一遍。我自己的体会是,越早把治理骨架搭起来,后面扩 Agent 数量时越轻松;反过来,等系统跑起来再补治理,成本会高出一个量级。