本文回答什么问题:nanobot 的会话怎么存?为什么用 JSONL 而不是 SQLite?原子写 + fsync 怎么保证不丢消息?session_key 重命名怎么用?
目标读者 :LLM Agent 开发者 / 系统架构师
预计阅读时间 :10 分钟
源码版本 :GitHub HKUDS/nanobot main 分支主线代码(仓库相对路径)
SessionManager(nanobot/session/manager.py)负责把所有回合的消息原子追加到 JSONL 文件。本章聚焦"为什么 JSONL"+"原子写怎么实现"+"重启后怎么恢复"。
1. 整体定位:为什么需要 SessionManager
如果不持久化,每次重启 nanobot 用户都会"失忆"------上轮的对话全部丢失。SessionManager 解决:
- 每条消息原子写(不丢)
- JSONL 格式(易读 / 易回放 / 易压缩)
- 重启后
_load_recent()自动恢复最近 50 条
核心要点速查(建议收藏)
- 核心文件 :
nanobot/session/manager.py(约 300 行) - 存储位置 :
~/.nanobot/sessions/<session_key>.jsonl - 格式:每行一个 JSON 对象(Message)
- 原子写 :写临时文件 +
os.replace()+fsync防丢 - 3 个核心方法 :
append(key, msg)/get_recent(key, n)/get_summary(key)
2. JSONL 格式
jsonl
{"role": "user", "content": "你好", "timestamp": "2026-08-07T10:00:00Z"}
{"role": "assistant", "content": "你好!有什么可以帮你的?", "timestamp": "2026-08-07T10:00:05Z"}
{"role": "user", "content": "Python 怎么读取 JSON?", "timestamp": "2026-08-07T10:00:30Z"}
{"role": "assistant", "content": "用 json 模块...", "timestamp": "2026-08-07T10:00:35Z"}
JSONL vs SQLite 优势:
- 易读(
cat即可看) - 易回放(
head -n 100取最近 100 条) - 易压缩(
gzip压 80%) - 简单实现(无需 SQLAlchemy / 迁移)
JSONL 劣势:
- 查询慢(全文扫描)
- 但 nanobot 只查"最近 N 条",无需全文搜索
3. 3 个核心方法
3.1 append(session_key, message)
python
def append(self, session_key: str, message: Message) -> None:
path = self._session_path(session_key)
line = json.dumps(message.to_dict(), ensure_ascii=False) + "\n"
# 原子写
tmp_path = path.with_suffix(".tmp")
with open(tmp_path, "a", encoding="utf-8") as f:
f.write(line)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, path) # 原子替换
3 步保证不丢:
- 写
.tmp临时文件 flush()+os.fsync()强制刷盘os.replace()原子替换(同一文件系统内 rename 是原子的)
3.2 get_recent(session_key, n=50)
python
def get_recent(self, session_key: str, n: int = 50) -> list[Message]:
path = self._session_path(session_key)
if not path.exists():
return []
# 从文件末尾读 N 行(避免加载全部)
with open(path, "rb") as f:
f.seek(0, 2) # 跳到末尾
size = f.tell()
# 反向扫描,直到读够 N 行
...
优化:不读完整文件,反向扫描最近 N 行------避免长会话读取慢。
3.3 get_summary(session_key)
python
def get_summary(self, session_key: str) -> str | None:
summary_path = self._session_path(session_key).with_suffix(".summary.md")
if not summary_path.exists():
return None
return summary_path.read_text(encoding="utf-8")
summary 文件 由 AutoCompact(第 16 章)生成,与消息文件分开存。
4. 4 个核心决策
决策 1 · 为什么用 JSONL?
简单 / 易读 / 易回放------SQLite 优势在 nanobot 用不到,劣势(迁移 / 依赖)反倒是负担。
决策 2 · 为什么原子写而不是直接 append?
直接 open(path, "a") 在断电 / kill -9 时可能丢消息;原子写保证要么完整要么完全没写。
决策 3 · 为什么每条消息一行而不是整个会话一个 JSON?
JSONL 支持追加(整个 JSON 必须 reparse-rewrite 整文件);JSONL 也支持反向扫描(每行独立)。
决策 4 · 为什么 session_key 默认 f"{channel}:{chat_id}"?
简单 + 隔离(详见第 05 章)。重命名机制见 §5。
5. 实战:session_key 重命名
python
# WebUI 提供"重命名会话"功能
async def rename_session(old_key: str, new_key: str):
old_path = Path.home() / ".nanobot" / "sessions" / f"{old_key}.jsonl"
new_path = Path.home() / ".nanobot" / "sessions" / f"{new_key}.jsonl"
old_path.rename(new_path) # 原子重命名
6. 实战:跨会话引用
yaml
# ~/.nanobot/config.yaml
sessionAlias:
work: "telegram:12345" # 别名 work -> 真实 session
home: "discord:67890"
work 可在 WebUI / CLI 当 session_key 用,实际指向 telegram:12345。
7. 常见问题 / 避坑
Q:session 文件太大怎么办?
A :du -sh ~/.nanobot/sessions/ 看大小;gzip ~/.nanobot/sessions/*.jsonl 压缩 80%;AutoCompact(第 16 章)会自动摘要旧消息。
Q:断电 / kill -9 后消息会丢吗?
A :------os.fsync() 强制刷盘。但当前正在写入的消息可能截断(因为是行级别,JSONL 解析会跳过截断行)。
Q:怎么导出 / 备份会话?
A :tar czf backup.tgz ~/.nanobot/sessions/;或单文件 cp <key>.jsonl ~/Desktop/
Q:删一个 session?
A :rm ~/.nanobot/sessions/<key>.jsonl;或 WebUI "delete session" 按钮。
8. 小结
- JSONL 格式 每行一个 Message
- 关键模块:原子写 临时文件 + fsync + os.replace
- 设计要点:3 个核心方法 append / get_recent / get_summary
- 常见坑**:存储位置
~/.nanobot/sessions/<key>.jsonl
本文要点速查
- JSONL + 原子写 见 §2 + §3.1
- get_recent 反向扫描 见 §3.2
- 重命名 + 别名 见 §5 + §6
- 下一步:第 15 章《记忆与 Dream 整合》------ MEMORY.md 怎么从 messages 提取
按角色推荐
- LLM Agent 开发者:必读(自定义 session 必读)
- 系统架构师:必读(持久化机制必读)
- LLM Provider 适配者:选读
- 聊天通道开发者:选读
- Tool / MCP 工具开发者:选读
下一步
- 第 15 章《记忆与 Dream 整合》 ------ MEMORY.md 怎么生成(主题群"Agent 核心",第 3 周)
- 第 16 章《自动压缩 AutoCompact》 ------ 长会话怎么摘要(主题群"Agent 核心",第 3 周)
tags :#nanobot #AI Agent #LLM #Python #源码解析 #会话管理 #持久化