上下文与 AgentState
无状态 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 字段 | 内容 |
|---|---|
getSessionId() | 本份状态所属的会话标识 |
getUserId() | 所属用户标识(匿名会话为 null) |
getContext() / contextMutable() | 当前对话历史(用户输入、assistant 回复、工具调用、工具结果) |
getSummary() | 压缩后的摘要(如果开了压缩) |
getPermissionContext() | 工具权限规则,见权限系统 |
getPlanModeContext() | Plan Mode 当前是否激活、计划文件路径 |
getTasksContext() | todo_write 维护的任务清单 |
getToolContext() | 工具组激活状态(activatedGroups) |
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 接口,任何后端都能接进来。选择哪一种,取决于你的部署形态:
| 实现 | 模块 | 适用场景 |
|---|---|---|
InMemoryAgentStateStore | agentscope-core | 单元测试 / 单进程演示;进程退出全部丢失 |
JsonFileAgentStateStore | agentscope-core | 单机开发、文件落盘即可恢复;不能跨节点共享。HarnessAgent 默认值,落在 ~/.agentscope/state/<agentId>/(可通过 agentscope.state.home 系统属性改根目录);单机 |
RedisAgentStateStore | agentscope-extensions-redis | 生产首选,多副本共享;支持 Jedis Lettuce Redisson(Standalone Cluster Sentinel) |
MysqlAgentStateStore | agentscope-extensions-mysql | 需要把状态沉淀进关系型库(审计、报表)时使用 |
切换非常简单——只在构造期 .stateStore(...) 一次:
警告
内置的
JsonFileAgentStateStore/InMemoryAgentStateStore仅适合单机。如果你已经在用filesystem(SandboxFilesystemSpec)或filesystem(RemoteFilesystemSpec)(分布式工作区),HarnessAgent 会强制要求状态存储也换成分布式后端,否则build()直接抛IllegalStateException——因为 sandbox 状态必须跨副本共享。请通过.distributedStore(...)或.stateStore(...)配置分布式后端(例如RedisDistributedStore)。
同 (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
需要旁路操作(例如管理台、审计、批量迁移)时,可以直接拿:
| 方法 | 说明 |
|---|---|
getContext() | 当前对话历史(不可变视图) |
contextMutable() | 可写入视图,谨慎使用 |
setSummary(...) / getSummary() | 自定义压缩摘要(自行实现压缩 middleware 时用) |
toJson() / fromJsonString(String) | 序列化与反序列化 |
清空会话对话上下文
若要让用户在不创建新会话的情况下开始新话题,可调用 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 不受影响。
提示
内存中的状态缓存会随单个 agent 实例服务过的不同 session 数量增长。大多数部署场景(几百个 session)的开销可以忽略。对于超大规模场景(单进程百万级 session),可以考虑 agent factory + 有界实例池——但由于
AgentState对象本身很轻量,这种情况很少出现。
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 状态。
可用字段:
| 方法 | 说明 |
|---|---|
getSessionId() / getUserId() | 内置字段,用于路由状态槽位与租户 |
getAgentState() / setAgentState(AgentState) | call-scoped 的 AgentState,由框架在 call 入口注入。中间件和工具应从这里读状态,而非 agent.getAgentState() |
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() | 空上下文 |
提示
AgentStateStore后端在 builder 时绑定,不能通过 RuntimeContext per-call 切换。per-call 变化的是它寻址的(userId, sessionId)槽位——按用户隔离时设置userId(或在存储上自定义keyPrefix),不要试图给每次 call 传不同的存储实例。
提示
在中间件和工具中访问
AgentState: 在 call 执行期间,始终使用RuntimeContext.resolveAgentState(ctx, agent)而非agent.getAgentState()。并发场景下,agent.getAgentState()返回的是最后一次活跃 session 的状态(多个 call 同时在飞时结果不确定),而ctx.getAgentState()返回的是本次 call 的 session 状态——这才是你需要的。
相关文档
智能体(Agent) ——
ReActAgent完整接口与 Builder 参数上下文压缩 —— 对话摘要、工具结果卸载、溢出恢复(建立在本页描述的 AgentState 基础之上)
记忆 —— 长期记忆与后台维护
权限系统 —— 权限规则的持久化