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 的设计初衷是个人使用,存在以下局限:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 模型?
- 无需公网暴露 Agent
:客户内网的 Agent 不需要开放端口 - 天然负载均衡
:空闲的 Agent 会主动拉取任务 - 容错性好
: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 → LEASEDint 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(100) NOT NULL,idempotency_key VARCHAR(200) NOT NULL COMMENT '幂等键',status VARCHAR(20) DEFAULT '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, // 110101199001011234roomNumber // 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 在线率SELECTDATE_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 任务成功率
-- 查询任务成功率SELECTtemplate_id,COUNT(*) AS total_runs,SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS 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 操作浏览器的可能性,但在企业级调度场景下存在明显局限:
- 无法分布式部署
:只能单机运行 - 缺乏幂等保护
:容易重复执行 - 没有故障恢复
:崩溃后任务丢失
SmartClaw 通过 Pull 模型 + 幂等键 + 租约机制 + 心跳检测 + 本地重试队列,实现了企业级 Agent 调度,支持 100+ Agent 并发,任务成功率 98.5%。
如果你想了解 SmartClaw 是如何接入企业微信实现”发消息即执行”的,欢迎继续阅读本系列的第⑤篇:《OpenClaw 只能命令行触发?SmartClaw 用企业微信实现”发消息即执行”》。
相关资源
- 系列文章
: -
第①篇:OpenClaw 火了之后,我为什么还用纯 Java 做了一套浏览器自动化平台? -
第②篇:OpenClaw 只能手动写脚本?我用 Chrome 插件实现了”录制即生成” -
第③篇:OpenClaw 填表总失败?SmartClaw 用 5 阶段降级策略搞定 React/Vue 应用 -
第⑤篇:[OpenClaw 只能命令行触发?SmartClaw 用企业微信实现”发消息即执行”] 持续更新
💬 互动交流
如果你在学习和使用过程中遇到问题,欢迎:1. 在评论区留言讨论2.如果觉得有帮助,点赞👍收藏📌关注➕,后续会持续分享SpringAI和AI工程的实战经验!
你的支持是我持续创作的最大动力!

夜雨聆风