(一)Grok-build-上下文窗口大小从哪来

0 阅读12分钟

最近几个月我一直是“yes-man”,一度觉得之前学习的重构、编码规范之类的东西快被淘汰了;前段时间 xAI开源了 grok-build,看了他们规范的 rust 项目结构和源码,心里惊叹一声“牛 x”,也许“重构”之类的技术已经像衣服一样,“功能性”的东西逐渐变得普适,它们的“美学价值”变得更有吸引力;

本文基于 Grok Build 源码,梳理 context_window 这个值从哪来、怎么变、被谁用。

一、启动时:四层逐级覆盖

Grok Build 启动时,按优先级从低到高合并出每个模型的 context_window。低优先级被高优先级覆盖:


图1:context_window 加载图

1.1 Layer 1:编译时内置

default_models.json 通过 include_str! 编译时嵌入二进制,随程序分发。当前内容:

{
  "default": "grok-4.5",
  "models": [
    {
      "id": "grok-4.5",
      "model": "grok-4.5",
      "name": "Grok 4.5",
      "context_window": 500000,
      "api_backend": "responses",
      "auto_compact_threshold_percent": 80,
      ...
    }
  ]
}

目前只有一个 grok-4.5 模型,context_window 是 500K,还自带 auto_compact_threshold_percent: 80(比全局默认的 85% 更激进)。

要是某个模型在所有来源里都找不到——比如用户在 config.toml 里加了个自定义模型(像 claude、gpt-4),又没指定 context_window——就兜底给 200K:

// agent/config.rs
pub fn fallback(slug: &str) -> Self {
    ModelInfo {
        context_window: NonZeroU64::new(200_000).unwrap(),
        ..
    }
}

1.2 Layer 2:远程拉取

启动时会请求服务端的模型列表接口。请求的 URL 按认证模式分三条路径:

// remote/client.rs — ListModelsEndpoint::from_endpoints()
if endpoints.has_custom_endpoint() {
    // 用户自定义 endpoint
    Self { url: endpoints.resolve_models_list_url(), auth: ApiKey }
} else if fetch_auth == ModelFetchAuth::ApiKey {
    // BYOK → "https://api.x.ai/v1/models"
    Self { url: format!("{}/models", endpoints.xai_api_base_url), auth: ApiKey }
} else {
    // 标准 Session → "https://cli-chat-proxy.grok.com/v1/models"
    Self { url: endpoints.resolve_models_list_url(), auth: Session }
}
  1. 标准 Session 启动时用 /models,运行时空闲恢复刷新用 /models-v2,是两个不同的端点。
  2. 请求发出前先查磁盘缓存。命中条件:grok 版本匹配、auth method 匹配、origin URL 匹配、TTL 未过期。命中就跳过网络请求,直接用缓存。

响应 JSON 结构是 { "data": [model_entry, ...] }。对每条 entry,parse_remote_model_value() 解析 context_window 时依次尝试五个字段名,兼容不同的 API 风格:

// remote/client.rs — parse_remote_model_value()
let meta = obj.get("_meta").and_then(|v| v.as_object());
let context_window = get_u64(obj, "contextWindow")               // ① 顶层 camelCase
    .or_else(|| get_u64(obj, "context_window"))                   // ② 顶层 snake_case
    .or_else(|| meta.and_then(|m| get_u64(m, "contextWindow")))   // ③ _meta.contextWindow
    .or_else(|| meta.and_then(|m| get_u64(m, "totalContextTokens"))) // ④ _meta.totalContextTokens
    .unwrap_or(DEFAULT_CONTEXT_WINDOW);                           // ⑤ 全没有 → 256,000
let context_window = std::num::NonZeroU64::new(context_window)?;  // 0 → 丢弃整条 entry

其中:

/// Default context window (256k) when the remote endpoint doesn't provide one.
pub(crate) const DEFAULT_CONTEXT_WINDOW: u64 = 256_000;

256K 不是"合理的默认值",而是"我不知道真实值"的占位标记。

"256K 保护"机制

远程返回 256K 时,系统会推断远程可能只是在用 fallback 占位。如果 Layer 1 里有更准的值,就继承 Layer 1:

// agent/config.rs — resolve_model_list()
let default_cw = DEFAULT_CONTEXT_WINDOW; // 256_000
for (key, entry) in prefetched.iter_mut() {
    let donor = resolved.get(key);
    if let Some(donor) = donor {
        if entry.info.context_window.get() == default_cw       // remote == 256K
            && donor.info.context_window.get() != default_cw   // 内置 != 256K
        {
            entry.info.context_window = donor.info.context_window;
        }
    }
}
resolved = prefetched; // 注意:完全替换,不是 merge

错误处理

远程获取失败不会阻塞启动,会优雅降级到 Layer 1;单条 entry 解析失败也只是跳过那条,不影响其他模型。

// agent/models.rs — prefetch_models_blocking_gated()
match fetch_models_blocking(endpoints, auth, fetch_auth) {
    Ok(FetchModelsResult { models, etag }) if !models.is_empty() => {
        let map = build_prefetched_map(models, api_base_url_override);
        cache.persist(&map, etag.as_deref(), cache_auth, &cache_origin);
        Some(map)
    }
    Ok(_) => { tracing::warn!("Models endpoint returned empty list"); None }
    Err(e) => { tracing::warn!("Failed to fetch models: {:?}", e); None }
}

1.3 Layer 3:用户配置

用户可以在 ~/.grok/config.toml 里配置模型时指定上下文窗口大小,不配的话就降级到上面两步。

[model.deepseek-v3]
model = "deepseek-chat"
base_url = "https://api.deepseek.com/v1"
context_window = 65536 --单位是 token

config.toml 只决定初始值,不会锁死运行时更新。设完之后,官方通道的动态更新(响应头升级、空闲恢复)仍然可能改它。对 BYOK 用户(通过配置 API Key 自定义 provider 的用户 )这不算问题——因为动态更新路径实际上都不生效(详见后文)。

所以对 BYOK 用户,在 config.toml 里显式设置 context_window 是唯一可靠途径。 不设置就默认 200K——小窗口模型永远不触发压缩直到 API 报错,大窗口模型过早压缩、浪费上下文。

1.4 Layer 4:调试环境变量

GROK_DEBUG_CONTEXT_WINDOW 是优先级最高的一层。它不光覆盖初始值,还会锁死所有运行时动态更新

// spawn.rs — session spawn 时
let context_window_override = std::env::var("GROK_DEBUG_CONTEXT_WINDOW")
    .ok()
    .and_then(|v| v.parse::<u64>().ok())
    .and_then(std::num::NonZeroU64::new);

// 写入 ChatState:override 优先于 baseline
let chat_state_sampling_config = SamplingConfig {
    context_window: context_window_override.unwrap_or(baseline_context_window),
    ..
};

// 同时写入 CompactionConfig,作为全局锁
CompactionConfig {
    context_window_override,  // Some(...) 时锁死一切运行时更新
    ..
}

锁死范围覆盖全部四条运行时更新路径:

链路检查点被锁死方式
HTTP 响应头session_setup.rscontext_window_override.is_none() → false,跳过
空闲恢复刷新session_setup.rs同上
API 错误反馈sampler_turn.rs同上
模型切换model_switch.rscontext_window_override.unwrap_or_else(...) → 强制用 override

模型切换的锁死尤其彻底:

// model_switch.rs
let new_context_window = self.compaction.context_window_override.unwrap_or_else(|| {
    // 只有 override 为 None 时才用新模型的 context_window
    NonZeroU64::new(sampling_config.context_window).unwrap_or(...)
});

哪怕 /model 切到一个 1M 窗口的模型,context_window 仍然锁死在环境变量指定的值。


二、运行时:四条动态更新路径

Session 启动后,context_window 不是一成不变的。运行中有四条路径能修改它,每条有不同的方向性约束。


图2:context_window 动态更新路径

2.1 路径 1:HTTP 响应头——只升不降

每次 API 调用后,extract_model_metadata() 从响应头里提取三个字段:

// xai-grok-sampler/src/client.rs — extract_model_metadata()
fn extract_model_metadata(headers: &HeaderMap) -> Option<ResponseModelMetadata> {
    let context_window = headers.get("x-grok-context-window")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());
    let max_completion_tokens = headers.get("x-grok-max-completion-tokens")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u32>().ok());
    let models_etag = headers.get("x-models-etag")
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string());
    // 三个字段任一非空即返回 Some
    if context_window.is_some() || max_completion_tokens.is_some() || models_etag.is_some() {
        Some(ResponseModelMetadata { context_window, max_completion_tokens, models_etag })
    } else { None }
}

提取到值后,handle_model_metadata_update() 执行更新,关键约束是只升不降

// session_setup.rs — handle_model_metadata_update()
if let Some(new_cw) = metadata.context_window.and_then(NonZeroU64::new)
    && current_config.context_window != new_cw
    && self.compaction.context_window_override.is_none()
{
    if new_cw < current_config.context_window {
        tracing::warn!(
            current_context_window = current_config.context_window.get(),
            header_context_window = new_cw.get(),
            "Ignoring context_window downgrade from response header"
        );
    } else {
        new_context_window = new_cw;
        config_changed = true;
    }
}

三个前置条件缺一不可:header 里有值、和当前值不同、context_window_override 没设。满足后才进入方向判断。

只升不降是一种防御策略:防止服务端临时配错把窗口缩小,导致激进压缩、丢上下文。

这三个 header(x-grok-context-windowx-grok-max-completion-tokensx-models-etag)都是 xAI 私有协议,第三方 provider 不会返回。代码本身不做 provider 判断——extract_model_metadata() 对所有 HTTP 响应都跑——但实际上只有官方 API 才有效。

2.2 路径 2:空闲恢复刷新——双向更新

maybe_refresh_model_metadata_on_resume() 在每次 turn 开始时执行。它有两道硬门锁,只对 xAI 官方通道生效:

// session_setup.rs — maybe_refresh_model_metadata_on_resume()
if !self.is_session_based_auth() { return; }       // 第一道门:auth 类型
// is_session_based_auth() 只接受 CachedToken、GrokCom、Oidc
// BYOK 用户的 auth method 是 XaiApiKey → 直接跳过

let idle_secs = (now_ms - last_request_ms) / 1000;
if idle_secs < Self::IDLE_REFRESH_THRESHOLD_SECS { return; }  // 空闲 < 600秒 → 跳过

if !is_cli_chat_proxy_url(base_url) { return; }    // 第二道门:URL 必须是官方
// 只接受 cli-chat-proxy.grok.com 或 localhost

代码里的"空闲"指的是距离上一次 API 请求的时间间隔:last_request_ms 是上次请求完成时记录的时间戳,now_ms 是这次 turn 开始的当前时间,差值换算成秒就是空闲时长。只有空闲时长达到 IDLE_REFRESH_THRESHOLD_SECS(600 秒)才往下走,没到就提前 return。也就是说,连续对话时每次 turn 间隔很短,这条路径基本不触发——它针对的是用户停了一会儿再回来继续的场景。

过了门锁,请求 /models-v2(注意不是启动时的 /models),从响应里找当前模型、提取新的 context_window:

let url = format!("{}/models-v2", base_url);
// ...
for entry in data {
    let parsed = parse_remote_model_value(entry, base_url)?;
    if parsed.model == *current_model {
        return Some((parsed.context_window, parsed.max_completion_tokens));
    }
}

和路径 1 不同,空闲恢复允许双向更新,升降都接受,空闲达到阈值后才重新拉取,拿到的又是权威 API 的完整模型元数据,大概率是正式变更而不是临时抖动。

if current_config.context_window != new_context_window
    && self.compaction.context_window_override.is_none()
{
    updated_config.context_window = new_context_window; // 升降都接受
    config_changed = true;
}

2.3 路径 3:API 错误反馈——纠错路径

API 返回 400 错误时,should_compact_on_error() 逐步判断要不要触发纠错压缩:

// compaction.rs — should_compact_on_error()
pub(crate) async fn should_compact_on_error(&self, err: &SamplingErrorInfo) -> bool {
    // 1. auto_compact 未被抑制(防止无限重试)
    if self.compaction.auto_compact_suppressed.load(Relaxed) != SUPPRESS_NONE {
        return false;
    }
    // 2. 错误响应必须携带 model_metadata(来自 HTTP 响应头)
    let Some(ref metadata) = err.model_metadata else { return false; };
    // 3. metadata 中必须有 context_window 且非零
    let Some(context_window) = metadata.context_window else { return false; };
    if context_window == 0 { return false; }
    // 4. 当前 token 数确实超过了错误中声明的窗口
    let estimated_total = self.chat_state_handle.get_estimated_total_tokens().await;
    estimated_total > context_window
}

model_metadata 来自 HTTP 响应头中的 x-grok-context-window(同路径 1 的提取逻辑),而非错误 body。第三方 provider 返回 400 时不会携带此 header,所以这条路径对 BYOK 基本不生效。

判定为 true 后,执行完整的纠错流程:

// sampler_turn.rs — handle_sampling_failure()
if self.should_compact_on_error(&error).await {
    let cw = error.model_metadata.as_ref()
        .and_then(|m| m.context_window)
        .expect("should_compact_on_error guarantees context_window");
    // 1. 覆盖 context_window(允许降级——这是与路径 1 的关键区别)
    if let Some(mut cfg) = self.chat_state_handle.get_sampling_config().await
        && let Some(new_cw) = NonZeroU64::new(cw)
        && self.compaction.context_window_override.is_none()
    {
        cfg.context_window = new_cw;
        self.chat_state_handle.update_sampling_config(cfg);
    }
    // 2. 立即触发 compaction
    self.run_compact_only(trigger_info).await?;
    // 3. 压缩完成后自动重试请求
    return Ok(SamplerFailureRecovery::CompactAndResubmit);
}

这是信号最强的纠错路径——API 明确告诉你"你以为的窗口大小是错的",系统无条件接受降级、压缩、重试,整个流程对用户透明。

2.4 路径 4:模型切换

用户执行 /model 切换时,系统从启动时 resolve_model_list() 合并产出的内存模型目录里查目标模型的 ModelEntry,直接读它的 info.context_window 字段作为新值——不会重新从远程拉。如果设了 GROK_DEBUG_CONTEXT_WINDOW 环境变量,就忽略新模型的值、强制用 override;如果 ModelEntry 里的 context_window 是 0 或解析失败,兜底到 DEFAULT_CONTEXT_WINDOW(256K)。最终值通过 update_sampling_config() 写进 ChatState,立即生效。

2.5 更新互斥锁

所有动态更新路径都有同一个前置检查:

// 出现在 handle_model_metadata_update()、maybe_refresh_model_metadata_on_resume()、
// handle_sampling_failure() 三处
self.compaction.context_window_override.is_none()

一旦 GROK_DEBUG_CONTEXT_WINDOW 环境变量被设置,这个 override 就不为 None,所有动态更新全部被跳过。这是调试用的硬锁。

2.6 动态更新不可用情况

用自己的 API Key 直连第三方模型时,上面四条动态更新路径的可用性:

路径可用?原因
响应头第三方不返回 x-grok-context-window
空闲恢复代码硬编码排除非官方 auth 和 URL
错误反馈依赖同一个私有 header
模型切换读本地配置,无限制

所以对 BYOK 用户,在 config.toml 里显式设置 context_window 是唯一可靠途径。 不设置就默认 200K——小窗口模型永远不触发压缩直到 API 报错,大窗口模型过早压缩、浪费上下文。


图3:BYOK 用户自定义上下文窗口大小路径

2.7 写入通道

所有运行时更新最终都走同一条路径写进 ChatState Actor:

ChatState Actor 是整个会话状态的权威存储,按 Actor 模型运行——一个独立任务持有全部状态(对话消息、token 计数、sampling_config 等),外部组件不直接访问字段,而是通过 handle 往 channel 发消息来读写。所有状态访问在 Actor 内串行处理,没有锁竞争。所以 context_window 一旦写进 Actor,就成了权威值,下游所有消费者——压缩判断、TUI 进度条、/context 面板——读到的都是同一个最新值,不存在多处缓存不一致。

// session_setup.rs — 所有路径的最终写入
let updated_config = SamplingConfig {
    context_window: new_context_window,
    max_completion_tokens: new_max_completion_tokens,
    ..current_config
};
self.chat_state_handle.update_sampling_config(updated_config);

update_sampling_config() 通过 mpsc channel 发给 ChatState Actor,Actor 端直接覆盖整个 sampling_config 字段。验证逻辑(像"只升不降")都在调用方做,Actor 端不做二次校验。


三、谁在消费这个值

3.1 压缩触发——核心用途

context_window 存在的根本原因就是决定什么时候压缩。判断公式统一且简洁:

// xai-token-estimation/src/lib.rs
pub fn exceeds_threshold(used: u64, context_window: u64, threshold_percent: u8) -> bool {
    used.saturating_mul(100) >= context_window.saturating_mul(threshold_percent as u64)
}

默认阈值 85%(DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT),可通过环境变量 GROK_AUTO_COMPACT_THRESHOLD_PERCENT、config.toml 的 auto_compact_threshold_percent、远程模型配置逐级覆盖。前面看到 grok-4.5 在 default_models.json 里自带 auto_compact_threshold_percent: 80,比全局默认更激进。

系统里有两套独立的压缩机制同时在跑,都用这个公式:

类型粒度判断函数说明
Inter-compaction会话级,turn 结束后check_auto_compact_needed()全局清理,把早期对话摘要化
Intra-compaction步骤级,agent 执行中should_compact()防止单次工具链撑爆窗口

Intra-compaction 还有一个压缩目标:context_window × 50%,也就是压到窗口一半以下,给后续操作留足空间。

3.2 压缩辅助决策

围绕核心触发,还有一圈辅助机制消费 context_window

消费者用途
Memory flush 预判压缩前提前把重要记忆写入持久存储
Two-pass prefire接近阈值时提前发起投机性压缩,减少用户等待
Inherited prefix 释放压缩后仍超阈值则释放继承前缀
Post-compact suppression压缩后仍超则标记抑制,防止无限循环

3.3 非压缩用途

消费者用途
x-compaction-at 请求头告知服务端本客户端的 compact 阈值
TUI 状态栏进度条实时显示 已用 / 总量 (X%)
/context 命令面板详细 token 分布和距自动压缩的剩余量

四、总览


图4:上下文窗口大小总览