上下文与自动压缩
这一层负责回答两个问题:现在用了多少上下文,以及什么时候必须把历史压掉。它与「压缩管道」分工明确:管道压的是单段文本,这里压的是整个会话的消息序列,并把摘要持久化到会话上。
占用是怎么算出来的
| 组成 | 算法 |
| 历史(history) | 只统计「尚未被摘要覆盖」的消息:ContextSummaryThroughMessageId 之前的消息直接排除 |
| 摘要(summary) | 会话上保存的摘要文本长度折算成 token |
| 系统提示(system) | 反推值:用最近一次真实上报的 PromptTokens 减去历史与摘要估算得到,拿不到时回退经验值(对话 1500 / 编码会话 3500) |
| 窗口(window) | 优先请求参数 contextWindow,其次模型的 ContextWindow,都没有则默认 128000 |
空会话时占用为 0,但面板仍固定返回 system / history / summary 三段,避免前端出现「少一段」的跳动。
自动压缩的触发条件
| 常量 | 默认 | 含义 |
AutoCompactRatio | 0.8 | 占用 ≥ 窗口的 80% 才自动压缩;未触发时直接返回 null,调用方忽略 |
DefaultKeepRecent | 6 | 保留最近 6 条消息不参与摘要 |
MaxTranscriptChars | 60000 | 交给摘要模型的历史最多截取到末尾 6 万字符,避免摘要提示本身超长 |
MaxSummaryAttempts | 3 | 摘要最多尝试 3 次:空返回或内容过短就立刻重试(不是固定等待) |
压缩时到底改了什么
- 从「上次摘要覆盖到的消息」之后开始取历史,保留最近
KeepRecent 条,其余交给摘要模型。
- 生成摘要后,把摘要写回会话的
ContextSummary,并把 ContextSummaryThroughMessageId 更新为「最后一条被纳入摘要的消息 ID」。
- 原始消息不动:压缩只影响之后送给模型的内容,历史消息仍可完整回看、导出。
- 压缩完成会发一条
notice(kind=compacted) 事件,前端在输入框上方显示提示条。
与压缩管道的分工
| 维度 | 压缩管道 | 上下文自动压缩(本页) |
| 作用对象 | 单段文本(历史片段 / 工具输出) | 整个会话的消息序列 |
| 触发 | 调用方显式调用(每轮前、单轮任务前) | 占用 ≥ 80% 窗口时自动触发 |
| 落点 | 只替换当次送入模型的文本 | 摘要持久化到 ContextSummary + ThroughMessageId |
| 失败影响 | 降级为原文,无副作用 | 失败只是不注入摘要,主流程继续 |
失败与降级
- 话题 / 会话不存在:返回
notFound。
- 没有可压缩的历史(
toSummarize.Count == 0):返回 context.noCompressibleHistory。
- 摘要模型不可用:返回 null,不写摘要。
- 摘要调用抛异常:只记 warning 并返回 null——自动压缩失败不阻断对话。
- 自动压缩失败时前端无提示条,用户可继续对话,占用会继续增长;需要手动压缩时可在上下文面板触发。
观测
- 日志前缀
[Context]:包含「压缩第 x/y 次未返回内容」「摘要过短」「压缩完成:N 条消息 → M 字符摘要(约 before → after tokens)」。
- 前端:对话页的上下文占用面板、编码会话右下角的占用环形图与压缩提示条。
- 用量侧:首轮输入字符与压缩后字符会被记录,便于评估压缩收益。
使用建议
- 长会话出现「模型忘记早期内容」时,先看占用是否已到 80%——到阈值会自动压缩,这是设计行为而不是 bug。
- 想让摘要更贴近当前任务:把
KeepRecent 调大(保留更多原文),代价是占用上升更快。
- 摘要质量差:换摘要模型(设置里可指定),或把
MaxTranscriptChars 调小让摘要更聚焦。
- 需要完整历史时(审计 / 复盘):消息本身从未被删改,直接看会话历史或导出即可。