保存并恢复对话#
每个请求都从全新的上下文开始。历史记录扩展会恢复之前的对话,因此你可以决定一个对话能存活多久、存放在哪里。默认客户端在内存中记住各个轮次,不会把它们持久化到磁盘。
标识不等于存储#
AgentRunConfig 为对话命名:
from zett_agent.agent import AgentRunConfig
config = AgentRunConfig(session_id="project-notes")
跨请求复用同一个 session_id 才能让历史记录扩展找到正确的消息。request_id 只是一次请求的关联标签,它不是幂等键,复用它不会对任何内容去重。
选择历史记录扩展#
扩展 |
生命周期 |
|---|---|
单个进程;不传 |
|
能跨重启存活;存储原始历史记录和压缩检查点。 |
|
你自己的实现 |
参见 实现存储适配器。 |
在脚本中保留历史记录#
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 可以在对话接近模型上限时摘要较早的轮次。最近的轮次会完整保留,包括它们的工具调用和工具结果,而不会让内容变小的摘要会被丢弃。参见 压缩而不丢失历史记录。