Skip to main content

无状态 Agent 引擎

ReActAgent(以及封装它的 HarnessAgent)采用无状态引擎设计:agent 实例本身只持有不可变的配置——system prompt、模型、工具集、中间件链——而所有 per-session 的可变数据都放在 AgentState 里,以 (userId, sessionId) 为索引。一个 agent 实例可以同时服务多个用户和会话,调用方只需在每次 call() 时传入不同的 RuntimeContext

这意味着什么

  • 不需要 agent-per-user 注册表。 一个 HarnessAgent 实例就能服务全部用户——每次请求只需传入不同的 RuntimeContext.userIdRuntimeContext.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() 结束,框架自动把整份 AgentStateagent_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(...) 一次:
内置的 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

多用户隔离

sessionIduserId 解决的不是同一件事:
  • sessionId —— 决定哪段对话是哪段,独立的 AgentState 快照。
  • userId —— 决定这段对话归谁,也决定文件落到谁的命名空间下,详见文件系统
两个用户的对话状态与文件路径互不干扰。生产部署如果想做 AgentState 级别的用户隔离,在 RuntimeContext 上设置 userId 即可:存储会按 (userId, sessionId) 寻址每个槽位(配合 RedisAgentStateStoreuserId 就是 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 不受影响。
内存中的状态缓存会随单个 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 状态。
可用字段:
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 基础之上)
  • 记忆 —— 长期记忆与后台维护
  • 权限系统 —— 权限规则的持久化