Skip to content

2.4 会话状态与持久化 ​

本节你会学到

  • Agentao 实例承载的 4 块状态分别在哪里
  • 哪些必须由宿主持久化才能跨进程重启
  • 三种部署形态(长驻池 / 每请求重建 / 混合)以及怎么选

一个运行中的 Agentao 实例承载的远不止"上一句 assistant 回复"。清楚每一块状态分别存在哪里,决定了你的服务是"重启后用户不丢上下文"还是"每次重启都从零开始"。

本节回答三个问题:

  1. 一个 Agentao 实例到底握着哪些状态?
  2. 哪些必须由宿主持久化才能扛住重启?
  3. 怎么优雅地还原它们?

2.4.1 四个状态桶 ​

每个 Agentao 实例有四块独立存储——彼此不同,持久化时必须分别对待。

桶存放位置close() 后还在吗?谁负责落盘
对话消息agent.messages(内存列表)❌ 否宿主
记忆(持久化).agentao/memory.db + ~/.agentao/memory.db(SQLite)✅ 是Agentao
会话摘要项目 SQLite 的 session_summaries 表✅ 是Agentao
技能激活状态agent.skill_manager.active_skills(内存字典)❌ 否宿主(恢复时需重激活)

一句话:落在 SQLite 的 Agentao 会管;落在 Python 列表/字典的要你自己管。

2.4.2 agent.messages — 逐轮对话流水 ​

对话的核心状态。OpenAI 风格消息字典列表:

python
agent.messages = [
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么可以帮忙?"},
    {"role": "user", "content": "跑一下 git status"},
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [
            {
                "id": "call_abc123",
                "type": "function",
                "function": {
                    "name": "run_shell_command",
                    "arguments": '{"command":"git status"}',
                },
            }
        ],
    },
    {"role": "tool", "tool_call_id": "call_abc123", "content": "On branch main…"},
    {"role": "assistant", "content": "你在 main 上,工作区干净。"},
]

agent.messages 里不包含的东西 ​

  • 系统提示——每次 chat() 都会由 AGENTAO.md + 日期 + 激活的技能 + 记忆块重建,不要落盘
  • 工具 schema——每次调用都会从工具注册表重建
  • 工作目录——构造时固定,之后不可变

持久化的硬规矩 ​

  1. 成对保留:带 tool_calls 的 assistant 消息和配对的 {role: "tool", tool_call_id: ...} 要存都存,要丢都丢。只存一半会破坏 OpenAI tool-call schema,下一次 chat() 直接报错
  2. 用 JSON 序列化:每条都是普通 dict,json.dumps(agent.messages) 安全可用
  3. 一个都别滤:不要以"太吵"为由过滤掉 tool 消息——LLM 需要它们理解自己做过什么

2.4.3 存储+还原配方 ​

最小模式——每个会话一行,消息列表整体存成 JSON:

python
import json
from pathlib import Path
from agentao import Agentao
from agentao.embedding.sessions import restore_agent_skills

def save_session(agent: Agentao, session_id: str, db) -> None:
    """每次 chat() 后调用。"""
    db.upsert(
        session_id,
        {
            "messages": json.dumps(agent.messages),
            "active_skills": list(agent.skill_manager.get_active_skills().keys()),
            "working_directory": str(agent.working_directory),
            "model": agent.get_current_model(),
        },
    )

def load_session(session_id: str, db) -> Agentao:
    """宿主启动时或请求到达时调用。"""
    row = db.get(session_id)
    if row is None:
        raise KeyError(session_id)

    agent = Agentao(
        working_directory=Path(row["working_directory"]),
        model=row["model"],
    )

    # 回放消息——add_message 不会触发 LLM。
    for msg in json.loads(row["messages"]):
        agent.messages.append(msg)  # 或:agent.add_message(msg["role"], msg["content"])

    # 用 Agentao 提供的 helper 重新激活技能,不要自己循环调用
    # ``skill_manager.activate_skill``。helper 会收窄手工改过的列表,把
    # "Error: ..." 返回值当作拒绝,并且会处理 MCP skill:从不重新激活它们
    # (它们的批准只属于原来的会话),在恢复的消息里隐去它们的内容,并重新
    # 打开 MCP Skills 门禁。直接调用 ``activate_skill`` 这些都不做,还会在
    # 没有任何人同意的情况下激活 MCP skill。
    restored, skipped = restore_agent_skills(agent, row["active_skills"])

    return agent

为什么直接 append 到 agent.messages? ​

add_message(role, content) 是公开 helper——但只处理纯文本。如果某条消息带有 tool_calls 或 tool_call_id,直接写 agent.messages 能保住完整结构。两条路径都支持。

2.4.4 记忆会自愈 ​

你不需要手动持久化记忆——Agentao 已经在做了。

python
agent = Agentao(working_directory=Path("/app/users/alice"))
# MemoryManager 会自动打开 /app/users/alice/.agentao/memory.db
# 上次会话的持久化记忆自动加载。

只要同一租户跨重启使用同一个 working_directory,记忆就能接上。这也是为什么多租户隔离强调每用户一个 working_directory——它同时决定了记忆的作用域。

2.4.5 会话摘要 — 别动它 ​

当上下文窗口满了,Agentao 的压缩管线会往 agent.messages 里写入 [Conversation Summary] 块,同时把同一份摘要存到 session_summaries(SQLite)。这一切对你透明:

  • 被压缩的消息仍然留在 agent.messages 里,所以你落盘的 JSON 依然可来回复盘
  • 重启后不需要另外拉 session_summaries——摘要块已经嵌在 messages 里了

宿主代码不要直接读写 session_summaries 表。那是压缩管线的内部管子,不是对接点。

2.4.6 还原到 ACP 服务端 ​

如果你走的是 ACP 路径,改用 session/load——上面那段 SDK 食谱不适用。宿主把之前抓下来的 {role, content} 通过线协议发回去:

json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "session/load",
  "params": {
    "sessionId": "sess-restored",
    "cwd": "/app/users/alice",
    "history": [
      {"role": "user",      "content": [{"type": "text", "text": "上次的问题"}]},
      {"role": "assistant", "content": [{"type": "text", "text": "上次的回答"}]}
    ]
  }
}

字段格式与 SDK 列表不同——ACP 把内容包成"类型化块"数组。完整 schema 见附录 C · session/load。

2.4.7 常见错误 ​

错误症状修
落盘时把 tool-call 对拆散下一次 chat() 报 tool_call_id not found整条列表存;不要过滤
把系统提示也持久化了日期过期、新技能不生效只存 agent.messages,系统提示自会重建
还原时用了不同的 working_directory记忆像是没了每租户固定 working_directory,跟消息一起存
忘了重激活技能还原后 agent 忘了"自己是谁"存 active_skills 列表;load 时重激活
clear_history() 后再回放clear_history() 同时会取消激活技能,这点容易忽略要么完整重建 agent;要么清完之后再手动激活

2.4.8 热池 vs. 按需重建 ​

模式适用优点缺点
热池(agent 在内存里过多轮)聊天 UI、IDE 集成零重建延迟;MCP / 技能状态已热活跃会话越多 RAM 越多;崩溃会吞没未落盘的轮次
每请求重建(每次从 DB 加载)Serverless、稀疏请求Pod 无状态,易扩缩每轮多 ~50–200 ms 用于回放 + 重开 MCP
混合(热池 + 回落 DB)SaaS 聊天机器人热会话快,冷会话自愈代码量多一些

生产部署常选混合模式:热池见 6.7 模式 B · 会话池 + TTL 淘汰,被淘汰的会话通过 2.4.3 还原。

TL;DR ​

  • 状态分布在 4 个桶:messages、记忆 DB、MCP 子进程、working_directory 内容。
  • 跨重启持久化:把 agent.messages 序列化进你的 DB;记忆和工作目录靠磁盘自然保留。
  • 恢复路径:重建 agent → 用 add_message(role, content) 逐条回放历史 → 像往常一样调 chat()。
  • 三种部署形态:长驻池(低延迟、要粘性会话)、每请求重建(无状态,每轮多 ~50–200 ms)、混合(热池 + DB 回落,SaaS 常用)。

下一节:2.5 运行时切换 LLM →