上下文压缩 · 窗口快满时怎么办

压缩在四个时机触发:开轮前(换了模型或已超限)、一轮中途(模型还要继续但窗口快满)、轮末(可选)与手动的 /compact。OpenAI 等支持的提供方走远程压缩:在历史末尾追加一个触发条目,由服务端返回一个不透明的压缩条目;其余提供方走本地压缩,让模型按提示词写交接摘要。压缩后历史被整体替换,只保留最近的用户消息,这是“只追加”原则唯一的常规例外。

作者 David更新于 第 16 篇(共 57 篇)

上下文压缩 · 窗口快满时怎么办

上一篇说,历史只追加、不改写,真正的例外只有压缩与回滚。这一篇讲压缩:什么时候触发、有哪几种实现、压缩后的历史长什么样。

用户看到的样子

对话很长时,Codex 会自动压缩上下文,界面上出现一个“上下文压缩”条目;也可以随时输入 /compact 手动压缩(见斜杠命令)。相关配置有 model_auto_compact_token_limit(触发阈值)、model_auto_compact_token_limit_scope(total 或 body_after_prefix,默认 total)、model_post_turn_compact_threshold_percent(轮末压缩,默认 0 即关闭)、model_context_window,以及自定义摘要提示词的 compact_prompt 与 experimental_compact_prompt_file(见配置项速查)。PreCompact、PostCompact 两个 hook 在压缩前后触发,返回 continue: false 可以中止(见 Hooks)。

用量怎么算、阈值在哪

判断“满没满”的是 context_window_token_status(codex-rs/core/src/session/context_window.rs:26)。当前用量取自 ContextManager::get_total_token_usage:以上一次响应里服务端报告的 total_tokens 为准,加上此后新追加条目的估算值(按字节粗估);服务端没声明已计入推理时,再加上更早轮次推理条目的估算。阈值来自模型元数据:

    pub fn auto_compact_token_limit(&self) -> Option<i64> {
        let context_limit = self
            .resolved_context_window()
            .map(|context_window| (context_window * 9) / 10);
        let config_limit = self.auto_compact_token_limit;
        if let Some(context_limit) = context_limit {
            return Some(
                config_limit.map_or(context_limit, |limit| std::cmp::min(limit, context_limit)),
            );
        }
        config_limit
    }

(codex-rs/protocol/src/openai_models.rs:525)

默认的 total 范围下,自动压缩阈值是上下文窗口的 90%,配置或模型目录给的 auto_compact_token_limit 只能把它调低。另有一道硬上限:窗口乘以模型的 effective_context_window_percent(默认 95)。用量达到任一个,token_limit_reached 就为真。body_after_prefix 范围下,阈值直接取配置值(没配时才用上面这个数),并且只和“本窗口开头那段前缀之后的增量”比,前缀大小优先取新窗口里第一次响应报告的输入 token;硬上限仍按全量算。普通请求若直接被服务端以上下文超窗拒绝,本轮以错误结束,但用量会被标记为已满,下一轮开轮前必然先压缩。

四个触发时机

时机位置条件压缩后注入初始上下文吗
开轮前run_pre_sampling_compact压缩兼容哈希变了;或换到窗口更小的模型且已超新模型的限制;或 token 已到阈值不注入
一轮中途run_turn 采样后模型还要继续,且 token 已到阈值注入到最后一条用户消息之前
轮末run_turn 收尾前用量越过 model_post_turn_compact_threshold_percent,且没有待处理输入不注入
手动Op::Compact 起一个 CompactTask用户执行 /compact不注入

开轮前的前两种情况用的是上一轮的模型:先用旧模型压缩一次,再交给新模型(原因分别记为 CompHashChanged 与 ModelDownshift)。旧模型压缩失败时,在 OpenAI 提供方且走 Codex 后端的前提下,再用当前模型重试一次。

最后一列的差别来自一个枚举:

/// Controls whether compaction replacement history must include initial context.
///
/// Pre-turn/manual compaction variants use `DoNotInject`: they replace history with a summary and
/// clear `reference_context_item`, so the next regular turn will fully reinject initial context
/// after compaction.
///
/// Mid-turn compaction must use `BeforeLastUserMessage` because the model is trained to see the
/// compaction summary as the last item in history after mid-turn compaction; we therefore inject
/// initial context into the replacement history just above the last real user message.
pub(crate) enum InitialContextInjection {
    BeforeLastUserMessage {
        world_state: Arc<WorldState>,
        step_context: Arc<StepContext>,
    },
    DoNotInject,
}

(codex-rs/core/src/compact.rs:57)

开轮前、轮末与手动压缩清空 reference_context_item,下一轮自然会按上一篇的机制重新注入完整上下文。一轮中途压缩后,循环直接接着采样,不会再经过开轮时的全量注入,所以初始上下文要直接放进替换后的历史;位置选在最后一条真实用户消息之前,因为按注释的说法,模型受过的训练是中途压缩后压缩结果应当是历史的最后一项。

三种实现

图表加载中…

选哪种由提供方能力决定。ProviderCapabilities::remote_compaction 为 V2 的有:内置的 OpenAI 提供方、Azure 的 Responses 端点、Amazon Bedrock;自定义提供方与 Ollama、LM Studio 都是 Unsupported,走本地压缩。功能开关 token_budget(开发中,默认关闭)打开时两者都不走,改为直接开一个新的上下文窗口,新窗口总是带着完整的初始上下文。

远程压缩 V2(codex-rs/core/src/compact_remote_v2.rs)几乎就是一次普通请求:

    let tool_router = &step_context.tool_router;
    input.push(ResponseItem::CompactionTrigger {});
    let prompt = Prompt {
        input,
        tools: tool_router.model_visible_specs(),
        parallel_tool_calls: true,
        base_instructions,
        output_schema: None,
        output_schema_strict: true,
        cyber_access_program: turn_context.cyber_access_program,
    };

(codex-rs/core/src/compact_remote_v2_attempt.rs:77)

输入是当前历史,末尾追加一个 CompactionTrigger 条目,工具清单、基础指令与普通请求相同,自动压缩时还复用这一轮的 ModelClientSession。这样压缩请求与之前的请求共享前缀,能沿用同一份提示缓存和 WebSocket 连接。发出前若估算的历史已经超过窗口,trim_function_call_history_to_fit_context_window 从历史末尾起,把连续的工具输出逐个换成 “Output exceeded the available model context and was truncated”,直到放得下或遇到非工具输出为止;改的只是这次请求的副本。服务端的响应里必须恰好有一个 Compaction 条目,否则报错。这类请求耗时长,流级重试最多 2 次(MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES),不用普通请求的 5 次。新历史由两部分组成:从最新往前保留的用户消息(外加少量 agent 间消息),总预算 64,000 token(RETAINED_MESSAGE_TOKEN_BUDGET);最后是那个 Compaction 条目。

本地压缩(codex-rs/core/src/compact.rs)把历史加一条用户消息发给当前模型,这条消息就是摘要提示词:默认的 SUMMARIZATION_PROMPT(codex-rs/prompts/templates/compact/prompt.md)要求模型写一份交接摘要,写明进度与关键决策、约束与用户偏好、剩余工作、继续所需的数据。compact_prompt 与 experimental_compact_prompt_file 只替换这条提示词,所以对走远程压缩的提供方不起作用。请求本身也可能超窗:

            Err(e) if matches!(e.details(), CodexErrorDetails::ContextWindowExceeded) => {
                if turn_input_len > 1 {
                    // Trim from the beginning to preserve cache (prefix-based) and keep recent messages intact.
                    error!(
                        "Context window exceeded while compacting; removing oldest history item. Error: {e}"
                    );
                    history.remove_first_item();
                    retries = 0;
                    continue;
                }
                sess.set_total_tokens_full(turn_context.as_ref()).await;
                return Err(e);
            }

(codex-rs/core/src/compact.rs:310)

超窗就从最旧的一条删起,保住最近的内容;其他错误按退避重试,次数上限是提供方的 stream_max_retries。拿到回复后,新历史是:从最新往前保留的用户消息,总预算 20,000 token(COMPACT_USER_MESSAGE_MAX_TOKENS),超出的那条截断;最后一条是摘要,前面加上 SUMMARY_PREFIX,告诉接手的模型“另一个模型已经做了一部分,这是它的摘要”。本地压缩完成后还会发一条警告:长对话与多次压缩会让模型变得不那么准确,最好开新线程保持对话小而专注。

压缩之后

三种实现都以 replace_compacted_history(codex-rs/core/src/session/mod.rs:4061)收尾:给新条目补齐 ID,在 state 锁里用 HistoryReplacement::Compaction 替换历史,把推理强度的固定值重置,然后往 rollout 写一条 RolloutItem::Compacted(带完整的替换后历史、窗口编号与 ID、压缩响应 ID 等),必要时跟上新的世界状态快照与 TurnContextItem。恢复会话时,重放逻辑直接取这份替换后的历史,不必重新压缩。压缩还会推进“自动压缩窗口”(codex-rs/core/src/state/auto_compact_window.rs):窗口编号加一、生成新的窗口 ID,请求元数据里的 x-codex-window-id 随之改变;最后排入一个来源为 compact 的 SessionStart hook,在下一次采样前触发。

顺带一提,thread_rollout_truncation.rs 名字里有 truncation,但它处理的是分叉与回滚时按用户轮次截断 rollout,与压缩无关。

和《从 LLM 到 Coding Agent》对照

自动压缩一篇的方案是“保留近期、摘要远古”,并强调摘要提示词决定成败、proactive 与 reactive 两种触发都要有。Codex 的本地压缩与之同构,但“近期”只保留用户消息原文,助手回复与工具输出都交给摘要;proactive 对应 90% 阈值与轮末压缩;reactive 则体现为普通请求超窗时本轮报错、用量记为已满,下一轮开轮先压缩,以及压缩请求自身超窗时从最旧一条删起再试。远程压缩 V2 把写摘要这件事整个搬到了服务端,客户端只负责追加触发条目、保留用户消息。那篇讲的“压缩与缓存天然打架”,在 Codex 里的缓解办法是:本地压缩超窗时从头部删,远程压缩与普通请求共享前缀。精细化上下文管理主张先处理占地方的工具结果,对应这里写入历史时按预算截断工具输出,以及压缩请求里把末尾工具输出换成占位文字的做法。


上一篇:上下文与历史 · 只追加、不改写 · 下一篇:指令从哪来 · AGENTS.md、系统提示词与协作模式

本页目录