系列文章:从零实现一个通用智能体
本篇主题:上下文窗口、结构化压缩与完整 transcript
代码版本:mx-claw 0.1.3
长任务最终都会撞上上下文窗口。简单删除旧消息会破坏 assistant tool call 与 tool result 的配对;把全部历史原样重发则会持续增加成本和延迟。
mx-claw 将两类数据分开:
text会话事实源:完整 transcript,永久保留 模型请求视图:系统消息 + 结构化摘要 + 最近安全尾部
压缩只影响本轮发给 Provider 的消息数组,不修改 SQLite 中的原始消息。
一次请求可能包含以下消息:
前五类决定任务边界,后两类记录执行过程。压缩必须优先保留边界,再压缩过程。
OpenAI-compatible 工具协议要求 tool result 紧跟对应 assistant tool call。只要截断边界落在 两者之间,Provider 就可能拒绝整段历史。
错误示例:
textassistant: 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() 会补齐中断结果或移除孤立结果,先恢复协议结构,再执行压缩。
mx-claw 不绑定特定模型的 tokenizer,而是使用保守估算:
tsconst 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
规则包括:
它不是精确计量器,目标是在缺少模型专用 tokenizer 时稳定地提前触发压缩。
压缩阈值由模型能力或显式配置决定:
tsconst threshold = Math.floor(
options.contextWindowTokens * options.thresholdRatio,
)
默认压缩比例是 0.8,最低保留最近约 8,000 token。请求估算低于阈值时,原消息数组直接
返回,不产生额外摘要。
每次模型迭代前都会重新检查上下文,因为工具结果会持续增加请求体积。压缩可能在一个 turn 中发生多次。
压缩器从会话尾部向前累计 token,直到达到 keepRecentTokens。尾部至少保留一条消息,
避免出现空上下文。
随后检查尾部第一条消息:
tswhile (start > 0 && conversation[start]?.role === 'tool') {
const toolCallId = conversation[start]?.toolCallId
// 向前寻找包含该 toolCallId 的 assistant 消息
}
如果尾部从 tool result 开始,索引会向前扩展,直到包含相应 assistant tool call。这个处理 保证协议段完整。
被压缩的旧消息不会只留下“之前发生了一些事情”。摘要包含四类可操作信息:
摘要示例:
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 索引和记忆快照不会被压缩器删除。它们是会话边界,不属于可丢弃 的过程历史。
本地估算可能低估某个 Provider 的真实 tokenizer。当 Provider 返回上下文过长错误时,Query Loop 会执行一次强制回退压缩:
tsif (
!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 的验证策略;user 与 project 记忆;冻结减少同一 session 内的指令漂移。任务进行到一半时,行为规则突然变化比使用稍旧的 索引更难排查。
压缩发生时会产生 context.compacted 事件,包含:
ts{
type: 'context.compacted',
beforeTokens: 92_400,
afterTokens: 24_100,
summary: '[compressed conversation context] ...'
}
CLI 和 Web 可以显示压缩状态,但不会用摘要覆盖原始 transcript。后续恢复、搜索和审计仍然 读取完整消息。
上下文管理的核心取舍是:对模型发送可压缩的工作视图,对系统保存不可丢失的事实。
下一篇进入工具层,说明模型动作如何从一段 JSON 变成受路径、权限和资源限制保护的真实 操作。


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