跳转到内容

高级上下文管理

Gemini CLI — 高级上下文管理

学习目标

  • 理解基于图结构的上下文表示与 DAG 节点管理
  • 掌握 ContextWorkingBuffer 双缓冲机制和处理器管线模式
  • 分析历史硬化和不变量验证的作用

前置知识

本章涉及上下文管理的通用原理,建议先阅读:

下文假设你已理解截断、压缩和摘要的基础策略,直接聚焦 Gemini CLI 的高级实现。


项目实践

图结构上下文表示

传统的上下文是线性消息数组,Gemini CLI 的 ContextManager 使用 DAG(有向无环图) 表示上下文:

Context Graph:
Node[System Prompt]
Node[User Message 1] ──→ Node[Assistant Response 1]
Node[Tool Output 1]
Node[File Content A] ──→ Node[Distilled Content A]
Node[Memory Summary]

每个节点(ConcreteNode)包含:

  • id:唯一标识
  • content:实际上下文内容
  • triggers:声明何时需要重新处理
  • metadata:token 数、类型等元信息

节点类型:系统提示、用户消息、工具输出、文件内容、记忆摘要等。

ContextWorkingBuffer 双缓冲

ContextWorkingBuffer 维护两份图状态:

缓冲区内容
Master(纯净图)未经处理的原始节点状态,所有处理器都从这里读取原始数据
Active(当前图)经过处理器处理后的节点状态,用于渲染 API History

应用处理器结果

buffer.applyProcessorResult(processorId, targets, returnedNodes):
1. 验证所有目标节点仍在缓冲区中(防御性检查)
2. 用 returnedNodes 替换 targets
3. 返回新的 ContextWorkingBuffer 实例(不可变性)

不可变性确保处理器结果的应用是原子的——要么全部应用,要么不应用,不会出现中间状态。

处理器管线(Pipeline)

PipelineOrchestrator 调度多个处理器按依赖顺序执行:

处理器示例

处理器操作
蒸馏处理器将长文件内容蒸馏为紧凑表示(去除空白、重复行、注释等)
掩码处理器隐藏工具输出中的敏感信息(如 API 密钥、个人数据)
压缩处理器将旧的工具输出摘要化
裁剪处理器截断过长的文件内容

ContextEventBus 事件驱动更新

处理器不直接修改图,而是通过 ContextEventBus 发布结果:

处理器完成后:
eventBus.publish({
processorId: "distillation",
targets: [node1, node2],
returnedNodes: [distilled1, distilled2]
})
ContextManager 监听:
eventBus.onProcessorResult((event) => {
// 验证目标节点仍在缓冲区中
if (!event.targets.every(t => bufferIds.has(t.id))) {
debugLogger.warn("Dropping result: targets no longer in buffer")
return
}
// 应用结果
buffer = buffer.applyProcessorResult(...)
})

防御性验证:在应用结果前,检查所有目标节点是否仍存在于缓冲区。如果节点已被删除(如用户取消了相关操作),丢弃该处理器结果。

渲染缓存(Redundant Render Cache)

ContextManager 缓存最近一次渲染结果,基于节点 ID 哈希值判断是否需要重新渲染:

if (lastRenderCache && nodesHash == cache.nodesHash) {
return cache.result // 命中缓存,跳过渲染
}

适用场景:用户发送了一条短消息,没有触发任何处理器,节点内容未变化。此时直接返回缓存结果,避免重复计算。

历史硬化(History Hardening)

hardenHistory() 确保渲染出的 API History 格式一致:

hardenHistory(history):
1. 验证角色顺序(user/model/tool_response 配对正确)
2. 确保 functionCall 后有对应的 functionResponse
3. 去除空消息
4. 合并连续的同角色文本消息

为什么需要硬化:处理器可能产生格式不一致的节点内容,硬化层保证输出给 API 的历史始终满足 Gemini API 的格式要求。

不变量检查器

checkContextInvariants() 在每次图更新后验证状态一致性:

checkContextInvariants(nodes):
- 所有节点 ID 唯一
- 依赖边不形成环
- 节点内容非空
- token 计数准确

任何不变量违反都会触发警告日志,帮助调试图状态不一致问题。

集成到 Agent 循环

ContextManager.renderHistory()processTurn() 开始前调用:

processTurn():
if (contextManagementEnabled && contextManager) {
const { history, apiHistory, pendingApiHistory, baseUnits } =
await contextManager.renderHistory(pendingRequest, signal)
// 使用处理后的 history 和 apiHistory 调用模型
this.getChat().setHistory(history)
apiHistoryOverride = [...apiHistory, finalPendingContent]
}

这使得每次 turn 的 API 调用都使用经过图管理和处理器优化的上下文。


问题与规避

图状态的复杂性

问题:图结构比线性数组复杂得多,调试状态不一致问题困难。

对策

  • 不变量检查器在每次更新后验证
  • debugLogger 记录关键操作(处理器触发、结果应用、缓存命中/未命中)
  • ContextWorkingBuffer 的不可变性使得状态变更可追踪

渲染延迟

问题:图渲染和处理器执行可能增加 turn 延迟。

对策

  • 渲染缓存避免无变化时的重复计算
  • 处理器并行执行(无依赖的处理器同时运行)
  • 增量更新:只处理触发器激活的节点,而非全图重渲染

节点删除的一致性

问题:当用户清除上下文或压缩后,处理器结果可能指向已不存在的节点。

对策onProcessorResult 中的防御性验证——检查所有目标节点是否仍在缓冲区中,不在则丢弃结果。


设计取舍

图结构 vs 线性数组

方案优势代价
图结构表达依赖关系、支持增量更新、多处理器并行实现复杂度高、需要不变量验证
线性数组简单直观、易于调试无法表达依赖、每次压缩需要全局处理

Gemini CLI 的选择:图结构,因为上下文来源多样化(文件、工具输出、记忆等),需要独立管理和增量更新。线性数组无法高效处理这种多样性。

事件驱动更新 vs 直接修改

方案优势代价
事件驱动(EventBus)处理器解耦、可拦截和审计结果、支持多消费者间接性增加,调试需要追踪事件流
直接修改简单直接处理器紧耦合、难以扩展

Gemini CLI 的选择:EventBus,因为未来可能添加更多处理器和消费者(如遥测、日志),事件驱动架构支持水平扩展。


参考来源