Skip to content

OpenAI Responses API 会话压缩 ​

前置:记忆机制、记忆压缩

通用的会话压缩通常由应用自己完成:删除较早的消息,或者让模型生成一份可读摘要。OpenAI Responses API 还提供了原生 Compaction 能力,由服务端把较长的运行上下文转换成更短的规范上下文。

它解决的是“下一轮怎样继续推理”,不是“聊天页面怎样展示历史”。产品仍然应该保存用户和助手的原始消息;OpenAI 返回的加密压缩项只用于后续模型请求。

text
应用数据库中的完整聊天记录
user -> assistant -> user -> assistant -> ...
                  |
                  | 只投影模型下一轮需要的状态
                  v
Responses API 运行上下文
较早的消息和推理状态 -> 加密 compaction 项 + 必要的保留项目

本文代码按 OpenAI Python SDK 2.x 编写。自定义 base_url 指向的兼容服务不一定实现 context_management 或 /responses/compact,需要以服务提供方文档为准。

一、先理解 compaction 项 ​

压缩结果中会出现 type="compaction" 的输出项。它携带后续推理需要的早期状态和 reasoning,但内容经过加密,对应用是不透明的,也不应该被解释成人类可读的对话摘要。

json
{
  "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:

python
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():

python
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_text

previous_response_id 只是让应用不用重复传输完整数组,不代表历史输入不占上下文或不计为输入 Token。是否压缩仍然是另一个问题。

四、主动调用 responses.compact() ​

如果应用希望自己控制压缩时机,可以调用独立的 /responses/compact 接口。这个接口是无状态的:传入当前完整运行上下文,得到下一阶段应该使用的规范上下文。

python
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

主动压缩最重要的规则是:

  1. 送入 responses.compact() 的原始上下文仍然必须处于模型窗口内。
  2. compacted.output 可能包含加密 compaction 项和少量保留项目。
  3. 返回结果是下一轮的规范上下文,应整体传入后续 responses.create()。
  4. 不要对 compacted.output 再做“只留最后一项”之类的二次裁剪。

五、自动压缩和主动压缩怎么选 ​

需求推荐方式原因
只是让长对话稳定继续服务端自动压缩接入代码少,Responses 在阈值处自动处理
需要在任务阶段结束时形成检查点主动压缩应用明确知道何时产生并保存新上下文
运行状态要落库并由 Worker 恢复主动压缩可以把 compacted.output 作为一个有版本的运行快照
使用 previous_response_id服务端自动压缩服务端已经负责连接历史,客户端不要自行裁剪
使用 Chat Completions 或多个模型供应商自建滑窗或摘要OpenAI compaction 项是 Responses API 专用格式

OpenAI 原生压缩和应用自建摘要也可以同时存在,但用途不同:原生 compaction 负责延续模型内部运行状态;应用摘要负责可读、可审计、可跨供应商的业务状态。不要把两者混成同一个字段。

六、生产系统仍要保存两套状态 ​

一个可落地的最小设计如下:

text
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
  用途:恢复下一轮模型运行状态

每一轮的处理顺序可以是:

  1. 把用户原始消息写入 conversation_messages。
  2. 从最新运行快照构造 Responses 请求。
  3. 调用模型并把助手可见文本写入 conversation_messages。
  4. 保存完整 response.output,其中可能包含 reasoning、工具调用或 compaction。
  5. 如果主动压缩,保存完整 compacted.output 并递增快照版本。
  6. 页面始终读取原始消息表,不读取加密 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 时应先查询服务文档,并对不支持的接口准备明确错误提示或自建摘要降级方案。

九、建议练习 ​

  1. 给手动 history 示例设置一个较低的测试阈值,打印每轮完整 response.output,观察何时出现 type="compaction"。
  2. 触发自动压缩后,比较裁剪前后请求体大小、输入 Token、缓存 Token 和回答一致性。
  3. 改用 responses.compact(),确认返回结果不只有一个 compaction 项,并把完整 compacted.output 用于下一轮。
  4. 数据库继续保存全部可见消息,只替换模型运行快照,确认聊天页面没有因为压缩丢失历史。
  5. 使用一个不支持压缩的兼容服务,验证程序能区分“接口不支持”和“上下文超长”两类错误。

十、官方资料 ​

OpenAI 原生压缩减少了应用自己维护模型内部状态的工作,但没有替代聊天记录、长期记忆、权限和审计系统。最重要的边界仍然是:完整历史由产品保存,压缩上下文只负责让下一轮模型继续工作。

持续学习,持续实践。