OpenAI Responses API 会话压缩
通用的会话压缩通常由应用自己完成:删除较早的消息,或者让模型生成一份可读摘要。OpenAI Responses API 还提供了原生 Compaction 能力,由服务端把较长的运行上下文转换成更短的规范上下文。
它解决的是“下一轮怎样继续推理”,不是“聊天页面怎样展示历史”。产品仍然应该保存用户和助手的原始消息;OpenAI 返回的加密压缩项只用于后续模型请求。
应用数据库中的完整聊天记录
user -> assistant -> user -> assistant -> ...
|
| 只投影模型下一轮需要的状态
v
Responses API 运行上下文
较早的消息和推理状态 -> 加密 compaction 项 + 必要的保留项目本文代码按 OpenAI Python SDK 2.x 编写。自定义 base_url 指向的兼容服务不一定实现 context_management 或 /responses/compact,需要以服务提供方文档为准。
一、先理解 compaction 项
压缩结果中会出现 type="compaction" 的输出项。它携带后续推理需要的早期状态和 reasoning,但内容经过加密,对应用是不透明的,也不应该被解释成人类可读的对话摘要。
{
"id": "cmp_...",
"type": "compaction",
"encrypted_content": "..."
}因此需要区分三种数据:
| 数据 | 用途 | 是否展示给用户 | 是否继续发给模型 |
|---|---|---|---|
| 原始 user / assistant 消息 | 页面展示、审计、搜索 | 是 | 按上下文策略选择 |
| 应用自己生成的结构化摘要 | 跨供应商压缩、回滚和调试 | 可选 | 是 |
| OpenAI compaction 项 | 恢复 Responses 内部运行状态 | 否 | 是 |
不要用 compaction 项替代数据库里的聊天记录。它不可读,也不适合承担用户查看历史、导出数据或人工审计的职责。
二、两种官方压缩方式
OpenAI 提供两条路径:
| 方式 | 触发者 | 调用入口 | 后续上下文怎么处理 | 适用场景 |
|---|---|---|---|---|
| 服务端自动压缩 | Responses API 根据阈值触发 | responses.create() | 正常追加完整 response.output | 希望少写状态控制代码 |
| 主动压缩 | 应用主动判断并调用 | responses.compact() | 用完整 compacted.output 替换旧运行上下文 | 需要明确检查点和持久化控制 |
两种方式都只压缩模型运行上下文,不会替你维护用户可见的聊天记录,也不等于跨会话长期记忆。
三、服务端自动压缩
在普通 Responses 请求中加入 context_management,并设置 compact_threshold:
from typing import Any
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.5"
# 这是压缩触发阈值,不是模型上下文窗口的上限。
# 实际项目应根据所用模型的上下文上限,为本轮输出、工具结果和安全余量
# 预留空间后再确定阈值,不能直接照抄示例数字。
COMPACT_THRESHOLD = 100_000
# 手动历史既包含普通字典,也包含 SDK 返回的 message、reasoning、compaction
# 和工具调用等对象,所以示例使用 Any 表达这种混合列表。
history: list[Any] = []
def keep_from_latest_compaction(items: list[Any]) -> list[Any]:
"""保留最新 compaction 项及其后的项目,缩小下一轮请求体。
只有在客户端手动维护 input 数组时才能这样处理。若使用
previous_response_id,历史由服务端连接,客户端不应手动裁剪。
"""
for index in range(len(items) - 1, -1, -1):
item_type = getattr(items[index], "type", None)
if item_type == "compaction":
return items[index:]
# 尚未触发压缩时,继续保留原来的完整运行上下文。
return items
def ask(user_input: str) -> str:
"""发送一轮请求,并把完整输出保存为下一轮上下文。"""
history.append(
{
"role": "user",
"content": user_input,
}
)
response = client.responses.create(
model=MODEL,
input=history,
# store=False 表示这个例子不依赖服务端保存 Response,运行上下文由
# 当前程序维护;官方文档也将这种配置说明为 ZDR-friendly。
store=False,
context_management=[
{
"type": "compaction",
"compact_threshold": COMPACT_THRESHOLD,
}
],
)
# 不能只保存 output_text。完整 output 中还可能有 reasoning、工具调用和
# compaction 项,它们都是下一轮恢复模型状态需要的数据。
history.extend(response.output)
# 官方说明允许删除最新 compaction 项之前的项目。最新压缩项已经携带继续
# 对话所需的早期状态,这样还能减少下一轮请求体和长尾延迟。
history[:] = keep_from_latest_compaction(history)
return response.output_text服务端在渲染后的上下文超过阈值时执行压缩。压缩项会出现在本次响应或响应流中,应用要像保存其他 response.output 项一样保存它。
阈值怎么定
compact_threshold 应明显低于模型上下文上限,因为同一个窗口还要容纳:
- 当前请求的输入项目;
- instructions 和工具定义;
- 工具调用与工具结果;
- 模型输出 Token;
- reasoning Token;
- Token 估算误差和安全余量。
不要等到请求已经超过上下文窗口才尝试压缩。压缩动作本身也需要读取仍然有效的上下文,生产系统应提前触发,并记录压缩前后的输入 Token、延迟和回答一致性。
与 previous_response_id 一起使用
使用 previous_response_id 时,本地只发送本轮新消息和上一轮响应 ID。仍然可以配置服务端自动压缩,但不要再执行 keep_from_latest_compaction():
from openai import OpenAI
client = OpenAI()
previous_response_id: str | None = None
def ask_with_response_id(user_input: str) -> str:
"""通过响应 ID 延续会话,并让服务端管理压缩后的上下文。"""
global previous_response_id
request = {
"model": "gpt-5.5",
"input": user_input,
# 该模式依赖服务端关联前置响应。本地数据库仍应保存用户可见消息,
# 不能把 response.id 当成产品聊天记录的唯一副本。
"store": True,
"context_management": [
{
"type": "compaction",
"compact_threshold": 100_000,
}
],
}
if previous_response_id is not None:
request["previous_response_id"] = previous_response_id
response = client.responses.create(**request)
previous_response_id = response.id
return response.output_textprevious_response_id 只是让应用不用重复传输完整数组,不代表历史输入不占上下文或不计为输入 Token。是否压缩仍然是另一个问题。
四、主动调用 responses.compact()
如果应用希望自己控制压缩时机,可以调用独立的 /responses/compact 接口。这个接口是无状态的:传入当前完整运行上下文,得到下一阶段应该使用的规范上下文。
from typing import Any
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.5"
# 此处的阈值只是示例。应根据模型窗口、最大输出、工具返回规模和线上指标
# 调整,而且必须在原始 history 仍能放进上下文窗口时完成压缩。
COMPACT_AT_INPUT_TOKENS = 100_000
history: list[Any] = []
def compact_if_needed(last_input_tokens: int) -> None:
"""超过应用阈值时,用官方压缩结果替换模型运行上下文。"""
if last_input_tokens < COMPACT_AT_INPUT_TOKENS:
return
compacted = client.responses.compact(
model=MODEL,
input=history,
)
# compacted.output 是服务端返回的完整规范上下文。它通常不只有一个
# compaction 项,还可能保留先前窗口中的必要项目,因此必须整体使用。
# 不要只挑 type="compaction" 的项目,也不要再次自行裁剪这份结果。
history[:] = list(compacted.output)
def ask(user_input: str) -> str:
"""完成一轮对话,并在安全阈值处主动压缩历史。"""
history.append(
{
"role": "user",
"content": user_input,
}
)
response = client.responses.create(
model=MODEL,
input=history,
store=False,
)
# 仍然保存完整 output,而不是只保存助手文本。
history.extend(response.output)
if response.usage is not None:
# 这里用本轮实际 input_tokens 作为下一阶段的触发信号。阈值需要留出
# 足够余量,避免新增 output 后的 history 已经无法交给 compact 接口。
compact_if_needed(response.usage.input_tokens)
return response.output_text主动压缩最重要的规则是:
- 送入
responses.compact()的原始上下文仍然必须处于模型窗口内。 compacted.output可能包含加密 compaction 项和少量保留项目。- 返回结果是下一轮的规范上下文,应整体传入后续
responses.create()。 - 不要对
compacted.output再做“只留最后一项”之类的二次裁剪。
五、自动压缩和主动压缩怎么选
| 需求 | 推荐方式 | 原因 |
|---|---|---|
| 只是让长对话稳定继续 | 服务端自动压缩 | 接入代码少,Responses 在阈值处自动处理 |
| 需要在任务阶段结束时形成检查点 | 主动压缩 | 应用明确知道何时产生并保存新上下文 |
| 运行状态要落库并由 Worker 恢复 | 主动压缩 | 可以把 compacted.output 作为一个有版本的运行快照 |
使用 previous_response_id | 服务端自动压缩 | 服务端已经负责连接历史,客户端不要自行裁剪 |
| 使用 Chat Completions 或多个模型供应商 | 自建滑窗或摘要 | OpenAI compaction 项是 Responses API 专用格式 |
OpenAI 原生压缩和应用自建摘要也可以同时存在,但用途不同:原生 compaction 负责延续模型内部运行状态;应用摘要负责可读、可审计、可跨供应商的业务状态。不要把两者混成同一个字段。
六、生产系统仍要保存两套状态
一个可落地的最小设计如下:
conversation_messages
- conversation_id
- message_id
- role
- visible_content
- created_at
用途:页面展示、搜索、审计、导出和删除
model_context_snapshots
- conversation_id
- snapshot_version
- response_id(使用服务端串联时)
- compacted_output(手动保存运行上下文时)
- source_message_range
- input_tokens
- created_at
用途:恢复下一轮模型运行状态每一轮的处理顺序可以是:
- 把用户原始消息写入
conversation_messages。 - 从最新运行快照构造 Responses 请求。
- 调用模型并把助手可见文本写入
conversation_messages。 - 保存完整
response.output,其中可能包含 reasoning、工具调用或 compaction。 - 如果主动压缩,保存完整
compacted.output并递增快照版本。 - 页面始终读取原始消息表,不读取加密 compaction 内容。
这样即使压缩状态损坏、供应商更换或用户要求导出记录,也不会因为只剩一段不可读的加密数据而无法恢复业务历史。
七、它和其他几个概念不一样
- Prompt Cache:复用相同输入前缀以改善成本或延迟;缓存输入仍属于上下文,不会替你缩短长对话。
previous_response_id:让服务端串联前后响应;它简化传参,但不等于压缩。- Conversations API:持久化消息、工具调用和工具输出等会话项目;持久化状态不等于上下文可以无限增长。
- 人工摘要:由应用控制的可读文本,便于展示、审计和跨模型使用,但存在遗漏或改写事实的风险。
- OpenAI compaction:Responses API 使用的加密规范上下文,适合模型继续推理,不适合人类阅读。
八、常见错误
只保存 output_text
这会丢失 reasoning、工具调用、工具结果关联和 compaction 项。手动维护 Responses 历史时,应保存完整 response.output。
只保留 standalone compact 返回的 compaction 项
compacted.output 可能还包含必要的原始项目。它整体才是下一轮的规范上下文,不能自行挑选。
已经超过模型窗口才调用压缩
压缩接口仍要读取传入上下文。应提前设置阈值,并给新增用户输入、输出、reasoning、工具结果和误差留空间。
使用 previous_response_id 时又手动裁剪
这两套状态管理边界会互相冲突。使用响应 ID 串联时只传新输入,让服务端管理已关联的历史和压缩项。
把 compaction 当成长期记忆
压缩状态服务于当前运行;用户偏好、项目事实和跨会话状态仍应由应用按权限、有效期和来源管理。
默认兼容服务一定支持
兼容服务可能只实现基础 /responses,没有 context_management 或 /responses/compact。使用自定义 OPENAI_BASE_URL 时应先查询服务文档,并对不支持的接口准备明确错误提示或自建摘要降级方案。
九、建议练习
- 给手动
history示例设置一个较低的测试阈值,打印每轮完整response.output,观察何时出现type="compaction"。 - 触发自动压缩后,比较裁剪前后请求体大小、输入 Token、缓存 Token 和回答一致性。
- 改用
responses.compact(),确认返回结果不只有一个 compaction 项,并把完整compacted.output用于下一轮。 - 数据库继续保存全部可见消息,只替换模型运行快照,确认聊天页面没有因为压缩丢失历史。
- 使用一个不支持压缩的兼容服务,验证程序能区分“接口不支持”和“上下文超长”两类错误。
十、官方资料
OpenAI 原生压缩减少了应用自己维护模型内部状态的工作,但没有替代聊天记录、长期记忆、权限和审计系统。最重要的边界仍然是:完整历史由产品保存,压缩上下文只负责让下一轮模型继续工作。