跳转到内容

上下文治理管线

上下文治理管线

学习目标

理解 Nanobot 如何在每次 LLM 请求前执行多层上下文治理,确保消息序列合法且符合 Token 预算。

前置知识

下文假设你已理解上述概念,直接聚焦 Nanobot 的具体实现。

项目实践

治理管线顺序

每次 LLM 请求前,AgentRunner.run() 执行以下管线:

微压缩(Microcompact)

_microcompact() 将旧的、可压缩的工具结果替换为单行摘要:

_COMPACTABLE_TOOLS = frozenset({
"read_file", "exec", "grep", "find_files",
"web_search", "web_fetch", "list_dir", "list_exec_sessions",
})
_MICROCOMPACT_KEEP_RECENT = 10 # 保留最近 10 个
_MICROCOMPACT_MIN_CHARS = 500 # 少于 500 字符不压缩

策略:保留最近 10 个可压缩工具结果的完整内容,更早的替换为 [read_file result omitted from context]。这避免了大文件读取结果持续占据上下文空间。

Token 裁剪(Snipping)

_snip_history() 根据 context_window_tokens 预算裁剪历史:

  1. 计算预算 = context_window_tokens - max_output_tokens - 安全缓冲(1024)
  2. 从后向前保留消息,直到超出预算
  3. 确保第一条非 system 消息是 user 角色(避免 system→assistant 被 GLM 等提供商拒绝)
  4. 如果裁剪后没有用户消息,保留最近 4 条作为最低限度

孤儿清理与回填

孤儿清理_drop_orphan_tool_results() 移除没有对应 assistant.tool_calls 的孤立 tool 消息。

回填_backfill_missing_tool_results() 为有 tool_calls 但没有对应 tool 结果的调用插入占位错误:

_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"

这确保了 role alternation(角色交替)不被破坏。

角色交替强制

LLMProvider._enforce_role_alternation() 确保消息序列符合 OpenAI 格式要求:

  • 合并连续的同角色消息(user→user 合并为一条)
  • 去掉末尾的 assistant 消息(大多数提供商不支持预填充)
  • 如果去掉后只剩 system 消息,将最后一条 assistant 转为 user 恢复
  • 确保第一条非 system 消息不是 bare assistant(插入合成用户消息)

问题与规避

GLM 的 system→assistant 拒绝

问题:Zhipu/GLM 拒绝 system→assistant 的消息序列(错误 1214)。

规避:在角色交替强制中,如果发现去掉末尾 assistant 后只剩 system 消息,将最后一条 assistant 转为 user 消息。

裁剪后角色交替断裂

问题:Token 裁剪可能产生 user→userassistant→assistant 的连续消息。

规避:裁剪后重新执行 _enforce_role_alternation(),合并连续同角色消息。

孤儿工具结果导致角色断裂

问题:当 tool 结果没有对应的 tool_call 时,会产生 assistant→tool 的非法序列。

规避:先清理孤儿,再回填缺失,确保每个 tool_call 都有对应的 tool 结果。

设计取舍

为什么在 run() 中治理而非在构建上下文时

优势:治理逻辑与执行管线在一起,可以在每次 iteration 前都执行,而非仅在 Turn 开始时执行一次。这对多轮工具调用场景尤为重要。

代价AgentRunner 承担了更多职责,不纯粹是”执行器”。

替代方案:在 ContextBuilder 中完成所有治理。但那样只在 Turn 开始时执行一次,无法处理中间轮次的消息变化。

参考来源