上下文与自动压缩

这一层负责回答两个问题:现在用了多少上下文,以及什么时候必须把历史压掉。它与「压缩管道」分工明确:管道压的是单段文本,这里压的是整个会话的消息序列,并把摘要持久化到会话上。

占用是怎么算出来的

组成算法
历史(history)只统计「尚未被摘要覆盖」的消息:ContextSummaryThroughMessageId 之前的消息直接排除
摘要(summary)会话上保存的摘要文本长度折算成 token
系统提示(system)反推值:用最近一次真实上报的 PromptTokens 减去历史与摘要估算得到,拿不到时回退经验值(对话 1500 / 编码会话 3500)
窗口(window)优先请求参数 contextWindow,其次模型的 ContextWindow,都没有则默认 128000
空会话时占用为 0,但面板仍固定返回 system / history / summary 三段,避免前端出现「少一段」的跳动。

自动压缩的触发条件

常量默认含义
AutoCompactRatio0.8占用 ≥ 窗口的 80% 才自动压缩;未触发时直接返回 null,调用方忽略
DefaultKeepRecent6保留最近 6 条消息不参与摘要
MaxTranscriptChars60000交给摘要模型的历史最多截取到末尾 6 万字符,避免摘要提示本身超长
MaxSummaryAttempts3摘要最多尝试 3 次:空返回或内容过短就立刻重试(不是固定等待)

压缩时到底改了什么

  1. 从「上次摘要覆盖到的消息」之后开始取历史,保留最近 KeepRecent 条,其余交给摘要模型。
  2. 生成摘要后,把摘要写回会话的 ContextSummary,并把 ContextSummaryThroughMessageId 更新为「最后一条被纳入摘要的消息 ID」。
  3. 原始消息不动:压缩只影响之后送给模型的内容,历史消息仍可完整回看、导出。
  4. 压缩完成会发一条 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 调小让摘要更聚焦。
  • 需要完整历史时(审计 / 复盘):消息本身从未被删改,直接看会话历史或导出即可。