当 Code Agent 对话越来越长:上下文预算管理的一次工程化拆解

Code Agent 运行几十步后,历史消息可能迅速逼近上下文窗口上限。开源项目 MyCodeAgent 的处理方式不是直接删旧消息,而是保留完整历史,只在每次调用前生成压缩后的模型视图。

Code Agent 的实际运行中,模型并不是简单地“记住”所有对话。随着用户输入、模型回答、工具调用和工具结果不断追加,历史消息会迅速膨胀。一个运行二三十步的编码智能体,历史中可能已经积累几百条消息、几万个 token。对于默认 128k token 的上下文窗口来说,问题并不是“能不能装下”,而是要不要把全量历史原样交给模型。

掘金上一篇关于 Code Agent 工程实现的文章,围绕开源项目 MyCodeAgent 拆解了这一问题的处理路径。其核心不是简单截断,而是将完整历史与模型输入分离:历史永不删除,压缩只作用于每次调用前生成的模型视图。这一设计把长对话管理从“删旧消息”变成了更可控的上下文工程管理。

历史与视图分离:完整日志不变,模型只看投影

在 MyCodeAgent 中,HistoryManager 负责保存完整事实日志。所有用户消息、模型回复、工具调用、工具结果以及摘要消息都会以 append-only 方式写入,不会被删除。它既是事实记录,也是 transcript 写入和崩溃恢复的依据。

真正发给大模型的,并不是这份完整历史本身,而是由 build_model_view() 在每个步骤中动态生成的 Model View。这个视图由系统消息和历史子集构成,可能包含压缩后的摘要,也可能保留最近若干轮原文。它只影响当前这一次 LLM 调用,不会反过来修改原始历史。

这种“历史”与“视图”的分离,是整套机制的关键前提。压缩被处理成一种读时操作,而不是写时截断。原始历史始终完整存在,系统可以随时重建视图;即便压缩环节出现问题,也不会破坏事实日志本身。

什么时候压缩:按 80% 阈值提前触发,并用更保守的估算方式

每个 ReAct step 开始时,系统会先执行上下文准备流程:先判断是否需要压缩,再构建本次发给模型的消息列表,最后调用 LLM。是否压缩由 ContextBudgetPolicy 判断。

其阈值并不是等到上下文窗口 100% 占满才触发,而是按 context_window 与 compression_threshold 相乘得到。文中给出的默认配置是 128000 token 的上下文窗口、0.8 的压缩阈值,因此触发点为 102400 token。这样做的目的,是在接近上限前提前压缩,给模型输出留出空间。

在 token 估算上,系统同时采用两个来源:一是基于当前消息和待发送输入,用字符数除以 3 近似估算;二是基于上一轮实际消耗 token 数,再加上待发送输入长度除以 3。两者取较大值,以避免字符估算偏低导致漏触发。文章也指出,这种估算并不精确,但足够保守。

相关配置还包括 min_retain_rounds,默认值为 10,用于决定压缩时最近多少轮对话仍保留原文。

如何压缩:按用户消息切轮,保留最近 10 轮,旧内容生成摘要

一旦触发压缩,ContextCompactor 会先按 user 消息边界切分轮次。每遇到一条 role 为 user 的消息,就开启新的一轮。随后系统保留最近 min_retain_rounds 轮原文,默认是最近 10 轮;更早的消息则交给摘要生成器处理。

摘要生成器会把旧消息序列化为文本,再调用 LLM 生成结构化摘要。提示词要求摘要覆盖已完成目标、关键决策、修改过的文件等内容。生成摘要后,系统会创建一个 checkpoint,包含摘要文本和分割点 retain_start_idx。这个分割点告诉后续投影逻辑:从哪里开始保留原文。

值得注意的是,compact() 本身并不修改 HistoryManager 中的任何消息。它只是在 CompactStore 中保存一个 checkpoint。原始消息仍然完整保留,模型之后看到的只是基于 checkpoint 重新生成的视图。

压缩后模型看到什么:摘要消息加最近原文

在每次 build_model_view() 中,ProjectionBuilder 会检查当前是否存在 active checkpoint。如果没有,模型看到的就是全量历史;如果已经有压缩 checkpoint,投影结果会变成“摘要消息 + 最近保留轮次原文”。

也就是说,压缩后的模型输入结构大致是:系统消息、一条 summary 消息,再加上最近 10 轮的完整 user、assistant、tool 消息。摘要部分承担“之前发生了什么”的作用,最近轮次则保留完整细节,供模型继续执行当前任务。

文章将这种机制称为“读时投影”。投影每次都从原始历史中读取,并动态生成当前步骤需要的消息列表。相比直接删除旧消息的截断方案,这种方式更接近可逆处理:checkpoint 可以替换,视图可以重建,原始信息不会因一次压缩动作而丢失。

主动压缩与被动兜底:失败时不破坏主流程

这套机制还区分了两种触发时机。第一种是主动触发:每个步骤开始时,系统估算上下文长度,如果超过阈值就提前压缩,属于预防性处理。

第二种是被动触发:当 LLM 调用因上下文过长抛出 PROMPT_TOO_LONG 异常时,系统会尝试 reactive_compact。如果压缩成功,就重新构建模型视图并重试;如果压缩失败,则进入错误处理路径。

摘要生成环节也有失败保护。summary_generator 内部设置了超时机制,默认 120 秒。若 LLM 摘要调用超时或失败,压缩会返回未执行状态,不会修改已有上下文。在主动触发场景下,系统会跳过本次压缩,继续使用未压缩历史进入当前步骤,下一步再重新判断;在被动触发场景下,如果压缩失败且无法恢复,循环才会终止。文章称这是最坏情况,由于主动触发通常会提前介入,实际出现概率较低。

对 Agent 工程的启示:上下文管理正在变成独立系统能力

这篇分析的价值,不只是讲清楚一个开源项目如何处理长对话,更在于它把 Agent 上下文管理拆成了几个明确的工程模块:事实日志、预算判断、压缩执行、视图投影和异常兜底。

  • 历史永不删除,保证事实记录与恢复能力;
  • 模型输入只是投影,压缩不污染原始数据;
  • 按 80% 阈值提前触发,为输出预留空间;
  • 用两种估算来源取较大值,降低漏触发风险;
  • 保留最近 10 轮原文,旧内容转为结构化摘要;
  • 摘要失败不强行继续,被动压缩作为最后兜底。

对于 Code Agent 来说,长对话不是偶发问题,而是常态。工具调用越多、任务步骤越长,上下文预算越容易成为瓶颈。MyCodeAgent 的方案提供了一个可参考的工程思路:不要把“上下文超限”简单理解成删旧消息,而应把它视为一套围绕事实日志、视图构建和预算控制展开的系统能力。

原创文章,作者:点点,如若转载,请注明出处:https://www.dian8dian.com/dang-code-agent-dui-hua-yue-lai-yue-chang-shang-xia-wen-yu

Like (0)
点点的头像点点
Previous 1小时前
Next 2024年10月29日

相关推荐