学习和理解 OpenAI的Responses:模型调用过程的原子事务?

Responses 的核心价值,不是把接口名称从 Chat Completions 换成了 responses,而是把模型调用从“返回一段文本”升级成了可组合、可追踪、可继续执行的结构化结果。一次调用可以同时产出最终消息、函数调用、内置工具调用和受控的推理摘要;应用不必再把所有信息塞进一段字符串里猜测和解析,而是可以按 item 类型决定下一步动作。

这个变化真正解决的是 Agent 工程里的三个问题:模型做了什么更容易追踪,工具调用怎么接续更清楚,失败后从哪里恢复更具体。最近和同事讨论 OpenAI 的 Responses API,我发现最容易混淆的不是请求怎么发,而是“一次 Response 到底装了什么”。我现在更愿意把它理解成:一次有结构的模型调用记录。它可以成为调试、审计和多轮协作的共同载体,但它不是数据库里的 ACID 事务,也不是默认把完整思维链交给用户。“原子事务”只是一种帮助理解的比喻:工程上真正要记录的是调用 ID、item 类型、工具结果、失败状态和业务幂等关系

┌─────────────────────────────────────────────────────────┐ │ Responses 协议心智图 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 你发起 ──→ 一次 Response(原子事务) │ │ │ │ │ ├── input (输入) │ │ ├── instructions (系统级设定) │ │ ├── tools (可选能力清单) │ │ │ ├── 内置工具 (web/file/code/MCP)│ │ │ └── 自定义函数 │ │ ├── previous_response_id (状态游标) │ │ └── stream / background (执行模式) │ │ │ │ 服务端返回 ──→ 一个 Response 对象 │ │ │ │ │ └── output[] (异构工件清单) │ │ ├── reasoning (思维证据) │ │ ├── tool_call (动作证据) │ │ │ ├── 入参 │ │ │ └── 结果 │ │ └── message (交付物) │ │ │ └─────────────────────────────────────────────────────────┘

一、先回到协议本身:Responses 在桌上长什么样

我不想一上来就讲"为什么是事务",先把协议本身摆出来。理解事务类比的前提,是先把"一次 Response 是什么"这件事看清。

端点是 POST /v1/responses。请求侧常见字段包括 inputinstructionstoolsprevious_response_idstreamtruncation。返回侧最重要的是一个 Response 顶层对象,它有 idstatusmodelusage 等元数据,以及一个异构的 output 数组。下面这张图先把对象关系摆清楚。

Responses 对象关系图

1. Response:一次调用的外壳

Response 是服务端返回的顶层对象,负责描述这次调用的身份、状态、模型、用量和输出。它不是“最后一句文本”的别名;真正的产物在 output[] 里。output 的元素是不同种类的 ResponseOutputItem,要根据每个 item 的 type 分支处理。

2. output[]:有顺序的异构产物列表

output[] 可以同时出现多个 item。常见的有 messagefunction_callreasoning,以及由内置工具产生的 web_search_callfile_search_callcomputer_callcode_interpreter_call、MCP 相关调用等。模型只输出文字时,列表里通常是一个 message;模型需要调用工具时,列表里可能先出现工具调用,之后还要继续发起下一次 Response。

{
  "id": "resp_a1",
  "status": "completed",
  "output": [
    { "type": "reasoning", "summary": [{ "type": "summary_text", "text": "需要先核对订单" }] },
    { "type": "function_call", "name": "query_order", "call_id": "call_1", "arguments": "{\"id\":\"8821\"}" },
    { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "我先帮你核对订单。" }] }
  ],
  "usage": { "input_tokens": 123, "output_tokens": 456 }
}

message 是面向用户或下游应用的消息,文本通常位于 content 中的 output_text,也可能是拒答或带注释的内容。function_call 是模型提出的自定义函数调用请求,关键字段是 name、JSON 字符串形式的 arguments 和用于关联回传的 call_id。应用执行函数后,要把 function_call_output 作为下一次请求的输入交回模型,而不是假设这一次 Response 已经包含了函数执行结果。

内置工具与自定义函数的处理方式也不同:内置工具通常由平台按工具类型执行,并在 output 中产生对应的工具 item;自定义 function 则是模型给应用的调用请求,应用必须自己执行,再把结果回传。reasoning 表示推理模型的推理相关 item,API 可以返回摘要,也可能返回受保护的加密内容;它不等于可随意展示的完整思维链。产品上应根据模型、参数、权限和合规要求决定是否保留、展示或只保留摘要。

这里有三个容易误读的点:第一, 是受控的推理摘要或内容,不应写成“完整思维链”;第二, 和 的确是同级 output item,但自定义函数的执行结果需要应用再回传;第三, 是这次 Response 产出的有序记录,却不保证一次调用完成整个 Agent loop

另外一个常被忽略的字段是 previous_response_id。把它填上,可以把本次请求接到上一条 Response;这让多轮交互形成一条服务端可引用的响应链。但它不是“自动完成整个 Agent loop”,也不替代业务侧的会话、权限和审计设计。

二、把它叫"原子事务",到底对不对

回到标题那个问题。我自己的结论是:方向对,但需要校准——它不是数据库那种 ACID 事务,把它当成那种事务会让你在工程上踩坑。

ACID 事务的四条性质套到 Response 上是这样的:

ACID 性质Response 的对应是否成立
Atomic 原子性一次调用形成一份响应记录⚠️只能类比——工具调用可能失败,且 Agent loop 可能跨多个 Response
Consistency 一致性输出符合某种约束⚠️ 取决于结构化输出、工具 schema 和业务校验
Isolation 隔离性并发不互相干扰❌ 不是 Response 自动保证的业务语义
Durability 持久性提交后不丢⚠️ 受存储、保留策略和应用落库方式影响

可以看到,“事务”这个词能帮你理解意图,但不能帮你理解机制。更贴切的类比是一条结构化事件记录,而不是 ACID 事务:

结构化记录视角下的 Response:
┌──────────────────────────────────────────┐
│  Response = 一次调用的结构化记录            │
│                                          │
│  · 有身份(identified)                    │
│    用 id / status / usage 描述这次调用       │
│                                          │
│  · 可串联(chainable)                     │
│    previous_response_id 可引用上一条响应     │
│                                          │
│  · 异构(heterogeneous)                   │
│    output[] 里放不同类型的 item             │
│                                          │
│  · 可审计(auditable)                     │
│    需结合应用日志与工具结果保存             │
└──────────────────────────────────────────┘

数据库事务的关键词是""(把状态从 A 改成 B);Response 的关键词是""(把"模型针对这次输入做了什么"记下来)。这个区别决定了你的工程心智——前者想着"如何回滚",后者想着"如何审计、回放、引用"。我自己后来把所有和 Response 相关的设计问题都翻译成"这是在生成一条调用记录"还是"这是在改一个状态",回答立刻就清楚了。

另外,previous_response_id 在这个类比里也找到了位置——它就是父事件引用。对话不再是客户端拼出来的 messages 数组,而是服务端按"上一笔事务 ID"自动续接的事件流。和 Git 的 commit 链是同一种思想:每一笔都能被未来的事件引用,也都能被审计系统反向引用。

三、用一次客服工单把这件事落地

抽象的类比讲到这里,我自己都觉得空。下面落到一个具体场景:客服 Agent。

客服系统本质上是一个工单(case)系统——用户发起的每一次咨询是一个工单,工单在生命周期里被分派、查询、升级、关单。把这套心智套到 Responses 上:

客服心智Responses 协议对应
工单conversationprevious_response_id
工单里的一次"客服动作"一次 Response
客服的受控推理摘要output[].reasoning
客服调了什么系统output[].function_call
客服最终回复用户的话output[].message
工单审计日志把所有 Response 的 output 按时序串起来

最关键的一个转换:Response 在客服里不是"模型的一次回答",而是"客服对工单的一次处置"。用户发一条消息进来,agent 可以做多次思考+多次工具调用,最终给一次回复——这一整段是一次 Response,对应工单日志里一条新的"客服动作"。

举一个真实的例子。用户问"我上周买的手机,订单 #8821,能退款吗?",agent 一次 Response 的 output 数组会是这样:

{
  "output": [
    { "type": "reasoning", "summary": [{ "type": "summary_text", "text": "需查订单和退款政策,判断是否在可退范围" }] },
    { "type": "function_call", "name": "query_order", "arguments": "{\"id\":\"8821\"}", "status": "completed" },
    { "type": "function_call", "name": "query_refund_policy", "arguments": "{\"amount\":4999}", "status": "completed" },
    { "type": "message", "content": [{ "type": "output_text", "text": "金额超 2000,需主管审批,是否继续?" }] }
  ]
}

在客服视角下,这个数组就是工单系统里一条结构化的处置记录——客服想了什么、查了什么、依据什么规则、最终回复了什么,全在一条记录里。

这套结构在客服场景下有三个特别有价值的地方。

第一,reasoning 摘要可以成为合规与培训资产,但必须受控我会把它当作辅助审计材料,而不是完整思维链,更不会默认展示给客服或用户。落库前要结合模型返回能力、权限和数据保留策略做脱敏与访问控制。

第二,每一次 Response 都是"敏感动作"的天然断点客服经常会触发不可逆动作(退款、改地址、注销账号),协议上正确的做法是:让"需要审批的动作"作为一次 Response 的终点,而不是中间步骤message 就是天然的用户确认位,用户在两次 Response 之间的"空白期"就是审批窗口。万一用户中途关掉 App,最坏情况是"已确认未执行"——可恢复,而不是已退款。

第三,失败 item 是客服 SLA 的关键指标数据库事务失败要么回滚要么挂掉;Response 的失败是显式可见的——失败的工具调用会以 出现在 output 里。比如订单系统临时挂了,监控不要只看 Response 的 status,要拆到 item 级别——query_order 失败和 process_refund 失败的处置路径完全不同,item 级失败率比 Response 级失败率更接近真实 SLA。

四、我自己踩过的几个认知误区

把这套理解用起来之后,回过头看自己之前的一些想法,确实绕了不少弯路。把最值得提醒的几条记一下。

误区一:把 Responses 当成 Chat Completions 的升级版它们是两种不同的心智模型——Chat Completions 是"消息流",Responses 是"事务流"。如果你只是把 messages 改名成 input,照旧用——你能用,但你会错过 80% 的协议价值。判断标准很简单:如果你还在解析 message.content 字符串、还在客户端维护 history,那你其实没在用 Responses 的能力。

误区二:以为 previous_response_id 只是"省 token 的小技巧"它看起来像是服务端帮你省了上下文成本,但本质是把对话所有权从客户端转移到了服务端——这是个架构决定,不是优化。一旦你用了 previous_response_id,删对话、改对话、回滚都变难。所以生产环境我通常的做法是:短期/测试对话用它(简单),长期/合规场景用 conversation 资源把历史与会话解耦,或者干脆自己维护精简的 messages。

误区三:以为所有工具调用都由同一种方式处理。内置工具通常由平台按工具类型执行,并在 output 中产生对应的工具 item;自定义 function 则是模型给应用的调用请求,应用必须自己执行,再把 function_call_output 回传。我自己第一次调 Responses 时,把这两类混在一起,重试和错误处理都写得不对。

误区四:以为 Responses 只是给 Agent 用的。普通对话也能使用它的统一 output 结构;但不要把 reasoning 当成“展示模型思考过程”的产品功能,面向用户展示时应优先使用最终 message 或经过审核的摘要。

误区五:把“一个 Response”当成“一个 Agent loop”。一次 Response 可能只产出一个工具调用,应用执行后还需要下一次 Response;所以重试、幂等和审批边界要围绕 item 和多次调用来设计。

给读者的启示

  1. 先记住对象层级Response 是外壳,output[] 是有序 item 列表,messagefunction_call、工具调用和 reasoning 都是 item。
  2. 把自定义函数的闭环补完整:模型产出 function_call,应用执行,结果以 function_call_output 回到下一次 input。
  3. 把 reasoning 当受控信息:它可以是摘要或加密内容,不等于可直接展示的完整思维链。
  4. 不要把一次 Response 等同于一次 Agent loop:工具调用、回传、继续生成,可能跨越多次 Response。
  5. “原子事务”只是一种帮助理解的比喻:工程上真正要记录的是调用 ID、item 类型、工具结果、失败状态和业务幂等关系。

回到最初的问题:Responses 可以被理解为“模型调用过程的一条结构化记录”,但不应被误读成数据库事务或完整 Agent loop。把对象关系和自定义工具闭环看清楚,output[] 就不再是一段需要猜测的 JSON,而是一份可以按类型处理、按调用串联、按业务审计的协议记录。