无状态 Agent 引擎
ReActAgent(以及封装它的 HarnessAgent)采用无状态引擎设计:agent 实例本身只持有不可变的配置——system prompt、模型、工具集、中间件链——而所有 per-session 的可变数据都放在 AgentState 里,以 (userId, sessionId) 为索引。一个 agent 实例可以同时服务多个用户和会话,调用方只需在每次 call() 时传入不同的 RuntimeContext。
这意味着什么
- 不需要 agent-per-user 注册表。 一个
HarnessAgent实例就能服务全部用户——每次请求只需传入不同的RuntimeContext.userId和RuntimeContext.sessionId。 - 并发天然支持。 不同
(userId, sessionId)的请求完全并行;相同(userId, sessionId)的请求自动串行,确保对话一致性。 - 状态完全内部化。 Agent 在 call 入口从存储加载
AgentState,call 退出时自动保存——调用方不需要直接管理 state 对象。 - per-call 隔离。 每次
call()使用自己的AgentState快照。中间件和工具通过RuntimeContext.getAgentState()(由框架在 call 入口注入)访问本次调用的状态,并发 call 之间互不可见。
AgentState
AgentStateStore 持久化的是一份 AgentState(io.agentscope.core.state.AgentState),它是 agent 当前”瞬时”运行状态的完整快照:
AgentState 还携带一个瞬态的、不序列化的 InterruptControl,用于 per-session 中断信号——详见下方Per-session 中断。
一次 call() 结束,框架自动把整份 AgentState 以 agent_state 这个键写进状态存储,按该次调用的 (userId, sessionId) 寻址。下次同 (userId, sessionId) 的 call() 会自动从存储读回——只要状态存储是分布式的(例如 Redis),不同进程、不同物理机上的 agent 实例都能拿到完全一致的状态。
自动持久化与恢复链路
ReActAgent 自带的,HarnessAgent 直接继承,无需额外配置。Agent 实例不绑定固定 session——每次调用读写的是其 RuntimeContext 指定的槽位(缺省回退到 builder 上的 defaultSessionId)。
单次call()期间的中间状态变更靠的是内存里的AgentState对象。状态存储不在每条消息后落盘,而是在 call 结束 / shutdown 时整体写入——所以对后端的吞吐压力很低。
内置与扩展实现
只要实现io.agentscope.core.state.AgentStateStore 接口,任何后端都能接进来。选择哪一种,取决于你的部署形态:
切换非常简单——只在构造期
.stateStore(...) 一次:
同 (userId, sessionId) 跨进程、跨机器实时恢复
只要状态存储是分布式的(例如 Redis),这一切就是自动的:- 故障转移:节点崩了,会话漂到另一个节点,用户感知不到。
- 滚动发布:旧 pod 退出前
shutdownManager自动保存,新 pod 接到流量时自动从存储还原,对话不会断。 - 跨场景接续:在 Web UI 里和 agent 聊到一半,切换到 CLI 工具继续聊——只要
(userId, sessionId)一致,记忆都在。
(userId, sessionId) 二元组决定命名空间:大多数场景只用 sessionId 就够;需要按用户分桶时再加上 userId。
多用户隔离
sessionId 和 userId 解决的不是同一件事:
sessionId—— 决定哪段对话是哪段,独立的AgentState快照。userId—— 决定这段对话归谁,也决定文件落到谁的命名空间下,详见文件系统。
AgentState 级别的用户隔离,在 RuntimeContext 上设置 userId 即可:存储会按 (userId, sessionId) 寻址每个槽位(配合 RedisAgentStateStore 时 userId 就是 Redis key 的一部分),而不是依赖文件路径分桶。
直接读写 AgentState
需要旁路操作(例如管理台、审计、批量迁移)时,可以直接拿:清空会话对话上下文
若要让用户在不创建新会话的情况下开始新话题,可调用clearContext。该方法保留相同的
(userId, sessionId),也保留权限、工具、任务和 Plan Mode 等非对话状态;它会清空模型可见的
消息缓冲和压缩摘要,并在 agent 配置了 AgentStateStore 时立即持久化结果。
1.0 中的
Memory 接口(InMemoryMemory / LongTermMemory 等)在 2.0 已 @Deprecated(forRemoval = true)。新代码请使用 AgentState.getContext() + AgentStateStore —— Memory 仅作为源代码兼容层保留。Per-session 中断
每份AgentState 都携带一个瞬态的 InterruptControl(io.agentscope.core.interruption.InterruptControl)——per-session 的中断信号,永远不会被序列化到状态存储(AgentState 上标记为 @JsonIgnore transient)。这使得可以精确中断某个 session 正在进行的 call,而不影响同一 agent 实例上的其他并发 call。
state.interruptControl().isInterrupted()。被触发后,循环进入 handleInterrupt 路径,保存状态并返回部分结果。
旧的无参 interrupt() 在单 session 场景下仍然有效——它会路由到当前活跃会话的 InterruptControl。
InterruptControl 是纯运行时信号,不会被持久化。如果某个 session 在故障转移后恢复到另一台机器,中断标志从清零状态开始。另一个 AgentState.shutdownInterrupted 标志(是会被持久化的)记录了该 session 是否被优雅停机中断——agent 可以在下次加载时检测并恢复。并发使用
由于 agent 是无状态引擎,单个实例天然支持并发请求:- 不同
(userId, sessionId)→ 完全并行,每次 call 使用各自独立的AgentState。 - 相同
(userId, sessionId)→ per-session 异步门按 FIFO 顺序串行化——无需外部锁即保证状态一致性。 interrupt(userId, sessionId)→ 精确命中单个 session,其他在飞 call 不受影响。
RuntimeContext —— per-call 元数据
RuntimeContext(位于 io.agentscope.core.agent)是一个轻量容器,在 agent.call(msgs, ctx) 中传入,hook 与 tool 在本次调用期间共享。其自由 / 类型属性不持久化;而 sessionId / userId 字段决定本次调用状态存储读写哪个 AgentState 槽位。在 call 入口,框架会把 call-scoped 的 AgentState 注入到 RuntimeContext 上,中间件和工具通过 ctx.getAgentState() 获取正确的 per-call 状态。
相关文档
- 智能体(Agent) ——
ReActAgent完整接口与 Builder 参数 - 上下文压缩 —— 对话摘要、工具结果卸载、溢出恢复(建立在本页描述的 AgentState 基础之上)
- 记忆 —— 长期记忆与后台维护
- 权限系统 —— 权限规则的持久化