Skip to content
生产级 Agent 运行时架构:可靠性、异常恢复与执行优化
1 Agent 运行时概述
LLM Agent 是一个以大模型为主控、通过工具调用来完成任务的程序。它的核心执行逻辑是“感知—规划—行动”的循环。在一个演示程序中,这个循环通常写成一段顺序代码:调用模型,拿到工具调用指令,执行工具,把结果放回上下文。
但同一个循环一旦进入实际部署,就会出现大量不确定性问题:模型可能输出非法参数,工具可能超时,外部 API 可能返回 5xx,进程可能在工具调用之后崩溃。因此,正式运行的 Agent 运行时不能只包含主循环,还需要在循环周围增加可靠性机制、异常恢复机制和性能优化机制。
1.1 Agent 主循环
下面用 TypeScript 写出一个最小主循环。其中的类型和函数用于表达概念,不映射到某个特定框架的公开 API。planner.nextAction 负责把上下文转换为模型输出,executor.run 负责调用具体工具。
typescript
interface AgentContext {
messages: Array<{ role: string; content: string }>;
step: number;
}
interface ToolAction {
type: 'tool';
toolName: string;
args: unknown;
}
interface FinishAction {
type: 'finish';
}
type PlanAction = ToolAction | FinishAction;
interface ToolCallEnvelope {
idempotencyKey?: string;
toolName: string;
args: unknown;
}
interface ToolExecutor {
run(envelope: ToolCallEnvelope): Promise<unknown>;
}
interface Planner {
nextAction(ctx: AgentContext): Promise<PlanAction>;
}
// appendToolResult 表示把工具执行结果写入上下文的辅助函数
function appendToolResult(
ctx: AgentContext,
action: ToolAction,
result: unknown
): AgentContext {
return {
...ctx,
messages: [
...ctx.messages,
{ role: 'tool', content: JSON.stringify(result) },
],
step: ctx.step + 1,
};
}
class MaxStepsExceededError extends Error {}
async function agentLoop(
initial: AgentContext,
planner: Planner,
executor: ToolExecutor,
maxSteps: number
): Promise<AgentContext> {
let ctx = initial;
for (let step = 0; step < maxSteps; step++) {
const action = await planner.nextAction(ctx);
if (action.type === 'finish') {
return ctx;
}
const result = await executor.run({
toolName: action.toolName,
args: action.args,
});
ctx = appendToolResult(ctx, action, result);
}
throw new MaxStepsExceededError();
}这个循环没有错误处理、没有超时、没有检查点,因此适合演示循环结构,但并不是正式运行时应该使用的结构。后续章节会在这个循环的基础上增加可靠性机制。
1.2 可靠性目标与关键指标
正式部署的 Agent 需要可量化的可靠性目标。Site Reliability Engineering(SRE)的方法可以适配到 Agent 场景:用 SLI 衡量行为,用 SLO 定义目标,用错误预算控制变更风险 [1]。
Agent 的 SLI 可以包括:
- 请求成功率:Agent 在约定时间内完成任务的请求比例;
- 工具调用成功率:所有工具调用中成功且结果有效的次数占比;
- 决策时延:从收到输入到生成行动计划的时间;
- 策略拒绝率:被策略引擎拦截的调用占总调用数的比例。
SLO 不是固定值,而是根据任务风险确定的阈值。错误预算等于 1 - SLO。当错误预算消耗过快时,需要限制 Agent 的自主度,例如把高风险操作改为人工审批。
2 总体架构:运行时组件与职责
正式运行的 Agent 运行时不是单个循环,而是一组相互协作的组件:
- 编排器(Orchestrator):维护主循环,决定下一步执行什么;
- 执行器(Executor):调用工具,处理超时、重试和并发;
- 工具注册中心(Tool Registry):保存工具的元数据和调用方法;
- 状态存储(State Store):保存上下文、检查点和已提交操作;
- 恢复管理器(Recovery Manager):对错误分类,执行恢复策略;
- 可观测性组件:负责日志、Trace 和指标。
下面用 TypeScript 接口描述组件之间的边界。这些接口是讨论用的抽象,用于表达职责划分。
typescript
interface AgentRuntime {
orchestrator: Orchestrator;
executor: ToolExecutor;
registry: ToolRegistry;
checkpoints: CheckpointStore;
recovery: RecoveryManager;
}2.1 编排器与执行器
编排器负责“下一步做什么”。它管理循环、调用模型、判断是否结束。执行器负责“怎么做”。它从工具注册中心取工具,执行调用,并把超时和重试逻辑限制在执行器内部。这样编排器不需要关心某个 API 返回的是 429 还是网络错误。
typescript
interface Planner {
nextAction(ctx: AgentContext): Promise<PlanAction>;
}
interface ToolExecutor {
run(envelope: ToolCallEnvelope): Promise<unknown>;
}2.2 工具注册中心
工具注册中心维护一份工具清单,供两方使用:执行器在运行时按名字查找工具;模型在规划时看到工具的 name、description 和参数 schema。
typescript
type JSONSchema = Record<string, unknown>;
interface Tool {
name: string;
description: string;
params: JSONSchema;
invoke(input: unknown): Promise<unknown>;
}
class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool): void {
this.tools.set(tool.name, tool);
}
get(name: string): Tool {
const tool = this.tools.get(name);
if (!tool) {
throw new UnknownToolError(name);
}
return tool;
}
listForModel(): Array<Pick<Tool, 'name' | 'description' | 'params'>> {
return [...this.tools.values()].map((tool) => ({
name: tool.name,
description: tool.description,
params: tool.params,
}));
}
}listForModel 是教学实现中的方法名,作用是过滤掉 invoke 实现,只向模型暴露描述信息,避免把内部实现暴露给模型。
2.3 状态存储与恢复管理器
状态存储保存 Agent 的执行状态。恢复管理器在错误发生后决定是否重试、是否回退、是否需要加载检查点。两者的职责不同:状态存储只负责持久化,恢复管理器负责恢复策略。
3 错误分类与可恢复性判定
要对错误做出恢复决策,首先需要对错误分类。
3.1 错误来源
根据来源,Agent 执行链路上的错误可以分为四类:
| 错误来源 | 示例 | 可重试性 |
|---|---|---|
| 输入错误 | 用户请求缺少必要信息 | 通常不可重试,需要询问用户 |
| 模型错误 | 模型输出非法参数、超过 token 上限、provider 返回 5xx | 部分可重试;非法输出可重新规划,provider 错误可换模型 |
| 工具错误 | API 超时、HTTP 429、500、权限被拒绝 | 超时和限流可重试;权限拒绝不可重试 |
| 基础设施错误 | 网络分区、进程崩溃、存储不可用 | 可重试,但需要恢复上下文 |
3.2 可恢复错误与不可恢复错误的边界
可恢复错误的特征是:错误是瞬时的,或者重试后可能消失,或者 Agent 可以通过改变行为来绕过。例如 HTTP 503 在短暂退避后可能成功;工具参数不符合 schema,修改参数后可以重新提交。
不可恢复错误的特征是:无论重试多少次,结果都相同。例如策略引擎拒绝、用户权限不足、业务规则不允许。这类错误不应该消耗重试预算,而应该直接进入降级或人工兜底。
在 Agent 场景中,策略绕过(policy bypass)和推理死锁(reasoning loop)属于特殊故障模式。策略绕过被策略引擎拒绝时,应计入故障阈值并记录完整上下文;推理死锁在 token 或迭代次数超预算时打开熔断,需要人工审查后才能恢复 [2]。
4 工具调用可靠性模式
工具调用是 Agent 产生副作用的位置,也是可靠性问题的集中点。
4.1 超时与重试退避
模型在等待工具结果时可能长时间挂起,因此每次工具调用都必须有超时。Node.js 中可以使用 AbortController 取消调用:
typescript
async function callWithTimeout<T>(
invoke: (signal: AbortSignal) => Promise<T>,
timeoutMs: number
): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await invoke(controller.signal);
} finally {
clearTimeout(timer);
}
}注意:AbortController 是浏览器和 Node.js 内置 API。只有底层调用方支持 signal 时才能真正取消远端请求,否则只能中止本地等待。
超时后做重试时,需要使用指数退避和抖动,避免同时重试导致放大压力:
typescript
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function retryWithBackoff<T>(
invoke: (signal: AbortSignal) => Promise<T>,
options: {
maxRetries: number;
initialDelayMs: number;
timeoutMs: number;
}
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
return await callWithTimeout(invoke, options.timeoutMs);
} catch (error) {
lastError = error;
if (attempt === options.maxRetries) {
break;
}
const delay = options.initialDelayMs * 2 ** attempt + Math.random() * 100;
await sleep(delay);
}
}
throw lastError;
}Math.random() * 100 是抖动,作用是打散同一批失败请求的重试时间。重试只适合可恢复错误;如果错误已经被判定为不可恢复,就不应进入 retry 循环。
4.2 幂等键与副作用控制
工具调用可能已经成功,但响应在网络上丢失。Agent 如果直接重试,可能重复下单、重复发送消息。解决方法是幂等键:每次工具调用携带一个唯一键,由工具提供方保存处理结果;当相同键再次到达时,直接返回上次结果。
typescript
import { randomUUID } from 'node:crypto';
interface ToolCallEnvelope {
idempotencyKey: string;
toolName: string;
args: unknown;
}
function createEnvelope(toolName: string, args: unknown): ToolCallEnvelope {
return {
idempotencyKey: `${toolName}:${randomUUID()}`,
toolName,
args,
};
}执行器需要保存已提交的结果:
typescript
interface IdempotencyStore {
lookup(key: string): Promise<unknown | undefined>;
save(key: string, value: unknown): Promise<void>;
}
class InMemoryIdempotencyStore implements IdempotencyStore {
private results = new Map<string, unknown>();
async lookup(key: string): Promise<unknown | undefined> {
return this.results.get(key);
}
async save(key: string, value: unknown): Promise<void> {
this.results.set(key, value);
}
}内存实现无法在进程崩溃后保留数据,实际实现应使用持久化存储。上面这个类适合演示写入和读取逻辑。
对于“创建订单”这类操作,idempotencyKey 必须由调用方生成唯一标识,而不能使用时间戳等可能重复的值。除了幂等,还需要控制副作用。工具注册中心可以声明工具的副作用类别:只读、内部写入、外部系统写入。在执行外部写入之前,编排器应确认当前自主度是否允许该操作;如果不允许,升级为人工授权。
4.3 熔断、降级与任务队列
熔断器用于保护 Agent 不反复调用一个已经失败的依赖。熔断器有三个状态:关闭(CLOSED)、打开(OPEN)、半开(HALF_OPEN)。
下面是一个教学用的简单实现:
typescript
enum CircuitState {
CLOSED,
OPEN,
HALF_OPEN,
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private openedAt = 0;
private halfOpenCalls = 0;
constructor(
private readonly failureThreshold = 5,
private readonly recoveryTimeoutMs = 60_000,
private readonly halfOpenMaxCalls = 3
) {}
async call<T>(invoke: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.openedAt < this.recoveryTimeoutMs) {
throw new CircuitOpenError();
}
this.state = CircuitState.HALF_OPEN;
this.halfOpenCalls = 0;
}
if (this.state === CircuitState.HALF_OPEN) {
if (this.halfOpenCalls >= this.halfOpenMaxCalls) {
throw new CircuitOpenError();
}
this.halfOpenCalls += 1;
}
try {
const result = await invoke();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.state = CircuitState.CLOSED;
this.halfOpenCalls = 0;
}
}
private onFailure(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.state = CircuitState.OPEN;
this.openedAt = Date.now();
this.halfOpenCalls = 0;
return;
}
this.failureCount += 1;
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
this.openedAt = Date.now();
}
}
}这段实现中,熔断器在连续失败达到阈值后进入 OPEN 状态,此时直接拒绝调用。经过 recoveryTimeoutMs 后进入 HALF_OPEN,允许少量探测请求。探测成功则恢复为 CLOSED,失败则重新打开。
在 Agent 场景中,熔断的触发条件不限于 HTTP 5xx。Microsoft 的 agent_sre 包中的 CircuitBreakerConfig 使用 failure_threshold、recovery_timeout_seconds、half_open_max_calls 三个参数,并定义了 policy bypass、timeout injection、deadlock injection 等故障类型 [3]。也就是说,策略绕过和推理死循环也可以作为熔断信号。
降级(degradation)是熔断后的动作。例如,当首选模型 API 熔断后,可以切换到备用模型;当高耗时工具失败后,可以返回缓存结果或给用户一个简化回答。降级策略应当写在恢复管理器中,而不是由模型即兴决定。
任务队列用于削峰和持久化。工具调用可以写入队列,由消费者按顺序执行。队列让调用在进程崩溃后仍然可以恢复,同时限制了并发,避免打垮外部系统。
5 状态持久化与检查点机制
Agent 的上下文保存在内存中时,进程崩溃会丢失全部状态。正式运行时需要持久化状态。
5.1 检查点数据模型
检查点需要足够信息,使恢复后的 Agent 能继续工作。一个最小检查点包含运行标识、步骤号、上下文快照、待执行动作和更新时间:
typescript
interface Checkpoint {
runId: string;
agentId: string;
step: number;
contextSnapshot: unknown;
pendingAction?: ToolCallEnvelope;
updatedAt: string;
}
interface CheckpointStore {
save(cp: Checkpoint): Promise<void>;
load(runId: string): Promise<Checkpoint | undefined>;
}contextSnapshot 是可以在恢复时反序列化的完整上下文。pendingAction 是已经规划但尚未确认执行结果的动作。保存 pendingAction 是为了在崩溃恢复后判断该动作是否已经执行过。
一个内存存储实现如下:
typescript
class InMemoryCheckpointStore implements CheckpointStore {
private checkpoints = new Map<string, Checkpoint>();
async save(cp: Checkpoint): Promise<void> {
this.checkpoints.set(cp.runId, cp);
}
async load(runId: string): Promise<Checkpoint | undefined> {
return this.checkpoints.get(runId);
}
}内存实现不能防进程崩溃,但适合测试。实际实现可以使用数据库,并将 runId 作为主键。
5.2 上下文快照与状态恢复
恢复过程首先加载检查点,然后重建 AgentContext。如果存在 pendingAction,需要向幂等存储确认该动作是否已经提交;未提交则执行,已提交则取回原结果并继续。
typescript
async function recover(
runId: string,
checkpoints: CheckpointStore,
idempotency: IdempotencyStore,
executor: ToolExecutor
): Promise<AgentContext> {
const cp = await checkpoints.load(runId);
if (!cp) {
return createInitialContext(runId);
}
let ctx = await hydrate(cp.contextSnapshot);
if (cp.pendingAction) {
const known = await idempotency.lookup(cp.pendingAction.idempotencyKey);
if (known !== undefined) {
ctx = appendToolResult(ctx, cp.pendingAction, known);
} else {
const result = await executor.run(cp.pendingAction);
await idempotency.save(cp.pendingAction.idempotencyKey, result);
ctx = appendToolResult(ctx, cp.pendingAction, result);
}
}
return ctx;
}createInitialContext 返回一个包含 runId 的初始上下文,hydrate 负责把 contextSnapshot 反序列化为 AgentContext,两者都是辅助函数。
检查点保存的时机决定了最多丢失多少进度。每步保存一次会让存储写入频繁,但恢复粒度最小;只在关键工具调用前保存会减少写入,但崩溃后可能要多重放几步。选择哪种频率,取决于工具调用的成本和副作用风险。
6 异常恢复与人工兜底
6.1 自校正与重规划
当工具执行失败,最简单的恢复策略是把错误信息告诉模型,让它自己修正。这个过程称为自校正(self-correction)。
typescript
async function runWithSelfCorrection(
planner: Planner,
executor: ToolExecutor,
initial: AgentContext,
maxAttempts: number
): Promise<AgentContext> {
let ctx = initial;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const action = await planner.nextAction(ctx);
if (action.type === 'finish') {
return ctx;
}
try {
const result = await executor.run({
toolName: action.toolName,
args: action.args,
});
ctx = appendToolResult(ctx, action, result);
} catch (error) {
ctx.messages.push({
role: 'user',
content: `工具调用失败: ${(error as Error).message}。请修正后重试。`,
});
}
}
throw new MaxAttemptsExceededError();
}使用自校正的前提是模型有能力分析错误。错误信息需要包含足够的上下文,例如参数校验错误、返回状态码和失败原因。如果错误是策略拒绝或权限不足,自校正不会改变结果,只会浪费 token。
重规划是比自校正更宽的策略。重规划不要求修正原动作,而是允许模型生成一个完全不同的计划。例如,原计划查询订单 API 失败,重规划可以先查本地缓存,或者请求用户提供订单号。
6.2 进程崩溃与外部 API 故障恢复
进程崩溃后的恢复依赖检查点和幂等键。恢复管理器加载检查点,通过 pendingAction 和幂等存储判断下一步。外部 API 故障恢复则依赖超时、重试和熔断。超时避免挂起,重试吸收瞬时故障,熔断防止在依赖不可用时继续消耗资源。
一个恢复管理器的最小接口如下:
typescript
interface RecoveryManager {
classify(error: unknown): ErrorClass;
recover(ctx: AgentContext, error: unknown): Promise<RecoveryAction>;
}
type RecoveryAction =
| { type: 'retry' }
| { type: 'replan' }
| { type: 'load_checkpoint'; runId: string }
| { type: 'ask_human' }
| { type: 'give_up' };classify 决定错误类别,recover 返回恢复动作。
6.3 人工兜底决策边界
有些错误不该由 Agent 自己处理。例如:
- 策略引擎拒绝高风险操作;
- 同样的动作重试多次仍然失败;
- 模型反复进入死循环,token 或迭代次数超预算;
- 信任分低于阈值。
这些情况下,恢复管理器应把执行状态保存为检查点,然后交给人工处理。人工兜底不一定是即时审批;它可以作为任务队列里的待处理项,由运营人员在界面中查看请求、上下文和失败原因,然后决定继续执行或终止。
7 执行优化:缩短 Agent 决策路径
7.1 并行工具调用
模型在一步中可能产生多个工具调用。如果这些调用没有依赖关系,可以并行执行,减少总决策时延。
typescript
type ToolAction = { type: 'tool'; toolName: string; args: unknown };
const batch: ToolCallEnvelope[] = plan.actions
.filter((action): action is ToolAction => action.type === 'tool')
.map((action) => createEnvelope(action.toolName, action.args));
const results = await Promise.all(batch.map((envelope) => executor.run(envelope)));并行不是无条件使用。两个工具都写入同一订单时,并发可能造成覆盖。执行器需要知道工具之间的依赖关系,通常做法是将工具声明为只读或读-写两类,只并行只读工具,或由编排器按依赖分组。并行调用还应限制最大并发数,避免打满外部 API。
7.2 缓存、上下文压缩与路由
工具结果缓存可以避免同一请求重复调用外部 API。缓存 key 由工具名和参数计算得到:
typescript
class TtlCache {
private store = new Map<string, { value: unknown; expiresAt: number }>();
async getOrSet<T>(
key: string,
compute: () => Promise<T>,
ttlMs: number
): Promise<T> {
const hit = this.store.get(key);
if (hit && hit.expiresAt > Date.now()) {
return hit.value as T;
}
const value = await compute();
this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
return value;
}
}模型请求本身也可以缓存。对于相同 prompt 和相同工具结果的请求,缓存可以降低成本和时延,但需要保证模型返回不会随时间变化,或设置较短的 TTL。
上下文压缩解决的是上下文窗口耗尽问题。当历史消息变长,Agent 可以丢弃早期低价值信息,或生成摘要替代完整历史:
typescript
async function trimContext(ctx: AgentContext, maxMessages: number): Promise<AgentContext> {
if (ctx.messages.length <= maxMessages) {
return ctx;
}
const removable = ctx.messages.slice(0, ctx.messages.length - maxMessages);
const summary = await summarize(removable);
ctx.messages = [
{ role: 'system', content: `早期对话摘要:${summary}` },
...ctx.messages.slice(-maxMessages),
];
return ctx;
}summarize 在这里表示一个生成摘要的辅助函数,实现时可以使用一个专用模型调用。
路由是另一种降低决策成本的手段:在调用主模型之前,先用轻量分类器判断请求类型,让它走专用 prompt 或专用模型。这样简单请求不会被复杂模型拖慢。
7.3 预取与推测执行
预取(prefetch)是在 Agent 等待模型响应时,预先准备它可能用到的数据。例如,用户输入“查一下天气再帮我定明天的闹钟”,系统可以在规划完成前预先拉取天气数据。预取如果猜错,会浪费一次工具调用,因此通常只对低成本的只读工具使用。
推测执行(speculative execution)同时尝试多个可能路径,最后选择第一个成功的结果。它适合外部依赖不稳定、单次调用延迟很高的情况,但会成倍增加成本。正式运行时需要为推测执行设置预算,不能无限制并发。
8 可观测性与安全审计
8.1 Trace、Span 与执行日志
Agent 一次运行会跨越多个模型调用和多个工具调用。要排查问题,需要把一次运行的所有 Span 关联到同一个 traceId。
一个最小 Span 结构如下:
typescript
interface Span {
traceId: string;
spanId: string;
parentSpanId?: string;
operation: string;
startedAt: number;
endedAt?: number;
attributes: Record<string, unknown>;
}某个工具调用的 Span 可以记录:
tool.nametool.args,如果是敏感参数则先脱敏;tool.result,同样需要脱敏;error.typeattempt
执行日志应该写结构化 JSON,而不是一段字符串,这样日志系统可以按字段查询。
一个 Trace 示例如下:
text
traceId: 0bf3a9... spanId: 0001 operation: agent.run
spanId: 0002 parent: 0001 operation: model.plan
spanId: 0003 parent: 0001 operation: tool.invoke tool.getName
spanId: 0004 parent: 0001 operation: model.plan
spanId: 0005 parent: 0001 operation: tool.invoke tool.sendEmail有了 Trace,可以定位时延开销在模型还是工具,也可以定位熔断发生在哪一步。
8.2 工具调用的访问控制与审计
工具注册中心不能只保存工具函数,还需要保存访问控制元数据。每种工具可以声明:
- 允许调用它的角色;
- 需要的权限范围;
- 是否涉及外部系统写入;
- 是否需要人工授权。
在调用之前,执行器应检查这些声明。审计日志需要记录:
- 谁发起了这次运行(用户、上游系统或定时任务);
- Agent 实例标识;
- 工具名、参数、结果摘要;
- 决策者(模型、人工还是策略引擎);
- 时间戳和 traceId。
审计日志与业务日志不同。它不能因为日志体积大而被截断;在金融、医疗等场景,审计日志可能需要长期保存。
9 测试与演练
9.1 模拟工具与故障注入
测试 Agent 时,真实工具会带来不确定性。一个常用做法是用模拟工具替换外部 API。模拟工具应当支持注入故障,才能测试重试、熔断和恢复逻辑。
下面是一个故障注入包装器:
typescript
class FaultyToolWrapper implements Tool {
constructor(
private readonly inner: Tool,
private readonly failRate: number,
private readonly delayMs: number
) {}
async invoke(input: unknown): Promise<unknown> {
if (Math.random() < this.failRate) {
throw new Error(`${this.inner.name} injected timeout`);
}
if (this.delayMs > 0) {
await sleep(this.delayMs);
}
return this.inner.invoke(input);
}
}故障注入的用例包括:
- 工具超时后是否触发重试;
- 熔断器打开后是否停止调用;
- 进程在工具调用后崩溃,恢复时不会重复调用同一个非幂等工具;
- 策略拒绝时是否进入人工兜底。
9.2 可靠性评估与灰度发布
可靠性评估需要与 SLO 绑定。在测试环境,可以人为注入故障,观察各项 SLI 是否仍在目标范围内。在灰度发布阶段,把新版本 Agent 的流量从低百分比逐步放大,同时监控错误预算消耗。错误预算消耗超过设定阈值时,自动回滚或降低自主度。
这个流程和普通微服务的灰度发布类似。不同点是 Agent 的输入输出不是固定 schema,因此还要评估任务完成质量的指标,而不仅仅是 HTTP 状态码。
10 边界、反模式与演进方向
10.1 常见反模式
以下设计会显著降低 Agent 的可靠性:
- 工具调用不设超时。进程会一直等待,模型调用占用的资源也不会释放。
- 重试非幂等操作且不携带幂等键。网络超时后重试容易产生重复副作用。
- 循环内不保存检查点。进程一崩溃,整个任务从头开始。
- 无限重试。错误预算被耗尽,外部系统被打满。
- 不记录 Trace。故障发生时无法判断是模型、工具还是网络的问题。
- 由模型自行决定是否执行高权限操作。缺少策略层拦截,风险不可控。
10.2 MCP、事件驱动与框架演进
MCP(Model Context Protocol)的目标是标准化工具调用和上下文获取方式。工具注册中心可以适配 MCP server,让不同 Agent 复用同一套工具实现。这样工具治理、权限和审计可以集中在一个边界上完成。
事件驱动是另一个演进方向。Agent 不再顺序执行“规划—行动”,而是通过事件总线消费任务。每次工具完成、策略决策、模型输出都作为事件发布。事件驱动的优点是崩溃恢复时可以重放事件,恢复真实状态;代价是引入消息队列和事件溯源会增加复杂性。
框架本身也在变化。早期框架专注于“让 Agent 更容易调用工具”,而趋于成熟的框架开始内置 SRE 组件。Microsoft 的 Agent Governance Toolkit 将 SLO、错误预算、熔断、混沌工程等作为包提供 [4]。对开发者来说,框架内置这些机制会减少自研成本,但理解其原理仍然是必要的。
