Skip to content

状态与会话

Agent 核心 已经说明:Agent 本身无状态,会话上下文、权限规则、压缩摘要、当前 reply_id 都显式由 AgentState 持有。本页聚焦"状态长什么样、怎么跨进程跨会话存下来、中断后如何从持久化状态恢复"。这是把 Agent 从单进程脚本变成多租户、多会话服务的前提。

版本基线:本文以 Python agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python >=3.11。 导入路径为 from agentscope.state import AgentState, Task, TaskContext(源: src/agentscope/state/__init__.py)与 from agentscope.app.storage import RedisStorage, StorageBase (源:src/agentscope/app/storage/__init__.py)。2.x 文档站存在版本化陷阱:非版本化的 /en/ URL 会 404,本页所有官方链接均使用 versions/2.0.4/en/ 形式。

为什么需要状态与会话

一次 reply 调用结束后,进程内存里的 AgentState 就持有这轮对话的全部上下文。但只要进程退出、 请求落到另一台机器、或者同一个用户与同一个 Agent 开了第二个对话窗,内存状态就丢了。要把 Agent 变成可恢复、可水平扩展的服务,必须解决两个问题:

  1. 状态外置:把 AgentState 从 Agent 实例里抽出来,序列化后存到后端。
  2. 会话寻址:用稳定的标识定位"哪个用户的哪个 Agent 的哪次会话",加载对应状态。

前端类比

如果你来自前端:

  • AgentState 类比为 Redux Store--它是一个可序列化的纯数据结构,Agent 是消费它的 reducer / 执行器。Redux 的"组件 = 纯函数 + Store"在这里对应"Agent = ReAct 循环 + AgentState"。
  • 把会话(session)类比为 cookie / sessionStorage:浏览器用 cookie 在请求间标识用户, AgentScope 用 (user_id, agent_id, session_id) 三元组在请求间定位状态。sessionStorage 关闭标签 即失效,类似进程内 AgentState 进程退出即丢失;要跨进程存活就得像 cookie 一样存到后端。

AgentScope 原生语义AgentState 是 Pydantic model,不是 Redux Store 那样的事件归约器,它没有 reducer、没有 dispatch、没有订阅。会话也不是 HTTP cookie--AgentScope 的 session_id 存在 AgentState.session_id 字段里,并作为 RedisStorage 寻址键的一部分。前端类比只是帮助理解"状态 外置 + 标识寻址"这个模式,不要把 Redux 的不可变更新、action 类型等概念套到 AgentState 上。

AgentState 结构

AgentState(来自 agentscope.state)是一个 Pydantic model,持有 Agent 恢复到原状态所需的全部 信息。以下字段全部源自我抓取的 src/agentscope/state/_state.py,不是文档摘要转述:

字段类型作用
session_idstr会话标识。每个会话为每个 Agent 维护一份独立的 AgentState
summarystr | list[TextBlock | DataBlock]上下文压缩后的摘要,会前置注入到 LLM 上下文
contextlist[Msg]未压缩的完整对话上下文,喂给 LLM 的主历史
reply_idstr当前回复标识,同时作为该回复最终消息的 id,用于中断与恢复时定位
cur_iterintReAct 循环当前迭代轮次
permission_contextPermissionContext权限上下文,传给 toolkit 决定工具权限,包含 HITL 中用户接受的规则
tool_contextToolContext工具上下文:读写文件缓存、已激活的 ToolGroup 名称
tasks_contextTaskContextPlan 模式下的任务列表状态
middle_contextdict[str, Any]中间件跨回复存取数据的上下文

几个要点:

  • session_id 默认用 _generate_id 自动生成。持久化时,RedisStorage 按外部传入的 session_id 寻址,而 AgentState.session_id 字段记录这份状态归属的会话--两者应保持一致。
  • reply_id 是中断恢复的关键。HITL 恢复事件(UserConfirmResultEvent 等)的 reply_id 必须与 暂停时的 agent.state.reply_id 一致,否则无法定位挂起状态。
  • 因为它是普通 Pydantic model,可以 model_dump() 序列化为 JSON 存到任意后端。内置的 RedisStorage 就是按这个三元组组织存储的。

AgentState 还提供两个辅助方法:append_context(name, blocks) 把内容块追加到当前 reply_id 对应的 assistant 消息;has_awaiting_tool_calls(name) 判断尾部消息是否有仍未收到结果的工具调用 (用于判断 Agent 是否停在 HITL 暂停态)。日常持久化场景一般不直接调它们,知道存在即可。

RedisStorage 持久化

RedisStorage(来自 agentscope.app.storage)是内置的会话状态后端,继承自抽象基类 StorageBase。它按 (user_id, agent_id, session_id) 三元组组织状态,是 async 上下文管理器,用 async with 打开和关闭连接池。本页只关心与状态相关的三个热路径方法(签名源自我抓取的 src/agentscope/app/storage/_base.py):

方法作用
get_session(user_id, agent_id, session_id) -> SessionRecord | None加载会话记录,其 .state 即保存的 AgentState;找不到返回 None
upsert_session(user_id, agent_id, config, state=None, session_id=None, ...) -> SessionRecord首次创建或更新会话。session_id=None 创建新会话,传入则更新现有会话
update_session_state(user_id, agent_id, session_id, state) -> None热路径,仅更新会话的可变 state;会话不存在时抛 KeyError

upsert_session 是首次创建会话的入口:它会写入不可变的 SessionConfig(模型、workspace 等配置) 并初始化 state。update_session_state 是每轮对话结束后的热路径,它假设会话已存在--所以第一次必须 先 upsert_session,后续轮次才能用 update_session_state 只更新 state。

下面是一个完整的"首次创建 -> 多轮对话 -> 持久化"示例。凭据通过环境变量注入,不要写死在代码里:

python
import asyncio
import os

from agentscope.agent import Agent
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.message import UserMsg
from agentscope.state import AgentState
from agentscope.app.storage import RedisStorage, SessionConfig, SessionSource

USER_ID = "u1"
AGENT_ID = "a1"
SESSION_ID = "s1"


def build_agent(state: AgentState) -> Agent:
    return Agent(
        name="my_agent",
        system_prompt="你是一个严谨的中文助手,回答前先确认事实。",
        model=DashScopeChatModel(
            credential=DashScopeCredential(
                api_key=os.environ["DASHSCOPE_API_KEY"],
            ),
            model="qwen-plus",
        ),
        state=state,
    )


async def chat_turn(storage: RedisStorage, content: str) -> str:
    # 1. 加载已有会话状态;首次为 None
    record = await storage.get_session(USER_ID, AGENT_ID, SESSION_ID)

    if record is None:
        # 2. 首次:upsert_session 创建会话并初始化 state
        state = AgentState()
        await storage.upsert_session(
            user_id=USER_ID,
            agent_id=AGENT_ID,
            config=SessionConfig(),
            state=state,
            session_id=SESSION_ID,
            source=SessionSource.USER,
        )
    else:
        # 3. 后续:从持久化记录恢复 state
        state = record.state

    # 4. 用恢复的 state 构造 Agent,执行 reply
    agent = build_agent(state)
    result = await agent.reply(UserMsg(name="user", content=content))

    # 5. 把更新后的 state 写回(热路径,仅更新可变 state)
    await storage.update_session_state(
        user_id=USER_ID,
        agent_id=AGENT_ID,
        session_id=SESSION_ID,
        state=agent.state,
    )
    return result.get_text_content()


async def main() -> None:
    async with RedisStorage(host="localhost", port=6379) as storage:
        print(await chat_turn(storage, "我叫苏振宇。"))
        # 进程重启后,下一轮仍能从 Redis 恢复,记得"苏振宇"
        print(await chat_turn(storage, "我刚才告诉了你什么?"))

asyncio.run(main())

关键点:每次 chat_turn 都从存储重新加载 state、重新构造 Agent。这就是"状态外置"的体现--Agent 实例是一次性的,真正跨请求存活的是 AgentState

会话生命周期

下面这张时序图说明一次完整的中断-恢复会话生命周期:客户端创建会话、从存储加载 state、Agent 执行 reply、持久化更新后的 state、运行中被中断、随后从持久化 state 重载并恢复。图前先说明:中断可以 发生在 Agent 运行中(task.cancel())或暂停态(UserInterruptEvent),恢复都是"重新从存储加载 state -> 构造 Agent -> 再次 reply"。

图后的解释:

  • 加载分支get_session 返回 None 走首次创建(upsert_session),返回记录则直接用 record.state。这两条分支在 chat_turn 示例里都有体现。
  • 中断不丢失状态:中断只取消正在运行的 task,已累积在 AgentState 里的上下文不会丢。但 必须在中断前或中断后把 state 写回存储,否则进程退出后内存里的更新就没了。
  • 恢复即重载:恢复不是"唤醒暂停的 Agent 实例",而是"从存储读 state -> 新建 Agent -> 再次 reply"。这就是为什么 Agent 可以无状态地水平扩展。

中断与恢复

中断与恢复的语义在 Agent 核心 已有说明,本页补充"如何与持久化 组合"。

Agent 正在运行--取消正在 await 的 task:

python
import asyncio

from agentscope.agent import Agent
from agentscope.message import UserMsg

async def chat(agent: Agent) -> None:
    async for event in agent.reply_stream(
        UserMsg(name="user", content="..."),
    ):
        ...

async def main() -> None:
    agent = Agent(...)
    task = asyncio.create_task(chat(agent))

    # 随时取消以中断 Agent
    await asyncio.sleep(1)
    task.cancel()

asyncio.run(main())

中断后,务必把当前 agent.state 写回存储,再在下次请求里重新加载恢复:

python
# 中断后持久化(task.cancel() 之后)
await storage.update_session_state(
    user_id=USER_ID,
    agent_id=AGENT_ID,
    session_id=SESSION_ID,
    state=agent.state,
)

# 下次请求:从存储重载 -> 新建 Agent -> 再次 reply 即恢复
record = await storage.get_session(USER_ID, AGENT_ID, SESSION_ID)
agent = build_agent(record.state)

Agent 处于暂停态(等待 HITL 确认或外部执行)--传入 UserInterruptEvent

python
from agentscope.event import UserInterruptEvent

async def main() -> None:
    agent = Agent(...)
    # Agent 此前已因 RequireUserConfirmEvent 暂停
    async for event in agent.reply_stream(
        UserInterruptEvent(reply_id=agent.state.reply_id),
    ):
        print(event)

UserInterruptEvent 会丢弃挂起状态、为每个未完成工具调用合成一个标记为"被用户中断"的 ToolResultBlock,让 Agent 回到可接收新输入的状态。若 Agent 未处于暂停态,该事件是 no-op。

迭代超限ReActConfig 控制最大迭代次数,超过时产出 ExceedMaxItersEvent 终止循环。这是兜底 机制,触发的 state 同样应该写回存储,以便下次会话能看到"为什么停了"。

与 1.x 的区别

1.x 用 state_dict / load_state_dict 管理 Agent 状态,语义类似一个自由 dict 的存取。2.x 把它 整体替换为 Pydantic model AgentState

维度1.x2.x
状态对象state_dict(dict)AgentState(Pydantic model)
存取方法load_state_dict / saveRedisStorage.get_session / update_session_state
寻址无统一三元组(user_id, agent_id, session_id)
类型安全Pydantic 校验,字段显式

1.x 的 state_dict / load_state_dict 在 2.x 已移除,不能作为 2.x 代码使用。完整迁移指引见 1.x 到 2.x 迁移

常见错误

  • state 未持久化就丢失:进程退出或请求落到另一台机器时,内存里的 AgentState 就没了。每轮 reply 结束后必须调用 update_session_state 把 state 写回存储;中断后也要写回,否则中断点 的上下文丢失。
  • session_id 混用RedisStorage 按外部传入的 session_id 寻址,而 AgentState.session_id 字段记录这份状态归属的会话。两者应保持一致,否则会出现"存进去的 state 用另一个 id 读不到"的 幽灵问题。
  • 误用 state_dict / load_state_dict:这是 1.x 接口,2.x 已移除。2.x 用 AgentState Pydantic model + RedisStorage,不要照搬 1.x 代码。
  • update_session_state 在会话不存在时调用:该方法假设会话已存在,会话不存在时抛 KeyError。 首次必须先 upsert_session 创建会话。
  • HITL 恢复时 reply_id 不匹配:恢复事件的 reply_id 必须与 agent.state.reply_id 一致, 否则无法定位挂起状态。从存储重载 state 时,reply_id 会随之恢复,直接用即可。
  • 把 Agent 当成状态容器序列化:Agent 自身无状态,跨进程恢复靠的是 AgentState 持久化,而不是 pickle Agent 实例。

先修

下一步

官方参考

学习文档整合站点