0

AI Agent长时任务超时中断与断点续跑实战

2026.06.06 | youres | 23次围观

引言:长时任务是一座必须跨过的桥

如果你做过AI Agent开发,一定遇到过这种情况:一个复杂的多步骤任务跑了20分钟,马上就要出结果了,突然网络抖动,LLM API超时,整个任务前功尽弃。用户骂街,老板质疑,你只能苦笑着重新开始。这篇文章讲的就是如何让你的AI Agent具备「断点续跑」能力——任务中断后,从断点恢复,而不是从头再来。

一、为什么AI Agent的长时任务这么脆弱

传统软件的重试机制,直接套到AI Agent上基本都会翻车。原因有三点:

  • 状态不可序列化:Agent的中间推理状态、工具调用上下文、LLM的thinking过程,这些东西不是简单的变量,很难存到数据库里
  • 非幂等工具调用:你的Agent可能已经给第三方API发了请求,重试一次就变成发两次,数据就重复了
  • Token消耗不友好:从头重试意味着重新跑一遍所有的LLM调用,Token费用直接翻倍

我自己踩过最惨的一次坑:一个RAG问答任务,跑了35分钟(大量文档分块+多轮推理),最后一步写数据库时网络断了。重试,又35分钟。老板问我为什么一个问答要花70分钟,我无法回答。

二、断点续跑的核心设计思路

断点续跑的本质是把任务的执行过程变成可持久化的状态机。每次进入下一个节点前,先把当前状态存下来。中断后恢复时,从存储中读出状态,从断点继续。

2.1 状态模型设计

一个可续跑的Agent状态需要包含以下字段:

interface AgentCheckpoint {
  taskId: string;           // 任务唯一ID
  currentNode: string;      // 当前执行到的节点
  context: Record;  // 全局上下文(变量、中间结果)
  toolCallHistory: Array<{  // 工具调用历史(用于幂等判断)
    toolName: string;
    params: any;
    result: any;
    timestamp: number;
  }>;
  llmCallHistory: Array<{   // LLM调用历史(避免重复推理)
    prompt: string;
    response: string;
    tokenUsage: number;
  }>;
  createdAt: number;
  updatedAt: number;
}

2.2 检查点(Checkpoint)存放位置的选择

存储方案优点缺点适用场景
本地SQLite零依赖,读写快,支持原子操作分布式场景不支持单机Agent,快速原型
Redis高性能,支持TTL自动清理需要额外部署生产环境,高并发
PostgreSQL支持JSONB,查询灵活,事务保证重量级企业级,需要复杂查询

我的建议:前期用SQLite快速验证,上线后用Redis。PostgreSQL有点杀鸡用牛刀,除非你的检查点数据需要复杂关联查询。

三、实战:给OpenClaw Agent加断点续跑

下面是我在一个真实项目中用的实现方案,经过生产验证。

3.1 检查点管理器

const fs = require('fs');
const path = require('path');

class CheckpointManager {
  constructor(dbPath = './agent_checkpoints.db') {
    this.dbPath = dbPath;
    this.initDB();
  }

  initDB() {
    // 使用SQLite存储检查点
    const Database = require('better-sqlite3');
    this.db = new Database(this.dbPath);
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS checkpoints (
        task_id TEXT PRIMARY KEY,
        node_name TEXT NOT NULL,
        context_json TEXT NOT NULL,
        tool_history_json TEXT,
        llm_history_json TEXT,
        created_at INTEGER,
        updated_at INTEGER
      );
    `);
  }

  save(taskId, nodeName, context, toolHistory, llmHistory) {
    const stmt = this.db.prepare(`
      INSERT INTO checkpoints 
        (task_id, node_name, context_json, tool_history_json, llm_history_json, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(task_id) DO UPDATE SET
        node_name = excluded.node_name,
        context_json = excluded.context_json,
        tool_history_json = excluded.tool_history_json,
        llm_history_json = excluded.llm_history_json,
        updated_at = excluded.updated_at
    `);
    stmt.run(
      taskId,
      nodeName,
      JSON.stringify(context),
      JSON.stringify(toolHistory),
      JSON.stringify(llmHistory),
      Date.now(),
      Date.now()
    );
  }

  load(taskId) {
    const row = this.db.prepare(
      'SELECT * FROM checkpoints WHERE task_id = ?'
    ).get(taskId);
    if (!row) return null;
    return {
      taskId: row.task_id,
      currentNode: row.node_name,
      context: JSON.parse(row.context_json),
      toolCallHistory: JSON.parse(row.tool_history_json || '[]'),
      llmCallHistory: JSON.parse(row.llm_history_json || '[]'),
    };
  }

  delete(taskId) {
    this.db.prepare('DELETE FROM checkpoints WHERE task_id = ?').run(taskId);
  }
}

module.exports = CheckpointManager;

3.2 在Agent执行循环中嵌入检查点

关键点:在每个节点执行前保存检查点,而不是执行后。这样即使节点执行到一半崩溃,重启后还能从当前节点重新执行(配合幂等工具调用)。

async function executeAgentTask(taskId, workflow) {
  const cpManager = new CheckpointManager();
  let checkpoint = cpManager.load(taskId);
  
  // 第一次执行 vs 恢复执行
  let currentNodeIndex = 0;
  let context = {};
  let toolHistory = [];
  let llmHistory = [];
  
  if (checkpoint) {
    // 恢复模式:从断点继续
    currentNodeIndex = workflow.nodes.findIndex(n => n.name === checkpoint.currentNode);
    context = checkpoint.context;
    toolHistory = checkpoint.toolCallHistory;
    llmHistory = checkpoint.llmCallHistory;
    console.log(`[恢复] 从节点 ${checkpoint.currentNode} 继续任务 ${taskId}`);
  } else {
    // 首次执行:创建检查点
    cpManager.save(taskId, workflow.nodes[0].name, {}, [], []);
  }
  
  // 执行工作流
  for (let i = currentNodeIndex; i < workflow.nodes.length; i++) {
    const node = workflow.nodes[i];
    
    // ⚠️ 关键:进入节点前先保存检查点
    cpManager.save(taskId, node.name, context, toolHistory, llmHistory);
    
    try {
      const result = await executeNode(node, context, toolHistory, llmHistory);
      
      // 更新上下文
      context = { ...context, ...result.contextUpdate };
      
      // 记录工具调用历史(用于幂等判断)
      if (result.toolCalls) {
        toolHistory.push(...result.toolCalls);
      }
      
      // 记录LLM调用历史(避免重复推理)
      if (result.llmCall) {
        llmHistory.push(result.llmCall);
      }
      
    } catch (err) {
      console.error(`[中断] 节点 ${node.name} 执行失败:`, err.message);
      // 检查点已经保存,下次可以从这个节点恢复
      throw err;
    }
  }
  
  // 任务完成,删除检查点
  cpManager.delete(taskId);
  console.log(`[完成] 任务 ${taskId} 执行成功`);
}

四、三个必须注意的坑

坑1:工具调用的幂等性

这是断点续跑最容易翻车的地方。假设你的Agent调用了一个「发送邮件」的工具,执行成功但还没保存检查点就崩溃了。重启后从检查点恢复,又调用一次「发送邮件」,用户就收到两封邮件。

解决方案:给每个工具调用加幂等键(Idempotency Key)

async function idempotentToolCall(toolName, params, toolHistory) {
  // 生成幂等键
  const idemKey = crypto.createHash('sha256')
    .update(toolName + JSON.stringify(params))
    .digest('hex');
  
  // 检查历史中是否已经执行过
  const existing = toolHistory.find(h => 
    h.idemKey === idemKey && h.status === 'success'
  );
  
  if (existing) {
    console.log(`[幂等] 工具 ${toolName} 已执行过,直接返回历史结果`);
    return existing.result;
  }
  
  // 执行工具调用
  const result = await callTool(toolName, params);
  
  // 记录到历史
  toolHistory.push({
    idemKey,
    toolName,
    params,
    result,
    status: 'success',
    timestamp: Date.now(),
  });
  
  return result;
}

坑2:LLM调用的缓存

断点续跑后,Agent可能会重新执行一遍之前已经做过的LLM推理,浪费Token。解决方法是把LLM的(prompt, response)对缓存起来。

注意:不是所有LLM调用都能缓存。如果prompt里包含时间戳、随机值,或者模型本身有随机性(temperature > 0),缓存的结果可能不适用。我只缓存temperature=0的确定性调用。

坑3:检查点存储的并发安全

如果你的Agent是多实例部署(比如K8s里跑多个Pod),检查点的读写会有并发问题。两个实例同时更新同一个task的检查点,状态就乱了。

解决方案:用数据库乐观锁或者分布式锁。我用一个简单的version字段做乐观锁:

-- 检查点表增加version字段
ALTER TABLE checkpoints ADD COLUMN version INTEGER DEFAULT 0;

-- 更新时带version检查
UPDATE checkpoints 
SET 
  node_name = ?,
  context_json = ?,
  version = version + 1
WHERE task_id = ? AND version = ?;
-- 如果受影响行数为0,说明版本冲突,需要重试

五、超时配置的最佳实践

光有断点续跑还不够,你还需要合理的超时配置,避免任务无限期挂起。根据我的经验:

  • LLM调用超时:设置30-60秒,超时后走重试(最多3次)
  • 工具调用超时:设置60-120秒,超过后记录失败,任务进入「待恢复」状态
  • 整个任务超时:设置2-6小时(根据业务而定),超过后标记为「已放弃」

超时后的处理策略:

  • 如果是网络超时:自动重试,指数退避(1s, 2s, 4s...)
  • 如果是业务错误(比如参数错误):不重试,直接失败
  • 如果是LLM限流(429错误):等待一段时间后重试,或者切换到备用模型

六、总结:让AI Agent真正生产可用的最后一公里

断点续跑听起来是个小功能,但它是AI Agent从Demo走向生产的关键一步。没有它,你的Agent就是个精致的花瓶——好看但经不起折腾。

核心要点回顾:

  • 把Agent的执行过程变成可持久化的状态机
  • 每个节点执行前保存检查点
  • 工具调用必须幂等,用Idempotency Key保证
  • LLM调用结果可以缓存(temperature=0的场景)
  • 超时配置要分层:LLM层、工具层、任务层

做了这些,你的AI Agent才能真正在生产环境跑得稳、跑得久。

相关阅读:AI Agent工作流性能监控与优化实战 | AI Agent多轮对话上下文管理实战 | AI工作流自动编排实战:多Agent协作的架构设计

版权声明

本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论