最近几个月我一直是“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 }
}
- 标准 Session 启动时用
/models,运行时空闲恢复刷新用/models-v2,是两个不同的端点。- 请求发出前先查磁盘缓存。命中条件: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.rs | context_window_override.is_none() → false,跳过 |
| 空闲恢复刷新 | session_setup.rs | 同上 |
| API 错误反馈 | sampler_turn.rs | 同上 |
| 模型切换 | model_switch.rs | context_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-window、x-grok-max-completion-tokens、x-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:上下文窗口大小总览