学习和理解 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。请求侧常见字段包括 input、instructions、tools、previous_response_id、stream 和 truncation。返回侧最重要的是一个 Response 顶层对象,它有 id、status、model、usage 等元数据,以及一个异构的 output 数组。下面这张图先把对象关系摆清楚。
1. Response:一次调用的外壳
Response 是服务端返回的顶层对象,负责描述这次调用的身份、状态、模型、用量和输出。它不是“最后一句文本”的别名;真正的产物在 output[] 里。output 的元素是不同种类的 ResponseOutputItem,要根据每个 item 的 type 分支处理。
2. output[]:有顺序的异构产物列表
output[] 可以同时出现多个 item。常见的有 message、function_call、reasoning,以及由内置工具产生的 web_search_call、file_search_call、computer_call、code_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 协议对应 |
|---|---|
| 工单 | conversation 或 previous_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 和多次调用来设计。
给读者的启示
- 先记住对象层级:
Response是外壳,output[]是有序 item 列表,message、function_call、工具调用和reasoning都是 item。 - 把自定义函数的闭环补完整:模型产出
function_call,应用执行,结果以function_call_output回到下一次 input。 - 把 reasoning 当受控信息:它可以是摘要或加密内容,不等于可直接展示的完整思维链。
- 不要把一次 Response 等同于一次 Agent loop:工具调用、回传、继续生成,可能跨越多次 Response。
- “原子事务”只是一种帮助理解的比喻:工程上真正要记录的是调用 ID、item 类型、工具结果、失败状态和业务幂等关系。
回到最初的问题:Responses 可以被理解为“模型调用过程的一条结构化记录”,但不应被误读成数据库事务或完整 Agent loop。把对象关系和自定义工具闭环看清楚,output[] 就不再是一段需要猜测的 JSON,而是一份可以按类型处理、按调用串联、按业务审计的协议记录。