为下面三个错误设计处理策略(重试/快速失败/补偿):
Level 1: 分类错误并设计重试策略错误 A: 调用支付 API 时收到 ETIMEDOUT 错误
错误 B: 插入数据库时收到 duplicate key 错误
错误 C: 上传文件到 S3 时收到 403 Forbidden 错误
要求:
- 判断每个错误是瞬态还是永久
- 说明应该如何处理(重试几次、用什么策略、或快速失败)
- 如果需要重试,写出重试代码片段
学习目标:
- 区分瞬态错误和永久错误
- 掌握重试策略和退避算法
- 学会设计补偿操作和回滚机制
前置要求:第 4 课:状态管理和上下文传递 | 下一课 第 6 课 >>
你的工作流完美运行了 10 次。第 11 次,在第 8 步,API 返回了 503 错误。工作流崩溃。
你加了 try-catch,捕获错误,打印日志,继续执行。第 12 次,数据库连接超时。工作流继续执行,但写入失败,数据不一致。
错误处理不是"加个 try-catch"那么简单。
在工作流中,错误处理需要回答三个问题:1
如果失败的是可选步骤(比如发通知),跳过它继续往下走,这种"非关键的地方失败了不耽误整体"的处理方式就叫优雅降级;但如果失败的是关键步骤,跳过反而会留下不一致状态,应该直接中止。
没有答案,你的工作流要么太脆弱(一个小错误就崩溃),要么太危险(忽略错误继续执行,留下不一致状态)。2
瞬态错误(Transient Errors): 暂时性的,重试后可能成功。3
常见瞬态错误:
特征: 通常由资源竞争、网络波动、临时过载引起,等一会儿重试通常能成功。
永久错误(Permanent Errors): 重试也不会成功,需要修复代码或配置。2
常见永久错误:
特征: 由配置错误、代码 bug、业务规则违反引起,重试只会浪费资源。
判断方法:
对于瞬态错误,重试是第一选择。但重试本身也有学问。3
问题: 如果服务过载导致错误,所有客户端同时重试会加剧过载(惊群效应)。
好处: 每次重试间隔加倍,给服务更多恢复时间,避免持续施压。3
好处: 抖动避免多个客户端在完全相同的时间重试,分散负载。3
这是生产环境的推荐策略。4
关键: 只重试瞬态错误,永久错误立即抛出,避免无意义的重试。5
问题: 如果一个服务持续失败(如数据库崩溃),每个请求都重试 3 次,会白白消耗资源并拖慢整个工作流。如果这个服务还是别的服务的依赖,失败会一路传导下去,变成级联故障。
断路器: 当错误率超过阈值时,暂时停止调用失败的服务,直接快速失败,避免资源浪费。1
关闭状态: 正常工作,请求正常通过,统计错误率。
打开状态: 服务被认为不可用,请求直接快速失败,不调用服务。
半开状态: 超时后尝试少量请求,如果成功则恢复关闭状态,否则继续打开。
使用场景: 调用外部服务、数据库、文件系统等可能批量失败的依赖。1
问题: 工作流执行了 3 个写操作(写数据库、发邮件、更新缓存),第 4 步失败了。如何撤销前 3 步?2
适用场景: 所有操作都在同一个支持事务的数据库中。
局限: 无法跨系统(如数据库 + 文件系统 + API 调用)。
思路: 为每个操作定义一个补偿操作,失败时执行补偿操作撤销已完成的步骤。4
关键点:
forward(正向操作)和 compensate(补偿操作)幂等: 执行 N 次和执行 1 次效果相同。2
好处: 如果步骤因为网络问题执行了两次(第一次超时但实际成功了),幂等性确保不会产生副作用。2
好的工作流在三个层次处理错误:
这一步把每次失败都写进 workflowState.errors,步骤名、错误信息、发生时间都留了下来——这就是错误日志,排查问题时靠的就是这份记录而不是记忆。
三层防护: 操作层重试、步骤层记录、工作流层恢复和通知。
下一课: 第 6 课:真实场景工作流 — 综合运用所有知识,构建三个生产级工作流:代码重构、文档生成、测试自动化
Vasanthan:处理基于代理的工作流中的失败 — https://medium.com/@vasanthancomrads/handling-failures-in-agent-based-workflows-c0fd9489b2ee ↩ ↩2 ↩3
Agents Arcade:代理系统中的错误处理 — https://agentsarcade.com/blog/error-handling-agentic-systems-retries-rollbacks-graceful-failure ↩ ↩2 ↩3 ↩4 ↩5
Augment Code:异步 AI 代理工作流如何在失败中生存 — https://www.augmentcode.com/guides/async-ai-agent-workflows ↩ ↩2 ↩3 ↩4
AWS Marketplace:代理编排 — https://aws.amazon.com/marketplace/build-learn/ai-agent-learning-series/agent-orchestration ↩ ↩2 ↩3
Temporal:AI 代理编排的 11 种生产失败模式 — https://www.xgrid.co/resources/temporal-ai-agent-orchestration-failure-patterns/ ↩
错误 A: 调用支付 API 时收到 ETIMEDOUT 错误
错误 B: 插入数据库时收到 duplicate key 错误
错误 C: 上传文件到 S3 时收到 403 Forbidden 错误
要求:
要求:
Jot down thoughts, sticking points, things you didn't get. Written to this course's appendix only — the lesson file is never touched.
function classifyError(error) {
// HTTP 状态码判断
if (error.status === 429) return 'transient'; // 速率限制
if (error.status >= 500) return 'transient'; // 服务端错误
if (error.status === 404) return 'permanent'; // 资源不存在
if (error.status === 401) return 'permanent'; // 权限问题
// 错误类型判断
if (error.code === 'ETIMEDOUT') return 'transient'; // 超时
if (error.code === 'ECONNREFUSED') return 'transient'; // 连接拒绝
if (error.code === 'ENOTFOUND') return 'permanent'; // DNS 失败
// 错误消息判断
if (error.message.includes('rate limit')) return 'transient';
if (error.message.includes('permission denied')) return 'permanent';
// 默认为永久(保守策略)
return 'permanent';
}
async function retryWithFixedDelay(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`尝试 ${attempt} 失败,${delay}ms 后重试...`);
await sleep(delay);
}
}
}
// 使用
const data = await retryWithFixedDelay(
() => fetchAPI('/users'),
3,
1000
);
async function retryWithExponentialBackoff(fn, maxAttempts = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`尝试 ${attempt} 失败,${delay}ms 后重试...`);
await sleep(delay);
}
}
}
// 延迟序列: 1s, 2s, 4s, 8s, ...
async function retryWithBackoffAndJitter(fn, maxAttempts = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * exponentialDelay;
const delay = exponentialDelay + jitter;
console.log(`尝试 ${attempt} 失败,${delay.toFixed(0)}ms 后重试...`);
await sleep(delay);
}
}
}
// 延迟序列(带随机性): 1.2s, 3.7s, 6.1s, ...
async function retrySelective(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const errorType = classifyError(error);
if (errorType === 'permanent') {
console.log('永久错误,不重试');
throw error;
}
if (attempt === maxAttempts) {
console.log(`${maxAttempts} 次重试均失败`);
throw error;
}
const delay = 1000 * Math.pow(2, attempt - 1) * (1 + Math.random());
console.log(`瞬态错误,${delay.toFixed(0)}ms 后重试...`);
await sleep(delay);
}
}
}
关闭(Closed) ──错误率 > 阈值──→ 打开(Open) ↑ ↓ └──测试成功──← 半开(Half-Open) ←─超时后class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5; // 失败多少次打开
this.resetTimeout = options.resetTimeout || 60000; // 60 秒后尝试恢复
this.state = 'closed';
this.failureCount = 0;
this.nextAttempt = null;
}
async execute(fn) {
// 打开状态:直接快速失败
if (this.state === 'open') {
if (Date.now() < this.nextAttempt) {
throw new Error('断路器打开,服务暂时不可用');
}
// 超时后进入半开状态
this.state = 'half-open';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'half-open') {
this.state = 'closed';
console.log('断路器恢复关闭状态');
}
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
this.nextAttempt = Date.now() + this.resetTimeout;
console.log(`断路器打开,${this.resetTimeout}ms 后尝试恢复`);
}
}
}
// 使用
const breaker = new CircuitBreaker({ failureThreshold: 3, resetTimeout: 30000 });
async function callAPI() {
return await breaker.execute(async () => {
return await fetch('/api/data');
});
}
async function transactionalWorkflow() {
const tx = await db.beginTransaction();
try {
await tx.insert('users', userData);
await tx.update('accounts', accountData);
await tx.insert('logs', logData);
await tx.commit();
console.log('事务提交成功');
} catch (error) {
await tx.rollback();
console.log('事务回滚');
throw error;
}
}
async function sagaWorkflow() {
const completed = [];
const steps = [
{
name: '创建订单',
forward: async () => {
const order = await createOrder(orderData);
return { orderId: order.id };
},
compensate: async (context) => {
await deleteOrder(context.orderId);
console.log(`补偿: 删除订单 ${context.orderId}`);
}
},
{
name: '扣减库存',
forward: async (context) => {
await decrementInventory(orderData.items);
return context;
},
compensate: async (context) => {
await incrementInventory(orderData.items);
console.log('补偿: 恢复库存');
}
},
{
name: '扣款',
forward: async (context) => {
await chargePayment(context.orderId, orderData.amount);
return context;
},
compensate: async (context) => {
await refundPayment(context.orderId);
console.log(`补偿: 退款订单 ${context.orderId}`);
}
},
{
name: '发送确认邮件',
forward: async (context) => {
await sendEmail(orderData.email, context.orderId);
return context;
},
compensate: async (context) => {
await sendEmail(orderData.email, '订单已取消');
console.log('补偿: 发送取消邮件');
}
}
];
let context = {};
try {
// 正向执行所有步骤
for (const step of steps) {
console.log(`执行: ${step.name}`);
context = await step.forward(context);
completed.push(step);
}
console.log('工作流成功完成');
return context;
} catch (error) {
console.log(`失败于: ${completed.length + 1}/${steps.length} 步`);
// 反向执行补偿操作
for (let i = completed.length - 1; i >= 0; i--) {
const step = completed[i];
try {
await step.compensate(context);
} catch (compensateError) {
console.error(`补偿失败: ${step.name}`, compensateError);
// 补偿失败需要人工介入
}
}
throw error;
}
}
// ❌ 非幂等: 重复执行会累加
async function incrementCounter(userId) {
const current = await getCounter(userId);
await setCounter(userId, current + 1);
}
// ✓ 幂等: 重复执行结果相同
async function setCounter(userId, value) {
await db.update('counters', { userId }, { value });
}
// ✓ 幂等: 用唯一 ID 去重
async function processOrder(orderId, orderData) {
// 检查是否已处理
const existing = await db.get('orders', orderId);
if (existing) {
console.log(`订单 ${orderId} 已处理,跳过`);
return existing;
}
// 首次处理
const result = await createOrder(orderData);
await db.insert('orders', { id: orderId, ...result });
return result;
}
async function callAPIWithRetry(endpoint) {
return await retryWithBackoffAndJitter(
async () => {
const response = await fetch(endpoint);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
},
3,
1000
);
}
async function workflowStep(stepName, fn) {
try {
console.log(`开始: ${stepName}`);
const result = await fn();
console.log(`完成: ${stepName}`);
return result;
} catch (error) {
console.error(`失败: ${stepName}`, error.message);
// 记录到状态
workflowState.errors.push({
step: stepName,
error: error.message,
timestamp: Date.now()
});
throw error;
}
}
async function robustWorkflow() {
const checkpointFile = '.workflow-checkpoint.json';
try {
// 加载检查点
let state = await loadCheckpoint(checkpointFile) || { phase: 'init' };
// 执行各阶段(带跳过逻辑)
if (state.phase === 'init') {
state.data = await workflowStep('收集数据', collectData);
state.phase = 'collected';
await saveCheckpoint(checkpointFile, state);
}
if (state.phase === 'collected') {
state.processed = await workflowStep('处理数据', () => processData(state.data));
state.phase = 'processed';
await saveCheckpoint(checkpointFile, state);
}
if (state.phase === 'processed') {
await workflowStep('保存结果', () => saveResults(state.processed));
state.phase = 'completed';
await saveCheckpoint(checkpointFile, state);
}
return state;
} catch (error) {
// 工作流级错误处理
console.error('工作流失败:', error);
// 通知人工
await notifyAdmin({
workflow: 'robustWorkflow',
phase: workflowState.phase,
error: error.message
});
throw error;
}
}