Agent 上下文管理:从内存 Session 到 MySQL 持久化与自动压缩
Agent 上下文管理:从内存 Session 到 MySQL 持久化与自动压缩
前言
在开发 Agent 时,很多人一开始会把“上下文”“Memory”“Session”“数据库聊天记录”“上下文压缩”混在一起。
但实际上,这几个概念解决的是完全不同的问题。
以一个 Coding Agent 为例:
用户输入
↓
AgentSession
↓
模型
↓
Tool
↓
Tool Result
↓
模型继续思考
模型之所以能知道“刚才发生了什么”,并不是因为模型自己拥有持续记忆,而是 Agent Runtime 一直维护着一份当前上下文。
如果会话很长,这份上下文会越来越大,最终超过模型的 Context Window。
于是又需要:
Context Compaction
同时,如果程序关闭之后还希望用户能够重新打开历史会话,就需要数据库持久化。
因此,一个完整的 Agent 上下文系统实际上至少包含三个部分:
Agent Context
+
Persistent History
+
Compaction Checkpoint
一、首先区分三个概念
可以先用一句话概括:
内存负责“模型现在需要知道什么”,数据库负责“这个会话完整发生过什么”,Compaction 负责“上下文太长时如何缩短”。
整体结构:
Agent 会话
│
┌──────────────┼──────────────┐
↓ ↓ ↓
当前 Context 完整历史记录 压缩检查点
│ │ │
内存 / Pi MySQL MySQL
│
↓
LLM
这三层不能混为一谈。
二、模型运行时的上下文存在哪里?
以 Pi SDK 为例,如果创建 Session:
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory()
});
那么当前会话运行期间,Pi 会在内存里维护 AgentSession。
其中包括:
User Message
Assistant Message
Tool Call
Tool Result
Compaction
当前状态
例如用户说:
帮我看看这个项目是什么项目
Pi 可能执行:
User
↓
LLM
↓
bash("dir")
↓
Tool Result
↓
LLM
↓
read("package.json")
↓
Tool Result
↓
LLM
↓
最终回答
当前有效上下文可能是:
System Prompt
User:
帮我看看这个项目是什么项目
Assistant:
我要查看目录
Tool:
package.json
src
tsconfig.json
Assistant:
我要读取 package.json
Tool:
package.json 内容
Assistant:
这是一个 TypeScript + Pi SDK 项目
这些内容由当前 AgentSession 管理。
三、每一轮模型调用都会重新带上 Context
LLM 本身没有真正的持续记忆。
它之所以知道上一轮发生了什么,是因为 Runtime 每次调用模型时,都会把当前上下文重新发送过去。
概念上:
第一次模型调用:
System
+
User
第二次:
System
+
User
+
Assistant Tool Call
+
Tool Result
第三次:
System
+
User
+
Assistant Tool Call
+
Tool Result
+
Assistant Tool Call
+
Tool Result
因此:
模型的“记忆”本质上是 Context 重放。
这也是 Agent Context Management 的基础。
四、为什么还需要 MySQL?
如果当前 Session 只存在内存:
pnpm dev
↓
AgentSession
↓
聊天
那么程序运行期间没有问题。
但是一旦:
Ctrl + C
关闭程序
服务器重启
进程崩溃
内存中的 Session 就不存在了。
因此需要 MySQL 做持久化。
MySQL 主要负责:
历史会话
完整聊天记录
用户查看
Web 页面展示
审计
恢复
统计
例如:
agent_session
保存:
session_id
user_id
project_id
title
created_at
updated_at
另外:
agent_message
保存完整消息:
id
session_id
role
content
created_at
例如:
1 user 帮我看看这个项目
2 assistant 这是一个 TypeScript 项目
3 user 帮我创建 README
4 assistant 已创建完成
五、运行期间每次聊天都查 MySQL 吗?
不需要。
这是一个很重要的设计。
正常运行:
打开 Session
↓
恢复一次上下文
↓
AgentSession 常驻内存
↓
用户连续聊天
用户每轮:
User
↓
Pi AgentSession
↓
LLM / Tool
↓
新消息写 MySQL
MySQL 只需要:
INSERT
新增本轮消息。
并不需要:
每轮 SELECT 全部历史
否则会非常低效。
所以在线运行时:
AgentSession
负责实时上下文
MySQL
负责增量持久化
六、什么时候需要查询 MySQL?
一般只有这些情况:
程序重新启动
用户打开历史会话
用户切换 Session
服务器重新创建 AgentSession
Session 被释放后重新加载
这时候才需要:
session_id
↓
查询数据库
↓
恢复有效上下文
↓
创建新的 AgentSession
因此 MySQL 更像:
Session 的持久化存储层。
而不是:
每次 LLM 调用都实时依赖的 Context Store。
七、问题来了:Context 会越来越长
如果用户一直聊天:
M1
M2
M3
...
M100
...
Context Token 会不断增长。
例如:
10K
30K
60K
100K
120K
模型都有 Context Window。
一旦接近极限,就必须压缩。
这就是:
Context Compaction
八、Context Compaction 是什么?
最简单理解:
旧消息很多
↓
调用 LLM 总结
↓
生成 Summary
↓
最近消息保持原文
例如原本:
M1
M2
M3
...
M100
Pi 判断:
M1 ~ M70 太老
M71 ~ M100 最近
于是:
M1 ~ M70
↓
LLM
↓
Summary A
最近的:
M71 ~ M100
继续保持原文。
最终给模型的 Context:
System Prompt
+
Summary A
+
M71
M72
...
M100
而不是:
M1 ~ M100
全部发送。
九、为什么一定要保留最近消息原文?
因为 Summary 是有损的。
例如历史:
用户:
帮我修改 UserController 第 87 行
Assistant:
修改完成
Tool:
具体代码...
如果全部总结成:
用户修改过 UserController
很多细节会丢失。
因此合理做法是:
旧内容 → Summary
最近内容 → 原样保留
这样既节省 Token,又保证近期操作细节完整。
十、Compaction 后 MySQL 会删除旧消息吗?
不会。
这是非常重要的原则:
上下文压缩只影响“模型当前看到什么”,不能影响数据库里的完整历史。
假设:
M1 ~ M70
已经被压缩成:
Summary A
MySQL 的 agent_message 仍然保存:
M1
M2
...
M70
M71
...
M100
一条都不删。
数据库负责:
完整事实
Pi Context 负责:
有效上下文
十一、那压缩结果应该怎么保存?
除了:
agent_message
还需要:
agent_compaction
例如:
id
session_id
summary
first_kept_message_id
tokens_before
created_at
假设:
Summary A
代表:
M1 ~ M70
而:
M71
开始继续保留原始消息。
数据库记录:
summary = Summary A
first_kept_message_id = 71
这就是一个:
Compaction Checkpoint
十二、first_kept 到底是什么?
first_kept 不是一条总结。
它只是一个“切割位置”。
例如:
M1
M2
...
M70
M71
...
M100
压缩结果:
Summary A = M1 ~ M70 的总结
first_kept = M71
真正恢复的有效 Context 是:
Summary A
+
M71
+
M72
+
...
+
M100
所以正确理解是:
Context
=
Summary
+
从 first_kept 开始到最新的消息
十三、程序重启以后怎么恢复?
假设程序关闭前:
agent_message:
M1
M2
...
M103
同时:
agent_compaction:
Summary A
first_kept = M71
程序重新启动。
用户选择:
session_id = 1001
第一步:
SELECT *
FROM agent_compaction
WHERE session_id = 1001
ORDER BY id DESC
LIMIT 1;
拿到:
Summary A
first_kept = 71
然后:
SELECT *
FROM agent_message
WHERE session_id = 1001
AND id >= 71
ORDER BY id;
得到:
M71
M72
...
M103
最终恢复:
System Prompt
+
Summary A
+
M71 ~ M103
然后重新创建 AgentSession。
十四、为什么不能直接加载全部历史?
假设:
M1 ~ M500
已经有:
160K Token
如果程序重启时直接:
SELECT 所有历史
↓
全部塞回模型
可能直接超过模型 Context Window。
而且上次 Pi 已经做过 Compaction:
160K
↓
Summary + Recent
↓
40K
如果重启后又把 160K 全部塞回去,相当于把压缩成果全部浪费。
所以:
保存 Compaction Checkpoint 是为了让 Session 恢复后延续原来的有效 Context 状态。
十五、第二次压缩会发生什么?
第一次:
M1 ~ M70
↓
Summary A
保留:
M71 ~ M100
继续聊天:
M101
...
M180
又快到 Context Window。
第二次:
Summary A
+
M71 ~ M150
↓
LLM
↓
Summary B
然后:
保留:
M151 ~ M180
当前 Context:
Summary B
+
M151 ~ M180
数据库:
agent_message
M1 ~ M180
全部存在。
agent_compaction:
Compaction 1
Summary A
first_kept = M71
Compaction 2
Summary B
first_kept = M151
恢复的时候只需要最新一次:
Summary B
+
M151 ~ M180
即可。
十六、因此会形成三个不同的“数据视图”
1. 完整历史
MySQL:
M1
M2
M3
...
M180
作用:
聊天记录
UI 展示
审计
统计
历史查询
2. Compaction Checkpoint
MySQL:
Summary B
first_kept = M151
作用:
知道有效 Context 如何恢复
3. 当前运行 Context
Pi 内存:
System Prompt
+
Summary B
+
M151 ~ M180
作用:
真正发送给 LLM
十七、完整架构
最终可以画成:
用户
│
↓
Pi AgentSession
│
当前有效 Context
│
┌─────────┴─────────┐
↓ ↓
Model Tools
│ │
└─────────┬─────────┘
↓
Agent Loop
│
↓
Context Token 增长
│
达到压缩阈值?
/ \
No Yes
│ │
继续 Compaction
│
旧消息 → Summary
最近消息 → 保留
│
↓
Summary + Recent
│
↓
继续 Agent
与此同时:
Agent Events
│
↓
MySQL
│
┌───────────────┼───────────────┐
↓ ↓ ↓
agent_session agent_message agent_compaction
会话信息 完整历史 压缩检查点
十八、最重要的几个原则
原则一
LLM 当前 Context
≠
数据库完整历史
模型不需要每次看到数据库所有内容。
原则二
在线运行
↓
AgentSession 管 Context
数据库
↓
只做增量持久化
不要每轮查询全部聊天历史。
原则三
Compaction
≠
删除历史
压缩只影响模型输入。
数据库始终保存完整历史。
原则四
恢复会话时:
最新 Summary
+
first_kept 之后的消息
而不是:
全部历史
重新塞进模型。
十九、Context 和长期 Memory 仍然不是一回事
即使我们已经有:
Session Context
MySQL History
Compaction
它们依然和:
Long-term Memory
不同。
例如:
用户喜欢 Java
用户喜欢详细注释
用户一直使用中文
这种属于:
User Memory
它可能跨越:
Session A
Session B
Session C
而 Session Context 只是:
当前这个会话发生过什么。
所以完整 Memory 架构可能是:
Memory
│
├── Session Context
│ 当前会话
│
├── Conversation History
│ MySQL 完整记录
│
├── Compaction
│ 当前会话压缩状态
│
└── Long-term Memory
用户画像 / 项目长期知识
二十、最终理解
现在可以把 Agent 上下文管理总结成一句话:
Agent 在线运行时,由内存中的 Session 管理模型当前真正需要的上下文;MySQL 保存完整历史;当上下文过长时,通过 LLM 生成 Summary,只保留“Summary + 最近消息”继续运行;数据库额外保存 Compaction Checkpoint,程序重启以后根据“最新 Summary + first_kept 之后的消息”恢复有效 Context。
最终关系:
完整历史
↓
MySQL
│
├───────────────┐
│ │
↓ ↓
Messages Compaction
│
↓
Summary + first_kept
│
↓
恢复 AgentSession
│
↓
当前有效 Context
│
↓
LLM
这套思路不只适用于 Pi。
实际上以后无论使用:
LangChain
LangGraph
Pi SDK
自己手写 Agent Runtime
都会遇到相同的核心问题:
哪些信息需要永久保存,哪些信息需要进入当前 Context,以及 Context 太长以后如何压缩和恢复。
这就是 Agent Context Management 最核心的工程问题。