Grok 上下文压缩机制怎么理解
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
一句话说清:Grok 的上下文压缩不是”把历史截断”,而是把一段已有对话交给模型重写成一个不透明的 compaction item,你把这个 item 原样放在下一次请求的最前面,模型继续对话时表现得像完整历史还在。 官方文档说明它保留的是 system prompt、附加的文件、之前轮次的推理内容以及一份压缩过的轮次记录,丢掉的是冗长的工具输出和来回拉扯的对话;调用形态是 POST /v1/responses/compact,请求体只要 input 和 model 两个必填项,返回一个 object 为 response.compaction 的对象。最容易踩的坑不在调用本身,而在接回:官方明确警告不要裁剪压缩输出、新的 user 轮次只能追加在 compaction item 之后。另外有一个反直觉的前提——压缩救不了一个已经超长的请求,因为要被压缩的那段对话本身必须先放得进上下文。
它到底压掉了什么,留下了什么
先把动机说清楚。多轮对话的计费逻辑是:每一次后续调用都会把之前所有消息重新发一遍,并为这些消息付输入 token。对话越长,重复付费的部分越大。压缩要解决的就是这件事。
官方文档对”保留什么”给了明确列举:system prompt、附加的文件、之前轮次的推理内容,以及一份被压缩过的轮次记录。对”丢掉什么”也给了明确说法:冗长的工具输出和来回的对话。这个取舍很符合直觉——agent loop 里体积最大的往往不是人和模型说的话,而是工具返回的一大坨结构化结果。
结果被装进一个字段叫 encrypted_content 的 blob 里。官方在这里加了一条 NOTE:把 encrypted_content 当作不透明的东西,不要解析、不要修改。你可以把这个 blob 存进自己的数据库,之后原样传回;它只有在被送回 xAI 的 API 时才有意义。换句话说,别指望从里面读出”模型到底记住了什么”来做调试——那不是给你看的。
官方给这套机制列的收益有四条:下一次调用只为压缩后的上下文付输入 token 而不是原始消息;载荷更小意味着首个 token 返回更快;更紧凑的上下文让模型专注在当前任务上而不是被陈旧的工具输出带偏;以及让多小时的 agent loop 一直待在模型上下文窗口以内。这四条里,第三条其实最值得留意——它讲的不是省钱而是质量,长 loop 跑到后半程注意力被旧信息稀释是真实存在的问题。
官方给的三个前置条件,缺一条就别压
文档在”何时压缩”这一节写得很硬:要同时满足下面全部三条才该压。
第一,对话已经大到每次调用的 input_tokens 开始伤害成本或延迟了。也就是说压缩不是默认动作,是有代价之后才做的补救。
第二,你仍然希望模型记得之前的轮次。文档在这里补了一句很实在的话——如果不需要记住,那就干脆开一段新对话。这条容易被忽略:很多场景其实是任务切换,直接重开比压缩更省。
第三,当前窗口仍然放得进模型的上下文限制。这条是三条里最反直觉的:压缩是把对话变小,它没法拯救一个已经超限的请求。 后面的”限制”一节又重复强调了一遍,说如果你的对话已经越过了 context_length_exceeded,那得先自己裁剪或者拆分,然后才能调压缩。所以压缩不能当成兜底方案用,必须提前触发。
至于”提前多少”,文档没有给一个官方阈值,只给了两种典型模式:在 agent loop 里每 N 轮调用一次压缩 API;或者当你自己的记账显示渲染后的上下文超过了你为自己这类负载选定的阈值时压一次。注意这里的措辞是”你自己选定的阈值”,官方把这个决定明确留给了使用方,别去找一个不存在的推荐值。
一次压缩调用长什么样
REST 端点是 POST /v1/responses/compact,官方对它的定位描述是:把一个完整的 Responses API 输入窗口压成一个更短的规范窗口。
请求体只有两个字段,都是必填:input 是你要压缩的那段对话内容,形态和传给 /v1/responses 的输入一致;model 是用来做压缩摘要的模型。第二个字段值得多说一句——它指的是执行压缩这件事的模型,不一定要和你主对话用的模型一样。文档在”限制与坑”一节里给了对应建议:如果你压缩得很频繁,挑一个更小、更快的模型来做压缩。
响应是一个 OpenAI 兼容的压缩对象:
id——这次压缩的稳定 ID,形如cmp_加一段 uuid,同一个值也会回显在内层的 compaction item 上object——固定是response.compactioncreated_at——压缩后对话生成时的 Unix 时间戳(秒)model——用于压缩摘要的模型output——一个数组,里面只有一个 compaction item,把它原样传给下一次/v1/responses调用output[].type——固定是compactionoutput[].encrypted_content——装着被压缩对话的不透明 blob
usage 里那几个字段怎么读
usage 是这套接口里最值得研究的部分,因为它是你判断”这次压缩划不划算”的唯一依据:
input_tokens——压缩前那段对话的 token 数input_tokens_details.cached_tokens——这些输入 token 里由提示缓存提供的数量output_tokens——为压缩记录生成的 token 数output_tokens_details.reasoning_tokens——压缩过程中产生的推理 token 数total_tokens——输入加输出的总量,含推理部分dropped_message_count——被折叠进这次压缩的输入消息条数
其中 output_tokens 官方给了一句很关键的解释:模型在下一次调用时重新展开的那个 blob,大致就是你被保留下来的 system prompt 再加上这么多 token。这句话把”压缩后我的上下文有多大”变成了一个可以直接读出来的量,而不用你去猜。把 input_tokens 和 output_tokens 一对比,这次压缩的收缩比例就出来了——这是每个团队自己去算的账,官方不会替你给一个通用数字。
dropped_message_count 则适合拿来做监控信号:如果它长期偏小,说明你压得太勤,压缩调用本身的开销可能吃掉了收益。别忘了压缩调用自己也是要付 token 的,文档专门把这条列进了”限制与坑”。想把这类账目化到日常,可以配合 API 成本监控怎么做 里的按维度拆分思路一起看。
接回对话:官方唯一认可的姿势
这一步是最容易翻车的地方,文档在这里挂了一个 WARNING 而不是 NOTE。
原话的意思是:不要裁剪压缩输出。把返回的 compaction item 当作对话新的”起点”,新的 user 轮次追加在它之后,绝不能放在它之前;删除或者重新排序压缩输出内部的条目会破坏这条链。
配套的正确做法是:拿到响应后,你可以安全地把原始消息从客户端状态里丢掉,用 compaction item 作为下一次请求的头部,然后把新的 user 轮次接在后面。在 OpenAI 兼容 SDK 里这写成把 compacted.output 整个展开进下一次 input 数组的最前面,官方示例的注释就一句话——原样使用压缩 item,不要修改。
用裸 HTTP 的话,下一次请求的 input 第一项是一个 type 为 compaction、带着 id 和 encrypted_content 的对象,后面才跟着新的用户消息。
顺带提一个容易踩空的字段:Responses API 的请求体里有一个 context_management,官方对它的说明是”可选的上下文管理指令(例如压缩),会被解析但尚未执行”。看到名字就以为能靠它自动压缩的,会白等——目前压缩仍然是你显式调 compact 端点。
SDK 里的两种写法
xAI 自家的 Python SDK 提供 client.chat.compact_context(model=..., messages=chat.messages),把 chat 累积的消息直接传进去。拿到返回值之后调 chat.append(compact),这个动作会清空 chat 对象内存里的消息列表,只用压缩 blob 作为新的种子,后续的 chat.sample() 就跑在压缩后的上下文上,而不是重放完整历史。
另一种是为长期运行的 agent loop 准备的便捷方法:在活的 Chat 对象上调 chat.compact(),它对当前消息执行压缩并就地替换掉这些消息。之后照常调 chat.sample() 即可,服务端会在下一次请求时把压缩过的前缀重新展开。官方示例里的模式很简单:设一个 compact_every 计数,循环里每满一轮就压一次,顺便打印压缩前后的消息条数和 dropped_message_count。
上面两个方法在 AsyncClient 上都有对应的 await 版本。
还有一个和推理模型强相关的细节:官方示例创建 chat 时传的是 use_encrypted_content=True,注释说明这是推理模型的推荐做法,作用是让之前轮次的模型推理内容能穿过这次压缩被保留下来。如果你用的是推理模型又漏了这个开关,压缩保住的东西会比你以为的少。
compact = client.chat.compact_context(
model="grok-4.6",
messages=chat.messages,
)
# 清空内存消息,只留压缩 blob 作为新起点
chat.append(compact)
官方点名的几个限制
除了前面已经说过的”必须先放得进上下文”,文档还列了这几条:
每次调用至多做一次压缩。 端点每个请求只跑一趟压缩,别指望一次调用做多级折叠。
encrypted_content 是不透明的。 不要解析、不要编辑、更不要把多个 blob 手工合并。要传就把完整的 output 数组(或者 SDK 里的 CompactContextResponse)原样传回去。这条其实堵死了一个很多人会想到的”聪明做法”——把两段历史各压一次再拼起来。
重复压缩是允许的。 对一段已经压缩过的对话,之后可以再压一次,比如上次压缩之后对话又变长了。所以长 loop 里的稳态就是压缩 item 在前、若干新轮次在后,涨到阈值再整体压一次。
压缩调用本身要花 token。 这在 usage.input_tokens 和 usage.output_tokens 里都能看到。频繁压缩时选更小更快的模型做压缩,是官方直接给出的建议。
和提示缓存的关系:互补,但别想当然
官方在这一页的”相关”里把提示缓存列为互补的成本手段,定位是针对未变化的提示前缀。两者的作用面确实不同:缓存省的是”前缀没变时的重复输入”,压缩省的是”前缀本身太长”。
但要小心别自己脑补两者的叠加效果。提示缓存文档写得很明白:任何对更早消息的更改都会破坏缓存,只能在末尾追加新消息。而压缩恰恰是把一整段前缀换成一个新的 item。至于压缩之后的 blob 在后续调用中如何参与缓存、命中怎么统计,官方文档里没有找到专门的说明——这一点建议以你自己账单和响应里的 cached_tokens 为准去观察,而不是照着推理下结论。
可以确认的只有一条事实:压缩响应的 usage.input_tokens_details.cached_tokens 这个字段,官方定义就是”输入 token 中由提示缓存提供的数量”,说明压缩调用自身的输入是有缓存统计口径的。
缓存那一侧的做法本身也值得单独理清,尤其是会话路由和消息顺序这两件事,可以看 怎么把 Grok 的缓存命中率做上去,以及跨厂商的通用视角 API 缓存计费机制。
接进自己的 agent loop 该按什么顺序做
如果要落地,建议按这个顺序推进:
先把记账做起来。你得先能读到每次调用的 input_tokens,否则”什么时候该压”就无从判断——官方的三个前置条件里第一条就依赖这个数。
再定自己的触发规则。要么按轮次每 N 轮压一次,要么按上下文体积过线就压。前者实现简单、行为可预测;后者更贴合真实负载,但需要你自己维护一份渲染后上下文大小的估算。
然后确认接回逻辑写对了。compaction item 必须是新请求 input 的第一项,新 user 消息在其后,输出数组不裁剪、不重排。这一条写错的表现往往不是报错,而是模型”忘事”,很难当场发现。
最后加两个监控项:dropped_message_count 用来判断压得是不是太勤,压缩调用的 total_tokens 用来核算压缩本身的开销。这两个数一起看,才能回答”这套机制在我这儿有没有回本”。
最容易栽的坑还是那个前置条件:等到请求已经报 context_length_exceeded 才想起来压缩,这时候压缩已经帮不上忙了,只能先自己裁剪或拆分。把触发线设在上下文限制之前留出余量,是这套机制唯一没有回旋余地的要求。