首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 的记忆是怎么存的?深挖会话管理的四种存储模型

Agent 的记忆是怎么存的?深挖会话管理的四种存储模型

作者头像
windealli
发布2026-06-24 20:12:29
发布2026-06-24 20:12:29
1940
举报
文章被收录于专栏:windealliwindealli

❝大模型本身是"过目就忘"的——每次 API 调用都是一张白纸。让 Agent 表现得"有记性",靠的是一层叫 「Session(会话管理)」 的基础设施。而这层基础设施里最核心、最容易决定一个框架天花板的,是一个朴素到容易被忽略的问题:「对话历史,到底用什么数据结构存?」 这篇文章不泛泛而谈"会话管理是什么",而是聚焦一个点钻到底:拆解四种典型的「历史存储模型」——线性数组、事件日志、树形日志、状态快照链。它们决定了你的 Agent 能不能"分支""回到过去""审计重放""扛住千万用户"。我们会结合 Pi、LangGraph、trpc-agent-go 三个真实框架的源码,看它们各自押注在哪种模型上、为什么这么选、代价是什么。❞

先建立一个前提:你存的历史 ≠ 你发给模型的历史

在聊四种模型之前,必须先钉死一个认知,否则后面全是糊涂账:

「"完整存储的对话历史"和"这一轮真正发给模型的上下文",是两个东西。」

很多人对会话管理的想象停留在"维护一个 messages 数组,发请求时整个扔过去"。早期确实如此,但今天所有正经的 Agent 框架,都把这两层「分开」了。

  • 「持久层(存储)」:忠实记录发生过的一切。每句话、每次工具调用、每段工具返回,原原本本地存着,通常「永不删除」。它追求的是"完整、可追溯、能复盘"。
  • 「运行层(上下文)」:每次要调模型时,从持久层"投影"出一份「当下最合适」的上下文——可能裁掉了远古消息、可能把一大段旧历史折叠成摘要、可能把超长工具输出换成占位符。它追求的是"精简、省 token、别撑爆窗口"。

这两层的目标天然矛盾,所以才要拆开:压缩、裁剪只动运行层那份"投影",持久层的原稿一个字不少。后面我们会拆几个真实框架,你会发现它们无一例外都有这道"投影"动作,只是各自起了不同的名字——这是成熟会话管理的共同基因。

「为什么开篇要强调这个?」 因为"存储模型"讨论的全是「持久层」这一侧——它用什么结构存、能不能分叉、能不能回溯。理解了"存储归存储、投影归投影",你才不会把"压缩历史"和"丢失历史"混为一谈。


四种存储模型,一张图看懂能力递进

持久层用什么数据结构存历史,直接决定了这个框架的能力上限。归纳起来有四种典型形态,能力一种比一种强,代价也一种比一种大:

模型

一句话

天然解锁的能力

代价

① 线性数组

顺序往后追加

简单读写

只能往前走

② 事件日志

不可变事件流

审计、状态重放

想回到过去要自己重放

③ 树形日志

节点带父指针

分叉、反悔、试错

无全局快照

④ 状态快照链

每步存全量状态

时间旅行、任意点分叉

体积膨胀最严重

下面逐一拆开。前两种是"打底",后两种是"绝活"——而后三种,恰好被三个真实框架分别押中了。


模型一:线性数组——朴素但够用的起点

最朴素的形态:一个 messages 数组,新消息往后追加,要发请求时整个数组拼起来扔给模型。这里说的"消息",就是一条「聊天记录」——谁(用户 / 助手 / 系统)说了什么,跟微信里一个个对话气泡差不多。

代码语言:javascript
复制
[ msg1 ] → [ msg2 ] → [ msg3 ] → [ msg4 ] → ...

它的优点和缺点都写在脸上:

  • 「优点」:实现简单到几乎不需要"框架"。读写都是 O(1) 追加,拼上下文就是数组拷贝。绝大多数玩具级 demo、教程里的 chatbot 都是这么干的。
  • 「缺点」:它是一条「单行道」。你无法从中间某个点"岔出去试另一条路",无法"回到三步前重来",也无法精细审计"第 5 步到底改了哪个状态"。一旦历史撑爆窗口,你只能在这个数组上做裁剪——而裁掉就是真的没了(除非你额外存一份)。

线性数组是理解后面三种模型的「基线」。你可以把后三种都看成是在回答同一个问题:"如何在保住简单追加的前提下,多解锁一种能力?"

  • 想要「可审计/可重放」 → 把"消息"升级成"事件" → 「事件日志」
  • 想要「可分叉/可反悔」 → 给每个节点加个"父指针" → 「树形日志」
  • 想要「可时间旅行」 → 干脆每步都存一份完整快照 → 「状态快照链」

模型二:事件日志——为"审计"和"多租户"而生(trpc-agent-go)

第一种进化:不再只存"消息",而是把发生的「每一件事」都记成一条「不可变事件(event)」

"消息"和"事件"听着像近义词,但举个具体例子立刻就能分清。假设用户说了句"帮我查下北京天气",Agent 调了天气工具,最后回答"北京今天晴,25℃"。这一来一回,两种视角记下来的东西「颗粒度完全不同」

「"消息"视角」(线性数组)——只记对话气泡,像一段微信聊天记录:

代码语言:javascript
复制
用户:帮我查下北京天气
助手:北京今天晴,25℃

「"事件"视角」(事件日志)——把这一回合背后「真正发生的每个动作」都记一笔,像一份执行流水账:

代码语言:javascript
复制
event1  用户消息    :帮我查下北京天气
event2  助手调用工具:weather(city="北京")
event3  工具返回结果:{ 天气:"晴", 气温:25 }
event4  助手消息    :北京今天晴,25℃
event5  状态变更    :last_city = "北京"

看出区别了吗?「"消息"是给人看的对话记录,颗粒度粗;"事件"是给系统看的执行流水,颗粒度细」——连"调了哪个工具、工具吐回了什么、状态被改成了啥"都一条不落地记下来。这种"细颗粒度 + 不可变追加",正是事件日志的精髓。

腾讯开源的 Go 框架 trpc-agent-go 就是事件日志的典型代表,画风是彻头彻尾的「工业级后端服务」

在它的 Session 结构体里,历史就是一条事件流 Events

代码语言:javascript
复制
type Session struct {
    ID       string
    AppName  string          // 应用
    UserID   string          // 用户
    State    StateMap        // 会话状态(支持 delta)
    Events   []event.Event   // ← 历史就是这条不可变事件流
    Summaries map[string]*Summary
    // ...
}

事件日志这套模型,解锁了两个线性数组给不了的关键能力:

「① 状态可重放、可审计。」 每条事件不只是"说了什么",还带着一个 StateDelta——这一步对会话状态做了哪些增量修改。于是 AppendEvent 时,框架追加事件的同时顺手把状态更新了。这意味着:你把事件流从头重放一遍,就能精确重建任意时刻的状态。出了线上问题,这就是天然的审计日志。

「② 天生为多租户设计。」 这是单机工具完全没有的概念。trpc-agent-go 用一个「三元组」<AppName, UserID, SessionID> 唯一确定一个会话——哪个应用、哪个用户、哪次会话,层层隔离。更狠的是它把「状态分了三级作用域」

作用域

前缀

活多久 / 给谁看

App 级

app:

整个应用共享的全局配置

User 级

user:

某个用户跨会话的长期偏好

Session 级

(无前缀)

仅本次会话

临时

temp:

用完即弃,不持久化

读取会话时,框架自动把 app 级、user 级状态 merge 进来,你在业务里拿到的是一份"合并好的视图"。

它的存储后端也是三家里最丰富的:内存、Redis、SQLite、PostgreSQL、MySQL、ClickHouse、带向量语义检索的 PGVector,还有啥都不存的 noop——全部藏在统一的 Service 接口背后,想换哪个换哪个:

代码语言:javascript
复制
type Service interface {
    CreateSession(...); GetSession(...); ListSessions(...); DeleteSession(...)
    AppendEvent(...)   // 追加事件,顺便更新状态
    // ...三级 State 操作、Summary 操作
}

「代价是什么?」 事件日志是「线性追加」的,它没有"父指针"也没有"全量快照"。所以它「没有分支」「没有时间旅行」——你想"回到过去某个点重新跑",得自己把事件重放到那个点。但对一个面向千万用户的在线服务来说,这恰恰是对的取舍:线性事件流足够直接、足够稳、足够扛并发。线上服务最需要的是"隔离"和"审计",而不是"后悔药"。


模型三:树形日志——把"反悔"做成一等公民(Pi)

第二种进化:在追加的基础上,给每个节点记一个"我的上一个节点是谁"(parentId)。于是历史不再是一条线,而是一棵可以「分叉」的树。

Pi 是一个 TypeScript 写的本地编程 Agent。作为给开发者用的工具,它对"试错"和"反悔"的需求极强——"我想回到三步之前那个版本重新试"是每天都在发生的高频刚需。于是它选了「树形模型」

在 Pi 里,会话不是数组,而是一棵「只追加的树」。每条记录是一个节点,都带着自己的 id 和父节点 parentId

代码语言:javascript
复制
interface SessionEntryBase {
    type: string;
    id: string;
    parentId: string | null;   // ← 指向上一个节点,串成树
    timestamp: string;
}

注意:节点类型不止"消息"——还有模型切换、压缩点、分支摘要、书签标签……它们都是树上平等的一等节点。另一个关键指针是 leafId,它指向"当前所在的节点"。这套设计带来几个很爽的能力:

  • 「持久化即文件」:整棵树逐行写进一个 JSONL 文件(放在 ~/.pi/agent/sessions/ 下),文件本身就是数据库,没有任何外部依赖。
  • 「追加永不修改」:已写下的节点绝不改、绝不删。所谓"分支",无非是把 leafId 指针挪到某个旧节点上,下次再追加就从那儿岔出新枝。
  • 「恢复很轻」:重建对话就是从叶子沿着 parentId 一路走回根。源码里这个走法朴素得可爱:
代码语言:javascript
复制
getBranch(fromId?: string): SessionEntry[] {
    const path: SessionEntry[] = [];
    let current = this.byId.get(fromId ?? this.leafId);
    while (current) {
        path.unshift(current);                        // 头插
        current = current.parentId
            ? this.byId.get(current.parentId) : undefined;  // 顺着父指针往上爬
    }
    return path;   // 从根到叶的完整路径
}
  • 「能 fork」createBranchedSession() 把"根到某节点"那条路径复制成一个新文件,于是你能从历史中间岔出一条全新对话,原对话毫发无损。

「代价是什么?」 树形模型解决了"分叉/反悔",但它「没有"任意时间点的全局快照"」——它存的是消息节点,不是每一步的完整运行时状态。而且 Pi 是为单人本地场景量身定做的,「没有跨用户、跨租户的概念」。它把"可反悔的树"做到了极致,但也只服务这一种场景。


模型四:状态快照链——用空间换"时间旅行"(LangGraph)

终极进化:干脆不要纠结存消息还是存事件了——「每走一步,就把当时的完整运行时状态拍一张快照」存下来,快照之间用父子指针串成一条链。

LangGraph(以及建立在它之上的 DeepAgents)来自 LangChain 团队,是目前最"框架化"的一套。它把会话管理收进了一个更通用的抽象:「图状态 + 检查点(Checkpoint)」

在 LangGraph 里,一次会话叫一个 「thread」,由调用时传入的 thread_id 标识:

代码语言:javascript
复制
config = {"configurable": {"thread_id": "abc-123"}}
await agent.astream(input, config=config)   # 带同一个 thread_id 就能续聊
state = await agent.aget_state(config)       # 随时把历史捞出来

它的存储模型是「快照链」:每执行一步,Checkpointer 就把当时的完整状态存成一张带 checkpoint_id 的快照,快照之间用 parent_checkpoint_id 串起来。后端可换:测试用内存版 MemorySaver,DeepAgents 生产环境默认用 AsyncSqliteSaver 落到本地 SQLite。

快照链的「杀手锏是时间旅行」:因为每一步的完整状态都被独立保存了,你理论上能指定任意一个历史 checkpoint_id,回到那一刻、甚至从那儿岔出一条新分支。(有意思的是:LangGraph 框架层支持这个能力,但 DeepAgents 这层产品暂时没把它暴露给最终用户。)

「代价是什么?体积。」 这是快照模型绕不开的痛点:每步都存全量状态,消息一多,存储量会按 「O(N²)」 膨胀(第 N 步的快照里塞着前 N 条消息,N 步累加就是平方级)。DeepAgents 为此专门做了优化——用一个叫 DeltaChannel 的机制,平时只存增量、每 50 步才拍一次全量快照,把增长从 O(N²) 压回 「O(N)」

代码语言:javascript
复制
class _DeepAgentState(AgentState):
    """messages 上挂 DeltaChannel,把 checkpoint 增长从 O(N²) 降到 O(N)。"""
    messages: Required[Annotated[
        list[AnyMessage],
        DeltaChannel(_messages_delta_reducer, snapshot_frequency=50)  # 每 50 步全量
    ]]

这其实暴露了快照模型的本质:「它用空间换时间旅行能力」。当空间代价太大时,就得引入"增量 + 周期全量快照"这种典型的存储优化手段把它压回来——这思路和数据库的 WAL、Redis 的 RDB+AOF 混合持久化如出一辙。


四种模型横向对比:能力越强,代价越大

把四种模型摆在一起,规律非常清晰:「能力是递增的,代价也是递增的,没有免费的午餐。」

维度

① 线性数组

② 事件日志

③ 树形日志

④ 状态快照链

代表框架

多数 demo

trpc-agent-go

Pi

LangGraph

数据结构

数组

不可变事件流

带父指针的树

带父链的全量快照

恢复对话

审计 / 重放

✅ 天然

一般

分支 / fork

✅ 一等能力

✅ 框架支持

时间旅行

需自己重放

✅ 天然

多租户 / 状态作用域

✅ 四级作用域

长期记忆走 Store

存储体积

最小

最大(需优化)

最适合

玩具 / 教程

多用户在线服务

本地编程助手

通用编排 / 复杂流程

选型其实不难,对号入座即可:

  • 「本地编程助手、需要频繁试错回溯」 → 学 Pi 的「树形日志」
  • 「面向千万用户的在线服务、要审计、要状态隔离」 → trpc-agent-go 的「事件日志」这套接口分层最省心。
  • 「复杂多 Agent 编排、要中断恢复甚至时间旅行」 → LangGraph 的「快照链」最对路(但要准备好为体积买单)。
  • 「快速验证想法的 demo」 → 线性数组,别过度设计。

三个横切认知,帮你少踩坑

钻完四种模型,有几个跨模型的认知值得单独拎出来,做任何"有记忆的 AI 系统"都用得上:

「① "快照"和"日志"是两种世界观。」 快照链存的是每一步的完整状态,所以天然能时间旅行,代价是体积;事件日志存的是增量事件,省空间、扛并发,但想"回到过去"就得自己重放。没有谁对谁错,只有一句话——「你的产品需不需要后悔药?」 需要,就为快照的体积买单;不需要,就享受日志的轻快。

「② 压缩必须"非破坏式"。」 不管哪种存储模型,历史压缩都应该「不删原稿」:Pi 把原始 JSONL 全留着、DeepAgents 把旧历史落盘到 .md 文件、trpc 的摘要也不动原始事件。原因很现实——"上下文偏大"顶多多花点钱,"历史被永久删掉"则无法挽回。回到开篇那句话:压缩只动「运行层的投影」,别动「持久层的原稿」

「③ Session ≠ Memory,存储模型只管前者。」 本文讲的四种模型,管的都是「某一次会话的短期历史」。而跨会话的「长期记忆」("这个用户是左撇子""上次帮他建过一个 React 项目")是另一套东西——LangGraph 用独立的 Store 存它,trpc-agent-go 有独立的 Memory 服务。设计时先想清楚:"这条信息该活一次对话,还是该跟着用户一辈子?"别把长期记忆塞进会话历史里。


结语:选数据结构,就是选你的产品上限

回头看,会话管理这层基础设施有个特别的地方:「它做得越好,你越感觉不到它的存在。」 用户只觉得"这个 AI 真聪明,记性真好",根本不会意识到背后有一套精密的存储结构在默默运转。

而这套存储结构的选择,几乎在项目第一天就悄悄定下了产品的能力天花板:

❝选了「线性数组」,你就放弃了分叉和审计; 选了「事件日志」,你拿到了审计和多租户,但放弃了时间旅行; 选了「树形日志」,你拥有了反悔,但要接受没有全局快照; 选了「状态快照链」,你解锁了时间旅行,但要为体积持续买单。 ❞

没有最优解,只有最匹配你产品形态的那一个。下次当你设计任何需要"记住上下文"的 AI 系统时,不妨在写第一行代码前先问一句:

「我的用户,到底需不需要"分叉""审计""回到过去"?」

想清楚了这一个问题,存储模型自然就选定了——剩下的,都是工程细节。


延伸阅读

  • LangGraph 是怎么让 Agent"记住"对话的?拆解 thread 与 Checkpoint
  • Agent 聊着聊着就"失忆"?拆解 DeepAgents 的上下文压缩黑科技
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 海天二路搬砖工 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先建立一个前提:你存的历史 ≠ 你发给模型的历史
  • 四种存储模型,一张图看懂能力递进
  • 模型一:线性数组——朴素但够用的起点
  • 模型二:事件日志——为"审计"和"多租户"而生(trpc-agent-go)
  • 模型三:树形日志——把"反悔"做成一等公民(Pi)
  • 模型四:状态快照链——用空间换"时间旅行"(LangGraph)
  • 四种模型横向对比:能力越强,代价越大
  • 三个横切认知,帮你少踩坑
  • 结语:选数据结构,就是选你的产品上限
  • 延伸阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档