Agent Scope Java 2.x 系列【26】Harness:Dokcer 沙箱集成

文章目录

  • [1. 简单示例](#1. 简单示例)
    • [1.1 环境准备](#1.1 环境准备)
    • [1.2 构建 HarnessAgent](#1.2 构建 HarnessAgent)
    • [1.3 运行测试](#1.3 运行测试)
  • [2. 跨调用恢复与快照机制](#2. 跨调用恢复与快照机制)
    • [2.1 跨调用恢复](#2.1 跨调用恢复)
    • [2.2 沙箱恢复优先级](#2.2 沙箱恢复优先级)
    • [2.3 快照存储实现](#2.3 快照存储实现)
  • [3. 分布式多副本部署](#3. 分布式多副本部署)
    • [3.1 三大必备配置项](#3.1 三大必备配置项)
      • [3.1.1 分布式 AgentStateStore](#3.1.1 分布式 AgentStateStore)
      • [3.1.2 远端持久化快照](#3.1.2 远端持久化快照)
      • [3.1.3 合理 IsolationScope 隔离粒度](#3.1.3 合理 IsolationScope 隔离粒度)
    • [3.2 并发控制](#3.2 并发控制)
  • [4. 自行管控沙箱生命周期](#4. 自行管控沙箱生命周期)
    • [4.1 场景一:复用外部已提前启动的 Docker 容器](#4.1 场景一:复用外部已提前启动的 Docker 容器)
    • [4.2 场景二:指定历史快照版本恢复沙箱](#4.2 场景二:指定历史快照版本恢复沙箱)
    • [4.3 场景三:多 Agent 共用同一个沙箱](#4.3 场景三:多 Agent 共用同一个沙箱)

1. 简单示例

1.1 环境准备

当前运行环境:

  • 操作系统:Linux 64
  • JDK 17
  • Dokcer 20.x

拉取镜像:

bash 复制代码
docker pull ubuntu:20.04

1.2 构建 HarnessAgent

基础构建代码:

java 复制代码
// 构建绑定 Docker 沙箱的 HarnessAgent
HarnessAgent agent = HarnessAgent.builder()
    .name("code-agent")
    .model(model)
    .workspace(workspace)
    // 声明文件系统底层为 Docker 沙箱,基础镜像 ubuntu20.04
    .filesystem(new DockerFilesystemSpec()
        .image("ubuntu:20.04"))
    .build();

1.3 运行测试

项目打包后本地运行:

bash 复制代码
java -jar agentscope-demo-0.0.1-SNAPSHOT.jar

启动后,在控制台会打印一些关于沙箱的日志:

java 复制代码
2026-06-24T15:31:50.942+08:00  WARN 3027805 --- [agentscope-demo] [           main] i.agentscope.harness.agent.HarnessAgent  : [harness] Sandbox mode is using a local AgentStateStore (JsonFileAgentStateStore). Sandbox state will not survive JVM restarts and cannot be shared across instances. For production, configure a distributed AgentStateStore via .stateStore(...).
2026-06-24T15:31:50.942+08:00  INFO 3027806 --- : HarnessAgent 'harness-demo' built [workspace=/root/.agentscope/workspace/demo-agent, filesystem=SandboxBackedFilesystem, subagents=true]

第一条 WARN 警告日志:

  • 当前沙箱模式使用本地文件存储 JsonFileAgentStateStore 保存沙箱元数据;
  • 沙箱状态无法在 JVM 重启后保留,也不能在多服务实例 / Pod 之间共享;
  • 生产环境请通过 .stateStore(...) 配置分布式状态存储。

第二条 INFO 构建完成日志:

  • 工作区路径:宿主本地目录 /root/.agentscope/workspace/demo-agentskills/knowledge 等文件会从此目录增量同步到 Docker 沙箱;
  • 文件系统底层:SandboxBackedFilesystem,代表当前开启沙箱隔离模式,所有 read_file/write_file/execute 全部走 Docker 容器,不是原生本地磁盘。

发起一个 read_file 对话:

可以看到自动创建了一个容器:

对话结束后,该容器实例会被停止、删除。

2. 跨调用恢复与快照机制

上述案例执行后,会出现一个报错问题,大概意思是:用户询问沙箱内文件路径,框架尝试复用用户 demo-user 上次的沙箱快照恢复环境,触发反序列化报错:

2.1 跨调用恢复

一次 agent.call() 对话执行完毕后,沙箱会把完整运行环境元数据打包为快照持久存储;下一次同 slot(同用户 / 会话)发起调用时,优先读取快照复原环境,不用重新初始化容器、重装依赖、重建文件,实现跨对话环境复用,这个整套能力统称「跨调用恢复」。

2.2 沙箱恢复优先级

每次 call 启动时执行时,框架按从快到慢依次判断:

  1. 容器进程存活、工作区完好(最优)
    • 上次对话结束仅执行 sandbox.stop() 停止进程,未执行 shutdown() 删除容器;
    • 本次直接 start() 复用原有容器,所有已安装包、临时文件全部保留,无重建开销。
  2. 容器已销毁,但存在可用快照 :容器被删除、进程丢失时,读取快照 JSON,反序列化出完整容器配置、工作区记录,新建容器并还原全部环境。
  3. 无任何快照记录(冷启动兜底) :找不到当前 slot 对应的快照,基于 WorkspaceSpec 全新初始化空白容器,同步宿主目录、重新执行依赖安装。

2.3 快照存储实现

提供了五种实现:

快照实现 存储载体 核心特性 适用场景
NoopSnapshotSpec(默认) 无持久化 不保存任何快照;容器销毁后环境永久丢失,下次必冷启动 本地快速调试、临时一次性对话
LocalSnapshotSpec 宿主机本地磁盘文件 快照落地本地目录,单机重启可恢复;多实例无法共享 单机长期部署、无集群需求
OssSnapshotSpec S3/OSS 兼容对象存储 远端云端持久化,多副本节点可拉取同一份快照 分布式集群、线上多 Pod 生产环境
RedisSnapshotSpec Redis 内存数据库 读写延迟极低,适合体积小的工作区快照 追求极速恢复、环境文件少的场景
JdbcSnapshotSpec MySQL 等关系库 BLOB 字段 复用现有数据库,无需额外中间件 已有 MySQL 基础设施,不想部署 OSS/Redis
OSS 快照配置示例:
java 复制代码
.filesystem(new DockerFilesystemSpec()
    .image("ubuntu:24.04")
    // 指定OSS存储快照,桶+路径前缀
    .snapshotSpec(new OssSnapshotSpec(ossClient, "my-bucket", "agentscope/")))

本地 File 快照配置示例:

java 复制代码
.filesystem(new DockerFilesystemSpec()
    .image("ubuntu:24.04")
    // 指定OSS存储快照,桶+路径前缀
    .snapshotSpec(new LocalSnapshotSpec(workspacePath)))

默认使用的是 NoopSnapshotSpec ,默认没有快照,道理上来说应该走冷启动兜底 ,但是框架还去执行 AgentState 序列化,所以导致了上面的报错 。

3. 分布式多副本部署

同一套 Agent 服务部署多台实例 / 多个 Pod 做负载均衡,用户请求会随机打到任意副本。

这种场景下,要求同一个用户的对话,无论路由到哪一台服务实例,都能读取到之前沙箱容器环境、安装的依赖、生成的文件,实现跨节点复用沙箱,不会每次切换实例都冷启动重建容器。

想要实现该能力,必须同时满足两大配套存储,搭配隔离粒度配置。

标准分布式构建代码:

java 复制代码
HarnessAgent.builder()
    .name("assistant")
    .model(model)
    .workspace(workspace)
    // 分布式状态存储,存放沙箱元数据+对话状态
    .stateStore(redisStateStore)
    .filesystem(new DockerFilesystemSpec()
        .image("ubuntu:24.04")
        .snapshotSpec(ossSnapshotSpec)      // 远端快照支撑跨副本恢复
        .isolationScope(IsolationScope.USER))// 用户粒度隔离,可省略(默认值)
    .build();

3.1 三大必备配置项

集群跨节点共享沙箱两大必备条件:

  1. 分布式 AgentStateStore
  2. 远端持久化快照

3.1.1 分布式 AgentStateStore

AgentStateStore 存储 slot 隔离键与沙箱元数据映射关系,包括 slot(用户 / 会话唯一标识)、容器 ID、快照指针、工作区就绪标记、Agent 运行时上下文等。

默认本地实现 JsonFileAgentStateStore 每个实例独立文件,数据隔离,跨 JVM / 跨实例无法互通,进程重启数据易损坏、序列化版本不兼容报错(之前遇到的日志问题)。

分布式 AgentStateStore 则所有服务副本共享同一份索引数据,任意实例通过 slot 就能查到对应沙箱快照地址,例如 RedisStateStore 全局统一存储,多实例共享、并发读写安全、支持集群。

3.1.2 远端持久化快照

持久化快照 真正存储沙箱完整环境快照,默认 NoopSnapshotSpec 无持久化,容器销毁环境直接丢失,分布式场景完全失效。可使用远端持久化快照,例如 OssSnapshotSpec / RedisSnapshotSpec / JdbcSnapshotSpec 等,容器销毁后环境不会丢失,多副本都能访问同一远端存储,任意节点均可拉取快照重建容器。

3.1.3 合理 IsolationScope 隔离粒度

默认 IsolationScope.USER 即可满足绝大多数多用户 SaaS 场景:同一 userId 全局共用一套沙箱环境;

若业务需要每个对话完全隔离,切换为 SESSION

3.2 并发控制

3.2.1 并发冲突根源

USER / AGENT / GLOBAL 三种隔离粒度,共享同一个 slot 沙箱环境:

  • 多台服务 Pod /副本同时收到同一用户请求;
  • 多线程并行创建、修改、持久化同一个 slot 对应的沙箱快照;
  • 后执行完成的请求会覆盖前面的状态,环境文件、安装依赖出现错乱、丢失、不一致。

SESSION 粒度天然无冲突:每个会话独立 slot,不存在多请求争抢同一环境,不需要加锁。

3.2.2 分布式锁

带锁的并发安全时序完整流程:

  1. 多副本同时收到同一用户请求,计算出同一个 slot
  2. 抢占 slot 对应的分布式锁,只有一个请求抢占成功,其余阻塞等待;
  3. 抢占成功的实例执行沙箱恢复/新建、文件操作、shell 执行;
  4. 对话结束停止容器、持久化快照;
  5. 自动释放锁,排队的下一条请求获取锁继续执行;
  6. 全程保证同一用户沙箱环境串行修改,不会出现状态覆盖。
(1)RedisDistributedStore

RedisDistributedStore 内部封装三件套,一行代码自动注入:

  1. 分布式 AgentStateStoreslot 索引存储)
  2. Redis远端快照 SnapshotSpec
  3. RedisSandboxExecutionGuard 分布式执行锁

代码示例:

java 复制代码
// 初始化Redis分布式统一存储
DistributedStore store = RedisDistributedStore.fromJedis(jedis);

HarnessAgent.builder()
    .distributedStore(store)
    .filesystem(new DockerFilesystemSpec()
        .image("ubuntu:24.04")
        .isolationScope(IsolationScope.USER))
    .build();

如果需要自定义锁超时时间、单独选用不同锁实现,在 DockerFilesystemSpec 显式覆盖锁配置,优先级高于 distributedStore 默认锁:

java 复制代码
.filesystem(new DockerFilesystemSpec()
    .image("ubuntu:24.04")
    .isolationScope(IsolationScope.USER)
    // 自定义Redis锁,租约30分钟自动过期,防止死锁
    .executionGuard(RedisSandboxExecutionGuard.builder(jedis)
        .leaseTtl(Duration.ofMinutes(30))
        .build()))

RedisSandboxExecutionGuard 实现底层原理:

  • 基于Redis SET key value NX PX 原子命令实现排他锁;
  • NX:仅 key 不存在时才抢占成功;
  • PX:设置租约过期时间,避免服务崩溃死锁。

优势 :无需分别配置 .stateStore().snapshotSpec().executionGuard(),统一由 Redis 承载,减少配置冗余。

(2)JdbcSandboxExecutionGuard

实现底层原理:

  • 基于 MySQL 内置函数 GET_LOCK() / RELEASE_LOCK() 数据库排他锁;
  • 适合无 Redis、仅依赖 MySQL 的架构。

使用示例:

java 复制代码
.filesystem(new DockerFilesystemSpec()
    .image("ubuntu:24.04")
    .isolationScope(IsolationScope.USER)
    // 自定义 Mysql 锁,租约 30 分钟自动过期,防止死锁
    .executionGuard(JdbcSandboxExecutionGuard .builder(dataSource)
        .leaseTtl(Duration.ofMinutes(30))
        .build()))
(3)自定义锁扩展

实现顶层接口 SandboxExecutionGuard,可对接 ZookeeperetcdRedisson 等自研分布式锁,适配企业现有中间件体系。

4. 自行管控沙箱生命周期

框架默认全权管理容器创建、启动、销毁,提供三类手动接管场景,通过 SandboxContext 注入单次调用上下文。

4.1 场景一:复用外部已提前启动的 Docker 容器

java 复制代码
// 业务手动创建并启动沙箱
Sandbox mySandbox = dockerClient.create(workspaceSpec, snapshotSpec, options);
mySandbox.start();

// 构造外部沙箱上下文
SandboxContext callCtx = SandboxContext.builder()
    .client(dockerClient)
    .externalSandbox(mySandbox) // 框架仅执行stop,最终销毁交由业务
    .build();

// 注入调用上下文执行对话
agent.call(msgs, RuntimeContext.builder()
    .sessionId("my-session")
    .put(SandboxContext.class, callCtx)
    .build()).block();

// 业务手动销毁容器
mySandbox.shutdown();

4.2 场景二:指定历史快照版本恢复沙箱

java 复制代码
// 反序列化快照状态字符串
SandboxState savedState = dockerClient.deserializeState(savedStateJson);
SandboxContext callCtx = SandboxContext.builder()
    .client(dockerClient)
    .externalSandboxState(savedState) // 按指定快照恢复,生命周期仍由框架管理
    .build();

4.3 场景三:多 Agent 共用同一个沙箱

将同一个 externalSandbox 实例传入多个Agent 的调用上下文,全部对话执行完成后,业务统一调用 shutdown() 释放容器。