保存并恢复对话#

每个请求都从全新的上下文开始。历史记录扩展会恢复之前的对话,因此你可以决定一个对话能存活多久、存放在哪里。默认客户端在内存中记住各个轮次,不会把它们持久化到磁盘。

标识不等于存储#

AgentRunConfig 为对话命名:

from zett_agent.agent import AgentRunConfig

config = AgentRunConfig(session_id="project-notes")

跨请求复用同一个 session_id 才能让历史记录扩展找到正确的消息。request_id 只是一次请求的关联标签,它不是幂等键,复用它不会对任何内容去重。

选择历史记录扩展#

扩展

生命周期

InMemoryMessageAccumulator

单个进程;不传 extensions 时的默认选项。

SQLiteSessionExtension

能跨重启存活;存储原始历史记录和压缩检查点。

你自己的实现

参见 实现存储适配器。

在脚本中保留历史记录#

from zett_agent.client import create_agent

client = await create_agent(model)
await client.run("My preferred language is Python.")
reply = await client.run("What language did I mention?")

使用真实模型时,答案的措辞会有所变化。关键的约定是:第二次请求能收到更早的用户消息和助手消息。复用客户端或同一个累加器实例即可保留这份记忆。用新的默认累加器创建新客户端会从零开始,即使复用同一个会话 ID 也是如此。不同的 ID 会选择彼此独立的历史记录。

配置持久对话#

from zett_agent.agent import AgentRunConfig
from zett_agent.extensions.sqlite import SQLiteSessionExtension
from zett_agent.extensions.tool_guidelines import ToolGuidelinesExtension

storage = SQLiteSessionExtension("sessions.sqlite")
config = AgentRunConfig(session_id="project-notes")
try:
    client = await create_agent(
        model,
        config=config,
        extensions=[storage, ToolGuidelinesExtension()],
    )
    await client.run("Remember my preference for Python.")
finally:
    await storage.close()

这会在进程的工作目录下创建或更新一个真实文件。请选择应用自己拥有的路径,并在重启期间保持稳定。该扩展也接受 ":memory:" 用于临时的 SQLite 数据库。省略路径会使用它的用户数据默认位置;在测试和多用户应用中请使用显式路径。

重新打开同一个数据库之后,用相同的 config 创建客户端并发送下一条用户消息。当前的系统指令和工具来自新客户端;持久化的系统提示词不会在模型上下文中累积。不再需要模型提供商时,请单独关闭它。

跨进程恢复#

async def main() -> None:
    # No demo touches the default database under the user's home directory.
    with TemporaryDirectory(prefix="zett-docs-") as directory:
        path = Path(directory) / "sessions.sqlite"
        session_id = "project-notes"
        config = AgentRunConfig(session_id=session_id)
        first_storage = SQLiteSessionExtension(path)
        try:
            first = await create_agent(TurnModel(), config=config, extensions=[first_storage])
            print((await first.run("Remember Python", metadata={"origin": "tutorial"})).content)
        finally:
            await first_storage.close()

        restored_storage = SQLiteSessionExtension(path)
        try:
            restored = await create_agent(TurnModel(), config=config, extensions=[restored_storage])
            print((await restored.run("Continue")).content)
            rows = await restored_storage.list_raw_messages(session_id)
            assert [row.message.role.value for row in rows] == [
                "system",
                "user",
                "assistant",
                "system",
                "user",
                "assistant",
            ]
            assert rows[0].metadata == {"origin": "tutorial"}
            assert len(await restored_storage.list_raw_messages(session_id, limit=2, offset=2)) == 2
            view = await restored_storage.storage.load(session_id)
            assert view.snapshot is None and len(view.raw_tail) == 6
            await restored_storage.update_session(session_id, title="Project notes")
            renamed = await restored_storage.get_session(session_id)
            assert renamed is not None and renamed.title == "Project notes"
            assert await restored_storage.delete_session(session_id)
            assert await restored_storage.list_raw_messages(session_id) == []
            print("Raw roles: system, user, assistant, system, user, assistant")
            print("Session restored, renamed, and deleted in a temporary database")
        finally:
            await restored_storage.close()

这个程序写入一个临时数据库,关闭它,再打开一个新客户端,并恢复第一个轮次——这就是真实应用可以照搬的模式。使用完毕后请用 await storage.close() 关闭扩展。

警告

每个智能体只使用一个可恢复历史记录的扩展。同时用 SQLite 和内存恢复,会让模型到底看到哪些消息变得不清楚。

同一个对话的两种视图#

通常你会同时需要给界面看的历史记录和给模型看的上下文,而一旦运行压缩,两者就不完全相同。

  • await storage.list_raw_messages(session_id) 按顺序返回原始记录,也就是你在对话记录中展示的内容。

  • await storage.storage.load(session_id) 返回模型上下文:最新的检查点,以及其后的原始消息。

压缩会添加一个检查点,它从不改写原始日志。存储扩展上的分页和列表辅助方法可以覆盖其余需求(after_sequence 为开区间,through_sequence 为闭区间)。

原始日志包含系统消息和只含工具调用的助手消息,而不只是可见的聊天内容。界面应该挑选自己要渲染的角色和字段,而不是把每条记录都当成面向用户的文本展示。

records = await storage.list_raw_messages(
    "project-notes", after_sequence=0, limit=50,
)
for record in records:
    print(record.sequence, record.message.role, record.message)

storage.storage.load 返回一个 SessionView,其中包含 snapshot、raw_tail 和一个 messages 投影。在恢复过程中,持久化扩展还会跳过被中断的进程留下的不完整的助手/工具批次,因此端点永远不会收到孤立的工具结果。原始记录仍然保留在原始日志中,可用于诊断。

列出、标记和删除对话#

sessions = await storage.list_sessions(limit=20, offset=0)
await storage.update_session("project-notes", title="Project notes")
summary = await storage.get_session("project-notes")

create_session 可以在首次运行之前创建一个空会话。parent_session_id 把一个被委派的对话链接到它的父对话;session_type 是应用自定义的整数分类,不是权限或运行时模式。

要主动删除存储的会话及其日志和检查点,请调用 await storage.delete_session(session_id)。这是删除而不是归档;请先停止活动请求,并遵循你的保留和备份策略。内存历史记录则使用 accumulator.clear(session_id)。会话 ID 不控制访问权限——在任何历史记录操作之前,都要先认证所有者。

中断的运行不是事务#

请求进行过程中,持久化会追加已完成的消息记录。之后出现的错误或取消不会撤销用户消息、已完成的工具结果或外部副作用。部分的文本增量不是最终的助手记录。重启之后,请让恢复投影选择有效的上下文,而不是用界面片段拼凑出来。关于重试和幂等性的决策,参见 集成到应用。

上下文耗尽时#

添加 CompactionExtension 可以在对话接近模型上限时摘要较早的轮次。最近的轮次会完整保留,包括它们的工具调用和工具结果,而不会让内容变小的摘要会被丢弃。参见 压缩而不丢失历史记录。

下一步:了解 模型看到什么,或者转到 扩展运行时,构建你自己的行为。