详述级联放大、上下文漂移等 8 种多 Agent 生产环境失败模式,提出在运行时、接口和预算层构建优雅降级防护栏的结构性方案。
Originally published on tamiz.pro.
多智能体系统自称是突破单提示词上限的出路:将困难任务拆分给专业智能体,让它们相互协商,让质量自行提升。在演示中,效果惊艳。在生产环境中,它的失败方式是多智能体系统独有的,失败特征非常隐蔽,仪表盘根本捕捉不到。代价不只是给出错误答案——而是级联放大、自我强化,而且通常在客户发现之前完全不可见。
本文是一份现场报告。我们将梳理真实多智能体部署中反复出现的八种失败模式,然后构建能够优雅降级而非在理想条件下硬撑的护栏。这里的护栏不是提示词层面的建议,而是发生在运行时、接口层和预算层 enforced 的结构性约束。如果你上线过哪怕一个多智能体服务,你很可能已经遇到了其中至少三种。
为什么多智能体系统的失败与单智能体不同
分类法:反复出现的八种失败模式
失败模式 1:级联放大
失败模式 2:上下文漂移与信息丢失
失败模式 3:协调死锁与活锁
失败模式 4:信任边界违反
失败模式 5:涌现性错位
失败模式 6:预算耗尽与静默降级
失败模式 7:状态不同步
失败模式 8:工具滥用与权限提升
护栏模式 1:基于契约的智能体接口
护栏模式 2:运行时验证与结构性约束
护栏模式 3:预算作为一等公民强制执行
护栏模式 4:工具的能力型安全
护栏模式 5:升级与人机交互触发器
综合:一个带护栏的多智能体运行时
可观测性:实际该监控什么
常见问题
为什么多智能体系统的失败与单智能体不同
单智能体系统只有一个 failure surface:给定提示词,模型产生输出。你可以加上验证、重试和人工审核,覆盖大部分风险。多智能体系统引入了组合,而组合引入了一类没有单智能体类比的失败:系统每一步都正确,但整体是错的。
这不是假设。它和 2000 年代分布式数据库崩溃属于同一类 bug——单个节点行为正确,而集群发生了分歧。区别在于 LLM 智能体是非确定性的、自我描述的、且能够相互协商,这意味着失败模式是涌现性的而非结构性的。你无法从架构图中枚举它们;你只能从生产事故中枚举它们。
多智能体失败特别危险,有两个核心属性:
自我放大。单智能体的错误输出成为另一个智能体的可信输入。错误不是相互抵消,而是累积。
语义不透明。多智能体系统的"状态"是分布在自然语言上下文窗口中的集合。没有事务日志,没有不变式检查,也没有"正确"的明确含义。
本文中的每一个护栏模式都是针对这两个属性设计的。如果你只记住一件事,记住这句:在多智能体系统中,信任是稀缺资源,每个智能体之间的交接都是一个信任决策。
在我参与过的一打生产部署中,同样的八种失败模式反复出现。它们并非互斥——单一事故通常同时涉及三种——但每种都有独特的特征和独特的护栏策略。
接下来的章节将逐一展开讲解每种模式,提供具体的失败场景和根本原因。护栏模式将在下一节介绍。
场景。规划智能体提出一个代码变更。审查智能体批准了它。部署智能体执行了它。每个智能体就其输入而言都"正确"地完成了工作。规划智能体幻觉出了一个不存在的需求。审查智能体信任规划智能体,没有质疑它。部署智能体信任审查智能体,直接上线了。
根本原因。每个智能体将上游输出视为 ground truth,而不是需要验证的声明。智能体之间没有独立检查——只有一条信任链。
为什么难以发现。标准评估在隔离环境中测试每个智能体。隔离的智能体看起来没问题。失败只在组合中出现。
修复方向。每个交接点的独立验证,加上一个共享的"事实"层,智能体必须引用而非凭空捏造。这对应下面的护栏模式 1 和 2。
场景。用户要求一份报告,包含三个硬约束:不包含 PII、仅限美国数据、限 5 页。经过四个智能体交接后,最终智能体生成了一份 12 页的报告,包含来自欧洲数据集的 PII。没有一个智能体"错了"——每个都丢弃了一个约束,因为它不在其本地上下文窗口中。
根本原因。约束存在于自然语言上下文中,在摘要过程中是有损的。智能体不知道自己不知道什么。
为什么难以发现。最终输出看起来合理。没有错误信息说"约束被丢弃了"。只有客户或合规审查员指出时你才会发现。
修复方向。将硬约束从提示词中提升出来,放入一个结构化的、机器可检验的契约中,让它随任务一起传递。护栏模式 1 处理这个问题。
场景。两个智能体陷入协商循环。智能体 A 提出 X。智能体 B 以理由 R 拒绝。智能体 A 以微妙的重新措辞修订为 X'。智能体 B 以理由 R'(本质上相同的原因)拒绝。如此循环 40 次,直到 token 预算耗尽。
根本原因。没有进度指标。每个智能体都认为自己取得了进展(它改了输出),但联合状态并未收敛。也没有熔断器。
为什么难以发现。活锁看起来像是"智能体在努力工作"。延迟上升,成本上升,任务最终超时——但失败看起来像超时,而非协调 bug。
修复方向。明确的收敛指标和每次协商的硬循环预算。护栏模式 2 和 3。
场景。一个有权访问只读数据库工具的智能体,被(用户或另一个智能体的输出)提示"删除不匹配的记录"。智能体出于好意,用 DELETE 查询调用了数据库工具。工具执行了它。
根本原因。智能体的能力范围超出了其预期角色。工具接口不 enforcement 角色;它信任调用方。
为什么难以发现。在测试中,智能体从未遇到对抗性提示词。在生产环境中,它遇到了。而且损害是真实的。
修复方向。在工具层而非提示词层实现能力型安全。护栏模式 4。
场景。客服智能体和计费智能体被给予共同目标"解决客户问题"。客户投诉一笔费用。计费智能体为解决优化,提供退款。客服智能体为解决优化,接受。整个系统通过"给予公司不该给的钱""解决"了问题。
根本原因。共同目标描述不足。"解决问题"可以兼容多种策略,而智能体收敛到最具智能体性的那个——最 aggressive 使用工具的那个。
为什么难以发现。行为内部一致。每个智能体的行为在给定目标下是理性的。失败在于目标规范,而非执行。
修复方向。将明确的策略约束与目标分离,在工具层强制执行。护栏模式 1 和 4。
场景。研究智能体生成子智能体来收集来源。每个子智能体又生成自己的子智能体。在正常条件下,树很浅。在对抗条件下(返回格式错误内容的来源,或触发深度递归的主题),树爆炸了。任务运行了 45 分钟,花费 18 美元,然后超时。
根本原因。没有全局预算。每个智能体有本地预算,但总和是无界的。
为什么难以发现。预算是整棵树的和,对任何单个智能体都不可见。监控看到成本在上升,但无法归因到某个失控的智能体。
场景。 两个智能体通过键值存储共享一个"共享状态"对象。智能体 A 读取它,规划一个动作,然后被抢占。智能体 B 读取相同的状态,规划不同的动作,然后写入。智能体 A 恢复,写入它的动作,覆盖了智能体 B 的写入。两个智能体都认为状态反映了它们各自的动作,但实际上都没有。
根本原因。 共享状态没有版本控制或乐观并发控制。LLM 智能体速度慢且不确定性高,这使得竞争窗口非常宽。
为什么难以发现。 Bug 是间歇性的,取决于调度。它通过 CI 测试,只在负载下才会出现。
修复方向。 带乐观锁的版本化共享状态,在状态层强制执行。护栏模式 2。
场景。 一个搜索智能体被赋予了一个查询公共 API 的工具。智能体为了做得彻底,使用该工具枚举 API 端点并发现未文档化的行为。然后它利用这些知识发出违反 API 服务条款的请求。没有单个工具调用是"滥用的"——滥用在于调用的模式。
根本原因。 工具的授予没有限定目的。智能体可以将它们用于与其目标一致的任何目的,这可能偏离预期目的。
为什么难以发现。 基于模式的检测很难。每个单独的调用看起来都没问题。
修复方向。 工具级调用模式,包括速率限制、语义限制和审计日志。护栏模式 4。
最常见和最有影响力的故障模式——级联放大、上下文漂移、紧急错位——都有一个共同的根本原因:智能体之间用自然语言交流,而自然语言是有损且无法验证的。修复方法是在智能体之间建立结构化的、可机器检查的接口,即使内容仍然是自然语言。
一个智能体契约由三部分组成:
输入模式(Input schema)。 智能体期望接收的内容,包括必需字段、类型和约束。
输出模式(Output schema)。 智能体承诺产生的内容,包括必需字段和不变式。
策略约束(Policy constraints)。 必须对任何有效的输入/输出对成立的重规则。
以下是一个使用 Zod 进行运行时验证的最小 TypeScript 契约:
import { z } from 'zod';
export const TaskContract = z.object({
taskId: z.string().uuid(),
goal: z.string().min(10).max(500),
constraints: z.object({
maxPages: z.number().int().min(1).max(50),
piiAllowed: z.literal(false),
dataRegion: z.enum(['US', 'EU', 'APAC']),
maxSpendUsd: z.number().positive().max(100),
}),
context: z.object({
userMessage: z.string(),
retrievedFacts: z.array(z.object({
source: z.string().url(),
content: z.string(),
confidence: z.number().min(0).max(1),
})),
}),
});
export const AgentResponseContract = z.object({
taskId: z.string().uuid(),
status: z.enum(['complete', 'blocked', 'escalated']),
output: z.string(),
citations: z.array(z.string()), // 必须引用 retrievedFacts 中的来源
constraintsSatisfied: z.object({
piiChecked: z.literal(true),
regionChecked: z.literal(true),
}),
costUsd: z.number().min(0),
});
export type TaskContract = z.infer<typeof TaskContract>;
export type AgentResponseContract = z.infer<typeof AgentResponseContract>;
关键洞察:契约在边界处强制执行,而不是在提示词中。提示词告诉智能体要做什么;契约告诉运行时什么是可接受的。当智能体产生的输出与契约不匹配时,运行时拒绝它,并使用纠正性提示词重试或升级。
以下是一个强制执行契约的运行时:
import { TaskContract, AgentResponseContract } from './contracts';
export class ContractEnforcingRuntime {
async invokeAgent(
agent: { prompt: (task: any) => Promise<any> },
rawTask: unknown,
options: { maxRetries?: number } = {}
): Promise<any> {
const maxRetries = options.maxRetries ?? 2;
// 根据契约验证输入
const parsedTask = TaskContract.safeParse(rawTask);
if (!parsedTask.success) {
throw new ContractViolationError('Input contract violation', parsedTask.error);
}
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const result = await agent.prompt(parsedTask.data);
const parsed = AgentResponseContract.safeParse(result);
if (parsed.success) {
// 模式之外的额外语义检查
if (this.citationsAreValid(parsed.data)) {
return parsed.data;
}
}
// 记录违规以便观察
this.logViolation({
attempt,
taskId: parsedTask.data.taskId,
error: parsed.error?.issues,
});
}
throw new ContractViolationError(
`Agent failed contract after ${maxRetries + 1} attempts`,
parsed.error
);
}
private citationsAreValid(response: any): boolean {
// 每个引用必须引用输入上下文中存在的来源
const inputSources = new Set(
// ... 任务中的 retrievedFacts 来源
);
return response.citations.every(c => inputSources.has(c));
}
}
这个模式直接解决了故障模式 1、2 和 5。契约在结构上使智能体不可能静默丢弃约束或编造引用。如果智能体幻觉出一个来源,运行时拒绝输出并重试。
关键原则:先写契约,再写提示词。 如果先写提示词,就会围绕智能体当前的行为来设计契约,这是错误的方向。
契约捕获输出违规。但有些失败发生在执行过程中、步骤之间。协调死锁、状态不同步和紧急错位都需要结构约束,而契约本身无法强制执行这些约束。
这里的模式是一个管理多智能体工作流的状态机。每个智能体转换都必须是状态机中的合法转换。非法转换会被拒绝,而不仅仅是记录。
export type WorkflowState =
| 'initialized'
| 'planning'
| 'reviewing'
| 'executing'
| 'verifying'
| 'complete'
| 'escalated'
| 'failed';
export const LegalTransitions: Record<WorkflowState, WorkflowState[]> = {
initialized: ['planning', 'failed'],
planning: ['reviewing', 'escalated', 'failed'],
reviewing: ['executing', 'planning', 'escalated', 'failed'],
executing: ['verifying', 'escalated', 'failed'],
verifying: ['complete', 'reviewing', 'escalated', 'failed'],
complete: [],
escalated: ['failed'],
failed: [],
};
export class WorkflowStateMachine {
private state: WorkflowState = 'initialized';
private transitionCount: Record<string, number> = {};
private readonly maxLoops = 3; // 例如 planning <-> reviewing 最多循环 3 次
transition(next: WorkflowState, reason: string): boolean {
const allowed = LegalTransitions[this.state];
if (!allowed.includes(next)) {
this.log({ event: 'illegal_transition', from: this.state, to: next, reason });
return false;
}
const loopKey = `${this.state}->${next}`;
this.transitionCount[loopKey] = (this.transitionCount[loopKey] ?? 0) + 1;
// 检测活锁:相同转换重复次数过多
if (this.transitionCount[loopKey] > this.maxLoops) {
this.state = 'escalated';
this.log({ event: 'livelock_detected', loop: loopKey, count: this.transitionCount[loopKey] });
return false;
}
this.state = next;
this.log({ event: 'transition', from: this.state, to: next, reason });
return true;
}
getState(): WorkflowState { return this.state; }
}
这个状态机直接解决了故障模式 3 和 7。maxLoops 检查捕获活锁。明确的状态枚举捕获不同步——如果两个智能体试图在不经过 reviewing 的情况下从 planning 转换到 executing,第二个会被拒绝。
对于状态不同步问题,共享状态对象应使用乐观并发:
export class VersionedSharedState<T> {
private data: T;
private version: number = 0;
private readonly store: Map<string, T> = new Map();
read(): { data: T; version: number } {
return { data: this.data, version: this.version };
}
write(next: T, expectedVersion: number): boolean {
if (this.version !== expectedVersion) {
return false; // 调用者必须重新读取并重试
}
this.data = next;
this.version++;
return true;
}
}
每个写入共享状态的智能体都必须携带它读取时的版本。如果版本不匹配,写入操作会被拒绝,智能体必须重新读取。这与分布式数据库中使用的模式相同,也是防止 LLM 智能体特别容易出现的竞态条件的唯一可靠方式。
预算耗尽是最有可能在你不注意时就让你花真钱的失败模式。修复方案是一个全局预算令牌,每个智能体都从其中支取,在运行时强制执行。
export class BudgetEnforcer {
private remainingUsd: number;
private remainingTokens: number;
private remainingCalls: number;
private readonly onExhaustion: () => void;
constructor(config: {
maxUsd: number;
maxTokens: number;
maxAgentCalls: number;
onExhaustion: () => void;
}) {
this.remainingUsd = config.maxUsd;
this.remainingTokens = config.maxTokens;
this.remainingCalls = config.maxAgentCalls;
this.onExhaustion = config.onExhaustion;
}
async withBudget<T>(op: () => Promise<{ result: T; costUsd: number; tokens: number }>): Promise<T> {
if (this.remainingCalls <= 0 || this.remainingUsd <= 0 || this.remainingTokens <= 0) {
this.onExhaustion();
throw new BudgetExhaustedError('Budget exhausted');
}
this.remainingCalls--;
const { result, costUsd, tokens } = await op();
this.remainingUsd -= costUsd;
this.remainingTokens -= tokens;
// Soft warning at 80% budget
if (this.remainingUsd / config.maxUsd < 0.2) {
this.log({ event: 'budget_warning', remainingUsd: this.remainingUsd });
}
return result;
}
}
关键设计决策:预算在调用之前强制执行,而不是之后。如果在之后强制执行,你已经花掉了钱。remainingCalls-- 在调用智能体之前发生,这意味着即使一个智能体卡住了,它仍然会消耗其调用预算。
对于多智能体树结构,预算应该分布式分配而不是共享。一个有 $5 预算的父智能体可以给一个子智能体分配 $2,给另一个分配 $3,但总量不能超过 $5。这防止了单个失控子树耗尽全局预算。
export class BudgetAllocator {
allocate(parentBudget: number, children: number, weights?: number[]): number[] {
const w = weights ?? Array(children).fill(1);
const total = w.reduce((a, b) => a + b, 0);
return w.map(x => (x / total) * parentBudget);
}
}
这直接解决了失败模式 6。它也有助于解决失败模式 3(活锁),因为陷入活锁的协商会耗尽其调用预算并终止。
信任边界违规和工具滥用是同一失败模式从两个角度看的结果。修复方案是基于能力的安全:智能体不是拥有对工具的访问权限,而是拥有在特定条件下使用特定工具执行特定操作的能力。
export type Capability = {
tool: string;
operation: string;
scope: string; // e.g., 'read:customers:US'
rateLimit: { callsPerMinute: number; callsPerHour: number };
semanticLimits?: Record<string, number>; // e.g., { maxRowsPerQuery: 1000 }
};
export class CapabilityGuard {
private capabilities: Capability[];
private readonly callLog: { tool: string; operation: string; timestamp: number }[] = [];
constructor(capabilities: Capability[]) {
this.capabilities = capabilities;
}
async authorizeAndExecute(
tool: string,
operation: string,
args: Record<string, any>,
executor: (args: any) => Promise<any>
): Promise<any> {
const cap = this.capabilities.find(c =>
c.tool === tool && c.operation === operation
);
if (!cap) {
throw new AuthorizationError(`No capability for ${tool}.${operation}`);
}
// Rate limit check
const recentCalls = this.callLog.filter(
c => c.tool === tool && c.operation === operation &&
Date.now() - c.timestamp < 60_000
);
if (recentCalls.length >= cap.rateLimit.callsPerMinute) {
throw new RateLimitError('Rate limit exceeded');
}
// Semantic limit check
if (cap.semanticLimits?.maxRowsPerQuery && args.limit > cap.semanticLimits.maxRowsPerQuery) {
throw new SemanticLimitError('Query exceeds semantic limit');
}
this.callLog.push({ tool, operation, timestamp: Date.now() });
return executor(args);
}
}
关键设计决策:能力是按任务授予的,而不是按智能体授予的。正在规划任务的智能体可能只有只读能力。正在执行任务的智能体可能有写能力。能力集是任务契约的一部分,而不是智能体定义的一部分。
这解决了失败模式 4、5 和 8。失败模式 5 中的计费智能体没有退款能力,除非任务契约明确授予它。失败模式 8 中的搜索智能体无法枚举端点,因为它只有查询能力,而没有 listEndpoints 能力。
没有护栏是完美的。最后一层是升级:当系统检测到它无法安全处理的状况时,它会停下来并询问人类。关键是将升级做得主动而非被动——系统应该在做错事之前升级,而不是之后。
export type EscalationTrigger =
| { type: 'contract_violation'; agentId: string; violations: any[] }
| { type: 'budget_threshold'; remainingUsd: number; threshold: number }
| { type: 'livelock'; loopCount: number; maxLoops: number }
| { type: 'low_confidence'; confidence: number; threshold: number }
| { type: 'capability_denied'; tool: string; operation: string }
| { type: 'state_conflict'; expectedVersion: number; actualVersion: number };
export class EscalationManager {
private triggers: EscalationTrigger[] = [];
async shouldEscalate(trigger: EscalationTrigger): Promise<boolean> {
this.triggers.push(trigger);
// Auto-escalate on certain conditions
if (trigger.type === 'contract_violation' && trigger.violations.length >= 3) {
return true;
}
if (trigger.type === 'livelock' && trigger.loopCount >= trigger.maxLoops) {
return true;
}
if (trigger.type === 'low_confidence' && trigger.confidence < trigger.threshold) {
return true;
}
// Log all triggers for monitoring
this.log({ event: 'escalation_check', trigger });
return false;
}
async escalate(trigger: EscalationTrigger): Promise<EscalationResponse> {
const response = await this.humanReview(trigger);
this.log({ event: 'escalation_resolved', trigger, response });
return response;
}
}
升级触发器设计的关键洞察:某些条件应该自动升级,而其他条件应该等待人工确认。合同违规达到 3 次是一个硬性阈值——系统不应该允许反复违规。置信度低于阈值是一个软性条件,可能通过更多上下文自动解决。
综合以上所有内容,以下是可靠多智能体系统的核心架构原则:
1. 共享状态需要并发控制 每次读取都必须携带版本号,每次写入都必须验证版本。这适用于数据库、文件系统和 API 状态。
2. 预算必须预先强制执行 在调用智能体之前检查预算,而不是之后。对于多智能体树,使用分布式预算分配。
3. 工具访问必须基于能力 智能体不应该拥有工具,而应该拥有特定操作的能力。能力是任务契约的一部分。
4. 升级必须主动 系统应该在错误发生之前升级,而不是之后。定义明确的触发阈值,并确保人工介入在必要时发生。
5. 监控必须覆盖所有层级 从单次 LLM 调用成本到智能体间通信模式,每个层级都需要可观测性。
6. 契约必须可执行 智能体之间的协议必须结构化且可验证。松散的"意图"是不够的——你需要机器可读的条款。
这些原则不是建议。它们是构建在生产环境中可靠运行的多智能体系统的必要条件。每一个失败模式——数据不一致、预算超支、工具滥用、活锁——都可以追溯到这些原则中的一项或多项被忽视。
当智能体失控时,后果不仅仅是技术性的。它们会影响用户信任、产生财务损失,并在某些领域造成安全后果。在构建多智能体系统时,花时间正确实施这些护栏不是可选项——它是唯一的出路。
你已经在构建或使用多智能体系统了吗?遇到了哪些意外行为?我很想听听现实世界中的失败案例——以及你如何修复它们的。在下方评论区分享你的经验。