第五章 记忆与会话管理:用 AgentStateStore 替换 Memory,实现多轮对话持久化
"早上问了 weather_agent 杭州天气,半小时后追问'那上海呢?'——它竟然记得前文。这是 2.0 用
AgentStateStore持久化 +RuntimeContext.sessionId的作用,对老版本Memory接口的彻底替代。"本章你将学到:
.stateStore()+RuntimeContext.sessionId()双钥匙机制、四种 Store 后端的选型与配置、以及生产环境下的多用户隔离。
5.1 为什么把 Memory 改成 AgentState + AgentStateStore?
1.x 的 Memory / InMemoryMemory / LongTermMemory 接口在 2.0.0-RC2 中被标为 @Deprecated(forRemoval = true),原因有三:
- 职责单一 —
Memory既是"当前对话上下文"又是"长期知识库",两件事放在一个抽象里很难扩展。 - 持久化无关 — 1.x 时代需要自己把
Memory序列化进 Redis/MySQL;2.0 把这件事下沉到AgentStateStore后端里,业务代码与持久化彻底解耦。 - 与 harness 解耦 —
HarnessAgent鼓励把状态放在 workspace 目录、subagent 文件、skill 仓库中,这些内容天然不适合塞进一个 Java 对象。
新模型:
| 概念 | 2.0 替代 | 说明 |
|---|---|---|
Memory(当前对话窗口) |
AgentState (框架内部对象,用户不直接操作) + AgentStateStore |
Agent 自动读写,你只需配 Store |
LongTermMemory |
workspace 下的 MEMORY.md + memory/YYYY-MM-DD.md + CompactionMiddleware / MemoryFlushMiddleware |
跨会话的稳定知识 |
| 自定义持久化 | AgentStateStore 后端(InMemoryAgentStateStore / JsonFileAgentStateStore / RedisAgentStateStore / MysqlAgentStateStore) |
跨进程、跨重启保留状态 |
核心认知:你只需要配好 Store + 传好 sessionId,其余全交给
HarnessAgent。AgentState是框架内部对象,日常开发不需要碰它。⚠️ 2.0 重大变更
旧代码里
agent.getMemory().add(...)、LongTermMemory.retrieve(...)、InMemoryMemory子类等 API 全部走@Deprecated通道,2.0 之后会被删除。本章末尾给出一份最小迁移清单。
5.2 第一个例子:双钥匙缺一不可
要实现跨调用的记忆,需要两样东西:
| 钥匙 | 作用 | 在哪配 |
|---|---|---|
AgentStateStore |
决定状态存哪里(内存 / 文件 / Redis / MySQL) | HarnessAgent.builder().stateStore(...) |
RuntimeContext.sessionId |
决定「这是哪场会话」——Store 查询的主键 | 每次 agent.call(msg, rt) 时传入 |
少任何一样,就没有记忆。 不配 Store 无法持久化;不传 sessionId 每次都是新会话。
import io.agentscope.core.RuntimeContext;
import io.agentscope.core.state.JsonFileAgentStateStore;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.harness.HarnessAgent;
import java.nio.file.Path;
import java.util.List;
public class Chapter05_Basic {
public static void main(String[] args) {
// (1)配 Store ———— 钥匙一:决定状态存哪
JsonFileAgentStateStore store = new JsonFileAgentStateStore(
Path.of("./workspace", "state"));
HarnessAgent agent = HarnessAgent.builder()
.name("weather_bot")
.sysPrompt("你是一个中文天气助手,每次回答不超过 50 字。")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build())
.workspace(Path.of("./workspace"))
.stateStore(store) // ← 钥匙一
.build();
// (2)传 sessionId ———— 钥匙二:决定是哪场会话
RuntimeContext rt = RuntimeContext.builder()
.sessionId("user-9527-2026-06-07")
.userId("9527")
.build();
// 第一轮
agent.call(
List.of(new UserMessage("user", "今天杭州天气怎么样?")), rt).block();
// 第二轮 ———— 同一个 sessionId,上下文自动恢复
System.out.println(agent.call(
List.of(new UserMessage("user", "那上海呢?")), rt).block().getTextContent());
}
}
内部流程(框架自动完成,你不用管):
agent.call(msg, rt)
|
+-- 根据 rt.sessionId 去 store 查有无旧状态
| |-- 有 --> 反序列化成 AgentState(内部对象)--> 恢复上下文
| \-- 无 --> 新建空的 AgentState
|
+-- 把 msg + agent 的回复追加进 AgentState
|
\-- 完成后把 AgentState 写回 store
你只做了两件事:.stateStore(store) + sessionId,剩下全自动。AgentState 是框架内部对象,日常开发不需要直接操作它。
5.3 AgentState 的三个核心方法
AgentState(io.agentscope.core.state)对外只暴露三件事:
| 方法 | 用途 |
|---|---|
List<Msg> getContext() |
只读视图,拿到当前上下文里所有消息 |
void addMessage(Msg msg) |
往上下文里追加一条消息(业务方主动注入时用) |
int size() |
当前上下文的消息条数 |
Msg在 2.0 依旧是不可变对象,任何想修改消息的尝试都需要新建一条。所以“清空上下文”也不是state.clear(),而是用新的Session实例,或在Session之上自己开一个新的sessionId。
5.4 RuntimeContext —— 给单次调用贴上标签
RuntimeContext 解决的是“同一份 agent 跑多个用户 / 多场对话”这件事。把它作为 call() 的第二个参数传入:
import io.agentscope.core.RuntimeContext;
agent.call(
List.of(new UserMessage("user", "晚上想喝粥")),
RuntimeContext.builder()
.sessionId("user-9527-2026-06-07")
.userId("9527")
.build());
字段说明:
sessionId— 把这一通对话归到某个会话桶。配合AgentStateStore后端时,这是主键。userId— 多租户场景下区分用户,方便RedisAgentStateStore命名空间隔离。- 其他 —
traceId/metadata用于在事件流里打点,配合streamEvents()做日志关联。
5.5 AgentStateStore —— 把状态搬到磁盘 / Redis / MySQL
5.5.1 不传 AgentStateStore:默认 InMemoryAgentStateStore
HarnessAgent.builder() 不显式 .stateStore(...) 时,框架默认安装一个 InMemoryAgentStateStore(每个 HarnessAgent 进程内独立)。但即便有默认 Store,不传 sessionId 也不会有跨调用记忆——Store 按 sessionId 做主键索引,空 key 每次覆盖。
重启进程、部署多实例时数据会丢。
5.5.2 单机文件版:JsonFileAgentStateStore
开发环境推荐用 JsonFileAgentStateStore,状态会落到 workspace/state/session-<id>.json,重启服务能恢复:
import io.agentscope.core.state.JsonFileAgentStateStore;
import io.agentscope.harness.HarnessAgent;
HarnessAgent agent = HarnessAgent.builder()
.name("weather_bot")
.sysPrompt("...")
.model(model)
.workspace(Path.of("./workspace"))
.stateStore(new JsonFileAgentStateStore(Path.of("./workspace/state")))
.build();
workspace/state/ 目录里会看到这样的文件:
workspace/
└── state/
├── session-user-9527-2026-06-07.json
└── session-user-9528-2026-06-07.json
5.5.3 生产环境:RedisAgentStateStore
生产环境必须把状态放 Redis。AgentScope 提供现成的 RedisAgentStateStore(在 agentscope-extensions-redis 模块里):
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-redis</artifactId>
<version>2.0.0-RC2</version>
</dependency>
import io.agentscope.extensions.redis.state.RedisAgentStateStore;
import io.agentscope.harness.HarnessAgent;
import redis.clients.jedis.UnifiedJedis;
UnifiedJedis client = new UnifiedJedis("redis://127.0.0.1:6379");
HarnessAgent agent = HarnessAgent.builder()
.name("weather_bot")
.sysPrompt("...")
.model(model)
.workspace(Path.of("./workspace"))
.stateStore(RedisAgentStateStore.builder()
.jedisClient(client)
.build())
.build();
RedisAgentStateStore 使用 Builder 模式配置:
jedisClient— 由你控制连接配置(单机、哨兵、集群均可通过UnifiedJedis实现)。- 也支持 Lettuce 和 Redisson 客户端适配,通过对应的
LettuceClientAdapter/RedissonClientAdapter。
5.5.4 重型企业级:MysqlAgentStateStore
需要事务、强一致、审计时,用 MysqlAgentStateStore(在 agentscope-extensions-mysql 模块里):
import io.agentscope.extensions.mysql.state.MysqlAgentStateStore;
import javax.sql.DataSource;
HarnessAgent agent = HarnessAgent.builder()
.stateStore(new MysqlAgentStateStore(dataSource, true))
.build();
迁移到 MySQL 时记得在首次启动前执行模块自带的 schema.sql,会创建 agent_state 表。
5.6 完整可运行示例
import io.agentscope.core.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.extensions.redis.state.RedisAgentStateStore;
import io.agentscope.harness.HarnessAgent;
import redis.clients.jedis.UnifiedJedis;
import java.nio.file.Path;
import java.util.List;
public class Chapter05_Redis {
public static void main(String[] args) {
UnifiedJedis client = new UnifiedJedis("redis://127.0.0.1:6379");
HarnessAgent agent = HarnessAgent.builder()
.name("weather_bot")
.sysPrompt("你是一个中文天气助手,每次回答不超过 50 字。")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build())
.workspace(Path.of("./workspace"))
.stateStore(RedisAgentStateStore.builder()
.jedisClient(client)
.build())
.build();
// user-9527 的第一通对话
agent.call(
List.of(new UserMessage("user", "今天杭州天气怎么样?")),
RuntimeContext.builder()
.sessionId("user-9527-2026-06-07")
.userId("9527")
.build())
.block();
// user-9528 的第一通对话 —— 同一份 agent 进程,state 完全隔离
agent.call(
List.of(new UserMessage("user", "上海今天冷不冷?")),
RuntimeContext.builder()
.sessionId("user-9528-2026-06-07")
.userId("9528")
.build())
.block();
// user-9527 半小时后再问 —— 上下文从 Redis 恢复
agent.call(
List.of(new UserMessage("user", "那上海呢?")),
RuntimeContext.builder()
.sessionId("user-9527-2026-06-07")
.userId("9527")
.build())
.block();
client.close();
}
}
这段代码里两个用户共享同一个
HarnessAgent实例——RuntimeContext.sessionId()是把状态映射到不同桶的钥匙,2.0 的这种设计可以让你在生产里只起 1 个 agent 进程服务所有用户。
5.7 与长期记忆(MEMORY.md)的协作
AgentStateStore 只负责“当前会话上下文”;想跨会话记忆用户的偏好 / 习惯,得借助 workspace 下的两套文件:
MEMORY.md— 一份长期稳定的“事实笔记”(用户昵称、过敏原、长期目标)。memory/YYYY-MM-DD.md— 按天追加的“事件笔记”(昨天吃了什么、今天计划做什么)。2.0 提供了两个
Middleware让这件事全自动:CompactionMiddleware— 当上下文超过 token 阈值时,把旧消息压缩成摘要写进MEMORY.md。MemoryFlushMiddleware— 每轮对话结束后把需要长期保留的内容刷到当日memory/<date>.md。
详细配置见第 18 章 Skill & 长期记忆。
5.8 最小迁移清单(1.x → 2.0)
AgentState是框架内部对象。迁移表中的agent.state()指 RC1 旧 API,RC2 中为agent.getAgentState()。这些接口仅在 Middleware 等编程场景使用,日常对话不需要。
| 1.x 用法 | 2.0 等价 |
|---|---|
agent.getMemory() 读上下文 |
agent.getAgentState().getContext()(内部 API) |
agent.getMemory().add(Msg) |
agent.getAgentState().addMessage(Msg)(内部 API) |
agent.getMemory().size() |
agent.getAgentState().size()(内部 API) |
new InMemoryMemory() |
默认行为,无需配置 |
LongTermMemory.retrieve(...) |
workspace 下 MEMORY.md + SkillRepository |
LongTermMemory.record(...) |
MemoryFlushMiddleware |
自定义 Memory 子类持久化 |
AgentStateStore 后端 |
agent.call(messages) |
agent.call(messages, RuntimeContext.empty()) |
agent.stream(messages) |
agent.streamEvents(messages, ctx) |
Msg.builder().role(USER)... |
new UserMessage("name", "text") |
5.9 本章小结
- 2.0 的记忆 =
.stateStore(store)+RuntimeContext.sessionId——两样缺一就没有记忆。 - 同一个
HarnessAgent实例可以服务多个用户 / 多场会话,靠RuntimeContext.sessionId()区分。 - 开发期用
JsonFileAgentStateStore、生产用RedisAgentStateStore/MysqlAgentStateStore,按需切换。 - Agent 在 RC2 中完全无状态化——同一实例可安全并发服务多
(userId, sessionId)组合。 - 跨会话的稳定知识靠 workspace 下的
MEMORY.md+memory/*.md+CompactionMiddleware/MemoryFlushMiddleware,见第 18 章。
下一章我们会在此基础上接入 Hook/Middleware 系统,演示怎么在每一轮里插桩做日志/计费/审计。