完整聊天记录,不应该等于模型上下文
Jarvis 如何分离可追溯档案与活动上下文,并在不删除历史的前提下压缩长会话。
很多对话应用最初只有一个 messages 数组。界面展示它,数据库保存它,每一轮也把它完整发送给模型。
在短对话里,这个设计简单而有效。对一个长期运行、会调用工具的 Agent 来说,它迟早会把三个不同问题混成一个:
- 用户需要看到的完整记录;
- 系统需要保存的审计档案;
- 模型完成下一步真正需要的上下文。
在 Jarvis 中,我最终把它们拆成了两层:完整档案与活动上下文。这个拆分看起来只是存储结构变化,实际决定了长会话能否压缩、恢复和解释。
为什么不能一直发送完整历史
完整历史不断增长,会带来几个直接问题。
首先是成本和延迟。即使使用本地模型,重复处理已经结束的工具输出、旧计划和中间解释,也会消耗上下文窗口与推理时间。
其次是注意力污染。模型看到的信息更多,不代表更容易完成当前步骤。过期计划、早期错误假设和大段工具结果可能挤压真正重要的约束。
最后是产品语义。如果为了控制上下文而删除旧消息,用户会失去审计与回看的依据;如果完全不删,模型上下文又会无限膨胀。
因此,“聊天记录”和“模型记忆”不应该是同一个数据结构的两个名字。
两层历史分别承担什么职责
Jarvis 的完整档案按消息批次追加保存。它服务于:
- 重新打开会话时还原 UI;
- 审计模型说过什么、调用过什么工具;
- 为全文检索提供数据来源;
- 在压缩策略变化后保留重新处理的可能。
活动上下文则是一份可以被替换的模型输入。它通常从完整档案初始化,但随后可以包含摘要与最近消息尾部,而不需要与 UI 中的历史逐条相同。
完整档案(append-only)
├─ UI 回看
├─ 审计与搜索
└─ 初始化活动上下文
活动上下文(replaceable)
├─ 摘要
├─ 最近消息
└─ 下一轮模型输入
数据库中,这两层也有不同位置:消息批次保存完整档案,conversation_context 保存当前活动消息、压缩次数和最近压缩时间。如果旧会话还没有活动上下文,系统会从完整档案回填一次,而不是要求迁移时重写所有历史。
自动压缩与手动压缩不是同一个触发方式
当前默认配置中,活动上下文超过 80 条消息时会自动压缩,并保留约 24 条最近消息作为连续性尾部。摘要支持增量更新:再次压缩时,旧摘要可以作为锚点,而不是每次从最早历史重新总结。
界面也提供手动 Compact。它允许用户在尚未超过阈值时主动收缩上下文,例如准备从讨论阶段进入实现阶段。
两种入口共用同一套摘要策略,但触发条件不同:
- 自动压缩只在超过阈值后发生;
- 手动压缩使用强制模式,短上下文也可以执行。
系统会比较压缩前后的消息数与估算 token 数。只有确实减少,才记录为一次有效压缩。
为什么最近消息仍然要保留
只保存一段总摘要会丢失局部结构。工具调用 ID、刚生成的文件路径、用户最新纠正和模型正在执行的计划,往往依赖最近几轮的精确顺序。
因此压缩结果不是“用一段摘要替换所有内容”,而是:
旧历史的结构化摘要 + 最近消息尾部。
摘要负责长期约束与已完成决策,消息尾部负责短期执行连续性。两者的比例仍需根据模型窗口、任务类型和工具密度调整,当前的 80/24 是工程默认值,不是适用于所有 Agent 的通用答案。
审批等待期间不能压缩
上下文压缩与工具审批存在一个容易忽略的竞态。
当 Agent 已经生成待审批工具调用时,活动上下文中包含调用 ID、工具参数和恢复运行所需的消息结构。如果此时允许用户压缩上下文,摘要过程可能改变或遗漏这些精确信息,使之后的批准无法安全对应原请求。
因此,Jarvis 在存在待审批请求时拒绝手动压缩;自动压缩也只会在没有待审批请求、一次运行正常结束后进行。前端同时禁用 Compact 和新消息输入,避免创建第二条并发语义。
这条规则体现了一个更一般的原则:
摘要可以压缩语义,但不应该跨越尚未完成的事务边界。
被压缩的信息如何找回来
压缩不是删除。完整档案仍然存在,并进入 SQLite FTS 索引。Agent 可以通过 search_past_messages 搜索已经离开活动上下文的内容,或者查找其他会话中的讨论片段。
这让“始终塞进 prompt”和“完全遗忘”之间多出第三种选择:
- 当前步骤高频需要的信息留在活动上下文;
- 旧信息保留在档案中;
- 真正需要时再检索回来。
它和 RAG 很相似,但检索对象不是外部知识库,而是 Agent 自己的工作历史。
压缩的风险不会因为架构正确而消失
分层解决了保存与模型输入之间的冲突,却没有自动保证摘要质量。
摘要仍可能:
- 丢掉一个后来变得重要的约束;
- 把不确定结论写成已经确认的事实;
- 合并两个相似但不同的文件、工具或决策;
- 在多次增量压缩后逐渐偏离原始证据。
因此,当前 UI 会显示活动消息数和估算 token,后端上下文状态还记录压缩次数与最近压缩时间;完整档案也始终保留。下一步更值得做的不是继续调一个固定阈值,而是为摘要加入来源指针、关键不变量和面向具体步骤的上下文投影。
我目前也没有足够的连续使用数据来声称压缩节省了多少时间或提高了多少成功率。现阶段可以确认的是架构性质:模型输入可以收缩,而用户历史不会随之消失。
一个更实用的判断框架
为长会话设计上下文时,我现在会分别问:
- 什么必须永久保存,供用户回看和审计?
- 什么必须精确保留,直到当前事务完成?
- 什么可以被摘要,但仍需保留原始来源?
- 什么只在特定步骤需要,可以按需检索?
- 当摘要失败时,系统能否回到完整档案重新构建?
只有先分开这些问题,“记忆”“上下文”“聊天历史”才不会成为含义模糊的同义词。
如果你更关心副作用发生前的控制边界,可以继续阅读:一个真正会停下来的本地 Agent。