2026-10-24
AI
0

目录

开篇
上下文由什么组成
为什么不能直接截断
保守 Token 估算
触发条件
安全尾部
结构化摘要
先微压缩,再摘要
压缩后的消息顺序
Prompt Too Long 重试
确定性回退
冻结上下文
观测与诊断
小结

系列文章:从零实现一个通用智能体
本篇主题:上下文窗口、结构化压缩与完整 transcript
代码版本:mx-claw 0.1.3

开篇

长任务最终都会撞上上下文窗口。简单删除旧消息会破坏 assistant tool call 与 tool result 的配对;把全部历史原样重发则会持续增加成本和延迟。

mx-claw 将两类数据分开:

text
会话事实源:完整 transcript,永久保留 模型请求视图:系统消息 + 结构化摘要 + 最近安全尾部

压缩只影响本轮发给 Provider 的消息数组,不修改 SQLite 中的原始消息。

上下文由什么组成

一次请求可能包含以下消息:

  • 固定 system prompt;
  • 冻结的项目指令与验证策略;
  • 当前会话冻结的 Skill 元数据;
  • 当前会话冻结的长期记忆快照;
  • 用户输入与图片附件;
  • assistant 文本和 tool calls;
  • tool results;
  • 历史压缩摘要。

前五类决定任务边界,后两类记录执行过程。压缩必须优先保留边界,再压缩过程。

为什么不能直接截断

OpenAI-compatible 工具协议要求 tool result 紧跟对应 assistant tool call。只要截断边界落在 两者之间,Provider 就可能拒绝整段历史。

错误示例:

text
assistant: tool_calls [read_file-1] tool: result(read_file-1) assistant: tool_calls [edit_file-1] tool: result(edit_file-1)

如果尾部从第二个 assistant 开始,却丢掉后续 tool result,历史结构就失效。压缩器因此需要 识别协议配对,而不是简单按消息数量截断。

另一个问题来自破损历史。进程中断可能留下没有结果的 tool call。发送请求前, repairToolCallTranscript() 会补齐中断结果或移除孤立结果,先恢复协议结构,再执行压缩。

保守 Token 估算

mx-claw 不绑定特定模型的 tokenizer,而是使用保守估算:

ts
const cjkCount = text.match(CJK_PATTERN)?.length ?? 0 const latinChars = text.replace(CJK_PATTERN, '').length const toolTokens = Math.ceil(JSON.stringify(toolCalls).length / 4) return cjkCount + Math.ceil(latinChars / 4) + imageCount * 2_000 + toolTokens

规则包括:

  • 每个 CJK 字符按一个 token 估算;
  • 拉丁字符约四个按一个 token 估算;
  • 每张图片预留 2,000 token;
  • 工具调用 JSON 单独计算。

它不是精确计量器,目标是在缺少模型专用 tokenizer 时稳定地提前触发压缩。

触发条件

压缩阈值由模型能力或显式配置决定:

ts
const threshold = Math.floor( options.contextWindowTokens * options.thresholdRatio, )

默认压缩比例是 0.8,最低保留最近约 8,000 token。请求估算低于阈值时,原消息数组直接 返回,不产生额外摘要。

每次模型迭代前都会重新检查上下文,因为工具结果会持续增加请求体积。压缩可能在一个 turn 中发生多次。

安全尾部

压缩器从会话尾部向前累计 token,直到达到 keepRecentTokens。尾部至少保留一条消息, 避免出现空上下文。

随后检查尾部第一条消息:

ts
while (start > 0 && conversation[start]?.role === 'tool') { const toolCallId = conversation[start]?.toolCallId // 向前寻找包含该 toolCallId 的 assistant 消息 }

如果尾部从 tool result 开始,索引会向前扩展,直到包含相应 assistant tool call。这个处理 保证协议段完整。

结构化摘要

被压缩的旧消息不会只留下“之前发生了一些事情”。摘要包含四类可操作信息:

  1. 最近四个用户目标;
  2. 编辑过的文件路径;
  3. 最近的工具失败、权限拒绝和禁止命令;
  4. 最后一条有效 assistant 状态。

摘要示例:

text
[compressed conversation context] Older transcript messages were summarized. Goals: - 修复 CI 中的 SQLite FTS 测试 Files and tools touched: - packages/storage/src/sqlite.ts Failures to avoid: - Tool failed: no such module: fts5 Last useful assistant state: - FTS 兼容索引已经实现,正在验证 Node 22。

摘要不是模型自由生成,而是确定性提取。这样更容易测试,也不会在压缩阶段额外发起一次 模型请求。

先微压缩,再摘要

上下文超阈值时,压缩器先尝试 microcompaction:只替换安全尾部之前的旧成功工具输出,把 大段构建日志或搜索结果改为字符数占位符。

微压缩不删除消息,也不拆开 assistant tool call 与 tool result。失败、权限拒绝和禁止命令 输出不会被微压缩,避免丢失关键纠错证据。

如果微压缩后已低于阈值,本轮不生成整段摘要;否则继续执行结构化摘要。

压缩后的消息顺序

最终请求视图是:

text
原始 system messages + 压缩摘要 system message + 最近安全尾部

固定 system prompt、Skill 索引和记忆快照不会被压缩器删除。它们是会话边界,不属于可丢弃 的过程历史。

Prompt Too Long 重试

本地估算可能低估某个 Provider 的真实 tokenizer。当 Provider 返回上下文过长错误时,Query Loop 会执行一次强制回退压缩:

ts
if ( !promptTooLongRetried && /prompt too long|context length|too many tokens/i.test(message) ) { promptTooLongRetried = true messages = fallback.messages continue }

重试只发生一次。第二次仍失败时,错误将正常进入 turn 失败路径,避免无限重试。

确定性回退

如果结构化压缩器本身抛错,Query Loop 不会停止任务,而是保留最多两个 system 消息、一条 固定回退摘要和最近六条对话消息。

回退路径仍会向前调整起始位置,避免窗口从 tool result 开始。它的信息量低于正常摘要, 但保持协议有效。

冻结上下文

项目指令、Skill 和长期记忆采用按会话冻结策略:

  • 新会话首回合读取 AGENTS.md 与 .mx-claw/AGENTS.md;
  • 新会话首回合读取 .mx-claw/project.json 的验证策略;
  • 新会话首回合读取 Skill catalog;
  • 新会话首回合读取 user 与 project 记忆;
  • 注入后的内容作为 system 消息持久化;
  • 会话内新增记忆或 Skill 只影响后续会话。

冻结减少同一 session 内的指令漂移。任务进行到一半时,行为规则突然变化比使用稍旧的 索引更难排查。

观测与诊断

压缩发生时会产生 context.compacted 事件,包含:

ts
{ type: 'context.compacted', beforeTokens: 92_400, afterTokens: 24_100, summary: '[compressed conversation context] ...' }

CLI 和 Web 可以显示压缩状态,但不会用摘要覆盖原始 transcript。后续恢复、搜索和审计仍然 读取完整消息。

小结

上下文管理的核心取舍是:对模型发送可压缩的工作视图,对系统保存不可丢失的事实。

  • 压缩前先修复工具协议;
  • 尾部不能从孤立 tool result 开始;
  • 固定 system 边界不能被摘要删除;
  • Provider 拒绝长上下文时只做一次确定性重试;
  • 压缩事件可观察,但原始历史不可改写。

下一篇进入工具层,说明模型动作如何从一段 JSON 变成受路径、权限和资源限制保护的真实 操作。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:繁星

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!