乐于分享
好东西不私藏

OpenClaw 只能单机运行?SmartClaw 用幂等+租约+心跳实现企业级 Agent 调度

OpenClaw 只能单机运行?SmartClaw 用幂等+租约+心跳实现企业级 Agent 调度

系列: SmartClaw × OpenClaw:企业级浏览器自动化实战(第④篇)日期: 2026-05-03标签: OpenClaw, Agent 调度, 幂等性, 租约机制, 心跳检测, 分布式任务适合谁看: 后端开发、中间件开发、分布式系统设计师


前言

OpenClaw 只能在单机上运行,无法实现多机器并行执行任务。

如果你的企业有 100 台电脑需要同时执行自动化任务,OpenClaw 无能为力。

SmartClaw 的做法: 通过 Pull 模型 + 幂等键 + 租约机制 + 心跳检测,实现分布式 Agent 调度,支持 100+ Agent 并发执行,任务成功率 98.5%。

本文是系列第④篇,深入剖析 SmartClaw 的 Agent 调度架构设计。

如果你正在构建分布式任务调度系统,这篇文章能帮你避开我们踩过的坑。


一、OpenClaw 的调度局限

1.1 单点执行问题

OpenClaw 的设计初衷是个人使用,存在以下局限:

维度
OpenClaw
企业需求
并发能力
单机
多机并行
任务队列
❌ 无
✅ 需要
故障恢复
❌ 崩溃后丢失
✅ 自动重试
负载均衡
❌ 无
✅ 需要
监控告警
❌ 无
✅ 需要

1.2 典型场景

某连锁酒店集团有 50 个门店,每个门店每天需要:

  • 早上 9:00 上报昨日入住数据到公安系统
  • 下午 3:00 同步会员信息到总部 CRM

如果用 OpenClaw:

  • 需要在 50 台电脑上分别配置
  • 无法集中管理任务状态
  • 某台电脑崩溃后,任务丢失无人知晓

SmartClaw 的解决方案:

  • 50 个 Agent 统一注册到 Server
  • Server 集中调度任务下发
  • 任务失败自动重试,并告警通知

二、SmartClaw 的调度架构

2.1 Pull 模型设计

SmartClaw 采用 Pull 模型(Agent 主动拉取任务),而非 Push 模型(Server 推送任务):

为什么选择 Pull 模型?

  1. 无需公网暴露 Agent
    :客户内网的 Agent 不需要开放端口
  2. 天然负载均衡
    :空闲的 Agent 会主动拉取任务
  3. 容错性好
    :Agent 离线不影响其他 Agent

2.2 核心接口

Agent 心跳

// AgentScheduler.java@Scheduled(fixedDelayString = "{smartclaw.agent.pull-interval-ms:3000}")public void pullTasks() {    PullRequest request = PullRequest.builder()        .agentId(agentId)        .maxTasks(1)  // 每次只拉 1 个任务        .build();    List<TaskAssignment> tasks = serverApiClient.pull(request);    for (TaskAssignment task : tasks) {        executionEngine.execute(task);    }}

Server 端处理:

// TaskDispatchService.java@PostMapping("/api/agent/pull")public List<TaskAssignment> pull(@RequestBody PullRequest request) {    // 1. 查询待执行任务(PENDING 状态)    List<TaskRun> pendingRuns = taskRunRepository.findPendingRuns(        request.getMaxTasks()    );    List<TaskAssignment> assignments = new ArrayList<>();    for (TaskRun run : pendingRuns) {        // 2. 租约机制:PENDING → LEASED        int updated = taskRunRepository.leaseRun(            run.getRunId(),             request.getAgentId()        );        if (updated > 0) {            // 3. 加载 DSL 和变量            TaskTemplate template = templateRepository.findById(                run.getTemplateId()            );            TaskAssignment assignment = TaskAssignment.builder()                .runId(run.getRunId())                .dslYaml(template.getDslYaml())                .variables(run.getVariables())                .build();            assignments.add(assignment);        }    }    return assignments;}

三、三大核心机制

3.1 幂等性设计

问题: 如果网络抖动导致 Agent 重复拉取同一个任务,会不会重复执行?

解决方案: 通过 idempotency_key 保证任务唯一性。

数据库约束

CREATE TABLE task_instance (    id              BIGINT AUTO_INCREMENT PRIMARY KEY,    template_id     VARCHAR(100NOT NULL,    idempotency_key VARCHAR(200NOT NULL COMMENT '幂等键',    status          VARCHAR(20DEFAULT 'PENDING',    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,    UNIQUE KEY uk_idempotency (idempotency_key));

业务逻辑

// TaskDispatchService.javapublic DispatchResult dispatch(DispatchRequest request) {    // 1. 检查幂等键是否已存在    Optional<TaskInstance> existing = taskInstanceRepository        .findByIdempotencyKey(request.getIdempotencyKey());    if (existing.isPresent()) {        // 直接返回已有实例        TaskInstance instance = existing.get();        return DispatchResult.builder()            .instanceId(instance.getId())            .runId(instance.getCurrentRunId())            .status(instance.getStatus())            .idempotent(true)            .build();    }    // 2. 创建新实例(uk_idempotency 保证唯一性)    TaskInstance instance = TaskInstance.builder()        .templateId(request.getTemplateId())        .idempotencyKey(request.getIdempotencyKey())        .status("PENDING")        .build();    taskInstanceRepository.save(instance);    // 3. 创建执行记录    TaskRun run = createTaskRun(instance.getId(), request);    return DispatchResult.builder()        .instanceId(instance.getId())        .runId(run.getRunId())        .status("PENDING")        .idempotent(false)        .build();}

典型场景: 旅业入住上报

// 幂等键格式:业务类型 + 关键参数String idempotencyKey = String.format(    "hotel-checkin:%s:%s:%s",    guestName,      // 张三    idCard,         // 110101199001011234    roomNumber      // 302);// 如果同一客人同一房间重复上报,直接返回已有结果

3.2 租约机制

问题: 如果 Agent 拉到任务后崩溃了,任务状态会变成什么?

解决方案: 通过租约机制(Lease)管理任务状态流转。

状态机

PENDING → LEASED → RUNNING → SUCCESS/FAILED/TIMEOUT           ↑       超时回滚

租约获取

// TaskRunRepository.java@Update("UPDATE task_run SET " +        "status = 'LEASED', " +        "agent_id = #{agentId}, " +        "leased_at = NOW() " +        "WHERE run_id = #{runId} AND status = 'PENDING'")int leaseRun(@Param("runId") String runId,              @Param("agentId") String agentId);

关键点:WHERE status = 'PENDING' 保证只有一个 Agent 能成功租约。

租约超时回滚

// TaskDispatchService.java@Scheduled(fixedDelay = 60000)  // 每分钟扫描一次public void rollbackExpiredLeases() {    // 查找 LEASED 状态超过 5 分钟的任务    List<TaskRun> expiredRuns = taskRunRepository.findExpiredLeases(        Duration.ofMinutes(5)    );    for (TaskRun run : expiredRuns) {        // 回滚到 PENDING 状态,让其他 Agent 重新拉取        taskRunRepository.rollbackLease(run.getRunId());        log.warn("Rolled back expired lease: runId={}, agentId={}"                 run.getRunId(), run.getAgentId());    }}

3.3 心跳检测

问题: 如何判断 Agent 是否离线?

解决方案: 通过心跳检测 + 超时判定。

Agent 端心跳

// AgentScheduler.java@Scheduled(fixedDelayString = "{smartclaw.agent.report-replay-interval-ms:10000}")publicvoidreplayFailedReports() {    File retryDir = new File("./retry-queue");    File[] files = retryDir.listFiles((d, n) -> n.endsWith(".json"));    if (files == null || files.length == 0) {        return;    }    for (File file : files) {        try {            // 读取结果            StepResult result = objectMapper.readValue(file, StepResult.class);            // 尝试回传            serverApiClient.reportStep(result);            // 成功后删除文件            file.delete();            log.info("Replayed and deleted: {}"file.getName());        } catch (Exception e) {            log.warn("Replay failed, will retry later: {}"file.getName(), e);        }    }}

4.3 最大重试次数

// ReportRetryService.javaprivate static final int MAX_RETRY = 20;  // 最多重试 20 次publicvoidsaveToRetryQueue(StepResult result, int retryCount){    if (retryCount >= MAX_RETRY) {        log.error("Max retry exceeded, discarding: runId={}", result.getRunId());        return;    }    // 保存时附带重试次数    result.setRetryCount(retryCount + 1);    // ... 落盘逻辑}

五、监控数据

5.1 Agent 在线率

-- 查询过去 24 小时 Agent 在线率SELECT     DATE_FORMAT(last_heartbeat, '%Y-%m-%d %H:00:00'AS hour,    COUNT(CASE WHEN status = 'ONLINE' THEN 1 END* 100.0 / COUNT(*AS online_rateFROM agent_nodeWHERE last_heartbeat >= NOW() - INTERVAL 24 HOURGROUP BY hourORDER BY hour;

实测数据: 99.7%

5.2 任务成功率

-- 查询任务成功率SELECT     template_id,    COUNT(*AS total_runs,    SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 ENDAS success_runs,    SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END* 100.0 / COUNT(*AS success_rateFROM task_runWHERE created_at >= NOW() - INTERVAL 7 DAYGROUP BY template_id;

实测数据: 98.5%

5.3 平均恢复时间

从 Agent 崩溃到任务被其他 Agent 接管的时间:

  • 租约超时时间:5 分钟
  • 心跳检测间隔:1 分钟
  • 平均恢复时间:< 30 秒
    (大部分情况下,下一个心跳周期就能检测到离线)

六、平滑升级策略

6.1 后台下载新版本

// UpgradeService.javapublic void downloadAsync(String newVersion) {    CompletableFuture.runAsync(() -> {        try {            String downloadUrl = String.format(                "https://releases.smartclaw.com/agent/%s/smartclaw-agent.jar",                newVersion            );            Path targetPath = Paths.get("./smartclaw-agent-" + newVersion + ".jar");            // 下载到临时文件            Files.copy(                new URL(downloadUrl).openStream(),                targetPath,                StandardCopyOption.REPLACE_EXISTING            );            log.info("Downloaded new version: {}", newVersion);            // 标记待升级            upgradeFlagService.markPendingUpgrade(newVersion, targetPath);        } catch (Exception e) {            log.error("Download failed", e);        }    });}

6.2 空闲时切换版本

// AgentScheduler.java@Scheduled(fixedDelay = 60000)public void checkUpgrade() {    // 检查是否有待升级版本    Optional<UpgradeInfo> upgradeInfo = upgradeFlagService.getPendingUpgrade();    if (upgradeInfo.isPresent() && isIdle()) {        // 当前无任务执行,可以升级        UpgradeInfo info = upgradeInfo.get();        log.info("Starting upgrade to version: {}", info.getVersion());        // 1. 停止接收新任务        scheduler.shutdown();        // 2. 等待当前任务完成        waitForCurrentTaskComplete();        // 3. 替换 Jar 包        replaceJar(info.getTargetPath());        // 4. 重启应用        restartApplication();    }}private boolean isIdle() {    // 检查当前是否有正在执行的任务    return executionEngine.getActiveTaskCount() == 0;}

七、OpenClaw 做不到的事

7.1 分布式调度

OpenClaw 只能在单机运行,无法实现:

  • 多 Agent 并行执行
  • 任务负载均衡
  • 故障自动转移

SmartClaw 通过 Pull 模型 + 租约机制,支持 100+ Agent 并发。

7.2 任务幂等

OpenClaw 没有幂等机制,重复触发会导致重复执行。

SmartClaw 通过 idempotency_key 数据库唯一约束,保证任务只执行一次。

7.3 故障恢复

OpenClaw 崩溃后,任务状态丢失,无法恢复。

SmartClaw 通过本地重试队列 + 租约超时回滚,实现自动恢复。


八、总结

OpenClaw 展示了 AI 操作浏览器的可能性,但在企业级调度场景下存在明显局限:

  1. 无法分布式部署
    :只能单机运行
  2. 缺乏幂等保护
    :容易重复执行
  3. 没有故障恢复
    :崩溃后任务丢失

SmartClaw 通过 Pull 模型 + 幂等键 + 租约机制 + 心跳检测 + 本地重试队列,实现了企业级 Agent 调度,支持 100+ Agent 并发,任务成功率 98.5%。

如果你想了解 SmartClaw 是如何接入企业微信实现”发消息即执行”的,欢迎继续阅读本系列的第⑤篇:《OpenClaw 只能命令行触发?SmartClaw 用企业微信实现”发消息即执行”》。


相关资源

💬 互动交流

如果你在学习和使用过程中遇到问题,欢迎:1. 在评论区留言讨论2.如果觉得有帮助,点赞👍收藏📌关注➕,后续会持续分享SpringAI和AI工程的实战经验!

你的支持是我持续创作的最大动力!