把 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 Direct | Azure OpenAI | 阿里云百炼 |
|---|---|---|---|
| 事实 owner | OpenAI Developer Docs | Microsoft Learn 与 Azure 资源配置 | 阿里云百炼产品文档 |
| 请求入口 | OpenAI 官方 Responses endpoint | Azure resource endpoint;model 可能是 deployment name | 百炼区域 base URL 和兼容路径 |
| 认证 | OpenAI project API key | Azure 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 合同以当前支持国家和地区列表为准。
把旧协议拆成六个迁移动作
保留原有业务函数,替换它外面的协议适配层:
- 把
client.chat.completions.create()换成client.responses.create(),把输入从messages改为input。 - 去掉旧
tools[].function的中间包装,让name、description、parameters直接位于 Responses function tool 上。 - 不再读取
choices[0].message.tool_calls;遍历response.output并按item.type分派。 - 把旧
role: "tool"结果改成type: "function_call_output"输入条目。 - 从
function_call.call_id原样复制关联键;不要使用 item 自身的id,也不要靠数组位置配对。 - 把工具结果加入下一次 Responses 请求,直到出现完整 final text 或明确失败。
OpenAI 的迁移指南拥有端点和数据形状事实;Function calling 指南拥有应用执行、call_id、strict 和并行调用的当前合同。
迁移时应明确写出 strict: true 或 strict: 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:
javascriptimport 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);
运行:
bashnpm 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 帮你自动选的
按控制权和合规要求选择一条路线:
- 应用手动回放 typed items:适合要自行存储、裁剪、审计或使用
store: false的团队。必须保留所有相关 output item,包括 function call 和必要的 reasoning item。 previous_response_id串联:应用 payload 更短,但不能同时传conversation。上一轮顶层instructions不会自动带入,因此每轮重发稳定指令;历史输入 token 仍计费。- 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、工具错误和
incompletefixture 都产生预期停止或结构化结果。
门三:状态与传输
- 只启用一种状态路线,并验证
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 和一次重复调用。只有最终文本完整、调用数量一致、没有重复副作用并能安全回滚时,才进入小流量发布。



