跳转到主要内容

Chat Completions 迁移 Responses API:函数调用完整循环与验收

5 分钟阅读API 指南

迁移前先确认你使用的是 OpenAI、Azure 还是阿里云合同;随后把 typed output、应用执行、call_id 关联、状态和回滚作为一个完整闭环验收。

Responses API 函数调用迁移流程:模型返回 function_call,应用校验并执行工具,以相同 call_id 回传 function_call_output,再取得最终回答,并由幂等账本阻止重复副作用

把 Chat Completions 的函数调用迁到 Responses API,第一步不是改 URL,而是确认谁拥有你的 API 合同。OpenAI Direct、Azure OpenAI 和阿里云百炼即使都能使用相似的 SDK,请求端点、认证、模型标识、状态存储和工具支持也可能不同。

合同确认后,迁移才进入同一条验收主线:从 response.output 读取全部 function_call,由应用校验和执行函数,以原始 call_id 返回 function_call_output,继续请求直到得到完整最终回答。任何 incomplete 响应、漏调用或重复副作用都应停止放量。

先在 staging 用一个只读的发货状态查询完成这条链路。成功标准不是 HTTP 200,而是新旧路径得到相同业务结果、每个调用只执行一次、最终回答完整,并且可以通过 feature flag 回滚。

先填 provider contract 卡片

不要把一个供应商的示例直接复制到另一个供应商。开始改代码前,为当前环境填完下面这张卡片:

合同问题OpenAI DirectAzure OpenAI阿里云百炼
事实 ownerOpenAI Developer DocsMicrosoft Learn 与 Azure 资源配置阿里云百炼产品文档
请求入口OpenAI 官方 Responses endpointAzure resource endpoint;model 可能是 deployment name百炼区域 base URL 和兼容路径
认证OpenAI project API keyAzure key 或 Entra 等 Azure 认证百炼 API key 与工作空间配置
状态与存储按 OpenAI 当前 store、Response、Conversation 规则以 Azure 当前实现与区域支持为准以百炼 response/session 生命周期为准
上线前证明对当前模型跑完整 custom-function loop对部署、端点、认证和 loop 分别验证对模型、参数覆盖、工具和 stream 逐项验证

如果你不能明确写出 base URL、模型或部署名、认证 owner、状态策略和工具支持,先停止迁移。OpenAI 原生测试成功不能证明 Azure 或百炼成功;兼容接口返回 200 也不能证明 call_id、streaming 和状态语义相同。

中国大陆读者还应先核对当前服务地区和账户资格。中文内容不等于 OpenAI 官方服务自动可用,本文不提供规避地区政策的方法。OpenAI 合同以当前支持国家和地区列表为准。

把旧协议拆成六个迁移动作

保留原有业务函数,替换它外面的协议适配层:

  1. client.chat.completions.create() 换成 client.responses.create(),把输入从 messages 改为 input
  2. 去掉旧 tools[].function 的中间包装,让 namedescriptionparameters 直接位于 Responses function tool 上。
  3. 不再读取 choices[0].message.tool_calls;遍历 response.output 并按 item.type 分派。
  4. 把旧 role: "tool" 结果改成 type: "function_call_output" 输入条目。
  5. function_call.call_id 原样复制关联键;不要使用 item 自身的 id,也不要靠数组位置配对。
  6. 把工具结果加入下一次 Responses 请求,直到出现完整 final text 或明确失败。

OpenAI 的迁移指南拥有端点和数据形状事实;Function calling 指南拥有应用执行、call_id、strict 和并行调用的当前合同。

迁移时应明确写出 strict: truestrict: false,不要依赖省略后的隐式归一化。严格模式下,每层对象都要满足当前支持的 JSON Schema 约束,通常包括 additionalProperties: false,且属性都列入 required;业务可选值使用 nullable 类型。如果你还没确定需求是“执行应用函数”还是“只返回符合 schema 的数据”,先看Structured Outputs 与函数调用的区别,不要把两种合同混在同一迁移里。

中文业务示例:查询订单发货状态

下面的 JavaScript 使用本地 fixture 查询订单 CN-7042 的发货状态,不会修改订单。它包含:

  • typed output 全量遍历;
  • allowlist、JSON 解析和应用参数校验;
  • 相同 call_id 回填;
  • 多调用处理与并行关闭;
  • 进程内重复执行保护;
  • incomplete 安全停止;
  • 手动状态续接和最大轮数。

安装当前 OpenAI JavaScript SDK,设置 OPENAI_API_KEY 与一个支持函数调用的 OPENAI_MODEL,再保存为 responses-fulfillment.mjs

javascript
import OpenAI from "openai"; const model = process.env.OPENAI_MODEL; if (!model) { throw new Error("请设置 OPENAI_MODEL"); } const client = new OpenAI(); const tools = [ { type: "function", name: "lookup_fulfillment_status", description: "查询订单是否已出库以及承运状态,只读", strict: true, parameters: { type: "object", properties: { order_ref: { type: "string", description: "订单引用,例如 CN-7042", }, }, required: ["order_ref"], additionalProperties: false, }, }, ]; const fulfillmentFixture = new Map([ [ "CN-7042", { warehouse_status: "shipped", carrier_status: "in_transit", last_scan: "杭州转运中心", }, ], ]); async function lookupFulfillmentStatus({ order_ref }) { return ( fulfillmentFixture.get(order_ref) ?? { warehouse_status: "not_found", carrier_status: "unknown", } ); } const handlers = new Map([ ["lookup_fulfillment_status", lookupFulfillmentStatus], ]); // 演示同一进程的调用级去重;生产写操作必须换成持久账本。 const executionLedger = new Map(); function validateCall(call) { const handler = handlers.get(call.name); if (!handler) { throw new Error(`工具不在 allowlist 中: ${call.name}`); } let args; try { args = JSON.parse(call.arguments); } catch { throw new Error("工具参数不是合法 JSON"); } if ( typeof args.order_ref !== "string" || !/^CN-\d{4}$/.test(args.order_ref) ) { throw new Error("order_ref 必须符合 CN-0000"); } return { handler, args }; } async function executeOnce(responseId, call) { const ledgerKey = `${responseId}:${call.call_id}`; if (executionLedger.has(ledgerKey)) { return executionLedger.get(ledgerKey); } const pending = (async () => { try { const { handler, args } = validateCall(call); return { ok: true, data: await handler(args) }; } catch (error) { return { ok: false, code: "tool_execution_rejected", message: String(error.message ?? error), }; } })(); // 先登记 Promise,避免同一进程并发重入。 executionLedger.set(ledgerKey, pending); return pending; } const instructions = [ "你是订单履约助手。", "回答发货状态前必须调用 lookup_fulfillment_status。", "不得猜测物流节点;工具失败时说明暂时无法核实。", ].join("\n"); const input = [ { role: "user", content: "订单 CN-7042 发货了吗?现在到哪里了?", }, ]; let finalText = ""; for (let round = 0; round < 6; round += 1) { const response = await client.responses.create({ model, instructions, input, tools, parallel_tool_calls: false, store: false, include: ["reasoning.encrypted_content"], }); // 不把部分文本误判为成功;先处理响应级不完整状态。 if (response.status === "incomplete") { const detail = JSON.stringify(response.incomplete_details ?? {}); throw new Error(`Responses 返回 incomplete,停止执行: ${detail}`); } input.push(...response.output); const calls = response.output.filter( (item) => item.type === "function_call", ); if (calls.length === 0) { if (!response.output_text) { throw new Error("响应没有函数调用,也没有完整最终文本"); } finalText = response.output_text; break; } for (const call of calls) { const result = await executeOnce(response.id, call); input.push({ type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result), }); } } if (!finalText) { throw new Error("工具循环达到上限,未得到最终回答"); } console.log(finalText);

运行:

bash
npm install openai OPENAI_MODEL="你已验证支持函数调用的模型 ID" node responses-fulfillment.mjs

本文没有把示意输出伪装成真实 OpenAI 结果。请在自己的授权账户和当前模型上运行,保存去敏 trace,再决定是否放量。

多调用与幂等:先确定业务语义

示例关闭 parallel_tool_calls,适合第一轮迁移;解析器仍遍历实际返回的全部调用。以后要启用并行,先把工具分成三类:

  • 独立只读:可限制并发后并行执行,每个结果单独保留 call_id
  • 有顺序依赖:按业务顺序串行,不能让完成时间决定回填身份。
  • 有副作用:默认串行,并加入持久账本、业务幂等键和必要的人审。

response.id + call_id 只能识别同一个 provider call。整次模型请求重发后可能产生新 ID,所以订单写入还必须有独立业务键,例如 shipment-update:{order_ref}:{operation}。先以数据库唯一约束写入 pending,再执行动作并保存最终状态;HTTP timeout 不能直接当成“未执行”。

未知工具、坏 JSON、参数越界和业务查询失败应返回结构化工具错误,让模型解释失败,而不是静默重试。任何未知写工具、无法判断幂等状态或 call/result 数量不一致都应停止迁移。

状态不是 SDK 帮你自动选的

按控制权和合规要求选择一条路线:

  1. 应用手动回放 typed items:适合要自行存储、裁剪、审计或使用 store: false 的团队。必须保留所有相关 output item,包括 function call 和必要的 reasoning item。
  2. previous_response_id 串联:应用 payload 更短,但不能同时传 conversation。上一轮顶层 instructions 不会自动带入,因此每轮重发稳定指令;历史输入 token 仍计费。
  3. Conversation 对象:适合跨任务或多进程持久会话。先定义访问控制、保留、删除和恢复策略,再把它作为唯一状态 owner。

Responses 默认存储响应;受数据保留约束的系统应重新核对 store: false、ZDR 和当前组织策略。具体行为以Conversation state 指南为准。

Streaming 的安全边界

旧 Chat 文本 chunk reducer 不能直接复用。Responses 会通过 response.output_item.added 创建 typed item,函数参数随后以 response.function_call_arguments.delta 分片,并在 response.function_call_arguments.done 完成。

应用应按 output_index 或 item identity 保存参数缓冲区,只在 done 后解析 JSON。若参数未完成就断线,丢弃本次未完成调用;若工具已成功而最终文本 stream 中断,从持久账本恢复结果,不要再次执行副作用。文本事件与参数事件交错时按 event type 路由。

先通过非流式完整循环,再根据官方Streaming Responses 指南增加 reducer、重连和事件去重。

中文团队的四道 staging 门

门一:合同与请求

  • contract 卡片中的 owner、base URL、认证、模型或部署名、状态与工具范围已经确认。
  • Responses function tool 使用扁平结构,strict 决策明确。
  • 相同 fixture 同时进入旧、新 adapter。

门二:执行与关联

  • 所有 output item 按 type 分派,所有调用都经过 allowlist 和参数验证。
  • 每个接受的调用只有一个匹配原始 call_id 的结果。
  • 两调用、未知工具、坏 JSON、工具错误和 incomplete fixture 都产生预期停止或结构化结果。

门三:状态与传输

  • 只启用一种状态路线,并验证 instructions、存储、token 和恢复行为。
  • stream 在 argument .done 前绝不执行工具。
  • 日志能串起 response ID、call ID、工具版本和业务键,但不记录密钥与敏感参数。

门四:灰度与回滚

  • replay 和进程重启测试都不会重复副作用。
  • 先开放内部流量与只读工具,一次只改变协议、并行或 streaming 中的一个变量。
  • feature flag 可以切回 Chat Completions,且回滚不会重放已经完成的业务动作。

下一步

如果基础请求或 API 计费还未完成,先用OpenAI API Key 与首次请求指南建立基线;项目归属不清时查看Organization/Project 排查,遇到 quota 错误时使用配额错误分层排查

然后在 staging 运行本文的 CN-7042 查询,保存旧、新两条去敏 trace,主动注入一次 incomplete 和一次重复调用。只有最终文本完整、调用数量一致、没有重复副作用并能安全回滚时,才进入小流量发布。

#OpenAI API#Responses API#Function Calling#JavaScript
分享文章: