上下文治理管线
上下文治理管线
学习目标
理解 Nanobot 如何在每次 LLM 请求前执行多层上下文治理,确保消息序列合法且符合 Token 预算。
前置知识
- 上下文压缩与 Token 优化 — 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 预算裁剪历史:
- 计算预算 =
context_window_tokens - max_output_tokens - 安全缓冲(1024) - 从后向前保留消息,直到超出预算
- 确保第一条非 system 消息是
user角色(避免system→assistant被 GLM 等提供商拒绝) - 如果裁剪后没有用户消息,保留最近 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→user 或 assistant→assistant 的连续消息。
规避:裁剪后重新执行 _enforce_role_alternation(),合并连续同角色消息。
孤儿工具结果导致角色断裂
问题:当 tool 结果没有对应的 tool_call 时,会产生 assistant→tool 的非法序列。
规避:先清理孤儿,再回填缺失,确保每个 tool_call 都有对应的 tool 结果。
设计取舍
为什么在 run() 中治理而非在构建上下文时
优势:治理逻辑与执行管线在一起,可以在每次 iteration 前都执行,而非仅在 Turn 开始时执行一次。这对多轮工具调用场景尤为重要。
代价:AgentRunner 承担了更多职责,不纯粹是”执行器”。
替代方案:在 ContextBuilder 中完成所有治理。但那样只在 Turn 开始时执行一次,无法处理中间轮次的消息变化。