对话历史与上下文压缩机制解析
对话变长以后,界面、存档和模型上下文各自怎么收,和跨会话记忆怎么分工。
来源 docs/技术博客/对话历史与上下文压缩机制解析.md
表述:本文面向 开发与运维,说明 Cadau 在对话历史 越来越多 时,界面展示、数据库持久化、模型上下文 三层分别如何处理,以及与工作智能体 跨会话记忆 的分工。产品侧用户说法见
docs/core-mechanisms/智能体记忆.md;记忆文件落盘见 工作智能体记忆存储位置解析。
日期:2026-07-16(补充工具循环检查点) 相关代码:backend/internal/api/handlers/chat.go、backend/internal/api/handlers/chat_context_roll.go、backend/internal/store/chat.go、backend/internal/agentmemory/flush.go
结论
Cadau 对「对话历史变多」采用 三层分离 策略:
| 层面 | 策略 | 是否删旧数据 |
|---|---|---|
| 数据库 | 全量持久化 chat_sessions + chat_messages | 否(无自动 TTL) |
| 前端 | 分页加载,按需「加载更早消息」 | — |
| 模型上下文 | 滚动摘要:较早轮次压缩进摘要,最近若干轮保留原文 | 否(仅影响送入模型的内容) |
工作智能体 在压缩前还会把即将滚出的片段 flush 到日笔记,并可选 提炼为长期记忆,避免重要信息随摘要丢失。
三层架构概览
flowchart LR
subgraph UI["前端展示"]
A1[最近 100 条]
A2[cursor 加载更早]
end
subgraph DB["数据库"]
B1[chat_messages 全量]
B2[chat_sessions.context_summary]
end
subgraph LLM["模型调用"]
C1[历史摘要]
C2[最近原文]
C3[当前用户消息]
end
UI --> DB
DB --> LLM三者 不是一回事:用户在界面能看到完整历史(分页拉取),数据库里消息 不会 因上下文压缩而删除,但发给模型的 payload 会被 控在字符预算内。
1. 持久化:数据库全量保留
表结构
| 表 / 字段 | 内容 |
|---|---|
chat_sessions | 会话元数据(标题、所属用户/工作区/智能体等) |
chat_sessions.context_summary | 滚动摘要正文(仅服务模型上下文) |
chat_sessions.verbatim_since_created_at | 保留原文的起始消息时间锚点 |
chat_sessions.verbatim_since_msg_id | 保留原文的起始消息 ID 锚点 |
chat_messages | 每条 user / assistant 消息正文、tool_trace_json、attachment_ids_json |
Schema 见 backend/internal/db/schema_postgres.sql(SQLite 见 schema.sql)。chat_messages.session_id 外键 ON DELETE CASCADE,删会话时级联删消息。
当前策略
- 无 按时间自动清理、归档或 TTL。
- 历史会一直增长,直到用户或管理员 主动删除会话,或通过 编辑重发 截断后续消息(
POST /api/v1/chat/truncate)。
相关 API
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /api/v1/chat/history | 按会话读消息,支持 limit + cursor |
| GET | /api/v1/chat/sessions | 会话列表,分页 |
| DELETE | /api/v1/chat/sessions/{id} | 删除会话(级联消息) |
| POST | /api/v1/chat/truncate | 从指定用户消息起删除该条及之后 |
2. 前端:分页加载与窗口化
前端通过 GET /chat/history 按需加载,不必一次拉全:
- 默认先取 最近一页(客户端常用
limit=50;服务端默认/上限见接口)。 - 响应含
older_cursor/newer_cursor/at_live_edge(next_cursor兼容旧义 = 更早方向)。 - 有界窗口:内存只保留视口附近约百条,向上/向下双向加载并回收对侧;机制见 会话消息窗口化加载(微信式)。
- 提问导航:
GET /chat/history/turns提供轻量索引,与消息窗口解耦。 - 会话列表同样分页(
GET /chat/sessions,默认limit=20)。
实现:client/web/src/api.ts、client/web/src/chatMessageWindow.ts、client/web/src/useChatSessions.ts。
UI 不因会话总轮数到上千而持有全部气泡正文;数据库里仍是完整记录。
3. 模型上下文:滚动摘要(核心)
真正受 上下文窗口 限制的是每次调用 LLM 时塞进模型的内容。入口:
prepareLLMConversationWithRollingContext—backend/internal/api/handlers/chat_context_roll.go- 发消息时先
ListChatMessages(..., 8000)读库,再滚动压缩 —backend/internal/api/handlers/chat.go
默认配置
环境变量(亦可在 mindlink.json → chat_context 覆盖):
| 参数 | 环境变量 | 默认值 | 含义 |
|---|---|---|---|
| 总字符预算 | CHAT_CONTEXT_MAX_RUNES | 120000 | 粗估上下文总上限 |
| 回复预留 | CHAT_CONTEXT_REPLY_RESERVE_RUNES | 8000 | 预留给模型输出 |
| 实际输入预算 | — | ≈ 112000 | max_runes - reply_reserve |
| 摘要触发阈值 | CHAT_CONTEXT_SUMMARIZE_THRESHOLD_PCT | 88 | 超过预算 88% 时开始压缩 |
| 最少保留原文轮数 | CHAT_CONTEXT_MIN_VERBATIM_MESSAGES | 6 | 至少保留最近 6 条消息原文 |
| 每批压缩条数 | CHAT_CONTEXT_SUMMARIZE_BATCH_MESSAGES | 4 | 每次把最早 4 条滚入摘要 |
| 按模型解析窗口 | CHAT_CONTEXT_RESOLVE_MAX_FROM_MODEL | false | true 时启动时按模型 token 窗口推导 max_runes |
预算计算:Config.ChatContextInputBudgetRunes() — backend/internal/config/config.go。
工作流程
flowchart TD
A[用户发新消息] --> B[从库读最多 8000 条历史]
B --> C{估算字符量是否超阈值/硬上限?}
C -->|否| D[摘要 + 最近原文 + 当前消息 → 调模型]
C -->|是| E[取 priorRows 最早 batch 条]
E --> F{是否工作智能体?}
F -->|是| G[FlushBeforeCompaction: 日笔记 + 可选长期记忆提炼]
F -->|否| H[跳过 flush]
G --> I[LLM mergeRollingSummary 合并进 context_summary]
H --> I
I --> J[更新 verbatim_since 锚点并写库]
J --> C
D --> K[返回回复 + context_budget]压缩细节
- 从
verbatim_since_*锚点起取 保留原文 的消息子集;锚点之前的内容已体现在context_summary。 - 估算
system prompt + 对话正文字符量(estimateLLMPayloadRunes)。 - 若超过 阈值(默认 88%)或 硬上限:
- 从 priorRows 头部取一批(默认 4 条); - 工作智能体:先 agentmemory.FlushBeforeCompaction; - 调用 LLM mergeRollingSummary 合并进摘要; - 更新 context_summary 与 verbatim_since_*,写回 chat_sessions。
- 循环最多 48 次,直到低于阈值或无法再滚(仍保留
min_verbatim条原文)。 - 摘要拼入 system prompt 前缀:
`` 【历史对话摘要(较早轮次已压缩;下文为多轮原文)】 …摘要正文… ``
重要边界
| 行为 | 说明 |
|---|---|
| 不删库 | 压缩只改变 送入模型的 payload;chat_messages 原文仍在 |
| 界面仍可见 | 用户「加载更早消息」可看到被摘要覆盖轮次的 全文 |
| 无法再滚 | 已达 min_verbatim 仍超预算时,会带超预算上下文继续调用(日志 chat_context_cannot_roll_more) |
| 锚点恢复 | 若锚点导致 verbatim 为空,会清空摘要与锚点并全量重算(chat_context_anchor_recover) |
前端反馈
聊天响应含 context_budget:used_runes、limit_runes、used_pct。Web 端展示 上下文用量圆环(client/web/src/api.ts → parseContextBudget)。
4. 工作智能体:压缩前记忆抢救
若是 工作区「我的智能体」 会话,滚入摘要前调用 FlushBeforeCompaction(backend/internal/agentmemory/flush.go):
- 日笔记:片段追加到
memory/YYYY-MM-DD.md(标题含「上下文压缩归档」)。 - 长期记忆提炼(可选):若
memory_auto_extract为 true(默认开启),调用ExtractFromBatch写入memory/entries/*.md。
对应产品机制:docs/core-mechanisms/智能体记忆.md — 「上下文压缩前归档」。
开关位于智能体 config_json:
MemoryAutoAppend *bool `json:"memory_auto_append,omitempty"` // 默认关
MemoryAutoExtract *bool `json:"memory_auto_extract,omitempty"` // 默认开
定义:backend/internal/runtimews/agentconfig.go。
5. 与长期记忆的分工
| 类型 | 存储 | 生命周期 | 用途 |
|---|---|---|---|
| 对话历史 | DB chat_messages | 至删会话/截断 | 工作记忆:当前会话完整流水 |
| 滚动摘要 | DB chat_sessions.context_summary | 随会话更新 | 仅控模型上下文 |
| 日笔记 | 磁盘 memory/YYYY-MM-DD.md | 今/昨注入,自然淡化 | 压缩归档 + 临时上下文 |
| 长期记忆 | 磁盘 memory/entries/*.md + MEMORY.md 索引 | 跨会话 | 偏好、决策、项目节点等 |
对话历史变多 不会自动 全部变成长期记忆;需用户点 「记住」、对话中明确「记住/别忘了」、压缩前自动提炼、或智能体调用 memory_write。
详见 工作智能体记忆存储位置解析。
6. 其它相关截断与压缩(非会话滚动摘要)
以下机制 独立 于「会话级」滚动摘要,但同样为避免单次请求上下文膨胀。
6.1 工具结果:主对话不截断
工具循环写入模型上下文的 tool 返回为 全文( unner.go)。前端 SSE 展示仍可对超长输出做展示层截断,不影响模型所见。
知识注入、附件提取等其它 rune 上限见各自模块,与工具循环检查点无关。
6.2 工具循环内:接近预算才做 LLM 检查点摘要
单条用户消息触发的 多轮 tools(RunAgentToolLoop)里,上下文可能因反复取数 / 写文件膨胀。
| 要点 | 做法 |
|---|---|
| 何时压缩 | 按 rune 估算当前 messages,对照 ChatContextInputBudgetRunes();达到约 88% 才触发(硬阈值约 95%,对齐会话摘要与 Claude Code) |
| 为何以前太早 | 旧实现用约 100KB 字节 触发;中文 JSON 约 3 字节/字,相当于预算的三成左右就会压 |
| 主路径 | 仅 LLM「已执行过程」交接摘要;禁止机械截断主对话 |
| 失败 | 摘要失败则 保持原文继续,不回落剪切 |
| 必须保留 | 用户主目标与未完成事项;upload_id / download_url / size_bytes |
| 近轮原文 | 最近若干条 tool 往返全文保留 |
| 代码 | context_compact.go、context_compact_llm.go;maybeCompactAgentToolContext |
日志:chat_agent_context_compact_llm、chat_agent_context_compact_llm_failed、chat_agent_context_compact_skipped。
与会话滚动摘要的分工:会话摘要压缩跨轮 user/assistant 历史;本节压缩同一轮内 tool 往返。
7. 排查与运维参考
日志关键字
| 日志事件 | 含义 |
|---|---|
| chat_context_summarized | 完成一批滚动摘要 |
| chat_context_cannot_roll_more | 已达最少原文保留,仍超预算 |
| chat_context_anchor_recover | 锚点异常,重置摘要 |
| chat_memory_flush_ok / chat_memory_flush_failed | 压缩前记忆 flush 结果 |
| chat_llm_history | 本次 conv 条数、字符量、预算 |
| chat_agent_context_compact_llm | 工具循环 LLM 检查点摘要成功 |
| chat_agent_context_compact_llm_failed | 检查点摘要失败(保持原文,不截断) |
| chat_agent_context_compact_skipped | 无可用模型时跳过压缩 |
本机查看
- 数据库:查 chat_sessions.context_summary、erbatim_since_* 与 chat_messages 条数。
- 运行时记忆:{RUNTIME_DIR}/{智能体ID}/memory/ 下日笔记与 entries(本地多为 ackend/tmp/runtime/)。
- 配置:环境变量或 mindlink.json → chat_context 段。
已知缺口(日后可处理)
| 项 | 现状 | 建议方向 |
|---|---|---|
| DB 体积 | 无自动归档/TTL | 运维策略:按工作区/用户清理旧会话,或冷存储 |
| 超 8000 条会话 | 读库上限 8000,更早消息不参与滚动 | 评估是否提高上限或分段摘要 |
| 摘要质量 | 依赖 LLM 合并,有损 | 关键信息引导用户「记住」或调 extract 策略 |
| SQLite schema | 旧版 chat_sessions 可能无 context_summary 列 | 迁移脚本对齐 postgres schema |
| 工具循环多次检查点 | 摘要链式损耗 | 可参考 Cursor 训练式 self-summary,加强「未完成」结构化字段 |
8. 相关文档
| 文档 | 说明 |
|---|---|
| 工作智能体记忆存储位置解析 | 记忆文件目录树与 DB 分工 |
| core-mechanisms/智能体记忆.md | 产品机制:分层、写入、注入 |
| 产品规格.md §3.5 | Workspace 文件范式 |
| 后端与Web设计.md | 会话/历史 API 产品化 |