如意Agent对话持久化与滚动记忆引擎设计:让AI记住你们聊过的每一句话

我是张大鹏,做了十多年人工智能,带过不少项目。说实话,最难的不是让AI生成一句漂亮的回答,是让它在聊了50轮之后还能记得你第一句说了什么。最近在给如意Agent设计对话持久化方案,顺便解决长对话上下文爆炸的问题,本文记录完整的设计思路和实现规划。


一、一个让人崩溃的现状

如意Agent跑了一段时间后,我发现对话数据散落在四个地方:

存储位置 内容 问题
data/memory/chat_history.json 渲染后的markdown,UI直接读 全文件覆盖写,4KB数据就7KB了
BaseSession.history(内存list) Claude content blocks,LLM真正吃的上下文 进程崩溃瞬间清零
temp/model_responses/<PID>.txt 文本日志,含<history>标签 PID命名在PyInstaller下脆弱
data/logs/logs.db(logstack) 结构化日志+traces 与对话无关联,trace_id找不回会话

核心病灶:4份事实源、3种数据格式、0个统一查询入口。

更难受的是长对话的上下文管理。每次新建会话都是一次"记忆断点",AI好像得了金鱼记忆。而如果把历史全部塞进prompt,100轮之后token费用能吓死人,响应速度也直线下降。

所以我决定做两件事:统一持久化 + 滚动记忆引擎


二、方案选型:为什么不用Mem0/Letta/LangChain

市面上做记忆的开源方案不少,我逐一评估过:

方案 优点 放弃理由
Mem0 声明式API,自动embedding 需外部向量服务,PyInstaller打包困难
Letta 持久化agent状态 架构太重,引入大量无关依赖
Zep 专为LLM对话设计 需独立服务部署,与单机桌面定位不符
LangChain Memory 生态成熟 已被官方标记为legacy,不建议新项目使用
自研(SQLAlchemy 2.0) 零额外依赖,完全可控 需自己实现,工作量较大

我的决策逻辑很简单------如意Agent是单机桌面应用,追求的是零配置开箱即用。引入任何一个外部服务都会破坏这个前提。而且记忆系统的核心逻辑并不复杂,一个设计清醒的自有实现比依赖随时可能变向的第三方库更可靠。

最终选择:SQLAlchemy 2.0 + Alembic + SQLite FTS5,自研滚动记忆引擎。


三、架构设计:独立chat.db + Repository模式

3.1 为什么独立chat.db,不合并进logs.db

维度 独立chat.db 合并logs.db
保留期 永久 logs 30天滚动
备份策略 用户可独立导出会话 备份必须连日志一起
故障域 日志库损坏不影响对话 一损俱损
schema演进 自由 受logstack现有规则约束

保留期与故障域的考量优先于JOIN便利性。

3.2 模块布局

复制代码
src/storage/chat/
├── __init__.py          # 导出ChatRepository、SummaryEngine、ContextBuilder
├── models.py            # Conversation、Message、ConversationSummary
├── engine.py            # create_engine、scoped_session、PRAGMA hooks
├── repository.py        # ChatRepository(CRUD API)
├── schema.py            # type-hint DTO(不依赖SQLAlchemy)
├── summary.py           # SummaryEngine(异步总结worker)
├── context.py           # ContextBuilder(上下文拼装)
├── search.py            # FTS5 wrapper(chat_search工具底层)
└── tokens.py            # TokenCounter抽象 + 启发式实现

alembic/                          # Alembic迁移管理
├── versions/
│   ├── 0001_initial_chat_schema.py
│   └── 0002_rolling_memory.py    # summaries表 + FTS5 + 触发器

3.3 数据模型

sql 复制代码
-- conversations: 一次对话会话
CREATE TABLE conversations (
    id           VARCHAR(32)  PRIMARY KEY,        -- 20260504_094030_129231
    title        VARCHAR(200) NOT NULL,
    model        VARCHAR(80),                     -- 末次使用的LLM名称
    created_at   DATETIME     NOT NULL,
    updated_at   DATETIME     NOT NULL,
    metadata     TEXT                              -- JSON扩展字段
);

-- messages: 对话中的每一轮发言
CREATE TABLE messages (
    id              INTEGER      PRIMARY KEY AUTOINCREMENT,
    conversation_id VARCHAR(32)  NOT NULL,
    turn            INTEGER      NOT NULL,        -- 在会话内的序号
    role            VARCHAR(16)  NOT NULL,        -- user | assistant | tool | system
    content         TEXT         NOT NULL,        -- 渲染后markdown(UI视图)
    raw_blocks      TEXT,                          -- JSON:Claude content blocks(预留)
    trace_id        VARCHAR(64),                   -- 对应logstack.traces.trace_id
    token_count     INTEGER,                       -- 估算token数(P2.5启用)
    created_at      DATETIME     NOT NULL,
    FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);

CREATE INDEX idx_messages_conv_turn ON messages(conversation_id, turn);
CREATE INDEX idx_messages_trace ON messages(trace_id);
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);

ID方案保留了现有的YYYYMMDD_HHMMSS_microseconds字符串主键------人类可读、天然按时序排序,不引入UUID破坏现有兼容性。


四、滚动记忆引擎:三层叠加设计

这是整个方案最核心也最有意思的部分。

4.1 为什么必须三层

单层方案的缺陷非常明显:

单层方案 问题
仅滑动窗口 早期对话丢失,AI失忆
仅滚动总结 关键细节被压缩失真
仅检索 AI知道"历史上说过",但不知道"刚才说过什么"
三层叠加 短期忠实 / 中期压缩 / 长期可查

4.2 三层架构详解

时间尺度 数据来源 注入方式 触发
L_window 滑动窗口 分钟级 最近N条messages(按token) 直接放入user/assistant槽位 每次对话
L_summary 滚动总结 小时~天 conversation_summaries累积 拼入system prompt 未压缩区 > 阈值时后台触发
L_recall FTS5检索 永久 messages_fts全文索引 AI主动调用chat_search工具 AI决策

4.3 滚动总结的工作原理

python 复制代码
class SummaryEngine:
    """后台滚动总结引擎。"""

    def notify_appended(self, conv_id: str) -> None:
        """对话新增消息后调用;引擎自行决定是否触发总结。"""

    def get_active_summary(self, conv_id: str) -> ConversationSummary | None:
        """读取当前活动总结(superseded_by IS NULL中to_turn最大者)。"""

触发逻辑

  1. notify_appended(conv_id) 入队
  2. worker计算unsummarized_tokens = sum(m.token_count for m in messages where turn > active_summary.to_turn)
  3. 若 > 阈值(默认8192)→ 调用cheap_llm生成新总结
  4. 新总结的from_turn = active.to_turn + 1或0;to_turn = 当前最大turn
  5. 旧总结的superseded_by指向新总结,保留历史链可追溯
  6. 失败:3次重试,仍失败则记ERROR日志,不阻塞用户对话

总结Prompt模板 (保存于assets/summary_prompt.txt):

复制代码
你是一位专业的对话归纳助手。请把下面对话浓缩成不超过800字的摘要,要求:
1. 保留用户的核心意图和决策
2. 保留AI给出的事实结论与推荐
3. 标注关键文件路径、命令、配置
4. 略去寒暄、重复确认、错误尝试中已被纠正的部分
5. 用中性叙述,不模仿原说话风格
{prior_summary_block}
{messages_block}

4.4 ContextBuilder:预算控制下的上下文拼装

python 复制代码
@dataclass
class ContextBudgets:
    total: int = 12288        # 总预算
    system: int = 4096        # base sys_prompt + L1-L4记忆
    summary: int = 2048       # 滚动总结
    window: int = 6144        # 滑动窗口(最近消息)

class ContextBuilder:
    def build(self, conv_id: str, base_system: str, memory_block: str) -> ContextPacket:
        # 拼装顺序:
        # system_prompt = base_system + "\n\n[长期记忆]\n" + memory_block + "\n\n[历史摘要]\n" + active_summary
        # messages = repo.list_messages(conv_id).filter(turn > active_summary.to_turn)
        #                  .iter_until_token_budget(window_budget, from='tail')

为什么按token触发而不是按消息数? 因为工具调用的输出体积差异巨大------一条web_scan可能5K token,一条问候只10 token。按消息数会两端失衡。


五、五个阶段的渐进迁移

这个方案不是一刀切,而是分5个阶段渐进实施,每阶段都可独立合并、独立回滚。

阶段 目标 工作量 风险
P0 基础设施 仓库具备SQLAlchemy + Alembic能力,但无任何调用方 0.5天
P1 UI切流 UI读写走chat.db,chat_history.json由DB派生导出 1天
P2 Session持久化 BaseSession.history也入库,/resume可完整恢复 1.5天 高(触及LLM调用链)
P2.5 滚动记忆引擎 滑动窗口+滚动总结+FTS5检索+默认续接会话 2天
P3 清理下线 移除JSON派生导出、temp/model_responses迁移 0.5天

5.1 P0:零行为变化的基础设施建设

添加依赖、模块骨架、Alembic配置、初始迁移。验收标准:alembic upgrade head可在临时目录创建空表;新代码mypy --strict + ruff零告警。

5.2 P1:UI切流

改造src/desktop/utils.py_load_history()_save_history(),让UI走chat.db。同时加一个兼容hook:每次保存后异步写chat_history.json,老脚本仍可读。

5.3 P2.5:永续会话体验

这是用户体验变化最大的阶段:

  • UI启动时自动打开最近会话,定位到末尾,不需要"新建对话"
  • "新建会话"按钮移到右上角菜单(不再放sidebar醒目位置)
  • 用户体感:永远在同一个聊天框,但底层有边界(便于归档/清理/导出)

六、关键设计决策

6.1 为什么用FTS5而不是向量检索

维度 FTS5(当前) 向量检索(sqlite-vec)
依赖 SQLite自带 需扩展 + embedding模型
启动成本 0 需选embedding(OpenAI/本地BGE等)
中文支持 unicode61默认能切(够用) 需中文embedding模型
准确度(关键词) 中(需chunk + rerank)
PyInstaller兼容 零问题 需测扩展加载
实施工作量 0.5天 2天+

结论:P2.5用FTS5满足80%场景。P3之后若用户反馈"找不到只记得意思的内容"再加向量层。

6.2 总结模型策略:固定便宜模型

mykey.py/config.yaml增加可选项summary_llm,默认指向GLM-Flash / Kimi-Auto / Haiku。未配置则降级到主对话LLM并打印warning。总结调用走异步worker,永远不阻塞用户对话。

6.3 跨线程安全

PySide6 UI线程、Agent线程、Summary worker三个线程会同时访问DB。配置方案:

python 复制代码
# connect_args + engine配置
create_engine(
    "sqlite:///data/memory/chat.db",
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
# PRAGMA配置
PRAGMA foreign_keys = ON
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL

scoped_session + check_same_thread=False + StaticPool,SummaryEngine自己持session_factory


七、测试策略

测试层级 范围 速度目标
单元(models) 字段约束、JSON roundtrip、ON DELETE CASCADE < 50ms
单元(repository) CRUD、事务边界、并发(threading) < 200ms
单元(summary) 触发阈值、superseded_by链接、失败重试 < 300ms
单元(context) token预算控制、丢弃策略、summary优先 < 100ms
单元(search) FTS5触发器同步、查询命中、跨会话隔离 < 100ms
集成(migration) JSON→DB完整性、幂等性、损坏JSON容错 < 1s
集成(rolling_chat) 100条消息全流程:append→summary→context < 5s

关键fixture

  • in_memory_enginesqlite:///:memory: + 全部表创建),所有单元测试用此
  • fake_cheap_llm(mock一个总是返回固定文本的session)
  • seeded_repo_100msgs(预填100条消息的repo,给集成测试用)

总结

维度 内容
核心思路 SQLAlchemy 2.0统一持久化 + 滑动窗口/滚动总结/FTS5三层记忆
数据库 独立data/memory/chat.db,不合并logs.db
迁移策略 P0→P1→P2→P2.5→P3五阶段渐进,每阶段可独立回滚
记忆预算 默认12K总预算(system 4K + summary 2K + window 6K)
总结触发 未压缩区token > 8192,后台异步触发
检索方案 P2.5用FTS5,P3后视反馈决定是否上向量层
会话体验 默认续接最近会话,"新建会话"收纳到菜单
预计工期 5.5工作日

参考资料


作者 :张大鹏
团队 :大鹏 AI 教育
日期:2026-05-05

我是张大鹏,10年全栈开发经验,目前专注于 AI + 全栈教育培训。关注我,每周分享AI和全栈开发领域的深度实战经验。

相关推荐
这张生成的图像能检测吗1 小时前
(论文速读)SPR-YOLO:面向模糊场景的轻量级交通流检测算法
人工智能·yolo·计算机视觉·目标追踪
独隅1 小时前
Anaconda 配置 Keras 环境的详细流程指南
人工智能·深度学习·keras
恋猫de小郭1 小时前
AI 时代开源协议将消亡,malus 讽刺性展示了这一点
前端·人工智能·ai编程
数字化顾问1 小时前
(97页PPT)麦肯锡战略规划制定方法及模板制品(附下载方式)
人工智能·物联网
Kiyra1 小时前
我是怎么把一个普通 AI 聊天项目改造成工程化 Agent Runtime 的
人工智能
PythonFun1 小时前
WPS AI隐藏玩法!自定义指令让办公效率起飞
人工智能·wps
weixin_417197052 小时前
王府井来了个新店员:穿机械骨骼,不会请假,还会表演节目
人工智能·机器人
lisw052 小时前
【计算机科学技术/AI领域】名词释义:词元(token)!
人工智能·机器学习·软件工程
qdprobot2 小时前
ESP32S3 AiTall V3 Mixly 图形化编程开发AI小智 MCP AIOT大模型对话开发视频教程Micropython小智AI系统
人工智能·micropython·esp32s3·图形化编程·mcp·mixly小智ai·大模型对话