Agent错误恢复
要点
- Agent 的执行链路比单次 API 调用长得多——一次任务可能经历多轮 LLM 调用和工具调用,任何一步出错都可能导致整条链路中断
- 工具调用的错误大致分三类:可重试的瞬时错误、不可重试的逻辑错误、部分成功的中间状态——不同类型的处理方式完全不同
- 重试策略不能简单地「失败就再来一次」,需要根据错误类型决定要不要重试、用什么间隔、最多重试几次
- 部分失败时 Agent 不能假装什么都没发生,也不能把所有结果丢弃——需要区分哪些步骤成功了、哪些失败了,然后决定怎么继续
- 涉及数据库写入、外部 API 操作的工具需要有回滚机制,失败时把已经产生的副作用撤掉
- 降级策略让 Agent 在主路径走不通时换一个代价更低但仍能完成任务的方式
- 错误日志要同时满足两个读者:给人看的需要结构化、可搜索;给 LLM 看的需要自然语言、包含上下文
1. 从一个问题开始
回到前面 Agent 循环的代码,工具执行失败时只是把错误信息塞回消息数组,交给 LLM 自行处理:
// src/agent/executor.ts(前面文章里的写法)
try {
const result = await handler(input)
return { toolCallId: toolCall.id, result }
} catch (error) {
return {
toolCallId: toolCall.id,
result: { error: `工具执行失败:${error instanceof Error ? error.message : '未知错误'}` },
}
}这种做法在演示场景下够用。但放到生产环境会碰到几个问题:
- 一个天气查询 API 因为网络抖动返回 500,Agent 直接把错误返回给 LLM,LLM 可能再调一次——如果网络还没恢复,又是一次失败。来回几次,用户的请求就被消耗完了,但任务根本没完成
- 一个订单创建工具执行到一半失败了——库存已扣减但订单记录没写入,数据库处于不一致状态
- Agent 调了 3 个工具,2 个成功 1 个失败,Agent 不知道哪些结果可以继续用
这些问题的共同点是:错误恢复不能只靠 LLM 自己判断,需要代码层面提供结构化的策略。
2. 错误分类:不是所有错误都该同一种方式处理
在写任何恢复逻辑之前,先把 Agent 会遇到的错误分成三类。分类的目的是让代码知道「该拿这个错误怎么办」——重试、跳过还是直接报错给用户。
2.1 可重试的瞬时错误
这类错误的特点是:当前这次失败了,但过一会儿再试大概率能成功。常见的原因:
- 网络超时、DNS 解析失败
- 外部 API 返回 429(限流)或 503(服务暂时不可用)
- 数据库连接池暂时耗尽
- 临时的资源竞争(乐观锁冲突)
这类错误的处理策略是等待一段时间后重试。关键是「等待」——不加间隔地立刻重试可能会让情况更糟(比如对方已经限流了,你继续高频请求只会继续被拒)。
2.2 不可重试的逻辑错误
这类错误重试多少次结果都一样:
- 参数校验失败(用户传了一个不存在的城市名)
- 权限不足(当前用户没有删除订单的权限)
- 资源不存在(查一个已经被删除的记录)
- 业务规则冲突(余额不足、库存不够)
这类错误不该重试,应该把错误原因反馈给 LLM,让 LLM 调整策略——比如换一个参数、换一个工具、或者直接告诉用户为什么做不到。
2.3 部分成功
一个工具调用里,有些操作成功了,有些失败了。比如:
- 批量创建 10 条记录,7 条成功 3 条失败
- 调用一个聚合接口,其中某个数据源超时,其他数据源正常返回
部分成功比完全失败更棘手——Agent 需要知道哪些成功了(可以继续用),哪些失败了(需要处理),然后决定是继续往下走还是整体回滚。
3. 用类型系统区分错误类型
先定义一个错误分类体系,让后续所有重试、降级、回滚逻辑都有统一判断依据。
// src/agent/errors.ts
export type ErrorCategory = 'transient' | 'permanent' | 'partial'
export class AgentToolError extends Error {
constructor(
message: string,
public readonly category: ErrorCategory,
public readonly toolName: string,
public readonly retryable: boolean = category === 'transient',
public readonly partialResult?: unknown,
) {
super(message)
this.name = 'AgentToolError'
}
}工具实现方在抛出错误时负责分类:
// src/tools/weather.ts
export async function fetchWeather(city: string): Promise<WeatherData> {
const res = await fetch(`https://api.weather.com/v1/${city}`, {
signal: AbortSignal.timeout(5000),
})
if (res.status === 429 || res.status >= 500) {
throw new AgentToolError(
`天气 API 返回 ${res.status}`,
'transient',
'get_weather',
)
}
if (res.status === 404) {
throw new AgentToolError(
`未找到城市「${city}」的天气数据`,
'permanent',
'get_weather',
)
}
return res.json()
}分类的准确性直接影响恢复策略的效果。如果一个参数错误被标记为 transient,Agent 会无意义地重试;如果一个网络超时被标记为 permanent,Agent 会过早放弃。
4. 重试策略:指数退避
对可重试错误,最常见的策略是指数退避(Exponential Backoff)。每次重试的等待时间翻倍,避免对故障服务造成压力。
// src/agent/retry.ts
import { AgentToolError } from './errors'
type RetryConfig = {
maxRetries: number // 最大重试次数
baseDelayMs: number // 初始等待时间(毫秒)
maxDelayMs: number // 最大等待时间上限
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 500,
maxDelayMs: 10000,
}
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {},
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs } = { ...DEFAULT_CONFIG, ...config }
let lastError: unknown
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
// 不可重试的错误直接抛出,不浪费重试次数
if (error instanceof AgentToolError && !error.retryable) {
throw error
}
if (attempt < maxRetries) {
// 指数退避:500ms -> 1000ms -> 2000ms -> ...
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs)
// 加一点随机抖动,防止多个请求同时恢复后再次撞车
const jitter = delay * 0.2 * Math.random()
await sleep(delay + jitter)
}
}
}
throw lastError
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}几个设计选择值得说明:
- 不可重试错误直接跳出 — 权限不足、参数错误这类错误,重试 100 次结果都一样,不该浪费时间
- 指数退避加上抖动 — 如果 100 个用户的 Agent 同时遇到 API 限流,不加抖动的重试会在同一时刻全部恢复,再次触发限流
- 设等待时间上限 — 指数增长很快,不设上限的话第 5 次重试可能要等 30 秒以上
在工具执行器里使用:
// src/agent/executor.ts
import { withRetry } from './retry'
export async function executeTool(toolCall: ToolCall): Promise<ToolResult> {
const handler = toolHandlers.get(toolCall.name)
if (!handler) {
return { toolCallId: toolCall.id, result: { error: `未知工具:${toolCall.name}` } }
}
try {
const result = await withRetry(() => handler(toolCall.arguments))
return { toolCallId: toolCall.id, result }
} catch (error) {
const message = error instanceof Error ? error.message : '工具执行失败'
return { toolCallId: toolCall.id, result: { error: message } }
}
}5. 部分失败处理
部分失败的处理取决于工具内部是否支持事务性。有些操作可以拆成独立的子步骤,每一步单独成功或失败;有些操作必须全部成功或全部回滚。
5.1 返回部分结果
对于批量操作,工具可以把已成功和失败的部分分开返回,让 Agent 决定后续怎么处理:
// src/tools/batch-create.ts
import { AgentToolError } from '../agent/errors'
interface BatchResult {
succeeded: Array<{ id: string; input: unknown }>
failed: Array<{ input: unknown; error: string }>
}
export async function batchCreateOrders(
items: Array<{ productId: string; quantity: number }>,
): Promise<BatchResult> {
const result: BatchResult = { succeeded: [], failed: [] }
for (const item of items) {
try {
const order = await createSingleOrder(item)
result.succeeded.push({ id: order.id, input: item })
} catch (error) {
result.failed.push({
input: item,
error: error instanceof Error ? error.message : '创建失败',
})
}
}
// 如果全部失败,抛出部分成功错误
if (result.succeeded.length === 0) {
throw new AgentToolError(
'所有订单项创建均失败',
'permanent',
'batch_create_orders',
)
}
// 部分失败时,把结果和失败信息一起返回
if (result.failed.length > 0) {
throw new AgentToolError(
`${result.failed.length}/${items.length} 项创建失败`,
'partial',
'batch_create_orders',
false,
result,
)
}
return result
}5.2 在 Agent 循环中处理部分成功
Agent 循环收到部分成功的结果时,需要把成功和失败的信息都传递给 LLM,让它决定下一步:
// src/agent/executor.ts
export async function executeTool(toolCall: ToolCall): Promise<ToolResult> {
const handler = toolHandlers.get(toolCall.name)
if (!handler) {
return { toolCallId: toolCall.id, result: { error: `未知工具:${toolCall.name}` } }
}
try {
const result = await withRetry(() => handler(toolCall.arguments))
return { toolCallId: toolCall.id, result }
} catch (error) {
if (error instanceof AgentToolError && error.category === 'partial') {
// 部分成功:把已完成和失败的部分都告诉 LLM
return {
toolCallId: toolCall.id,
result: {
status: 'partial_success',
succeeded: error.partialResult,
failed: error.message,
guidance: '以下项目已成功,以下项目失败。请告知用户成功部分的结果,并对失败部分说明原因。',
},
}
}
// 完全失败
const message = error instanceof Error ? error.message : '工具执行失败'
return { toolCallId: toolCall.id, result: { error: message } }
}
}LLM 收到 partial_success 后,可以告诉用户「10 项中 7 项已创建成功,3 项因库存不足失败」,而不是只说「创建失败」。
6. 状态回滚
当工具执行涉及多个步骤且必须全部成功时,中途失败需要把已完成的步骤撤掉。最常见的方式是记录每一步的操作,失败时按相反顺序撤销。
// src/agent/rollback.ts
type RollbackAction = () => Promise<void>
export class RollbackContext {
private actions: RollbackAction[] = []
private logs: string[] = []
// 注册一个回滚操作——在执行完正向操作之后调用
register(action: RollbackAction, description: string) {
this.actions.unshift(action) // 插入到头部,回滚时按 LIFO 顺序
this.logs.push(`已注册回滚:${description}`)
}
// 执行所有已注册的回滚操作
async rollbackAll(): Promise<{ rolledBack: string[]; failed: string[] }> {
const rolledBack: string[] = []
const failed: string[] = []
for (let i = 0; i < this.actions.length; i++) {
try {
await this.actions[i]()
rolledBack.push(`回滚成功:第 ${i + 1} 步`)
} catch (error) {
const msg = error instanceof Error ? error.message : '未知错误'
failed.push(`第 ${i + 1} 步回滚失败:${msg}`)
}
}
return { rolledBack, failed }
}
}在一个多步骤工具中使用:
// src/tools/order-workflow.ts
import { RollbackContext } from '../agent/rollback'
import { AgentToolError } from '../agent/errors'
export async function createOrderWithInventory(
input: { productId: string; quantity: number; userId: string },
) {
const ctx = new RollbackContext()
try {
// 第一步:扣减库存
const inventory = await deductInventory(input.productId, input.quantity)
ctx.register(
() => restoreInventory(input.productId, input.quantity),
'恢复库存',
)
// 第二步:创建订单记录
const order = await createOrderRecord({
userId: input.userId,
productId: input.productId,
quantity: input.quantity,
inventorySnapshotId: inventory.snapshotId,
})
ctx.register(
() => cancelOrderRecord(order.id),
'取消订单记录',
)
// 第三步:扣款
await chargeUser(input.userId, order.totalAmount)
ctx.register(
() => refundUser(input.userId, order.totalAmount),
'退款',
)
return { orderId: order.id, status: 'completed' }
} catch (error) {
// 任何一步失败,回滚前面所有步骤
const result = await ctx.rollbackAll()
throw new AgentToolError(
`订单创建失败,已回滚。回滚结果:${result.rolledBack.join(';')}${result.failed.length > 0 ? `;回滚失败:${result.failed.join(';')}` : ''}`,
'permanent',
'create_order_with_inventory',
)
}
}这个模式有一个需要注意的地方:回滚本身也可能失败。 比如退款 API 超时,回滚动作没有完成。这种情况下,错误日志里需要记录回滚失败的详情,后续需要人工介入或定时任务补偿。
7. 降级策略
降级是指主路径走不通时,换一个代价更低但功能受限的方式继续完成任务。降级的核心问题是:在丢失部分能力的情况下,还能给用户多少有用的结果。
7.1 工具级降级
一个工具调用失败后,尝试用另一个功能较弱但可用的替代方式:
// src/tools/fallback.ts
type ToolHandler = (input: Record<string, unknown>) => Promise<unknown>
interface FallbackChain {
primary: { name: string; handler: ToolHandler }
fallbacks: Array<{ name: string; handler: ToolHandler; note: string }>
}
export async function executeWithFallback(
chain: FallbackChain,
input: Record<string, unknown>,
): Promise<{ result: unknown; usedFallback: string | null }> {
try {
const result = await chain.primary.handler(input)
return { result, usedFallback: null }
} catch (primaryError) {
// 主路径失败,记录错误后尝试降级
console.warn(`主工具 ${chain.primary.name} 失败:`, primaryError)
for (const fallback of chain.fallbacks) {
try {
const result = await fallback.handler(input)
return { result, usedFallback: fallback.note }
} catch {
// 当前降级也失败,继续尝试下一个
}
}
// 所有降级路径都走不通
throw primaryError
}
}使用示例——主路径调用精确的地理编码 API,降级后使用缓存的静态数据:
// src/tools/weather-fallback.ts
import { executeWithFallback } from './fallback'
const weatherChain: FallbackChain = {
primary: {
name: 'get_weather_live',
handler: (input) => fetchWeatherLive(input.city as string),
},
fallbacks: [
{
name: 'get_weather_cache',
handler: (input) => fetchWeatherFromCache(input.city as string),
note: '使用缓存数据(可能不是最新的)',
},
{
name: 'get_weather_static',
handler: (input) => fetchWeatherStatic(input.city as string),
note: '使用历史同期数据(仅供参考)',
},
],
}
export async function getWeatherWithFallback(input: Record<string, unknown>) {
const { result, usedFallback } = await executeWithFallback(weatherChain, input)
return {
...result as Record<string, unknown>,
_fallbackNote: usedFallback,
}
}降级成功后返回的 _fallbackNote 字段会被传递给 LLM,让它在回复用户时说明数据来源——「天气数据来自缓存,可能不是最新的」。
7.2 Agent 级降级
某些情况下不是某个工具失败,而是整条 LLM 调用链路出了问题(比如 LLM API 不可用)。这时可以让 Agent 跳过推理,用预定义的规则给出回复:
// src/agent/fallback-handler.ts
type FallbackRule = {
keywords: string[]
response: string
}
const fallbackRules: FallbackRule[] = [
{
keywords: ['天气', '气温', '下雨'],
response: '暂时无法查询实时天气,建议查看中国天气网或手机天气 App。',
},
{
keywords: ['订单', '物流', '快递'],
response: '暂时无法查询订单信息,请前往订单中心查看,或联系客服处理。',
},
]
export function getFallbackResponse(userInput: string): string | null {
for (const rule of fallbackRules) {
if (rule.keywords.some((kw) => userInput.includes(kw))) {
return rule.response
}
}
return null
}在路由中使用:
// src/routes/agent.ts
import { getFallbackResponse } from '../agent/fallback-handler'
agentApp.post('/chat', async (c) => {
const { message } = await c.req.json<{ message: string }>()
try {
const reply = await runAgent(message, systemPrompt)
return c.json({ reply })
} catch (error) {
// Agent 主流程失败,尝试降级回复
const fallback = getFallbackResponse(message)
if (fallback) {
return c.json({ reply: fallback, _degraded: true })
}
return c.json({ reply: '服务暂时不可用,请稍后再试。' }, 503)
}
})降级回复的质量不如 Agent 正常推理的结果,但至少用户不会看到一个错误页面。
8. 超时控制
Agent 的一次任务可能涉及多轮 LLM 调用和工具调用,如果某一轮卡住,整条链路会被阻塞。需要在三个层面设置超时:
// src/agent/timeout.ts
// 1. 单次工具调用超时
export async function withTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
toolName: string,
): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
return await Promise.race([
fn(),
new Promise<never>((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new AgentToolError(
`工具「${toolName}」执行超时(${timeoutMs}ms)`,
'transient',
toolName,
))
})
}),
])
} finally {
clearTimeout(timer)
}
}
// 2. 单次 LLM 调用超时
export async function callLLMWithTimeout(
messages: Message[],
timeoutMs = 30000,
): Promise<LLMResponse> {
return withTimeout(
() => callLLM(messages),
timeoutMs,
'llm_call',
)
}
// 3. 整个 Agent 任务超时
export async function runAgentWithDeadline(
userInput: string,
systemPrompt: string,
totalTimeoutMs = 60000,
): Promise<string> {
const deadline = Date.now() + totalTimeoutMs
// 把 deadline 传进循环,每轮检查剩余时间
for (let i = 0; i < MAX_ITERATIONS; i++) {
if (Date.now() > deadline) {
return '任务处理时间过长,已中断。请简化请求后重试。'
}
// ... 正常循环逻辑
}
return '已达到最大执行轮次。'
}三层超时的分工:
- 工具级:防止单个外部 API 调用卡住(比如天气 API 没响应)
- LLM 调用级:防止模型推理时间过长
- 任务级:防止整条链路无限循环或反复重试
9. 错误日志与监控
错误日志要同时服务两个读者:人需要排查问题,LLM 需要理解上下文做出正确决策。两者的格式要求不同,不该混在一起。
9.1 返回给 LLM 的错误信息
给 LLM 的错误信息用自然语言,包含三件事:发生了什么、可能的原因、可以怎么调整。
// src/agent/error-formatter.ts
export function formatErrorForLLM(error: unknown, toolName: string): string {
if (error instanceof AgentToolError) {
switch (error.category) {
case 'transient':
return `工具「${toolName}」暂时不可用(${error.message})。已尝试重试但仍未恢复。你可以稍后再次尝试,或者换一个工具完成同样的目标。`
case 'permanent':
return `工具「${toolName}」无法完成操作:${error.message}。这个问题不会因为重试而解决,请调整参数或选择其他方式。`
case 'partial':
return `工具「${toolName}」部分完成:${error.message}。请根据已成功和失败的部分决定下一步。`
}
}
return `工具「${toolName}」执行失败,原因未知。请尝试换一种方式。`
}9.2 结构化日志(给人和监控系统看)
服务端日志用结构化格式,方便搜索和统计:
// src/agent/error-logger.ts
interface ErrorLog {
timestamp: string
level: 'warn' | 'error'
event: string
toolName: string
userId: string
errorCategory: string
errorMessage: string
attemptCount: number
requestId: string
}
export function logToolError(
error: unknown,
toolName: string,
userId: string,
attemptCount: number,
requestId: string,
) {
const log: ErrorLog = {
timestamp: new Date().toISOString(),
level: error instanceof AgentToolError && error.category === 'transient' ? 'warn' : 'error',
event: 'tool_execution_error',
toolName,
userId,
errorCategory: error instanceof AgentToolError ? error.category : 'unknown',
errorMessage: error instanceof Error ? error.message : '未知错误',
attemptCount,
requestId,
}
console.error(JSON.stringify(log))
}9.3 错误统计指标
在请求的 response header 或者管理接口里暴露错误统计,用于监控 Agent 的健康度:
// src/agent/metrics.ts
class AgentMetrics {
private counts = {
totalCalls: 0,
transientErrors: 0,
permanentErrors: 0,
partialSuccesses: 0,
rollbacks: 0,
fallbacks: 0,
}
recordToolResult(toolName: string, result: 'success' | 'transient' | 'permanent' | 'partial' | 'rollback' | 'fallback') {
this.counts.totalCalls++
switch (result) {
case 'transient': this.counts.transientErrors++; break
case 'permanent': this.counts.permanentErrors++; break
case 'partial': this.counts.partialSuccesses++; break
case 'rollback': this.counts.rollbacks++; break
case 'fallback': this.counts.fallbacks++; break
}
}
getSummary() {
const { totalCalls, transientErrors, permanentErrors } = this.counts
return {
...this.counts,
// 瞬时错误率——过高说明外部依赖不稳定
transientErrorRate: totalCalls > 0 ? transientErrors / totalCalls : 0,
// 不可恢复错误率——过高说明工具配置或参数有问题
permanentErrorRate: totalCalls > 0 ? permanentErrors / totalCalls : 0,
}
}
}这些指标可以帮助判断系统的健康状况。瞬时错误率持续升高可能意味着外部 API 出了问题;不可恢复错误率升高可能是 LLM 在生成错误的工具参数;回滚次数增多可能意味着工具逻辑有 bug。
10. 整合:带错误恢复的 Agent 循环
把前面所有的模块串起来,看一个完整的带错误恢复的 Agent 循环:
// src/agent/loop-recovery.ts
import { withRetry } from './retry'
import { withTimeout } from './timeout'
import { formatErrorForLLM } from './error-formatter'
import { logToolError } from './error-logger'
import type { Message } from './types'
const MAX_ITERATIONS = 10
const TOOL_TIMEOUT_MS = 10000
const TASK_TIMEOUT_MS = 60000
export async function runAgentWithRecovery(
userInput: string,
systemPrompt: string,
userId: string,
): Promise<string> {
const messages: Message[] = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userInput },
]
const deadline = Date.now() + TASK_TIMEOUT_MS
const requestId = crypto.randomUUID()
for (let i = 0; i < MAX_ITERATIONS; i++) {
// 检查任务级超时
if (Date.now() > deadline) {
messages.push({
role: 'assistant',
content: '任务处理时间过长,已中断。建议简化请求后重试。',
})
return '任务处理时间过长,已中断。建议简化请求后重试。'
}
let response: LLMResponse
try {
response = await callLLMWithTimeout(messages, 30000)
} catch (error) {
// LLM 调用失败,直接告知用户
return 'AI 服务暂时不可用,请稍后再试。'
}
// 没有工具调用,返回最终回复
if (response.toolCalls.length === 0) {
return response.text
}
messages.push({
role: 'assistant',
content: response.text,
toolCalls: response.toolCalls,
})
// 执行工具,带重试、超时和错误格式化
for (const toolCall of response.toolCalls) {
let result: unknown
let attemptCount = 0
try {
result = await withRetry(
() => withTimeout(
() => executeTool(toolCall),
TOOL_TIMEOUT_MS,
toolCall.name,
),
{ maxRetries: 2, baseDelayMs: 500 },
)
// 注意:withRetry 成功时返回的是 executeTool 的结果
// executeTool 内部已经处理了错误分类和回滚
} catch (error) {
attemptCount = 3 // 已用完重试次数
result = { error: formatErrorForLLM(error, toolCall.name) }
logToolError(error, toolCall.name, userId, attemptCount, requestId)
}
messages.push({
role: 'tool',
toolCallId: toolCall.id,
content: JSON.stringify(result),
})
}
}
return 'Agent 已达到最大执行轮次,任务未能完成。'
}整个循环的错误恢复逻辑:
- 每轮循环开始前检查任务级超时
- LLM 调用有独立的超时控制,失败时直接告知用户
- 工具调用带重试(指数退避)和超时,重试失败后格式化错误信息给 LLM
- 错误信息用自然语言告诉 LLM 发生了什么、怎么调整
- 结构化日志记录每次工具错误的详情,用于监控和排查
总结
回顾一下这篇的要点:
- Agent 的错误分三类:可重试的瞬时错误(网络超时、限流)、不可重试的逻辑错误(权限不足、参数错误)、部分成功(批量操作中部分失败)。不同类型的错误用不同的方式处理
- 重试策略用指数退避加抖动,避免对故障服务造成雪崩压力。不可重试的错误直接跳过重试
- 部分失败的工具把成功和失败的部分分开返回,Agent 把两部分信息都传递给 LLM 做后续决策
- 多步骤工具用 RollbackContext 记录正向操作,失败时按 LIFO 顺序回滚。回滚本身也可能失败,需要记录日志等待人工介入
- 降级分两层:工具级用 fallback chain 切换替代数据源,Agent 级用预定义规则兜底
- 超时控制在三个层面:单次工具调用、单次 LLM 调用、整个任务
- 错误信息分两种格式:给 LLM 的用自然语言带上下文,给人和服务端监控的用结构化 JSON
错误恢复是 Agent 从演示走向生产的必经之路。前面几节实现的工具注册、权限控制、记忆系统、错误恢复加在一起,构成了一个可以应对真实场景的 Agent 框架。