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 模型?
- 无需公网暴露 Agent:客户内网的 Agent 不需要开放端口
- 天然负载均衡:空闲的 Agent 会主动拉取任务
- 容错性好:Agent 离线不影响其他 Agent
2.2 核心接口
Agent 心跳
// AgentScheduler.java
@Scheduled(fixedDelayString = "${smartclaw.agent.heartbeat-interval-ms:30000}")
public void heartbeat() {
HeartbeatRequest request = HeartbeatRequest.builder()
.agentId(agentId)
.version(agentVersion)
.host(InetAddress.getLocalHost().getHostName())
.uptime(getUptime())
.load(getCpuLoad())
.build();
serverApiClient.heartbeat(request);
}
Server 端处理:
// AgentService.java
@PostMapping("/api/agent/heartbeat")
public HeartbeatResponse heartbeat(@RequestBody HeartbeatRequest request) {
// 更新最后心跳时间
agentNodeRepository.updateLastHeartbeat(
request.getAgentId(),
LocalDateTime.now()
);
// 检查是否有新版本
String latestVersion = configService.getLatestAgentVersion();
boolean hasUpdate = !latestVersion.equals(request.getVersion());
return HeartbeatResponse.builder()
.ok(true)
.serverTime(LocalDateTime.now())
.hasUpdate(hasUpdate)
.latestVersion(latestVersion)
.build();
}
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(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.java
public 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.heartbeat-interval-ms:30000}")
public void heartbeat() {
try {
HeartbeatResponse response = serverApiClient.heartbeat(
HeartbeatRequest.builder()
.agentId(agentId)
.version(agentVersion)
.build()
);
// 如果有新版本,后台下载
if (response.isHasUpdate()) {
upgradeService.downloadAsync(response.getLatestVersion());
}
} catch (Exception e) {
log.error("Heartbeat failed", e);
// 心跳失败不影响任务执行,只是标记离线
}
}
Server 端离线检测
// AgentService.java
@Scheduled(fixedDelay = 60000) // 每分钟扫描一次
public void detectOfflineAgents() {
// 查找 90 秒未心跳的 Agent
List<AgentNode> offlineAgents = agentNodeRepository
.findOfflineAgents(Duration.ofSeconds(90));
for (AgentNode agent : offlineAgents) {
// 标记为 OFFLINE
agentNodeRepository.updateStatus(
agent.getAgentId(),
"OFFLINE"
);
// 将该 Agent 正在执行的任务回滚
taskRunRepository.rollbackAgentRuns(agent.getAgentId());
// 发送告警
alertService.sendAlert(
"Agent 离线",
String.format("Agent %s 已离线,最后心跳:%s",
agent.getAgentId(),
agent.getLastHeartbeat())
);
}
}
四、本地重试队列(创新点)
问题: 如果 Agent 执行完任务,但网络中断导致结果无法回传 Server,怎么办?
解决方案: 本地重试队列。
4.1 落盘存储
// ReportRetryService.java
public void saveToRetryQueue(StepResult result) {
File retryDir = new File("./retry-queue");
retryDir.mkdirs();
File retryFile = new File(retryDir, result.getRunId() + ".json");
try {
objectMapper.writeValue(retryFile, result);
log.info("Saved to retry queue: {}", retryFile.getAbsolutePath());
} catch (IOException e) {
log.error("Failed to save to retry queue", e);
}
}
4.2 定时重放
// ReportRetryService.java
@Scheduled(fixedDelayString = "${smartclaw.agent.report-replay-interval-ms:10000}")
public void replayFailedReports() {
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.java
private static final int MAX_RETRY = 20; // 最多重试 20 次
public void saveToRetryQueue(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_rate
FROM agent_node
WHERE last_heartbeat >= NOW() - INTERVAL 24 HOUR
GROUP BY hour
ORDER BY hour;
实测数据: 99.7%
5.2 任务成功率
-- 查询任务成功率
SELECT
template_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_rate
FROM task_run
WHERE created_at >= NOW() - INTERVAL 7 DAY
GROUP BY template_id;
实测数据: 98.5%
5.3 平均恢复时间
从 Agent 崩溃到任务被其他 Agent 接管的时间:
- 租约超时时间:5 分钟
- 心跳检测间隔:1 分钟
- 平均恢复时间:< 30 秒(大部分情况下,下一个心跳周期就能检测到离线)
六、平滑升级策略
6.1 后台下载新版本
// UpgradeService.java
public 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工程的实战经验!
你的支持是我持续创作的最大动力!

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)