AI Agent 核心管理逻辑:工具的管理和调度

简单来说,AI Agent 就是一个能够自主感知环境、制定计划、采取行动来完成特定目标的系统。

从架构的角度来看,又可以分为感知模块(包括状态上下文和意图上下文,而意图上下文是一个更难一些的问题),推理模块(LLM 主控部分),记忆模块,行动模块。

今天我们要聊的是贯穿于各个模块中的工具,工具不仅仅是行动模块,在感知,记忆中都有可能用到工具。

1. OpenManus 的工具管理和调度

看了 OpenManus 的代码,整个工程中 Tool 的占比还是比较大的,这也不是一个大而全的框架。

直接看代码

1.1 BaseTool 基础类

class BaseTool(ABC):
    name: str
    description: str  
    parameters: Dict[str, Any]
    
    @abstractmethod
    async def execute(self, tool_input: Dict[str, Any]) -> ToolResult

name 用于标识,description 给 LLM 看,parameters 定义输入规范,execute 执行具体逻辑。这几个字段,已经涵盖了工具管理的核心要素。

1.2 ToolCollection 管理器

工具集合的管理通过 ToolCollection 类实现,核心是一个字典:

class ToolCollection:
    def __init__(self):
        self.tool_map: Dict[str, BaseTool] = {}

整个实现有以下三个小点:

  • O(1) 的查找性能。用工具名作为 key,直接查找,没有遍历,没有复杂的匹配逻辑。
  • 统一的执行接口。所有工具调用都通过 execute 方法,传入工具名和参数,返回统一的 ToolResult。这种一致性让上层调用变得极其简单。
  • 统一的错误处理。工具不存在、执行失败都会返回 ToolFailure,不会抛异常打断流程。这种设计让系统更健壮。
def add_tool(self, tool: BaseTool) -> None:
    if tool.name in self.tool_map:
        logger.warning(f"Tool {tool.name} already exists, overwriting")
    self.tool_map[tool.name] = tool

在添加工具时可以覆盖已有工具。

1.3 Think-Act 循环

OpenManus 实现了标准的 ReAct(Reasoning and Acting)模式,但做了很多细节优化:

async def step(self) -> None:
    await self.think()  # 思考:决定用什么工具
    await self.act()    # 行动:执行工具调用

这个实现是在 ReActAgent 中定义的,think 阶段让 LLM 分析当前状态并选择工具,act 阶段执行工具并收集结果。两个阶段分离,让整个流程可控、可调试。

1.4 工具选择

工具选择是整个系统的核心:

async def think(self) -> None:
    response = await self.llm.ask_tool(
        messages=self.memory.get_messages(),
        system_prompts=self.system_prompts,
        available_tools=self.available_tools.get_tool_schemas(),
        tool_choices=self.tool_choices
    )

它把几个关键信息都传给 LLM:

  • 历史对话(messages):让 LLM 了解上下文
  • 系统提示(system_prompts):定义 Agent 的行为规范
  • 可用工具(available_tools):告诉 LLM 有哪些工具可用
  • 选择模式(tool_choices):控制是否必须选择工具

这种设计让工具选择既灵活又可控。

1.5 批量执行和结果处理

OpenManus 支持一次选择多个工具并行执行:

async def act(self) -> None:
    if self.tool_calls:
        for tool_call in self.tool_calls:
            observation = await self.execute_tool(tool_call)
            self.memory.add_message(ToolMessage(observation, tool_call.id))

每个工具的执行结果都会记录到 memory 中,供下一轮思考使用。

1.6 特殊工具处理

OpenManus 预留了特殊工具的处理机制:

def handle_special_tool(self, tool_call: ToolCall) -> str:
    if tool_call.name == "Terminate":
        self.should_stop = True
        return "Task completed successfully."
    return f"Unknown special tool: {tool_call.name}"

Terminate 工具用于优雅终止,这种设计避免了硬编码的退出逻辑。你可以轻松添加其他特殊工具,比如 Pause(暂停)、Checkpoint(保存状态)等。

2. Gemini CLI 的工具管理和调度

Gemini CLI 把所有工具都扔给大模型,让它自己选

// 就这么简单,所有工具一股脑给 LLM
async setTools(): Promise<void> {
  const toolRegistry = this.config.getToolRegistry();
  const toolDeclarations = toolRegistry.getFunctionDeclarations();
  const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
  this.getChat().setTools(tools);
}

现在的大模型已经足够聪明,能够根据上下文选择合适的工具。与其花大力气设计复杂的工具选择策略,不如相信 AI 的判断力。但会有可能存在工具「爆炸」的情况,这个后面我们再聊。

2.1 三层工具发现机制

Gemini CLI 在工具发现方面设计了三层机制:

1. 内置核心工具

这些是精心挑选的常用工具,覆盖了大部分日常开发需求:

  • 文件操作:ls、read-file、write-file、edit
  • 代码搜索:grep、ripgrep、glob
  • 系统交互:shell
  • 网络请求:web-fetch、web-search
  • 记忆管理:memory

每个工具都经过精心设计,接口清晰,功能专一。比如 edit 工具,不仅能编辑文件,还支持预览修改。

2. 命令行工具发现

这是个很巧妙的设计。我们可以通过配置一个发现命令,让系统自动发现和注册新工具:

// 执行发现命令,解析返回的工具声明
async discoverAndRegisterToolsFromCommand(): Promise<void> {
  const command = this.config.getConfigOptions().toolDiscoveryCommand;
  if (!command) return;
  
  const result = await exec(command);
  const functions = JSON.parse(result.stdout);
  
  for (const func of functions) {
    this.registerTool(new DiscoveredTool(func));
  }
}

这意味着我们可以轻松扩展工具集,只要我们的工具能输出符合格式的 JSON 声明即可。

3. MCP 服务器集成

MCP(Model Context Protocol)是个通用的协议,Gemini CLI 对它的支持相当完善:

// 支持多种传输协议
- stdio:标准输入输出
- SSE:服务器推送事件
- HTTP:REST API
- WebSocket:双向通信

通过 MCP,我们可以接入各种专业工具服务器,比如数据库查询、API 调用、专业领域工具等。每个 MCP 服务器的工具都是隔离的,避免了命名冲突。

2.2 工具调度器

Gemini CLI 的工具调度器采用了清晰的状态机模型:

// 工具执行状态流转
validating → scheduled → awaiting_approval → executing → success/error/cancelled

在执行前先验证参数,避免无效调用:

export type ValidatingToolCall = {
  status: 'validating';
  request: ToolCallRequestInfo;
  tool: AnyDeclarativeTool;
  invocation: AnyToolInvocation;
};

根据工具风险等级,提供不同的确认策略:

// 三种确认模式
ApprovalMode.DEFAULT    // 标准确认
ApprovalMode.AUTO_EDIT  // 自动编辑确认
ApprovalMode.YOLO       // 无确认模式(勇者模式)

YOLO 模式的命名有点意思,”You Only Live Once人生苦短,活出精彩

独立的工具调用可以并行执行,提高效率:

// 并行处理多个工具调用
for (const fnCall of functionCalls) {
  // 不等待,直接调度下一个
  this.handlePendingFunctionCall(fnCall);
}

工具执行过程中的输出会实时展示,用户体验很好:

// 实时更新输出流
onUpdate?: (output: string) => void;

2.3 错误处理机制

1. 错误类型分类

系统定义了完整的错误类型枚举 ToolErrorType,包括:

通用错误:

  • INVALID_TOOL_PARAMS – 工具参数无效
  • UNKNOWN – 未知错误
  • UNHANDLED_EXCEPTION – 未处理的异常
  • TOOL_NOT_REGISTERED – 工具未注册
  • EXECUTION_FAILED – 执行失败

文件系统错误:

  • FILE_NOT_FOUND – 文件未找到
  • FILE_WRITE_FAILURE – 文件写入失败
  • PERMISSION_DENIED – 权限拒绝
  • PATH_NOT_IN_WORKSPACE – 路径不在工作空间内

Shell 特定错误:

  • SHELL_EXECUTE_ERROR – Shell 执行错误

2. 工具调用状态管理

系统定义了完整的工具调用状态类型:

export type ToolCall =
  | ValidatingToolCall      // 验证中
  | ScheduledToolCall       // 已调度
  | ErroredToolCall         // 错误
  | SuccessfulToolCall      // 成功
  | ExecutingToolCall       // 执行中
  | CancelledToolCall       // 已取消
  | WaitingToolCall;        // 等待批准

3. 错误处理流程

验证阶段错误处理

_schedule 方法中,系统会:

  1. 工具注册检查:验证工具是否已注册
  2. 参数验证:使用 tool.build(args) 验证参数
  3. 异常捕获:捕获验证过程中的所有异常
try {
  const invocationOrError = this.buildInvocation(toolInstance, args);
  if (invocationOrError instanceof Error) {
    return {
      status: 'error',
      request: reqInfo,
      response: createErrorResponse(
        reqInfo,
        invocationOrError,
        ToolErrorType.INVALID_TOOL_PARAMS,
      ),
      durationMs: 0,
    };
  }
} catch (error) {
  // 处理验证异常
}

执行阶段错误处理

attemptExecutionOfScheduledCalls 方法中,系统会:

  1. 执行异常捕获:使用 Promise .catch() 捕获执行异常
  2. 错误响应生成:调用 createErrorResponse 生成标准化错误响应
  3. 状态更新:将工具调用状态设置为 ‘error’
promise
  .then(async (toolResult: ToolResult) => {
    // 处理成功结果
  })
  .catch((error: Error) => {
    this.setStatusInternal(
      callId,
      'error',
      createErrorResponse(reqInfo, error, ToolErrorType.UNHANDLED_EXCEPTION),
    );
  });

4. 错误响应格式

createErrorResponse 函数生成标准化的错误响应:

const createErrorResponse = (
  request: ToolCallRequestInfo,
  error: Error,
  errorType: ToolErrorType | undefined,
): ToolCallResponseInfo => ({
  callId: request.callId,
  error,
  responseParts: [{
    functionResponse: {
      id: request.callId,
      name: request.name,
      response: { error: error.message },
    },
  }],
  resultDisplay: error.message,
  errorType,
  contentLength: error.message.length,
});

2.4 超时策略

1. AbortSignal 机制

系统使用 AbortSignal 实现超时和取消控制:

调度层面的取消

schedule 方法中:

schedule(
  request: ToolCallRequestInfo | ToolCallRequestInfo[],
  signal: AbortSignal,
): Promise<void> {
  if (this.isRunning() || this.isScheduling) {
    return new Promise((resolve, reject) => {
      const abortHandler = () => {
        // 从队列中移除请求
        const index = this.requestQueue.findIndex(
          (item) => item.request === request,
        );
        if (index > -1) {
          this.requestQueue.splice(index, 1);
          reject(new Error('Tool call cancelled while in queue.'));
        }
      };
      
      signal.addEventListener('abort', abortHandler, { once: true });
      // ...
    });
  }
}

执行层面的取消

在工具执行过程中:

  1. 执行前检查
if (signal.aborted) {
  this.setStatusInternal(
    reqInfo.callId,
    'cancelled',
    'Tool call cancelled by user.',
  );
  continue;
}
  1. 执行后检查
.then(async (toolResult: ToolResult) => {
  if (signal.aborted) {
    this.setStatusInternal(
      callId,
      'cancelled',
      'User cancelled tool execution.',
    );
    return;
  }
  // 处理成功结果
})

2. Shell 工具特殊处理

Shell 工具有特殊的超时和取消机制:

进程 ID 跟踪

if (invocation instanceof ShellToolInvocation) {
  const setPidCallback = (pid: number) => {
    this.toolCalls = this.toolCalls.map((tc) =>
      tc.request.callId === callId && tc.status === 'executing'
        ? { ...tc, pid }
        : tc,
    );
    this.notifyToolCallsUpdate();
  };
  promise = invocation.execute(
    signal,
    liveOutputCallback,
    shellExecutionConfig,
    setPidCallback,
  );
}

Shell 执行配置

系统通过 ShellExecutionConfig 配置 Shell 执行环境:

export interface ShellExecutionConfig {
  terminalWidth?: number;
  terminalHeight?: number;
  pager?: string;
  showColor?: boolean;
  defaultFg?: string;
  defaultBg?: string;
}

3. 输出截断机制

为防止输出过大导致内存问题,系统实现了输出截断:

if (
  typeof content === 'string' &&
  toolName === ShellTool.Name &&
  this.config.getEnableToolOutputTruncation() &&
  this.config.getTruncateToolOutputThreshold() > 0 &&
  this.config.getTruncateToolOutputLines() > 0
) {
  const truncatedResult = await truncateAndSaveToFile(
    content,
    callId,
    this.config.storage.getProjectTempDir(),
    threshold,
    lines,
  );
  content = truncatedResult.content;
  outputFile = truncatedResult.outputFile;
}

如果我们要构建自己的 Agent 系统,Gemini CLI 有这些值得学习的地方:

  1. 工具发现机制:三层发现机制很灵活,既有内置工具保底,又能动态扩展。
  2. 状态机调度:清晰的状态流转,便于调试和监控。
  3. 环境感知:根据运行环境自动调整行为,提升用户体验。
  4. MCP 协议支持:接入标准协议,获得生态系统支持。
  5. 声明式工具定义:工具定义和实现分离,接口清晰。

2.5 潜在的问题

Gemini 这种设计也有一些潜在问题:

  1. 上下文膨胀:所有工具都提供给 LLM,可能导致 token 消耗增加。
  2. 工具选择不可控:完全依赖 LLM 选择,缺少人为干预手段。
  3. 调试困难:当工具很多时,难以追踪 LLM 为什么选择某个工具。

3. Shopify Sidekick 的工具管理策略

文章地址:https://shopify.engineering/building-production-ready-agentic-systems

Shopify 团队在其文章中告诉我们一个他们踩的坑:工具数量的增长不是线性问题,而是指数级复杂度问题

他们把这个过程分成了三个阶段:

0-20 个工具:蜜月期。每个工具职责清晰,调试简单,系统行为可预测。这个阶段你会觉得”Agent 开发也不过如此嘛”。

20-50 个工具:混乱期。工具边界开始模糊,组合使用时产生意想不到的结果。这时候你开始怀疑人生:”为什么调用查询工具的同时会触发邮件发送?”

50+ 个工具:崩溃期。同一个任务有多种实现路径,系统变得无法理解。调试像在迷宫里找出口,修一个 bug 引入三个新 bug。

当工具数量超过 30 个后,整个系统变得难以维护。Shopify 把这个问题叫做”Death by a Thousand Instructions”(千条指令之死),系统提示词变成了一个充满特殊情况、相互冲突的指导和边缘案例处理的怪物。

3.1 Just-in-Time Instructions

面对工具管理的混乱,Shopify 团队找到了一个巧妙的解决方案:Just-in-Time (JIT) Instructions

这个思路其实很简单,可能的实现如下:

# 传统方式:所有指令都塞在系统提示词里
system_prompt = """
你是一个AI助手...
当用户查询客户时,使用customer_query工具...
当用户需要发邮件时,先检查权限...
如果是VIP客户,要特别注意...
处理订单时要考虑库存...
(还有500行各种规则)
"""

# JIT方式:按需提供指令
def get_tool_instructions(tool_name, context):
    if tool_name == "customer_query":
        if context.is_filtering:
            return "使用customer_tags进行标签筛选,使用customer_status进行状态筛选..."
        else:
            return "直接查询客户基本信息..."
    # 只在需要时提供相关指令

这种设计的好处:

  1. 上下文精准:LLM 只看到当前需要的指令,不会被无关信息干扰。
  2. 缓存友好:核心系统提示词保持稳定,可以利用 LLM 的提示词缓存,提高响应速度。
  3. 灵活迭代:可以根据不同场景、用户群体、A/B 测试动态调整指令,不需要改动核心系统。
  4. 可维护性:每个工具的指令独立管理,修改一个工具的行为不会影响其他工具。

4. 小结

OpenManus 体现了优雅设计,Gemini CLI 展示简化尝试,而 Shopify Sidekick 展示了实战中的智慧。

当我们要构建生产级的 Agent 系统:

  • 不要低估工具管理的复杂性
  • 不要高估 LLM 的工具选择能力
  • 不要忽视生产环境的残酷性

Agent 系统不是一次性工程,而是需要不断进化的。我们需要不断学习、调整、优化,才能构建出真正可靠的 AI Agent。

当我们的 Agent 要服务真实用户时,严谨的工程实践比炫酷的技术更重要。这可能不够酷,但这是通往生产环境的必经之路。

以上。

Agent 核心策略:Manus、Gemini CLI 和 Claude Code 的上下文压缩策略和细节

做 AI Agent 开发的都需要考虑上下文爆炸的问题,不仅仅是成本问题,还有性能问题。

许多团队选择了压缩策略。但过度激进的压缩不可避免地导致信息丢失。

这背后的根本矛盾在于:Agent 需要基于完整的历史状态来决策下一步行动,但我们无法预知当前的某个观察细节是否会在未来的某个关键时刻变得至关重要。

前面一篇文章讲了整个上下文的管理策略,今天着重聊一下上下文管理的压缩策略和细节。

下面根据 Manus、Gemini CLI 和 Claude Code 这三个项目的源码来聊一下上下文的压缩。

三个产品,有三个不同的选择,或者说设计哲学。

Manus 的选择是:永不丢失。 他们认为,从逻辑角度看,任何不可逆的压缩都带有风险。所以他们选择了一条看似”笨拙”但实际上很聪明的路:把文件系统当作”终极上下文”。

Claude Code 的选择是:极限压榨。 92% 的压缩触发阈值,这个数字相当激进。他们想要榨干上下文窗口的每一个 token,直到最后一刻才开始压缩。

Gemini CLI 的选择是:稳健保守。 70% 就开始压缩,宁可频繁一点,也要保证系统的稳定性。

这三种选择没有对错,只是适用场景不同。接下来我们逐个分析。

1. Manus

Manus 的压缩最让我印象深刻的是其在可恢复性上的策略。

Manus 团队认为:任何不可逆的压缩都带有风险。你永远不知道现在丢掉的信息是不是未来解决问题的关键。

他们选择了不做真正的删除,而是将其外部化存储。

传统做法是把所有观察结果都塞进上下文里。比如让 Agent 读一个网页,它会把整个 HTML 内容都存下来,可能就是上万个 token。Manus 不这么干。

当 Agent 访问网页时,Manus 只在上下文中保留 URL 和简短的描述,完整的网页内容并不保存。需要重新查看网页内容时,通过保留的 URL 重新获取即可。这就像是你在笔记本上记录”参见第 23 页的图表”,而不是把整个图表重新画一遍。

文档处理也是同样的逻辑。一个 100 页的 PDF 文档,Manus 不会把全部内容放进上下文,而是只记录文档路径、页数、最后访问的位置等元信息。当 Agent 需要查看具体内容时,再通过文件路径读取相应的页面。

Manus 把文件系统视为”终极上下文”——一个容量几乎无限、天然持久化、Agent 可以直接操作的外部记忆系统。

文件系统的层级结构天然适合组织信息。Agent 可以创建不同的目录来分类存储不同类型的信息:项目背景放一个文件夹,技术细节放另一个文件夹,错误日志单独存放。需要时按图索骥,而不是在一个巨大的上下文中大海捞针。

这不是简单的存储。Manus 训练模型学会主动使用文件系统来管理自己的「记忆」。当发现重要信息时,Agent 会主动将其写入特定的文件中,而不是试图把所有东西都记在上下文里。就像一个经验丰富的研究员,知道什么该记在脑子里,什么该写在笔记本上,什么该归档保存。

在 Manus 团队对外的文章开头,指出了为什么必须要有压缩策略:

第一,观察结果可能非常庞大。与网页或 PDF 等非结构化数据交互时,一次观察就可能产生数万个 token。如果不压缩,可能一两次操作就把上下文占满了。

第二,模型性能会下降。这是个很多人忽视的问题。即使模型声称支持 200k 的上下文窗口,但实际使用中,超过一定长度后,模型的注意力机制效率会显著下降,响应质量也会变差。这就像人的工作记忆一样,信息太多反而会降低处理效率。

第三,成本考虑。长输入意味着高成本,即使使用了前缀缓存等优化技术,成本依然可观。特别是在需要大量交互的场景下,成本会快速累积。

在具体实现过程中,可恢复压缩有几个关键点:

保留最小必要信息。对于每个外部资源,只保留能够重新获取它的最小信息集。网页保留 URL,文档保留路径,API 响应保留请求参数。这些信息占用的空间极小,但足以在需要时恢复完整内容。

智能的重新加载时机。不是每次提到某个资源就重新加载,而是根据上下文判断是否真的需要详细内容。如果只是确认文件存在,就不需要读取内容;如果要分析具体细节,才触发加载。

缓存机制。虽然内容不在上下文中,但 Manus 会在本地维护一个缓存。最近访问过的资源会暂时保留,避免频繁的重复加载。这个缓存是独立于上下文的,不占用宝贵的 token 额度。

2. Claude Code

Claude Code 的策略完全是另一个极端——他们要把上下文用到极致。

2.1 92% 的阈值

这个数字有一些讲究。留 8% 的缓冲区既保证了压缩过程有足够的时间完成,又避免了频繁触发压缩带来的性能开销。更重要的是,这个缓冲区给了系统一个「反悔」的机会——如果压缩质量不达标,还有空间执行降级策略。

const COMPRESSION_CONFIG = {
  threshold0.92,           // 92%阈值触发
  triggerVariable"h11",    // h11 = 0.92
  compressionModel"J7()",  // 专用压缩模型
  preserveStructuretrue    // 保持8段结构
};

2.2 八段式结构化摘要

Claude Code 的压缩不是简单的截断或摘要,而是八段式结构。这个结构我们可以学习一下:

const COMPRESSION_SECTIONS = [
  "1. Primary Request and Intent",    // 主要请求和意图
  "2. Key Technical Concepts",        // 关键技术概念
  "3. Files and Code Sections",       // 文件和代码段
  "4. Errors and fixes",              // 错误和修复
  "5. Problem Solving",               // 问题解决
  "6. All user messages",             // 所有用户消息
  "7. Pending Tasks",                 // 待处理任务
  "8. Current Work"                   // 当前工作
];

每一段都有明确的目的和优先级。「主要请求和意图」确保 Agent 永远不会忘记用户最初想要什么;「关键技术概念」保留重要的技术决策和约束条件;「错误和修复」避免重复踩坑;「所有用户消息」则保证用户的原始表达不会丢失。

这种结构的好处是,即使经过多次压缩,Agent 仍然能保持工作的连贯性。关键信息都在,只是细节被逐步抽象化了。

2.3 专用压缩模型 J7

Claude Code 使用了一个专门的压缩模型 J7 来处理上下文压缩。这不是主模型,而是一个专门优化过的模型,它的任务就是理解长对话并生成高质量的结构化摘要。

async function contextCompression(currentContext{
  // 检查压缩条件
  if (currentContext.tokenRatio < h11) {
    return currentContext;  // 无需压缩
  }
  
  // 调用专用压缩模型
  const compressionPrompt = await AU2.generatePrompt(currentContext);
  const compressedSummary = await J7(compressionPrompt);
  
  // 构建新的上下文
  const newContext = {
    summary: compressedSummary,
    recentMessages: currentContext.recent(5),  // 保留最近5条
    currentTask: currentContext.activeTask
  };
  
  return newContext;
}

AU2 负责生成压缩提示词,它会分析当前上下文,提取关键信息,然后构造一个结构化的提示词给 J7。J7 处理后返回符合八段式结构的压缩摘要。

2.4 上下文生命周期管理

Claude Code 把上下文当作有生命周期的实体来管理。这个设计理念很先进——上下文不是静态的数据,而是动态演化的有机体。

class ContextManager {
  constructor() {
    this.compressionThreshold = 0.92;  // h11 = 0.92
    this.compressionModel = "J7";      // 专用模型
  }
  
  async manageContext(currentContext, newInput) {
    // 1. 上下文更新
    const updatedContext = this.appendToContext(currentContext, newInput);
    
    // 2. 令牌使用量检查
    const tokenUsage = await this.calculateTokenUsage(updatedContext);
    
    // 3. 压缩触发判断
    if (tokenUsage.ratio >= this.compressionThreshold) {
      // 4. 八段式压缩执行
      const compressionPrompt = await AU2.generateCompressionPrompt(updatedContext);
      const compressedSummary = await this.compressionModel.generate(compressionPrompt);
      
      // 5. 新上下文构建
      return this.buildCompressedContext(compressedSummary, updatedContext);
    }
    
    return updatedContext;
  }
}

整个流程是自动化的。每次有新的输入,系统都会评估是否需要压缩。压缩不是一次性的动作,而是持续的过程。随着对话的进行,早期的详细内容会逐渐被抽象化,但关键信息始终保留。

2.5 优雅降级机制

当压缩失败时,系统不会死板地报错或者强行应用低质量的压缩结果,而是有一整套 Plan B、Plan C。这种”永不放弃”的设计理念,让系统在各种极端情况下都能稳定运行。

降级策略包括:

  • 自适应重压缩:如果首次压缩质量不佳,会调整参数重试
  • 混合模式保留:压缩旧内容,但完整保留最近的交互
  • 保守截断:最坏情况下,至少保证系统能继续运行

2.6 压缩后的信息恢复

虽然 Claude Code 的压缩是有损的,但它通过巧妙的设计最小化了信息损失的影响。压缩后的八段式摘要不是简单的文本,而是结构化的信息,包含了足够的上下文让 Agent 能够理解之前发生了什么,需要做什么。

特别值得一提的是第 6 段”All user messages”。即使其他内容被压缩了,用户的所有消息都会以某种形式保留。这确保了用户的意图和需求不会在压缩过程中丢失。

2.7 实践指南

Claude Code 在实践中还有一些最佳实践:

  • 定期使用 /compact 命令压缩长对话:用户可以主动触发压缩,不必等到自动触发
  • 在上下文警告出现时及时处理:系统会在接近阈值时发出警告,用户应该及时响应
  • 通过 Claude.md 文件保存重要信息:将关键信息外部化,减少上下文消耗

3. Gemini CLI

Gemini CLI 选择了一条中庸之道,或者说是实用之道。Gemini CLI 项目开源了,这部分的说明会多一些。

3.1 70/30?

Gemini CLI 选择了 70% 作为压缩触发点,30% 作为保留比例。这个比例我们也可以参考学习一下:

为什么是 70% 而不是 92%

  • 更早介入,避免紧急压缩导致的卡顿
  • 给压缩过程留出充足的缓冲空间
  • 适合轻量级应用场景,不追求极限性能

30% 保留的合理性:

  • 刚好覆盖最近 5-10 轮对话
  • 足够维持上下文连续性
  • 不会让用户感觉”突然失忆”

共背后的逻辑是:宁可频繁一点地压缩,也要保证每次压缩都是从容的、高质量的。

3.2 精选历史提取

Gemini CLI 有个独特的概念叫精选历史”。不是所有的历史都值得保留,系统会智能地筛选有效内容:

function extractCuratedHistory(comprehensiveHistory: Content[]): Content[] {
  if (comprehensiveHistory === undefined || comprehensiveHistory.length === 0) {
    return [];
  }
  const curatedHistory: Content[] = [];
  const length = comprehensiveHistory.length;
  let i = 0;
  while (i < length) {
    // 用户轮次直接保留
    if (comprehensiveHistory[i].role === 'user') {
      curatedHistory.push(comprehensiveHistory[i]);
      i++;
    } else {
      // 处理模型轮次
      const modelOutput: Content[] = [];
      let isValid = true;
      // 收集连续的模型轮次
      while (i < length && comprehensiveHistory[i].role === 'model') {
        modelOutput.push(comprehensiveHistory[i]);
        // 检查内容有效性
        if (isValid && !isValidContent(comprehensiveHistory[i])) {
          isValid = false;
        }
        i++;
      }
      // 只有当所有模型轮次都有效时才保留
      if (isValid) {
        curatedHistory.push(...modelOutput);
      }
    }
  }
  return curatedHistory;
}

这个策略的巧妙之处在于:

  • 用户输入全部保留:所有用户输入都被视为重要信息,无条件保留
  • 模型轮次有条件保留:连续的模型轮次被视为一个整体进行评估
  • 全有或全无的处理:要么全部保留,要么全部丢弃,避免了复杂的部分保留逻辑

3.3 内容有效性判断

什么样的内容会被认为是无效的?Gemini CLI 有明确的标准:

function isValidContent(content: Content): boolean {
  // 检查 parts 数组是否存在且非空
  if (content.parts === undefined || content.parts.length === 0) {
    return false;
  }
  for (const part of content.parts) {
    // 检查 part 是否为空
    if (part === undefined || Object.keys(part).length === 0) {
      return false;
    }
    // 检查非思考类型的 part 是否有空文本
    if (!part.thought && part.text !== undefined && part.text === '') {
      return false;
    }
  }
  return true;
}

无效内容包括:空响应、错误输出、中断的流式响应等。这种预过滤机制确保进入压缩流程的都是高质量的内容。

3.4 五段式结构化摘要

相比 Claude Code 的八段式,Gemini CLI 的五段式更简洁,但涵盖了所有关键信息:

1. overall_goal - 用户的主要目标
2. key_knowledge - 重要技术知识和决策
3. file_system_state - 文件系统当前状态
4. recent_actions - 最近执行的重要操作
5. current_plan - 当前执行计划

压缩时,系统会生成 XML 格式的结构化摘要。这种格式的好处是结构清晰,LLM 容易理解和生成,同时也便于后续的解析和处理。

3.5 基于 Token 的智能压缩

Gemini CLI 的压缩不是简单的定时触发,而是基于精确的 token 计算:

async tryCompressChat(
  prompt_id: string,
  force: boolean = false,
): Promise<ChatCompressionInfo | null> {
  const curatedHistory = this.getChat().getHistory(true);

  // 空历史不压缩
  if (curatedHistory.length === 0) {
    return null;
  }

  const model = this.config.getModel();

  // 计算当前历史的 token 数量
  const { totalTokens: originalTokenCount } =
    await this.getContentGenerator().countTokens({
      model,
      contents: curatedHistory,
    });

  // 获取压缩阈值配置
  const contextPercentageThreshold =
    this.config.getChatCompression()?.contextPercentageThreshold;

  // 如果未强制压缩且 token 数量低于阈值,则不压缩
  if (!force) {
    const threshold =
      contextPercentageThreshold ?? COMPRESSION_TOKEN_THRESHOLD; // 默认 0.7
    if (originalTokenCount < threshold * tokenLimit(model)) {
      return null;
    }
  }

  // 计算压缩点,保留最后 30% 的历史
  let compressBeforeIndex = findIndexAfterFraction(
    curatedHistory,
    1 - COMPRESSION_PRESERVE_THRESHOLD, // COMPRESSION_PRESERVE_THRESHOLD = 0.3
  );

  // 确保压缩点在用户轮次开始处
  while (
    compressBeforeIndex < curatedHistory.length &&
    (curatedHistory[compressBeforeIndex]?.role === 'model' ||
      isFunctionResponse(curatedHistory[compressBeforeIndex]))
  ) {
    compressBeforeIndex++;
  }

  // 分割历史为需要压缩和需要保留的部分
  const historyToCompress = curatedHistory.slice(0, compressBeforeIndex);
  const historyToKeep = curatedHistory.slice(compressBeforeIndex);

  // 使用 LLM 生成历史摘要
  this.getChat().setHistory(historyToCompress);
  const { text: summary } = await this.getChat().sendMessage(
    {
      message: {
        text: 'First, reason in your scratchpad. Then, generate the <state_snapshot>.',
      },
      config: {
        systemInstruction: { text: getCompressionPrompt() },
      },
    },
    prompt_id,
  );

  // 创建新的聊天历史,包含摘要和保留的部分
  this.chat = await this.startChat([
    {
      role: 'user',
      parts: [{ text: summary }],
    },
    {
      role: 'model',
      parts: [{ text: 'Got it. Thanks for the additional context!' }],
    },
    ...historyToKeep,
  ]);
}

这个实现有几个细节值得注意:

  • 支持强制压缩:通过 force 参数,用户可以主动触发压缩
  • 智能分割点选择:确保压缩点在用户轮次开始,避免打断对话逻辑
  • 两阶段压缩:先生成摘要,再重建对话历史

3.6 多层压缩机制

Gemini CLI 的压缩是分层进行的,每一层都有特定的目标:

第一层:内容过滤:过滤掉无效内容、thought 类型的部分,确保进入下一层的都是有价值的信息。

第二层:内容整合:合并相邻的同类内容,比如连续的纯文本 Part 会被合并成一个,减少结构冗余。

第三层:智能摘要:当 token 使用量超过阈值时,触发 LLM 生成结构化摘要。

第四层:保护机制:确保关键信息不被压缩丢失,比如用户的最新指令、正在进行的任务等。

3.7 模型适配的 Token 限制

不同的模型有不同的 token 限制,Gemini CLI 对此有精细的适配:

export function tokenLimit(model: Model): TokenCount {
  switch (model) {
    case 'gemini-1.5-pro':
      return 2_097_152;
    case 'gemini-1.5-flash':
    case 'gemini-2.5-pro':
    case 'gemini-2.5-flash':
    case 'gemini-2.0-flash':
      return 1_048_576;
    case 'gemini-2.0-flash-preview-image-generation':
      return 32_000;
    default:
      return DEFAULT_TOKEN_LIMIT; // 1_048_576
  }
}

系统会根据使用的模型自动调整压缩策略。对于支持超长上下文的模型(如 gemini-1.5-pro 的 200 万 token),可以更宽松;对于受限的模型,会更积极地压缩。

3.8 历史记录的精细处理

recordHistory 方法负责记录和处理历史,实施了多个优化策略:

  1. 避免重复:不会重复添加相同的用户输入
  2. 过滤思考过程:thought 类型的 Part 会被过滤掉,不进入最终历史
  3. 合并优化:相邻的模型轮次会被合并,相邻的纯文本也会合并
  4. 占位符策略:如果模型没有有效输出,会添加空的占位符保持结构完整

3.9 压缩的用户体验设计

Gemini CLI 特别注重压缩对用户体验的影响:

  • 无感压缩:70% 的阈值确保压缩发生在用户察觉之前
  • 连续性保持:保留 30% 的最新历史,确保当前话题的连贯性
  • 透明反馈:压缩前后的 token 数量变化会被记录和报告

4. 写在最后

研究完这三个项目的源码,我最大的感受是:压缩策略的选择,本质上是对「什么是重要的」这个问题的回答。 Manus 说「所有信息都可能重要,所以我不删除,只是暂时收起来」;Claude Code 说「结构化的摘要比原始细节更重要」;Gemini CLI 说「用户体验比技术指标更重要」。三种回答,三种哲学。

这让我想起一句话:在 AI 时代,真正稀缺的不是信息,而是注意力。 上下文压缩就是在教 AI 如何分配注意力——什么该记住,什么可以忘记,什么需要随时能找回来。

这是人类智慧的核心能力之一。我们每天都在做类似的决策:重要的事情记在心里,次要的写在本子上,琐碎的存在手机里。Manus、Claude Code 和 Gemini CLI 只是用不同的方式在教 AI 做同样的事。

没有完美的压缩策略,只有最适合你场景的策略。 选择哪种策略不重要,重要的是理解它们背后的设计智慧,然后根据自己的需求做出明智的选择。

以上。

从 Claude Code到 Gemini CLI,AI Agent 的上下文管理策略

对于一个与大型语言模型(LLM)打过交道的开发者来说,上下文管理都是一个绕不开的核心问题。它不仅决定了 AI 的智能程度,也直接关系到系统的性能和成本。

上周研究了各家 Agent 系统的实现,各家的上下文管理策略都不相同。最简单最傻的策略是一个不断累加对话历史,这种策略很快就会遇到 Token 限制和 API 的成本问题。

如果你是一个技术负责人,或者正在开发 AI Agent 相关的产品,需要在性能和成本之间找到平衡点,这篇文章应该对你有一些帮助。

今天所聊的内容是基于对 Claude Code、Manus、Gemini CLI,OpenManus 等多个项目的分析,以及自己在实践中的一些思考。

为什么要做上下文管理?

最新的 LLM 现在提供 128K Token 或更多的上下文窗口。听起来还挺多的,但在真实世界的 Agent 场景中,这通常远远不够。

尤其是当 Agent 与网页或PDF等非结构化数据交互时,Token 数需求会爆炸。

并且,随着 Token 数的增加,模型性能会在超过一定长度后明显下降,这就像让一个人同时记住一本书的所有细节,理论上可能,实际上很难做好。

就算我们的大模型有更多的窗口上下文支持,成本也是一个需要考虑的问题,就算有前缀缓存这样的优化,但传输和预填充每个 Token 都是要付费的。

为了解决这些问题,许多团队选择了压缩策略。但过度激进的压缩不可避免地导致信息丢失。

这个问题的本质在于 Agent 必须根据所有先前状态预测下一个动作——而我们无法可靠地预测哪个观察结果可能在十步之后变得至关重要。从逻辑角度看,任何不可逆的压缩都带有风险。

接下来我们看一看各项目的上下文管理策略,看看从中能否给到各位看官一些启发。

OpenManus 的上下文管理策略

OpenManus 采用了一个相对简单直接的上下文管理方案,主要特点是:

  1. 轻量级消息列表机制
  • 使用固定长度(默认100条)的消息列表作为内存存储
  • 采用简单的 FIFO(先进先出)策略,超出限制时截断最早的消息
  • 没有智能的上下文压缩或摘要机制
  1. Token 限制处理
  • 实施硬性 token 检查,超限直接抛出异常终止
  • 缺乏优雅的降级策略或自适应窗口裁剪
  • 在长对话或工具密集场景中容易触碰上限

虽然上下文管理比较简单,但是 OpenManus 为不同使用场景提供了定制化的上下文处理,如浏览器场景会动态注入浏览器状态,截图保存等

总的来说,这是一个原型实现,并不适合作为生产级环境使用,如果要上到生产环境需要自行做精细化的处理和架构。

Manus 的上下文管理策略

Manus 没有开源,但是其官方有发一篇文章出来。

Manus 采用了一种创新的方法:将文件系统作为终极上下文存储,而不是依赖传统的内存中上下文管理。

文件系统作为存储有如下的核心特性:

  • 无限容量:文件系统大小不受限制
  • 天然持久化:数据自动保存,不会丢失
  • 直接操作:智能体可以主动读写文件
  • 结构化记忆:不仅是存储,更是结构化的外部记忆系统

相对于传统的将完整的观察结果保存在上下文中,容易超限,Manus 实现了可恢复的信息压缩

  • 观察结果指向外部文件(Document X, File Y)
  • 上下文中只保留引用,不保存完整内容
  • 需要时可以从文件系统恢复完整信息

具体实现:

  • 网页内容可从上下文移除,只保留 URL
  • 文档内容可省略,只保留文件路径
  • 实现上下文压缩的同时不会永久丢失信息

Manus 团队认为,如果状态空间模型能够掌握基于文件的记忆管理:

  • 将长期状态外部化而非保存在上下文中
  • SSM 的速度和效率优势可能开启新型智能体
  • 基于 SSM 的智能体可能成为神经图灵机的真正继任者

与 OpenManus 的简单消息列表管理,Manus 的方案更加成熟:

  • OpenManus:固定长度消息列表,硬性截断,缺乏智能管理
  • Manus:文件系统作为无限外部记忆,可恢复压缩,主动记忆管理

Claude Code 的上下文管理

Claude Code 没有开源代码,但是国外有大神反编译其源码(虽然大神自己说:这并非真正意义上的反编译或逆向工程尝试,而更像是对 Claude 团队杰出工作的致敬。)

地址:southbridge-research.notion.site/claude-code…

通过反编译内容的分析,可以大概了解一些其策略和比较巧妙的点:

TodoWrite 工具

Claude Code 引入 TodoWrite 工具,支持模型主动维护自己的 To-Do 列表,替代传统的多 Agent 分工策略。

其优势:

  • 专注:Prompt 中反复提醒模型参考 ToDo,始终聚焦目标。
  • 灵活:「交错思考」机制使得 ToDo 可动态增删。
  • 透明:用户可实时查看计划与进度,提高信任度。

Token 统计的反向遍历

Toke 统计从后往前查找这个细节相当精妙。大部分系统都是傻乎乎地从头遍历,但 Claude Code 意识到了一个关键事实:Token 使用情况的统计信息总是出现在最新的 assistant 回复里。这种”知道去哪找”的优化思路,把原本可能的 O(n) 操作优化到了 O(k),在高频调用场景下,这种优化带来的性能提升是指数级的。

92% 阈值

留 8% 的缓冲区既保证了压缩过程有足够的时间完成,又避免了频繁触发压缩带来的性能开销。更重要的是,这个缓冲区给了系统一个”反悔”的机会——如果压缩质量不达标,还有空间执行降级策略。

8 段式结构化摘要

Claude Code 的 8 段式结构特别值得借鉴:

markdown

体验AI代码助手
代码解读
复制代码

1. Primary Request and Intent - 主要请求和意图

2. Key Technical Concepts - 关键技术概念

3. Files and Code Sections - 文件和代码片段

4. Errors and Fixes - 错误和修复

5. Problem Solving - 问题解决过程

6. All User Messages - 所有用户消息

7. Pending Tasks - 待处理任务

8. Current Work - 当前工作状态

优雅降级

当压缩失败时,系统不会死板地报错或者强行应用低质量的压缩结果,而是有一整套 Plan B、Plan C。从自适应重压缩,到混合模式保留,再到最后的保守截断——每一步都在努力保护用户体验。这种”永不放弃”的设计理念,让系统在各种极端情况下都能稳定运行。

向量化搜索

长期记忆层引入向量搜索,实际上是在为 AI 构建一个”联想记忆”系统。当用户提出新问题时,系统不仅能看到当前对话,还能”回忆”起过去处理过的类似问题。这种跨会话的知识迁移能力,让 Claude Code 从一个简单的对话工具进化成了一个真正的智能编程助手。

Gemini-cli 的上下文管理

Gemini-cli 的上下文管理走了一条和 Claude Code 相似但更加轻量的路线。它的核心理念很简单:文件系统就是天然的数据库

三层混合存储架构

与 Claude Code 类似,Gemini-cli 也采用了分层设计,但实现更加简洁:

第一层:纯内存工作区

  • 存储当前会话的聊天历史、工具调用状态、循环检测状态
  • 零延迟访问,不涉及任何 I/O 操作
  • 会话结束即清空,不留痕迹

第二层:智能压缩层

  • 触发阈值:70%(比 Claude Code 的 92% 更保守)
  • 保留策略:最新 30% 的对话历史
  • 压缩产物:5 段式结构化摘要

第三层:文件系统持久化

  • 全局记忆:~/.gemini/GEMINI.md
  • 项目记忆:向上递归查找直到项目根目录
  • 子目录上下文:向下扫描并尊重忽略规则

70/30

Gemini-cli 选择了 70% 作为压缩触发点,30% 作为保留比例。这个比例设计很有讲究:

为什么是 70% 而不是 92%?

  • 更早介入,避免紧急压缩导致的卡顿
  • 给压缩过程留出充足的缓冲空间
  • 适合轻量级应用场景,不追求极限性能

30% 保留的合理性

  • 刚好覆盖最近 5-10 轮对话
  • 足够维持上下文连续性
  • 不会让用户感觉”突然失忆”

5 段式压缩:够用就好

相比 Claude Code 的 8 段式结构,Gemini-cli 的压缩更简洁:

markdown

体验AI代码助手
代码解读
复制代码

1. overall_goal - 用户的主要目标

2. key_knowledge - 重要技术知识和决策

3. file_system_state - 文件系统当前状态

4. recent_actions - 最近执行的重要操作

5. current_plan - 当前执行计划

忽略规则

Gemini-cli 的 .geminiignore 机制是个亮点:

独立但兼容

  • 可以单独在非 git 仓库中生效
  • .gitignore 并行工作,互不干扰
  • 每个工具都有独立的忽略开关

明确的约束

  • 修改 .geminiignore 需要重启会话才生效
  • 这不是 bug,而是 feature——避免运行时状态混乱

Gemini-cli 的设计哲学可以总结为:不求最优,但求够用

它没有追求理论上的完美压缩比,也没有搞复杂的向量检索,而是用最简单的方案解决了 80% 的问题。这种务实的态度在工程实践中往往更受欢迎——系统简单意味着 bug 少,维护容易,用户上手快。

特别是”文件系统就是数据库”这个理念,虽然听起来有点”土”,但在实际使用中却异常可靠。你不需要担心数据库挂了、连接断了、事务死锁了…文件就在那里,看得见摸得着,出了问题 cat 一下就知道怎么回事。

这种设计思路值得很多过度工程化的项目学习:有时候,简单就是最好的复杂。

小结

上下文是智能的边界,压缩是性能的艺术。

在与大型语言模型打交道的过程中,上下文管理已成为决定智能上限与系统稳健性的关键。虽然现代 LLM 提供了百万级 Token 的窗口,但在实际 Agent 场景中,这远远不够,尤其当涉及非结构化数据(如网页、PDF)时,Token 使用会迅速膨胀。即使有前缀缓存等机制,成本与性能的双重压力仍然存在。因此,上下文压缩成了必选项——但压缩得太激进,又会导致信息丢失,损害 Agent 的决策能力。

聪明的系统不是记住所有,而是记住该记住的。

应对上下文限制的最佳方式不是简单保留或截断历史,而是构建一个具备“记忆力”的智能系统。Claude Code 以三层记忆架构为核心(短期、高速;中期、结构化压缩;长期、跨会话向量化搜索),同时引入 TodoWrite 工具,让模型自我管理计划任务。这使得 Agent 能专注目标、灵活调整、透明运行,形成类人思维般的任务记忆系统。关键机制如 Token 反向遍历、92% 阈值缓冲、8段式摘要结构与优雅降级策略,共同打造了一个稳健又高效的上下文生态。

工程的智慧在于‘够用’,而非‘极致’。

对比 Gemini-cli、OpenManus 与 Manus 的上下文策略,可以看出不同系统在工程实现上的取舍哲学。Gemini-cli 采用实用主义的轻量分层设计,70/30 压缩策略既简单又高效,让用户可控又无需担心性能瓶颈;Manus 则大胆将文件系统作为智能体的“外部大脑”,通过引用而非存储规避 Token 限制;而 OpenManus 则为最小可运行原型提供了基础模板。这些方案展现出一个共识:上下文不一定要复杂,关键在于是否服务于目标。

以上。