目录
- [AgentScope AgentState 与状态存储](#AgentScope AgentState 与状态存储)
-
- [1. 概述:无状态引擎](#1. 概述:无状态引擎)
- [2. AgentState 结构](#2. AgentState 结构)
- [3. 自动持久化与恢复链路](#3. 自动持久化与恢复链路)
- [4. AgentStateStore 接口](#4. AgentStateStore 接口)
- [5. 五种内置实现](#5. 五种内置实现)
- [6. 跨进程、跨机器恢复](#6. 跨进程、跨机器恢复)
- [7. Per-session 中断](#7. Per-session 中断)
- [8. 直接读写 AgentState](#8. 直接读写 AgentState)
- [9. RuntimeContext ------ per-call 元数据](#9. RuntimeContext —— per-call 元数据)
- [10. 并发规则](#10. 并发规则)
- [11. 多租户隔离](#11. 多租户隔离)
- [12. 注意事项](#12. 注意事项)
- [13. 1.0 → 2.0 迁移对照](#13. 1.0 → 2.0 迁移对照)
- [14. 完整可运行示例](#14. 完整可运行示例)
- [15. 分布式存储后端详解(Redis / MySQL / OSS)](#15. 分布式存储后端详解(Redis / MySQL / OSS))
-
- [15.1 DistributedStore 统一接口](#15.1 DistributedStore 统一接口)
- [15.2 混合后端](#15.2 混合后端)
- [15.3 Redis 后端](#15.3 Redis 后端)
- [15.4 MySQL / JDBC 后端](#15.4 MySQL / JDBC 后端)
- [15.5 阿里云 OSS 后端](#15.5 阿里云 OSS 后端)
AgentScope AgentState 与状态存储
ReActAgent(以及封装它的HarnessAgent)采用无状态引擎 设计:Agent 实例只持有不可变配置,所有 per-session 的可变数据放在AgentState里,以(userId, sessionId)为索引持久化。一个实例服务所有用户,天然支持并发与跨进程恢复。
1. 概述:无状态引擎
text
HarnessAgent (单例,不可变配置)
│
├── 不可变配置:sysPrompt、model、toolkit、middlewares
│
├── state cache(内存)
│ ├── ("alice", "s1") → AgentState ← call(..., RC(alice, s1))
│ └── ("bob", "s2") → AgentState ← call(..., RC(bob, s2))
│
└── per-session 门:同 (uid, sid) 串行,不同 (uid, sid) 并行
核心特性:
| 特性 | 说明 |
|---|---|
| 一个实例服务所有用户 | 不需要 agent-per-user 注册表,每次 call 传不同 RuntimeContext |
| 并发天然支持 | 不同 (userId, sessionId) 完全并行,相同 session 自动 FIFO 串行 |
| 状态完全内部化 | call 入口自动加载,call 退出自动保存,调用方无需管理 state 对象 |
| per-call 隔离 | 每次 call 使用自己的 AgentState 快照,并发 call 之间互不可见 |
| 跨进程恢复 | 只要 stateStore 是分布式的(如 Redis),不同 JVM / 物理机可共享同一份状态 |
1.0 兼容说明 :1.0 中的
Memory接口(InMemoryMemory/LongTermMemory等)在 2.0 已@Deprecated(forRemoval = true)。新代码请使用AgentState.getContext()+AgentStateStore。
2. AgentState 结构
AgentState(io.agentscope.core.state.AgentState)是 Agent 当前运行状态的完整快照:
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
getSessionId() |
String |
会话标识(索引键) |
getUserId() |
String |
用户标识(匿名会话为 null) |
getContext() |
List<Msg> |
当前对话历史(不可变只读视图) |
contextMutable() |
List<Msg> |
对话历史(可写入视图,中间件就地改写用) |
getSummary() |
String |
压缩后的摘要(开启上下文压缩时) |
getPermissionContext() |
PermissionContextState |
工具权限规则与模式(见权限系统文档) |
getToolContext() |
ToolContextState |
工具组激活 / 停用状态(activatedGroups) |
getTasksContext() |
TaskContextState |
todo_write 维护的任务清单 |
getPlanModeContext() |
PlanModeContextState |
Plan Mode 当前是否激活、计划文件路径 |
interruptControl() |
InterruptControl |
瞬态 ,@JsonIgnore transient,不持久化 |
状态分类:
text
可持久化(写入 stateStore)
├── context 对话历史
├── summary 压缩摘要
├── permissionContext 权限规则
├── toolContext 工具组激活状态
├── tasksContext 任务清单
├── planModeContext Plan Mode 状态
└── shutdownInterrupted 优雅停机中断标志(会持久化)
瞬态(不序列化,每次新 call 重新初始化)
└── interruptControl 运行时中断信号
3. 自动持久化与恢复链路
#mermaid-svg-ysTqWW6F7hWjeFwA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ysTqWW6F7hWjeFwA .error-icon{fill:#552222;}#mermaid-svg-ysTqWW6F7hWjeFwA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ysTqWW6F7hWjeFwA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ysTqWW6F7hWjeFwA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ysTqWW6F7hWjeFwA .marker.cross{stroke:#333333;}#mermaid-svg-ysTqWW6F7hWjeFwA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ysTqWW6F7hWjeFwA p{margin:0;}#mermaid-svg-ysTqWW6F7hWjeFwA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster-label text{fill:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster-label span{color:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster-label span p{background-color:transparent;}#mermaid-svg-ysTqWW6F7hWjeFwA .label text,#mermaid-svg-ysTqWW6F7hWjeFwA span{fill:#333;color:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA .node rect,#mermaid-svg-ysTqWW6F7hWjeFwA .node circle,#mermaid-svg-ysTqWW6F7hWjeFwA .node ellipse,#mermaid-svg-ysTqWW6F7hWjeFwA .node polygon,#mermaid-svg-ysTqWW6F7hWjeFwA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ysTqWW6F7hWjeFwA .rough-node .label text,#mermaid-svg-ysTqWW6F7hWjeFwA .node .label text,#mermaid-svg-ysTqWW6F7hWjeFwA .image-shape .label,#mermaid-svg-ysTqWW6F7hWjeFwA .icon-shape .label{text-anchor:middle;}#mermaid-svg-ysTqWW6F7hWjeFwA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ysTqWW6F7hWjeFwA .rough-node .label,#mermaid-svg-ysTqWW6F7hWjeFwA .node .label,#mermaid-svg-ysTqWW6F7hWjeFwA .image-shape .label,#mermaid-svg-ysTqWW6F7hWjeFwA .icon-shape .label{text-align:center;}#mermaid-svg-ysTqWW6F7hWjeFwA .node.clickable{cursor:pointer;}#mermaid-svg-ysTqWW6F7hWjeFwA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ysTqWW6F7hWjeFwA .arrowheadPath{fill:#333333;}#mermaid-svg-ysTqWW6F7hWjeFwA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ysTqWW6F7hWjeFwA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ysTqWW6F7hWjeFwA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ysTqWW6F7hWjeFwA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ysTqWW6F7hWjeFwA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ysTqWW6F7hWjeFwA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster text{fill:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA .cluster span{color:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ysTqWW6F7hWjeFwA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ysTqWW6F7hWjeFwA rect.text{fill:none;stroke-width:0;}#mermaid-svg-ysTqWW6F7hWjeFwA .icon-shape,#mermaid-svg-ysTqWW6F7hWjeFwA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ysTqWW6F7hWjeFwA .icon-shape p,#mermaid-svg-ysTqWW6F7hWjeFwA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ysTqWW6F7hWjeFwA .icon-shape .label rect,#mermaid-svg-ysTqWW6F7hWjeFwA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ysTqWW6F7hWjeFwA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ysTqWW6F7hWjeFwA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ysTqWW6F7hWjeFwA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-ysTqWW6F7hWjeFwA .startNode>*{fill:#FFF4E6!important;stroke:#E67E22!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .startNode span{fill:#FFF4E6!important;stroke:#E67E22!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .startNode tspan{fill:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .processNode>*{fill:#E8F4FD!important;stroke:#2980B9!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .processNode span{fill:#E8F4FD!important;stroke:#2980B9!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .processNode tspan{fill:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .ioNode>*{fill:#E8F8E8!important;stroke:#2b8a3e!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .ioNode span{fill:#E8F8E8!important;stroke:#2b8a3e!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .ioNode tspan{fill:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .gateNode>*{fill:#FFF9DB!important;stroke:#f08c00!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .gateNode span{fill:#FFF9DB!important;stroke:#f08c00!important;color:#333!important;}#mermaid-svg-ysTqWW6F7hWjeFwA .gateNode tspan{fill:#333!important;} 同 session 串行 / 不同 session 并行
call(msgs, RuntimeContext)
per-session 门
(uid, sid)
从缓存 / stateStore
加载 AgentState
注入 RuntimeContext
rc.setAgentState(state)
推理循环
中间件就地改写 contextMutable()
保存 AgentState
stateStore.save(uid, sid, 'agent_state', state)
返回结果
关键设计点:
- 状态存储不在每条消息后落盘 ,而是在
call结束 / shutdown 时整体写入------对后端吞吐压力很低 - 这套机制是
ReActAgent自带的,HarnessAgent直接继承,无需额外配置 - Agent 实例不绑定固定 session------每次调用读写的是
RuntimeContext指定的槽位(缺省回退到 builder 上的defaultSessionId)
4. AgentStateStore 接口
java
public interface AgentStateStore {
// 保存状态(key 通常是 "agent_state")
void save(String userId, String sessionId, String key, State state);
// 按 (uid, sid, key) 加载状态
<T extends State> Optional<T> get(String userId, String sessionId,
String key, Class<T> type);
// 判断该 (uid, sid) 是否有状态
boolean exists(String userId, String sessionId);
// 删除整个 session 的状态
void delete(String userId, String sessionId);
// 列出某个用户的所有 sessionId
Set<String> listSessionIds(String userId);
}
5. 五种内置实现
| 实现 | 模块 | 适用场景 |
|---|---|---|
InMemoryAgentStateStore |
agentscope-core |
单元测试 / 单进程演示;进程退出全部丢失 |
JsonFileAgentStateStore |
agentscope-core |
HarnessAgent 默认 ,落盘到 ~/.agentscope/state/<agentId>/(根目录可通过 -Dagentscope.state.home 改);单机开发 |
RedisAgentStateStore |
agentscope-extensions-redis |
生产首选(热数据),多副本共享;支持 Jedis / Lettuce / Redisson(Standalone / Cluster / Sentinel) |
MysqlAgentStateStore |
agentscope-extensions-mysql |
需要把状态沉淀进关系型库(审计、报表、教育/医疗合规)时使用 |
OssAgentStateStore |
agentscope-extensions-oss |
冷数据归档:对象存储(阿里云 OSS / S3 / MinIO),成本极低、11 个 9 持久性,但延迟高(10~100ms),不适合每次 call 都读写的热路径 |
五种存储按性能 / 成本 / 可靠性定位:
text
延迟(从低到高):
InMemory < Redis < MySQL < JsonFile < OSS
成本(从低到高):
OSS < MySQL < JsonFile < Redis < InMemory
持久性(从弱到强):
InMemory(重启丢)< JsonFile(单机磁盘)< Redis(依赖 AOF)< MySQL < OSS(11 个 9)
选型速查:
| 你的场景 | 选哪个 |
|---|---|
| 跑单元测试 | InMemoryAgentStateStore |
| 本地开发 / demo | JsonFileAgentStateStore(默认,不用配) |
| 生产多 pod + 高并发 | RedisAgentStateStore(开 AOF) |
| 教育/医疗/金融合规,要审计 | MysqlAgentStateStore |
| session 结束后长期归档,没人再访问 | OssAgentStateStore(可配合 Redis 做冷热分层) |
切换示例:
java
// 1. 单机默认:省略 .stateStore(...),自动用 JsonFileAgentStateStore
HarnessAgent agent = HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(workspace)
.build();
// 2. 多副本生产:使用 Redis
JedisPooled jedis = new JedisPooled("redis://redis.prod:6379");
HarnessAgent agent = HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(workspace)
.stateStore(new RedisAgentStateStore(jedis))
.distributedStore(RedisDistributedStore.fromJedis(jedis))
.build();
单用户 / CLI 场景的最简装配(基于本地示例 StateExample):
java
Path sessionPath = Paths.get(System.getProperty("user.home"),
".agentscope", "examples", "sessions");
AgentStateStore stateStore = new JsonFileAgentStateStore(sessionPath);
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.sysPrompt("You are a helpful AI assistant with persistent memory.")
.toolkit(new Toolkit())
.stateStore(stateStore)
.defaultSessionId("default_session") // 设了之后 call(msg) 不用传 ctx
.model(model)
.build();
// 之后直接 call(msg),框架自动 load 默认 session / save 回默认 session
agent.call(new UserMessage("Hello")).block();
关键 :
JsonFileAgentStateStore的构造器接收一个Path目录,落盘文件命名规则为<sessionId>_agent_state.json。
Warning :如果已经在用分布式工作区(SandboxFilesystemSpec/RemoteFilesystemSpec),HarnessAgent 会强制要求 状态存储也换成分布式后端,否则build()直接抛IllegalStateException------因为 sandbox 状态必须跨副本共享。
6. 跨进程、跨机器恢复
只要 stateStore 是分布式的(如 Redis),同 (userId, sessionId) 可在不同进程间无缝恢复:
#mermaid-svg-1KjOKdS7yE7cRhbb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1KjOKdS7yE7cRhbb .error-icon{fill:#552222;}#mermaid-svg-1KjOKdS7yE7cRhbb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1KjOKdS7yE7cRhbb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1KjOKdS7yE7cRhbb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1KjOKdS7yE7cRhbb .marker.cross{stroke:#333333;}#mermaid-svg-1KjOKdS7yE7cRhbb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1KjOKdS7yE7cRhbb p{margin:0;}#mermaid-svg-1KjOKdS7yE7cRhbb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster-label text{fill:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster-label span{color:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster-label span p{background-color:transparent;}#mermaid-svg-1KjOKdS7yE7cRhbb .label text,#mermaid-svg-1KjOKdS7yE7cRhbb span{fill:#333;color:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb .node rect,#mermaid-svg-1KjOKdS7yE7cRhbb .node circle,#mermaid-svg-1KjOKdS7yE7cRhbb .node ellipse,#mermaid-svg-1KjOKdS7yE7cRhbb .node polygon,#mermaid-svg-1KjOKdS7yE7cRhbb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1KjOKdS7yE7cRhbb .rough-node .label text,#mermaid-svg-1KjOKdS7yE7cRhbb .node .label text,#mermaid-svg-1KjOKdS7yE7cRhbb .image-shape .label,#mermaid-svg-1KjOKdS7yE7cRhbb .icon-shape .label{text-anchor:middle;}#mermaid-svg-1KjOKdS7yE7cRhbb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1KjOKdS7yE7cRhbb .rough-node .label,#mermaid-svg-1KjOKdS7yE7cRhbb .node .label,#mermaid-svg-1KjOKdS7yE7cRhbb .image-shape .label,#mermaid-svg-1KjOKdS7yE7cRhbb .icon-shape .label{text-align:center;}#mermaid-svg-1KjOKdS7yE7cRhbb .node.clickable{cursor:pointer;}#mermaid-svg-1KjOKdS7yE7cRhbb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1KjOKdS7yE7cRhbb .arrowheadPath{fill:#333333;}#mermaid-svg-1KjOKdS7yE7cRhbb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1KjOKdS7yE7cRhbb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1KjOKdS7yE7cRhbb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1KjOKdS7yE7cRhbb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1KjOKdS7yE7cRhbb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1KjOKdS7yE7cRhbb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster text{fill:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb .cluster span{color:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1KjOKdS7yE7cRhbb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1KjOKdS7yE7cRhbb rect.text{fill:none;stroke-width:0;}#mermaid-svg-1KjOKdS7yE7cRhbb .icon-shape,#mermaid-svg-1KjOKdS7yE7cRhbb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1KjOKdS7yE7cRhbb .icon-shape p,#mermaid-svg-1KjOKdS7yE7cRhbb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1KjOKdS7yE7cRhbb .icon-shape .label rect,#mermaid-svg-1KjOKdS7yE7cRhbb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1KjOKdS7yE7cRhbb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1KjOKdS7yE7cRhbb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1KjOKdS7yE7cRhbb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-1KjOKdS7yE7cRhbb .nodeNode>*{fill:#E8F4FD!important;stroke:#2980B9!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .nodeNode span{fill:#E8F4FD!important;stroke:#2980B9!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .nodeNode tspan{fill:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .storeNode>*{fill:#E8F8E8!important;stroke:#2b8a3e!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .storeNode span{fill:#E8F8E8!important;stroke:#2b8a3e!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .storeNode tspan{fill:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .callNode>*{fill:#FFF9DB!important;stroke:#f08c00!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .callNode span{fill:#FFF9DB!important;stroke:#f08c00!important;color:#333!important;}#mermaid-svg-1KjOKdS7yE7cRhbb .callNode tspan{fill:#333!important;} 节点 B(JVM-2,不同物理机)
Redis(共享状态存储)
节点 A(JVM-1)
save
load
HarnessAgent
call(aliceMsg, RC(alice, 's-001'))
(alice, s-001)
AgentState
HarnessAgent
call(nextMsg, RC(alice, 's-001'))
java
// 节点 A:开启对话
HarnessAgent agentA = HarnessAgent.builder()
.stateStore(redisStore)
.build();
agentA.call(msg, RuntimeContext.builder()
.sessionId("alice-2026-06-02-001")
.userId("alice")
.build()).block();
// 节点 B:不同物理机、独立 JVM、同一个 Redis
HarnessAgent agentB = HarnessAgent.builder()
.stateStore(redisStore)
.build();
// 自动从 Redis 加载节点 A 保存的 AgentState
agentB.call(nextMsg, RuntimeContext.builder()
.sessionId("alice-2026-06-02-001")
.userId("alice")
.build()).block();
意义:
| 场景 | 效果 |
|---|---|
| 故障转移 | 节点崩了,会话漂到另一个节点,用户感知不到 |
| 滚动发布 | 旧 pod 退出前自动保存,新 pod 接到流量时自动还原,对话不会断 |
| 跨场景接续 | Web UI 聊到一半,切到 CLI 继续聊------只要 (uid, sid) 一致,记忆都在 |
7. Per-session 中断
每份 AgentState 携带瞬态 InterruptControl(io.agentscope.core.interruption.InterruptControl),永远不会被序列化 到状态存储(@JsonIgnore transient)。这使得可以精确中断某个 session 正在进行的 call,而不影响同一 Agent 实例上的其他并发 call。
java
// 1. 中断指定 session------只有该 session 的 call 收到信号
agent.interrupt("alice", "session-001");
// 2. 带注入用户消息的中断(推理循环停下来后,把这条消息当作下一个输入)
agent.interrupt("alice", "session-001",
Msg.userMsg("请停下来做个总结。"));
推理循环在每次迭代前检查 state.interruptControl().isInterrupted(),触发后进入 handleInterrupt 路径,保存状态并返回部分结果。
两种中断标志的区别:
| 标志 | 是否持久化 | 用途 |
|---|---|---|
InterruptControl(瞬态) |
否 | 运行时主动中断某个 session;故障转移后从清零状态开始 |
AgentState.shutdownInterrupted |
是 | 记录该 session 是否被优雅停机中断;下次加载时可检测并恢复 |
旧的无参
interrupt()在单 session 场景下仍然有效------它会路由到当前活跃会话的InterruptControl。
8. 直接读写 AgentState
旁路操作(管理台、审计、批量迁移)时可以直接拿:
java
import io.agentscope.core.state.AgentState;
// 1. 获取状态
AgentState state = agent.getAgentState("alice", "session-001");
System.out.println("messages: " + state.getContext().size());
// 2. 序列化 / 反序列化
String json = state.toJson();
AgentState restored = AgentState.fromJsonString(json);
// 3. 访问子上下文
state.getPermissionContext(); // 权限
state.getTasksContext(); // 任务清单
state.getPlanModeContext(); // Plan Mode
getAgentState() 有两种重载:
| 方法 | 适用场景 |
|---|---|
agent.getAgentState(userId, sessionId) |
多租户、指定任意会话 |
agent.getAgentState()(无参) |
builder 设了 defaultSessionId 时,读默认 session 的状态(本地示例常用) |
| 方法 | 说明 |
|---|---|
getContext() |
当前对话历史(不可变视图) |
contextMutable() |
可写入视图,谨慎使用 |
setSummary(...) / getSummary() |
自定义压缩摘要(自行实现压缩 middleware 时用) |
toJson() / fromJsonString(String) |
序列化与反序列化 |
agent.close():
虽然 auto-save 已经在每次 call() / stream() 后自动落盘,但 agent.close() 会显式 flush in-flight 状态。此外 GracefulShutdownManager 在 Agent 构造时就注册了 state saver,JVM shutdown 时也会自动 flush------不需要手动处理关闭钩子。
清空会话上下文(clearContext):
让用户在不创建新会话的情况下开始新话题:
java
// 方式一:直接传 (userId, sessionId)
agent.clearContext("alice", "session-001");
// 方式二:传 RuntimeContext
agent.clearContext(RuntimeContext.builder()
.userId("alice")
.sessionId("session-001")
.build());
- 保留相同的
(userId, sessionId),也保留权限、工具、任务、Plan Mode 等非对话状态 - 只清空模型可见的消息缓冲和压缩摘要
- 配置了
AgentStateStore时立即持久化 - 请在该会话当前请求完成后调用------它不会取消正在执行的调用;下一次 call 会使用已清空的上下文
9. RuntimeContext ------ per-call 元数据
RuntimeContext(io.agentscope.core.agent.RuntimeContext)是一个轻量容器,在 agent.call(msgs, ctx) 中传入,hook 与 tool 在本次调用期间共享。其自由 / 类型属性不持久化 ;而 sessionId / userId 字段决定本次调用读写哪个 AgentState 槽位。
java
import io.agentscope.core.agent.RuntimeContext;
RuntimeContext ctx = RuntimeContext.builder()
.userId("alice")
.sessionId("s-001")
.put("request_id", "req-2026-06-01-abc")
.put(MyTenantInfo.class, new MyTenantInfo("tenant-7"))
.build();
Msg result = agent.call(List.of(new UserMessage("Hi")), ctx).block();
| 方法 | 说明 |
|---|---|
getSessionId() / getUserId() |
内置字段,用于路由状态槽位与租户 |
getAgentState() / setAgentState(AgentState) |
call-scoped 的 AgentState,由框架在 call 入口注入 |
resolveAgentState(ctx, agent) |
静态辅助方法 :优先返回 ctx.getAgentState(),回退到 agent.getAgentState() |
get(String) / put(String, Object) |
字符串键存取 |
get(Class<T>) / put(Class<T>, T) |
按类型存取(typed singleton) |
getExtra() |
直接拿到字符串属性 map(可变视图) |
RuntimeContext.empty() |
空上下文 |
关键 Tip :在中间件和工具中访问
AgentState时,始终使用RuntimeContext.resolveAgentState(ctx, agent),而不是agent.getAgentState()。并发场景下,agent.getAgentState()返回的是最后一次活跃 session 的状态(多个 call 同时在飞时结果不确定),而ctx.getAgentState()返回的是本次 call 的 session 状态。
AgentStateStore 后端在 builder 时绑定 ,不能通过 RuntimeContext per-call 切换。per-call 变化的只是它寻址的(userId, sessionId)槽位------按用户隔离时设置userId(或在存储上自定义keyPrefix),不要试图给每次 call 传不同的存储实例。
10. 并发规则
单个 HarnessAgent 实例天然支持并发请求:
java
HarnessAgent agent = HarnessAgent.builder()
.name("SharedAssistant")
.model(model)
.workspace(workspace)
.stateStore(redisStore)
.build();
// 不同用户------完全并行,没有竞争
Mono<Msg> aliceCall = agent.call(aliceMsg, RuntimeContext.builder()
.userId("alice").sessionId("s1").build());
Mono<Msg> bobCall = agent.call(bobMsg, RuntimeContext.builder()
.userId("bob").sessionId("s2").build());
Mono.zip(aliceCall, bobCall).block(); // 并行执行
// 同一用户、同一 session------自动串行
Mono<Msg> call1 = agent.call(msg1, RuntimeContext.builder()
.userId("alice").sessionId("s1").build());
Mono<Msg> call2 = agent.call(msg2, RuntimeContext.builder()
.userId("alice").sessionId("s1").build());
// call2 排在 call1 后面------对话历史始终一致
Flux.merge(call1, call2).collectList().block();
| 场景 | 行为 |
|---|---|
不同 (userId, sessionId) |
完全并行,每次 call 使用各自独立的 AgentState |
相同 (userId, sessionId) |
per-session 异步门按 FIFO 顺序串行化------无需外部锁即保证状态一致性 |
interrupt(userId, sessionId) |
精确命中单个 session,其他在飞 call 不受影响 |
内存占用 Tip :内存中的状态缓存会随单个 Agent 实例服务过的不同 session 数量增长。大多数部署场景(几百个 session)开销可忽略;超大规模(单进程百万级 session)可考虑 agent factory + 有界实例池------但
AgentState对象本身很轻量,这种情况很少出现。
11. 多租户隔离
sessionId 和 userId 解决的不是同一件事:
| 字段 | 决定什么 |
|---|---|
sessionId |
哪段对话是哪段------独立的 AgentState 快照 |
userId |
这段对话归谁------也决定文件落到谁的命名空间下 |
java
agent.call(msg, RuntimeContext.builder()
.sessionId("alice-1").userId("alice").build()).block();
agent.call(msg, RuntimeContext.builder()
.sessionId("bob-1").userId("bob").build()).block();
// 两个用户的对话状态与文件路径互不干扰
// RedisAgentStateStore 下 userId 就是 Redis key 的一部分
生产部署做 AgentState 级别的用户隔离,在 RuntimeContext 上设置 userId 即可:存储按 (userId, sessionId) 寻址每个槽位,而不是依赖文件路径分桶。
12. 注意事项
| 事项 | 说明 |
|---|---|
| 状态落盘时机 | 不在每条消息后落盘,而是 call 结束 / shutdown 时整体写入 |
| 分布式工作区强制分布式 stateStore | 用 SandboxFilesystemSpec / RemoteFilesystemSpec 时,单机 JsonFile 会 build 抛异常 |
| 中间件 / 工具取状态 | 用 RuntimeContext.resolveAgentState(ctx, agent),不要直接 agent.getAgentState() |
InterruptControl 不持久化 |
故障转移后中断标志清零;shutdownInterrupted 会持久化 |
Memory 接口已废弃 |
2.0 用 AgentState.getContext() + AgentStateStore 替代 |
clearContext 时机 |
在该会话当前请求完成后调用,不会取消正在执行的 call |
| stateStore 不能 per-call 切换 | builder 时绑定;per-call 只能切 (userId, sessionId) 槽位 |
| 落盘时机补充 | 每次 call() / stream() 后自动 save;agent.close() 显式 flush;GracefulShutdownManager 在 JVM shutdown 时自动 flush |
| 落盘文件命名 | JsonFileAgentStateStore 写 <sessionId>_agent_state.json 到配置目录 |
13. 1.0 → 2.0 迁移对照
从本地示例 StateExample.java 的迁移注释整理:
| 1.0 旧 API | 2.0 新 API | 说明 |
|---|---|---|
legacy.session.JsonFileAgentStateStore |
io.agentscope.core.state.JsonFileAgentStateStore |
包路径迁移 |
legacy.memory.InMemoryMemory / .memory() |
删除 | 对话历史现在由 AgentState 持有 |
agent.loadFrom(session, id) |
.stateStore(store).defaultSessionId(id) on builder |
启动时自动 load,不需要手动调 |
agent.saveTo(session, id) |
自动 save(每次 call 后) | 不需要手动调 |
agent.getMemory().getMessages() |
agent.getAgentState().getContext() |
取对话历史 |
无 defaultSessionId |
.defaultSessionId("...") |
设了之后 call(msg) 不用传 RuntimeContext |
典型迁移前后对比:
java
// 1.0 写法
Agent agent = new ReActAgent(model, toolkit);
agent.loadFrom(sessionStore, "session-001");
agent.call(msg);
List<Msg> history = agent.getMemory().getMessages();
agent.saveTo(sessionStore, "session-001");
// 2.0 写法
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.model(model)
.stateStore(new JsonFileAgentStateStore(path))
.defaultSessionId("session-001")
.build();
agent.call(msg); // 自动 load + save
List<Msg> history = agent.getAgentState().getContext(); // 读历史
14. 完整可运行示例
以下基于本地官方示例 StateAutoSaveExample.java 整理,演示跨实例自动恢复 :同一个 JsonFileAgentStateStore 目录下,第一个 JVM 实例发完消息退出,第二个 JVM 实例启动后能自动加载历史并继续对话。
java
public class StateAutoSaveExample {
private static final String SESSION_ID = "auto-save-demo";
private static final Path SESSION_DIR = Paths.get("/tmp/agentscope-sessions");
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("DASHSCOPE_API_KEY");
Files.createDirectories(SESSION_DIR);
// ── Phase 1: 第一个 Agent 实例,发两条消息 ──
AgentStateStore store1 = new JsonFileAgentStateStore(SESSION_DIR);
ReActAgent agent1 = buildAgent("alice", apiKey, store1);
agent1.call(new UserMessage("user", "My favourite colour is blue.")).block();
agent1.call(new UserMessage("user", "My lucky number is 7.")).block();
System.out.println("History size: "
+ agent1.getAgentState().getContext().size() + " messages");
agent1.close(); // 显式 flush(auto-save 已在每次 call 后落盘,这里是兜底)
// ── Phase 2: 第二个 Agent 实例,同一个 sessionId,自动从磁盘恢复 ──
AgentStateStore store2 = new JsonFileAgentStateStore(SESSION_DIR);
ReActAgent agent2 = buildAgent("alice", apiKey, store2);
System.out.println("History size after reload: "
+ agent2.getAgentState().getContext().size() + " messages");
// Agent 能回忆起 Phase 1 说过的内容
Msg reply = agent2.call(new UserMessage("user",
"What is my favourite colour and lucky number?")).block();
System.out.println("Agent: " + reply.getTextContent());
// 预期:agent 回答 blue 和 7
agent2.close();
}
private static ReActAgent buildAgent(String userId, String apiKey,
AgentStateStore stateStore) {
return ReActAgent.builder()
.name("SessionAgent")
.sysPrompt("You are a helpful assistant. Remember what the user tells you.")
.model(DashScopeChatModel.builder()
.apiKey(apiKey).modelName("qwen-plus").stream(true)
.formatter(new DashScopeChatFormatter())
.build())
.stateStore(stateStore)
.defaultSessionId(SESSION_ID + "-" + userId)
.build();
}
}
关键观察点:
| 现象 | 说明 |
|---|---|
Phase 2 的 agent2.getAgentState().getContext().size() 不为 0 |
构造时自动从 <sessionId>_agent_state.json 加载 |
| 第二个实例能答出 blue / 7 | 对话历史完全恢复 |
不需要任何手动 load() / save() |
builder 配了 stateStore + defaultSessionId 后全自动 |
agent1.close() 不是必须的 |
auto-save 已在每次 call 后落盘,close 只是兜底 flush |
15. 分布式存储后端详解(Redis / MySQL / OSS)
15.1 DistributedStore 统一接口
AgentScope 把所有需要分布式持久化的组件统一到 DistributedStore 接口下。一行配置即可让 Agent 的状态、工作区文件系统、沙箱快照和并发锁全部切到同一个分布式后端。
java
// Redis 一键配置
DistributedStore store = RedisDistributedStore.fromJedis(
new JedisPooled("redis://localhost:6379"));
HarnessAgent agent = HarnessAgent.builder()
.name("my-agent")
.model("dashscope:qwen-plus")
.distributedStore(store)
.filesystem(new RemoteFilesystemSpec()
.isolationScope(IsolationScope.USER))
.build();
能力矩阵:
| 功能组件 | 接口 | Redis | OSS | MySQL |
|---|---|---|---|---|
| Agent 状态持久化 | AgentStateStore |
RedisAgentStateStore |
OssAgentStateStore |
MysqlAgentStateStore |
| 工作区文件系统 KV | BaseStore |
RedisStore |
OssBaseStore |
JdbcStore |
| 沙箱快照 | SandboxSnapshotSpec |
RedisSnapshotSpec |
OssSnapshotSpec |
JdbcSnapshotSpec |
| 沙箱并发锁 | SandboxExecutionGuard |
RedisSandboxExecutionGuard |
--- | JdbcSandboxExecutionGuard |
OSS 不提供 SandboxExecutionGuard ------对象存储不适合做分布式锁。需要 sandbox 并发控制的 OSS 用户,用
DistributedStore.builder()混入 Redis 的 guard。
优先级(配置冲突时):
text
显式 builder 方法(.stateStore()、.filesystem() 上的 .snapshotSpec() 等)
> distributedStore 自动注入
> 本地默认(JsonFileAgentStateStore、NoopSnapshotSpec 等)
Versioning(乐观并发)支持:
| 后端 | 支持 CAS |
|---|---|
| Redis / Postgres / MySQL / InMemory | 是(putIfVersion) |
| JsonFile / OSS / COS / JPA | 否(last-writer-wins) |
多副本部署建议选支持 versioning 的后端,避免并发写冲突。
15.2 混合后端
不同组件可以来自不同的存储后端:
java
DistributedStore mysql = MysqlDistributedStore.create(dataSource);
DistributedStore redis = RedisDistributedStore.fromJedis(jedis);
// MySQL 管状态和文件,Redis 管沙箱锁和快照
DistributedStore mixed = DistributedStore.builder()
.agentStateStore(mysql.agentStateStore())
.baseStore(mysql.baseStore())
.sandboxSnapshotSpec(redis.sandboxSnapshotSpec())
.sandboxExecutionGuard(redis.sandboxExecutionGuard())
.build();
HarnessAgent.builder()
.distributedStore(mixed)
.filesystem(new DockerFilesystemSpec().image("ubuntu:24.04"))
.build();
15.3 Redis 后端
依赖:
xml
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-redis</artifactId>
<version>${agentscope.version}</version>
</dependency>
模块本身不强制依赖某一 Redis 客户端,按项目实际使用引入(Jedis / Lettuce / Redisson)。
AgentStateStore 三种客户端配置:
java
// Jedis
AgentStateStore store = RedisAgentStateStore.builder()
.jedisClient(new JedisPooled("redis://localhost:6379"))
.keyPrefix("myapp:session:")
.build();
// Lettuce 集群
AgentStateStore store = RedisAgentStateStore.builder()
.lettuceClusterClient(RedisClusterClient.create(
RedisURI.create("localhost", 7000)))
.build();
// Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
AgentStateStore store = RedisAgentStateStore.builder()
.redissonClient(Redisson.create(config))
.build();
存储结构:
- 单值:
{prefix}{userId}/{sessionId}:{stateKey}--- Redis String(JSON) - 列表:
{prefix}{userId}/{sessionId}:{stateKey}:list--- Redis List(JSON items) - 增量写入:通过 hash 摘要 + 计数比较,仅 append 新增项
沙箱锁(基于 SET NX PX 租约):
java
SandboxExecutionGuard guard = RedisSandboxExecutionGuard.builder(jedis)
.keyPrefix("myapp:guard:")
.leaseTtl(Duration.ofMinutes(30))
.retryInterval(Duration.ofMillis(500))
.build();
选型建议:
| 场景 | 建议 |
|---|---|
| 多副本生产,追求低延迟 | 首选 Redis |
| 已有 Redis 集群 | Lettuce Cluster 或 Redisson Sentinel |
| 小工作区 + 短 TTL 快照 | Redis 快照可以,但注意内存 |
| 大工作区快照 | 混合后端:Redis 管状态和锁,OSS 管快照 |
15.4 MySQL / JDBC 后端
依赖:
xml
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-mysql</artifactId>
<version>${agentscope.version}</version>
</dependency>
数据库驱动按实际使用的版本自行引入(如 mysql-connector-j、postgresql)。
AgentStateStore 配置:
java
// 自动建库建表
AgentStateStore store = new MysqlAgentStateStore(dataSource, true);
// 自定义库名 / 表名
AgentStateStore store = new MysqlAgentStateStore(
dataSource, "agentscope_prod", "session_state", true);
表结构 (自动创建):user_id、session_id、state_key、state_value(LONGTEXT JSON)、state_type、updated_at 等列。库名 / 表名仅允许 [a-zA-Z_][a-zA-Z0-9_-]*,长度 ≤ 64。
支持的方言 (JdbcStore 自动检测):
| 数据库 | 方言类 |
|---|---|
| MySQL / MariaDB | MysqlJdbcStoreDialect |
| PostgreSQL | PostgresJdbcStoreDialect |
| H2 | H2JdbcStoreDialect |
| SQLite | SqliteJdbcStoreDialect |
并发安全 :putIfVersion 通过单语句 CAS UPDATE ... WHERE version = ? 实现。
沙箱锁(基于 MySQL GET_LOCK()):
java
SandboxExecutionGuard guard = JdbcSandboxExecutionGuard.builder(dataSource)
.keyPrefix("myapp:lock:")
.lockTimeout(Duration.ofMinutes(30))
.build();
注意:MySQL named locks 是 server 级别的(非 database 级别)。在共享 MySQL 实例时,使用唯一的
keyPrefix避免冲突。
选型建议:
| 场景 | 建议 |
|---|---|
| 已有 MySQL,不想引入 Redis | 首选 MySQL |
| 需要 SQL 审计 / 报表 / 联表查询 | MySQL |
| 快照数据量大(>100MB) | MySQL BLOB 可行但推荐 OSS |
| 追求最低延迟 | Redis |
15.5 阿里云 OSS 后端
依赖:
xml
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-oss</artifactId>
<version>${agentscope.version}</version>
</dependency>
一键配置:
java
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
DistributedStore store = OssDistributedStore.create(ossClient, "my-bucket", "agentscope/");
AgentStateStore 配置:
java
AgentStateStore store = OssAgentStateStore.builder()
.ossClient(ossClient)
.bucketName("my-bucket")
.keyPrefix("agentscope/state/")
.build();
沙箱快照(大工作区首选):
java
SandboxSnapshotSpec spec = new OssSnapshotSpec(ossClient, "my-bucket", "agentscope/snapshot/");
// 或者从 endpoint + AK/SK 直接构造
SandboxSnapshotSpec spec = new OssSnapshotSpec(
"oss-cn-hangzhou.aliyuncs.com",
accessKeyId, accessKeySecret,
"my-bucket", "agentscope/snapshot/");
不提供 SandboxExecutionGuard------需要并发锁时混合 Redis:
java
DistributedStore ossStore = OssDistributedStore.create(ossClient, "my-bucket", "agentscope/");
DistributedStore mixed = DistributedStore.builder()
.agentStateStore(ossStore.agentStateStore())
.baseStore(ossStore.baseStore())
.sandboxSnapshotSpec(ossStore.sandboxSnapshotSpec())
.sandboxExecutionGuard(
RedisDistributedStore.fromJedis(jedis).sandboxExecutionGuard())
.build();
选型建议:
| 场景 | 建议 |
|---|---|
| 大容量快照(>100MB 工作区) | 首选 OSS |
| 阿里云生态,已有 OSS bucket | OSS |
| 需要 sandbox 并发锁 | 混合 OSS + Redis |
| 追求低延迟 | Redis |
安全提示:
- 生产环境建议使用 RAM Role + STS 临时凭证,避免在代码中硬编码 AK/SK
- 为快照 bucket 配置生命周期规则(如 7 天自动过期),避免存储成本失控