Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • nanobot 会话与记忆系统深度分析

nanobot 会话与记忆系统深度分析

概述

nanobot 的会话与记忆系统是一个轻量级但设计精巧的双存储架构,旨在为 AI 助手提供持久的上下文感知能力。系统分为两个核心模块:

  1. 会话管理器 - 管理对话历史,支持跨渠道的会话隔离
  2. 记忆系统 - 持久化存储重要信息,分为长期记忆和日记式记录

设计目标

  • 轻量级 - 使用纯文本格式(JSONL、Markdown),无需数据库
  • 渠道隔离 - 不同聊天渠道的会话完全独立
  • 持久化 - 所有数据持久化到磁盘,支持程序重启后恢复
  • 可读性 - 使用人类可读的格式,便于调试和人工审查
  • 内存缓存 - 热数据缓存在内存中,提升性能

会话管理器

类结构

Session 类数据结构

Session 类位于 /home/sujie/dev/github/nanobot/nanobot/session/manager.py:14-59:

@dataclass
class Session:
    key: str  # channel:chat_id
    messages: list[dict[str, Any]] = field(default_factory=list)
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    metadata: dict[str, Any] = field(default_factory=dict)

消息存储格式

每条消息包含以下字段(session/manager.py:28-37):

{
    "role": "user" | "assistant",
    "content": "消息内容",
    "timestamp": "2024-01-15T10:30:00.123456",
    # 可选的额外字段通过 **kwargs 注入
}

关键方法

  1. add_message (line 28-37): 添加消息到会话,自动更新 updated_at
  2. get_history (line 39-53): 获取 LLM 格式的消息历史,支持截断
  3. clear (line 55-58): 清空会话消息

SessionManager 职责

初始化 (line 68-71)

def __init__(self, workspace: Path):
    self.workspace = workspace
    self.sessions_dir = ensure_dir(Path.home() / ".nanobot" / "sessions")
    self._cache: dict[str, Session] = {}

关键点:

  • 会话存储在 ~/.nanobot/sessions/ 目录
  • 使用 _cache 字典实现内存缓存,减少磁盘 I/O

会话获取流程

核心方法实现

1. get_or_create (line 78-98) - 获取或创建会话

def get_or_create(self, key: str) -> Session:
    # 检查缓存
    if key in self._cache:
        return self._cache[key]

    # 尝试从磁盘加载
    session = self._load(key)
    if session is None:
        session = Session(key=key)

    self._cache[key] = session
    return session

2. _load (line 100-134) - 从磁盘加载会话

def _load(self, key: str) -> Session | None:
    path = self._get_session_path(key)

    if not path.exists():
        return None

    try:
        messages = []
        metadata = {}
        created_at = None

        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue

                data = json.loads(line)

                if data.get("_type") == "metadata":
                    metadata = data.get("metadata", {})
                    created_at = datetime.fromisoformat(data["created_at"])
                else:
                    messages.append(data)

        return Session(
            key=key,
            messages=messages,
            created_at=created_at or datetime.now(),
            metadata=metadata
        )
    except Exception as e:
        logger.warning(f"Failed to load session {key}: {e}")
        return None

3. save (line 136-154) - 保存会话到磁盘

def save(self, session: Session) -> None:
    path = self._get_session_path(session.key)

    with open(path, "w") as f:
        # 先写入元数据行
        metadata_line = {
            "_type": "metadata",
            "created_at": session.created_at.isoformat(),
            "updated_at": session.updated_at.isoformat(),
            "metadata": session.metadata
        }
        f.write(json.dumps(metadata_line) + "\n")

        # 写入所有消息
        for msg in session.messages:
            f.write(json.dumps(msg) + "\n")

    self._cache[session.key] = session

会话历史截断策略

get_history 方法 (line 39-53) 实现了截断逻辑:

def get_history(self, max_messages: int = 50) -> list[dict[str, Any]]:
    # 获取最近的 max_messages 条
    recent = self.messages[-max_messages:] if len(self.messages) > max_messages else self.messages

    # 转换为 LLM 格式(只保留 role 和 content)
    return [{"role": m["role"], "content": m["content"]} for m in recent]

设计亮点:

  • 默认截断为 50 条消息,控制 token 使用
  • 转换时只保留 role 和 content 字段,符合 LLM API 要求
  • 保留原始消息中的 timestamp 和其他元数据用于持久化

记忆系统架构

类结构

MemoryStore 位于 /home/sujie/dev/github/nanobot/nanobot/agent/memory.py:9-110。

存储结构

长期记忆 (MEMORY.md)

示例文件位于 /home/sujie/dev/github/nanobot/workspace/memory/MEMORY.md:1-24:

# Long-term Memory

This file stores important information that should persist across sessions.

## User Information
(Important facts about the user)

## Preferences
(User preferences learned over time)

## Project Context
(Information about ongoing projects)

## Important Notes
(Things to remember)

---

*This file is automatically updated by nanobot when important information should be remembered.*

日记式记忆 (YYYY-MM-DD.md)

按日期分隔的日记文件,每天一个:

  • 文件路径:workspace/memory/2024-01-15.md
  • 自动添加日期标题:# 2024-01-15

记忆写入策略

1. append_today (line 32-44) - 追加到今日笔记

def append_today(self, content: str) -> None:
    today_file = self.get_today_file()

    if today_file.exists():
        existing = today_file.read_text(encoding="utf-8")
        content = existing + "\n" + content
    else:
        # 新建文件时添加日期标题
        header = f"# {today_date()}\n\n"
        content = header + content

    today_file.write_text(content, encoding="utf-8")

2. write_long_term (line 52-54) - 覆盖写入长期记忆

def write_long_term(self, content: str) -> None:
    self.memory_file.write_text(content, encoding="utf-8")

设计区别:

  • append_today 是追加模式,适合记录零散信息
  • write_long_term 是覆盖模式,适合维护结构化知识

最近记忆查询

get_recent_memories (line 56-80) - 获取最近 N 天的记忆

def get_recent_memories(self, days: int = 7) -> str:
    from datetime import timedelta

    memories = []
    today = datetime.now().date()

    for i in range(days):
        date = today - timedelta(days=i)
        date_str = date.strftime("%Y-%m-%d")
        file_path = self.memory_dir / f"{date_str}.md"

        if file_path.exists():
            content = file_path.read_text(encoding="utf-8")
            memories.append(content)

    return "\n\n---\n\n".join(memories)

特点:

  • 从今天开始向前回溯
  • 如果某天没有记录则跳过
  • 用 --- 分隔不同的日期

持久化设计

JSONL 格式详解

会话文件使用 JSONL (JSON Lines) 格式,每行一个 JSON 对象:

~/.nanobot/sessions/telegram_123456789.jsonl

文件结构:

{"_type":"metadata","created_at":"2024-01-15T10:00:00","updated_at":"2024-01-15T11:30:00","metadata":{}}
{"role":"user","content":"Hello","timestamp":"2024-01-15T10:00:00"}
{"role":"assistant","content":"Hi there!","timestamp":"2024-01-15T10:00:01"}
{"role":"user","content":"How are you?","timestamp":"2024-01-15T11:30:00"}

JSONL 的优势

  1. 流式处理 - 可以逐行读取,无需加载整个文件到内存
  2. 易于追加 - 新消息只需在文件末尾追加一行
  3. 可读性好 - 人类可以轻松查看和编辑
  4. 版本控制友好 - git diff 能清晰显示每行变化
  5. 容错性强 - 单行损坏不影响其他消息

元数据行设计

第一行作为元数据行,使用 _type: "metadata" 标识:

{
    "_type": "metadata",
    "created_at": "2024-01-15T10:00:00",
    "updated_at": "2024-01-15T11:30:00",
    "metadata": {}
}

优势:

  • 快速读取会话基本信息(无需解析所有消息)
  • 支持未来扩展(可添加更多元数据字段)
  • 便于会话列表功能(list_sessions 方法只需读取第一行)

文件路径生成和安全处理

_get_session_path (line 73-76):

def _get_session_path(self, key: str) -> Path:
    safe_key = safe_filename(key.replace(":", "_"))
    return self.sessions_dir / f"{safe_key}.jsonl"

safe_filename (utils/helpers.py:69-75):

def safe_filename(name: str) -> str:
    """Convert a string to a safe filename."""
    # 替换不安全字符
    unsafe = '<>:"/\\|?*'
    for char in unsafe:
        name = name.replace(char, "_")
    return name.strip()

安全处理流程:

  1. 将 : 替换为 _(避免路径冲突)
  2. 过滤文件系统不支持的字符
  3. 移除首尾空格

示例转换:

  • telegram:123456789 → telegram_123456789.jsonl
  • cli:direct → cli_direct.jsonl

会话上下文使用

session_key 生成规则

在 bus/events.py:20-23 中定义:

@property
def session_key(self) -> str:
    """Unique key for session identification."""
    return f"{self.channel}:{self.chat_id}"

示例:

  • Telegram 私聊:telegram:123456789
  • Telegram 群组:telegram:-1001234567890
  • CLI:cli:direct

Agent Loop 中的会话流程

代码实现

AgentLoop._process_message (agent/loop.py:123-216):

async def _process_message(self, msg: InboundMessage) -> OutboundMessage | None:
    # 获取或创建会话
    session = self.sessions.get_or_create(msg.session_key)

    # 更新工具上下文
    message_tool = self.tools.get("message")
    if isinstance(message_tool, MessageTool):
        message_tool.set_context(msg.channel, msg.chat_id)

    # 构建初始消息
    messages = self.context.build_messages(
        history=session.get_history(),  # 从会话获取历史
        current_message=msg.content,
        media=msg.media if msg.media else None,
    )

    # Agent Loop 处理...
    # ...

    # 保存到会话
    session.add_message("user", msg.content)
    session.add_message("assistant", final_content)
    self.sessions.save(session)

    return OutboundMessage(
        channel=msg.channel,
        chat_id=msg.chat_id,
        content=final_content
    )

跨渠道会话隔离机制

通过 session_key = "{channel}:{chat_id}" 实现隔离:

渠道chat_idsession_key会话文件
Telegram123456789telegram:123456789telegram_123456789.jsonl
WhatsApp5511999888777whatsapp:5511999888777whatsapp_5511999888777.jsonl
CLIdirectcli:directcli_direct.jsonl

隔离效果:

  • 同一用户在不同渠道的对话相互独立
  • 每个渠道可以有独立的会话历史
  • 渠道切换不会干扰其他渠道的上下文

会话历史在 LLM 调用中的作用

ContextBuilder.build_messages (agent/context.py:115-147):

def build_messages(
    self,
    history: list[dict[str, Any]],  # 来自 session.get_history()
    current_message: str,
    skill_names: list[str] | None = None,
    media: list[str] | None = None,
) -> list[dict[str, Any]]:
    messages = []

    # 1. System prompt (包含记忆)
    system_prompt = self.build_system_prompt(skill_names)
    messages.append({"role": "system", "content": system_prompt})

    # 2. Conversation history
    messages.extend(history)

    # 3. Current user message
    user_content = self._build_user_content(current_message, media)
    messages.append({"role": "user", "content": user_content})

    return messages

最终消息结构:

[
  {
    "role": "system",
    "content": "# nanobot 🐈\n\n# Memory\n\n## Long-term Memory\n..."
  },
  {
    "role": "user",
    "content": "What's my name?"
  },
  {
    "role": "assistant",
    "content": "I don't have your name recorded."
  },
  {
    "role": "user",
    "content": "My name is Alice."
  }
]

记忆集成

记忆在系统提示中的位置

ContextBuilder.build_system_prompt (agent/context.py:27-70):

def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
    parts = []

    # 1. 核心身份
    parts.append(self._get_identity())

    # 2. Bootstrap 文件 (AGENTS.md, SOUL.md, USER.md, etc.)
    bootstrap = self._load_bootstrap_files()
    if bootstrap:
        parts.append(bootstrap)

    # 3. Memory 上下文
    memory = self.memory.get_memory_context()  # ← 这里集成记忆
    if memory:
        parts.append(f"# Memory\n\n{memory}")

    # 4. Skills (always-loaded 和 available)
    # ...

    return "\n\n---\n\n".join(parts)

长期记忆和今天笔记的组装逻辑

MemoryStore.get_memory_context (agent/memory.py:90-110):

def get_memory_context(self) -> str:
    """
    Get memory context for the agent.

    Returns:
        Formatted memory context including long-term and recent memories.
    """
    parts = []

    # 长期记忆
    long_term = self.read_long_term()
    if long_term:
        parts.append("## Long-term Memory\n" + long_term)

    # 今天笔记
    today = self.read_today()
    if today:
        parts.append("## Today's Notes\n" + today)

    return "\n\n".join(parts) if parts else ""

系统提示中的记忆部分:

# Memory

## Long-term Memory
# Long-term Memory

This file stores important information that should persist across sessions.

## User Information
(Alice prefers Python over JavaScript)

## Preferences
(Always include code examples in responses)

---

## Today's Notes
# 2024-01-15

- Discussed Python vs JavaScript with Alice
- She is working on a web scraper project

记忆检索策略

当前实现使用完整内容加载策略:

  1. 长期记忆 - 每次完整加载 MEMORY.md
  2. 今天笔记 - 每次加载今天的 YYYY-MM-DD.md

特点:

  • 简单直接,无需复杂索引
  • 适合小规模记忆文件(<10KB)
  • 随着记忆增长可能需要优化(如实现向量检索)

潜在的优化方向:

  • 使用 get_recent_memories(days=7) 替代完整加载
  • 实现语义搜索(向量化 + 相似度检索)
  • 按重要性分级(必须记住 vs 可选)

关键代码位置索引

模块文件路径关键类/方法行号
会话管理
Session 类nanobot/session/manager.pySession14-59
SessionManager 类nanobot/session/manager.pySessionManager61-203
创建/获取会话nanobot/session/manager.pyget_or_create78-98
加载会话nanobot/session/manager.py_load100-134
保存会话nanobot/session/manager.pysave136-154
获取历史nanobot/session/manager.pyget_history39-53
记忆系统
MemoryStore 类nanobot/agent/memory.pyMemoryStore9-110
读取今日笔记nanobot/agent/memory.pyread_today25-30
追加今日笔记nanobot/agent/memory.pyappend_today32-44
读取长期记忆nanobot/agent/memory.pyread_long_term46-50
写入长期记忆nanobot/agent/memory.pywrite_long_term52-54
获取最近记忆nanobot/agent/memory.pyget_recent_memories56-80
获取记忆上下文nanobot/agent/memory.pyget_memory_context90-110
上下文构建
ContextBuilder 类nanobot/agent/context.pyContextBuilder12-218
构建系统提示nanobot/agent/context.pybuild_system_prompt27-70
构建消息列表nanobot/agent/context.pybuild_messages115-147
Agent Loop
AgentLoop 类nanobot/agent/loop.pyAgentLoop24-330
处理消息nanobot/agent/loop.py_process_message123-216
事件系统
InboundMessagenanobot/bus/events.pysession_key property20-23
工具函数
安全文件名nanobot/utils/helpers.pysafe_filename69-75
今日日期nanobot/utils/helpers.pytoday_date52-54

深挖价值点

1. JSONL 元数据行设计 (session/manager.py:120-124)

技术点:在流式文件中嵌入元数据

为什么值得深挖:

  • 避免了元数据与数据分离的复杂性
  • 无需额外索引文件即可快速获取会话信息
  • 为未来扩展预留了空间(可在 metadata 中添加用户标签、会话类型等)

适用场景:

  • 日志系统(元数据:日志级别、模块)
  • 事件流(元数据:事件类型、时间戳)
  • 数据管道(元数据:批次信息、校验和)

2. 会话历史截断与 LLM 格式转换 (session/manager.py:39-53)

技术点:分离存储格式与 API 格式

为什么值得深挖:

  • 存储完整数据(timestamp、metadata)用于调试和分析
  • API 只传输必要字段(role、content)节省 token
  • 灵活的截断策略支持不同的 LLM 模型

扩展可能:

  • 实现智能截断(保留关键上下文,丢弃重复对话)
  • 添加消息重要性评分(按重要性而非时间排序)
  • 支持滑动窗口(保持上下文连续性)

3. 双存储记忆架构 (agent/memory.py)

技术点:长期记忆 vs 短期笔记的分离

为什么值得深挖:

  • 模拟人类记忆模式(长期知识 + 短期工作记忆)
  • 不同数据适合不同的更新策略(覆盖 vs 追加)
  • 为记忆迁移和压缩提供基础

扩展可能:

  • 自动化记忆迁移(将频繁访问的笔记升级为长期记忆)
  • 记忆压缩(总结多日笔记为长期知识)
  • 记忆遗忘机制(删除过时的短期笔记)

4. 跨渠道会话隔离 (bus/events.py:20-23)

技术点:复合键设计的简单实现

为什么值得深挖:

  • 无需复杂的会话映射表
  • 代码简洁,易于理解
  • 支持动态添加新渠道

扩展可能:

  • 会话共享机制(允许跨渠道共享部分上下文)
  • 会话迁移(将一个渠道的对话迁移到另一个渠道)
  • 会话合并(同一用户在不同渠道的会话合并)

5. 记忆在系统提示中的组装 (agent/context.py:47-50)

技术点:动态提示词构建

为什么值得深挖:

  • 支持提示词模块化(身份、记忆、技能独立)
  • 为条件加载提供基础(根据场景选择记忆内容)
  • 便于调试(可以单独查看各模块生成的提示)

扩展可能:

  • 实现记忆重要性排序(只加载相关的记忆)
  • 添加记忆占位符(LLM 可以请求特定记忆)
  • 支持多轮记忆加载(先加载摘要,LLM 需要时再加载详情)

总结

nanobot 的会话与记忆系统展示了"简单即美"的设计哲学:

  1. JSONL + Markdown - 使用最简单的文本格式,避免数据库依赖
  2. 内存缓存 + 磁盘持久 - 平衡性能和可靠性
  3. 渠道隔离 - 通过复合键实现,无需复杂映射
  4. 双存储记忆 - 区分长期知识和短期笔记,符合认知规律

这个系统虽然只有约 300 行核心代码,但完整实现了会话管理、记忆存储、上下文构建等关键功能,是轻量级 AI 助手框架的优秀参考实现。