从零实现 GeekAgent —— Day6 多会话与持久化

💡 原文中文,约11700字,阅读约需28分钟。
📝

内容提要

本文介绍GeekAgent Day6实现多会话与持久化功能。通过Sessions类管理多条独立会话,支持创建、切换、列表操作;新增storage.ts实现磁盘快照,可保存/加载整个会话集合;退出时自动保存,default会话自动生成8位ID。设计上保留单一Chat对象,切换时同步消息,确保会话互不干扰且跨进程恢复。

🔎

延伸解读

设计取舍:单一 Chat 与多会话管理

文章强调不创建多个 Chat 对象,而是保留单一 Chat,用 Sessions 管理多条 history。这避免了复制 OpenAI client 和工具循环的冗余,让模型调用逻辑无需感知会话切换。这种设计将“会话管理”与“对话执行”解耦,便于后续扩展存储或并发,但要求切换时同步消息,否则 Map 中的引用可能过期。

持久化策略:快照与恢复的边界

存档仅包含 current 和 sessions 两层,通过 Object.fromEntries 序列化 Map。Sessions 负责校验结构,storage 只做文件读写,职责分离。恢复时先校验再替换,避免坏存档污染内存。但文章也指出,异常终止(如 SIGKILL)不会触发自动保存,且多进程并发写会互相覆盖,这些是当前实现的局限。

default 会话的 ID 生成机制

为避免每次启动的 default 会话覆盖上次保存的同名会话,退出时若当前仍是 default,会生成 8 位 UUID 前缀作为新 ID,并检查碰撞。这保证了用户能通过打印的 ID 找回会话,但仅适用于未手动命名的会话。手动创建的会话(如 /new work)则保留原 ID,不额外生成。

Q&A

GeekAgent Day6 实现了哪些功能?

Day6 实现了多会话管理和持久化功能。具体包括:支持创建、切换、列出多个独立会话(/new、/open、/sessions),以及将整个会话集合保存到磁盘(/save)和从磁盘恢复(/load),并在退出时自动保存。

为什么 GeekAgent 不创建多个 Chat 对象来管理多会话?

因为 Chat 对象除了 history 还包含 OpenAI client、模型配置和完整工具循环,复制多个对象只是为了保存多条数组,职责太重。因此保留唯一的 Chat,新增 Sessions 类来管理 Map<string, Message[]>,切换时同步消息。

GeekAgent 的会话 ID 有什么限制?为什么?

会话 ID 只能包含字母、数字、短横线和下划线(正则:/^[a-zA-Z0-9_-]+$/)。因为 ID 会作为存档中的键,限制字符可以简化命令格式,并确保将来更换存储方式时无需修改会话规则。

GeekAgent 的存档文件结构是怎样的?

存档文件是 JSON 格式,包含两层:current 字段记录当前会话 ID,sessions 字段保存 ID 到消息数组的映射。例如:{"current":"work","sessions":{"default":[],"work":[{"role":"user","content":"..."}]}}。

为什么退出时 default 会话要换一个 ID?

因为 default 是每次启动时临时创建的名字,如果直接保存,下次启动时新建的 default 会话会覆盖之前的存档,导致用户无法通过稳定 ID 找回会话。因此退出时会将 default 重命名为一个 8 位随机 ID,并打印出来供用户后续使用。

GeekAgent 如何实现退出时自动保存?

通过监听 readline 的 close 事件(在 /exit、Ctrl+D 或输入流关闭时触发),在 close 回调中调用 nameDefault()(如果需要)和 saveAll() 保存整个会话集合。保存完成后不强制退出,让异步写文件自然完成。

GeekAgent 的 Sessions 和 storage 模块职责如何划分?

Sessions 负责会话规则,如校验 ID、消息格式、当前 ID 是否存在等;storage 只负责文件操作,如 mkdir、读写 JSON 文件,不理解会话结构。这样职责分离,未来更换存储方式时无需修改会话规则。

GeekAgent Day6 明确没有实现哪些功能?

Day6 明确没有实现:异常终止保存(如 SIGKILL、断电或崩溃时不会触发 close,最后修改可能丢失)、会话管理(不能删除或重命名会话)、并发写保护(多个进程保存时,最后写入者会覆盖前者)。

🏷️

标签

➡️

继续阅读