全部文章
发布于 2026-07-16

对话历史与上下文压缩机制解析

对话变长以后,界面、存档和模型上下文各自怎么收,和跨会话记忆怎么分工。

来源 docs/技术博客/对话历史与上下文压缩机制解析.md

表述:本文面向 开发与运维,说明 Cadau 在对话历史 越来越多 时,界面展示、数据库持久化、模型上下文 三层分别如何处理,以及与工作智能体 跨会话记忆 的分工。产品侧用户说法见 docs/core-mechanisms/智能体记忆.md;记忆文件落盘见 工作智能体记忆存储位置解析

日期:2026-07-16(补充工具循环检查点) 相关代码backend/internal/api/handlers/chat.gobackend/internal/api/handlers/chat_context_roll.gobackend/internal/store/chat.gobackend/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_jsonattachment_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_edgenext_cursor 兼容旧义 = 更早方向)。
  • 有界窗口:内存只保留视口附近约百条,向上/向下双向加载并回收对侧;机制见 会话消息窗口化加载(微信式)。
  • 提问导航GET /chat/history/turns 提供轻量索引,与消息窗口解耦。
  • 会话列表同样分页(GET /chat/sessions,默认 limit=20)。

实现:client/web/src/api.tsclient/web/src/chatMessageWindow.tsclient/web/src/useChatSessions.ts

UI 因会话总轮数到上千而持有全部气泡正文;数据库里仍是完整记录


3. 模型上下文:滚动摘要(核心)

真正受 上下文窗口 限制的是每次调用 LLM 时塞进模型的内容。入口:

  • prepareLLMConversationWithRollingContextbackend/internal/api/handlers/chat_context_roll.go
  • 发消息时先 ListChatMessages(..., 8000) 读库,再滚动压缩 — backend/internal/api/handlers/chat.go

默认配置

环境变量(亦可在 mindlink.jsonchat_context 覆盖):

参数环境变量默认值含义
总字符预算CHAT_CONTEXT_MAX_RUNES120000粗估上下文总上限
回复预留CHAT_CONTEXT_REPLY_RESERVE_RUNES8000预留给模型输出
实际输入预算≈ 112000max_runes - reply_reserve
摘要触发阈值CHAT_CONTEXT_SUMMARIZE_THRESHOLD_PCT88超过预算 88% 时开始压缩
最少保留原文轮数CHAT_CONTEXT_MIN_VERBATIM_MESSAGES6至少保留最近 6 条消息原文
每批压缩条数CHAT_CONTEXT_SUMMARIZE_BATCH_MESSAGES4每次把最早 4 条滚入摘要
按模型解析窗口CHAT_CONTEXT_RESOLVE_MAX_FROM_MODELfalsetrue 时启动时按模型 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]

压缩细节

  1. verbatim_since_* 锚点起取 保留原文 的消息子集;锚点之前的内容已体现在 context_summary
  2. 估算 system prompt + 对话正文 字符量(estimateLLMPayloadRunes)。
  3. 若超过 阈值(默认 88%)或 硬上限

- 从 priorRows 头部取一批(默认 4 条); - 工作智能体:先 agentmemory.FlushBeforeCompaction; - 调用 LLM mergeRollingSummary 合并进摘要; - 更新 context_summaryverbatim_since_*,写回 chat_sessions

  1. 循环最多 48 次,直到低于阈值或无法再滚(仍保留 min_verbatim 条原文)。
  2. 摘要拼入 system prompt 前缀:

`` 【历史对话摘要(较早轮次已压缩;下文为多轮原文)】 …摘要正文… ``

重要边界

行为说明
不删库压缩只改变 送入模型的 payloadchat_messages 原文仍在
界面仍可见用户「加载更早消息」可看到被摘要覆盖轮次的 全文
无法再滚已达 min_verbatim 仍超预算时,会带超预算上下文继续调用(日志 chat_context_cannot_roll_more
锚点恢复若锚点导致 verbatim 为空,会清空摘要与锚点并全量重算(chat_context_anchor_recover

前端反馈

聊天响应含 context_budgetused_runeslimit_runesused_pct。Web 端展示 上下文用量圆环client/web/src/api.tsparseContextBudget)。


4. 工作智能体:压缩前记忆抢救

若是 工作区「我的智能体」 会话,滚入摘要前调用 FlushBeforeCompactionbackend/internal/agentmemory/flush.go):

  1. 日笔记:片段追加到 memory/YYYY-MM-DD.md(标题含「上下文压缩归档」)。
  2. 长期记忆提炼(可选):若 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无可用模型时跳过压缩

本机查看

  1. 数据库:查 chat_sessions.context_summary、 erbatim_since_* 与 chat_messages 条数。
  2. 运行时记忆:{RUNTIME_DIR}/{智能体ID}/memory/ 下日笔记与 entries(本地多为 ackend/tmp/runtime/)。
  3. 配置:环境变量或 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.5Workspace 文件范式
后端与Web设计.md会话/历史 API 产品化