Chat Completions 迁移 Responses API:函数调用完整循环与验收
迁移时保留业务函数,重写它外面的协议循环:遍历 output、校验并执行调用、用原 call_id 回传结果、继续请求。只有工具执行与最终回答都完成,才算迁移成功。
文章目录

把 Chat Completions 的函数调用迁到 Responses API,需要改的是一整条请求与执行循环:模型返回 function_call 输出项,应用校验参数和权限、执行函数,把结果作为 function_call_output 带上原 call_id 回传,再请求模型给出最终回答。换了端点、拿到 HTTP 200,甚至看到“已经查好了”,都不能证明业务函数执行过。
已有业务函数可以继续用;首先迁移非流式、只读路径,再分别验证状态、并行和 streaming。下面用订单 CN-7042 的发货查询给出完整 JavaScript 程序,并说明哪些失败应返回工具错误,哪些情况必须停止。示例查询的是本地演示数据;本次只做离线程序与合成事件检查,没有请求模型,也没有验证 SDK 加载、服务器接受或真实订单系统。
OpenAI 当前仍支持 Chat Completions,并建议新项目使用 Responses。是否迁移现有系统,还要结合模型和工具能力;不能把“推荐”理解为所有旧应用今天都会失效。OpenAI 迁移指南
先确认供应商入口和模型能否保留
记录当前 base URL、认证方式、模型或部署名、有效推理强度、状态策略和业务函数版本。它们共同决定新请求该发到哪里,以及出现问题时能恢复到哪条旧路径。
| 你实际使用的服务 | 迁移时先核对什么 | 不可直接照搬的部分 |
|---|---|---|
| OpenAI 官方 API | OpenAI 文档、项目权限和当前模型参数 | 第三方网关的响应保存期限、认证和模型别名 |
| Azure OpenAI | Azure 资源入口、认证和部署名;官方迁移文档中的 /openai/v1 属于 Azure | OpenAI 的 API key、base URL 及账户设置 |
| 阿里云百炼或兼容网关 | 服务自己的区域或工作空间入口、模型支持、事件和状态规则 | 不能仅凭 OpenAI-compatible 推出整个工具循环和保留策略一致 |
Microsoft 的迁移说明和百炼 Responses 兼容说明分别约束各自服务。例如百炼文档中的七天响应 ID 有效期,不能作为 OpenAI 官方响应 ID 的通用规则。
模型先保持原选择,推理参数按端点改写:Chat 的 reasoning_effort 对应 Responses 的 reasoning.effort。截至 2026 年 10 月 7 日,GPT-6 Astra、GPT-6.1 Sol 的工具调用需要 Responses;GPT-6 Sol/Luna 在 Chat 中调用函数须使用 reasoning_effort: "none"。Astra 和 6.1 Sol 不支持 none,因此也不能把它们的 Responses 工具请求原样切回 Chat。非 none 推理还有采样、logprob 参数限制,迁移前按当前模型兼容说明核对,不顺带把模型升级当成已验证的改进。
把旧协议拆成六个迁移动作
业务查询函数不变,修改它外面的适配层:
| Chat Completions 旧写法 | Responses 对应写法 | 为什么要改 |
|---|---|---|
client.chat.completions.create() | client.responses.create() | 端点和响应对象改变 |
messages | input | 输入可以包含消息、调用和结果等不同类型的项 |
tools[].function.name 等嵌套字段 | tools[].name、parameters 等扁平字段 | 自定义函数定义结构改变 |
choices[0].message.tool_calls | 遍历 response.output 中的 function_call | 输出数组还可能含消息、推理等其他项 |
role: "tool"、tool_call_id | type: "function_call_output"、call_id | 结果须配对原始函数调用 |
| 从 assistant 文本重建历史 | 保留全部相关输出项并转成下一轮输入 | 不能丢掉调用、推理信息及消息 phase |
这里的关联键是 function_call.call_id,不是输出项自身的 id,也不是数组下标。每个接受的调用都须返回一个对应结果,然后继续请求;模型可以再次调用函数,不能假设第二次请求一定给最终文本。函数调用指南

函数工具显式选择 strict: true 或 strict: false。严格模式要求每层对象有 additionalProperties: false,且 properties 中全部字段都在 required 中;可选值可用允许 null 的类型表示,仍须注意其他约束是否也允许 null。Responses 省略 strict 时会尝试严格归一化,无法兼容时可回退到尽力调用,并非无条件严格,也并非必然报 400。明确的 strict: true 配上不兼容 schema 则会被拒绝。严格模式规则
Schema 管参数形状,应用仍须验证订单格式、访问范围和可执行函数。如果你实际只想让最终答案符合 JSON Schema,而不需要查询或修改业务系统,先看 Structured Outputs 与 Function Calling 的选择。
JavaScript 完整循环:查询订单发货状态
保存为 responses-fulfillment.mjs。运行前需要 Node.js、与你安装版本相符的 OpenAI JavaScript SDK、后端环境变量 OPENAI_API_KEY 和已确认支持函数调用的 OPENAI_MODEL。此程序使用当前官方文档中的 toResponseInputItems 导入路径;本次没有安装或加载 SDK,实际项目须核对版本是否提供该导出。密钥和模型设置可按 API Key 与首次请求指南完成。
下面的 authorizedOrders 是后端已批准的访问范围演示。生产中应来自登录身份与权限系统,不能由模型或用户参数自报;fulfillment 则是只读假数据,替换它时保留相同的权限检查和结果合同。
import OpenAI from "openai";
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
const toolName = "lookup_fulfillment_status";
const tools = [{
type: "function", name: toolName,
description: "查询已获授权订单的发货和运输状态,只读",
strict: true,
parameters: {
type: "object",
properties: { order_ref: { type: "string" } },
required: ["order_ref"], additionalProperties: false,
},
}];
const fulfillment = new Map([["CN-7042", {
warehouse_status: "shipped", carrier_status: "in_transit",
last_scan: "杭州转运中心",
}]]);
export async function queryFulfillment({
create, toInputItems, model, authorizedOrders,
maxRounds = 6, budgetMs = 30000,
}) {
if (!model || !(authorizedOrders instanceof Set)) {
throw new Error("缺少模型或后端授权范围");
}
const input = [{ role: "user", content: "订单 CN-7042 发货了吗?到哪里了?" }];
const instructions = "先查询订单,再根据工具结果回答;查询失败时不可猜测物流。";
const deadline = Date.now() + budgetMs;
const ledger = new Map();
const audit = [];
const checkBudget = () => {
if (Date.now() >= deadline) throw new Error("应用处理时间已用尽");
};
async function dispatch(call) {
try {
if (call.name !== toolName) throw new Error("未知函数");
const args = JSON.parse(call.arguments);
if (!args || Array.isArray(args) || typeof args !== "object" ||
Object.keys(args).length !== 1 ||
typeof args.order_ref !== "string" || !/^CN-\d{4}$/.test(args.order_ref)) {
throw new Error("参数必须只含合法 order_ref");
}
if (!authorizedOrders.has(args.order_ref)) throw new Error("无权查询此订单");
const data = fulfillment.get(args.order_ref);
if (!data) return { ok: false, code: "not_found" };
// 这里只读本地假数据;业务接口应在同一授权检查之后调用。
return { ok: true, data };
} catch {
return { ok: false, code: "tool_rejected" };
}
}
for (let round = 0; round < maxRounds; round += 1) {
checkBudget();
const response = await create({
model, instructions, input: [...input], tools,
tool_choice: round === 0 ? { type: "function", name: toolName } : "auto",
parallel_tool_calls: false, store: false,
include: ["reasoning.encrypted_content"],
});
checkBudget();
if (response.status !== "completed") {
throw new Error(`响应未完成: ${response.status}; ${JSON.stringify(
response.incomplete_details ?? response.error ?? null
)}`);
}
if (typeof response.id !== "string" || !Array.isArray(response.output)) {
throw new Error("响应结构缺失");
}
const calls = new Map();
for (const item of response.output) {
if (!["message", "reasoning", "function_call"].includes(item.type)) {
throw new Error(`此示例未实现输出类型: ${item.type}`);
}
if (item.type !== "function_call") continue;
if (typeof item.call_id !== "string" || !item.call_id ||
typeof item.name !== "string" || typeof item.arguments !== "string" ||
(item.status && item.status !== "completed")) {
throw new Error("函数调用缺少有效身份或未完成");
}
const signature = JSON.stringify([item.name, item.arguments]);
const prior = calls.get(item.call_id);
if (prior && prior.signature !== signature) throw new Error("同一 call_id 内容冲突");
calls.set(item.call_id, { item, signature });
}
if (calls.size === 0) {
const parts = [];
for (const item of response.output) {
if (item.type !== "message") continue;
for (const part of item.content ?? []) {
if (part.type === "refusal") throw new Error("模型拒绝,未交付查询回答");
if (part.type === "output_text" && item.role === "assistant" &&
(!item.phase || item.phase === "final_answer")) parts.push(part.text);
}
}
const text = parts.join("\n").trim();
if (!text) throw new Error("没有完整最终文本");
if (!audit.some((entry) => entry.ok)) throw new Error("没有成功查询,不能验收业务成功");
return { text, audit };
}
// 官方 helper 转换所有相关输出,保留推理项和消息 phase。
input.push(...toInputItems(response.output));
for (const { item: call, signature } of calls.values()) {
checkBudget();
const key = JSON.stringify([response.id, call.call_id]);
let entry = ledger.get(key);
if (entry && entry.signature !== signature) throw new Error("重放调用内容冲突");
if (!entry) {
// 先登记待执行 Promise;dispatch 在微任务中执行,避免并发重入窗口。
entry = { signature, pending: Promise.resolve().then(() => dispatch(call)) };
ledger.set(key, entry);
}
const result = await entry.pending;
audit.push({ response_id: response.id, call_id: call.call_id, ok: result.ok });
input.push({ type: "function_call_output", call_id: call.call_id,
output: JSON.stringify(result) });
}
}
throw new Error("调用轮数已用尽,未取得最终回答");
}
const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("请设置已验证的 OPENAI_MODEL");
const client = new OpenAI();
const result = await queryFulfillment({
model, authorizedOrders: new Set(["CN-7042"]),
create: (request) => client.responses.create(request),
toInputItems: toResponseInputItems,
});
console.log(result.text);
console.log(JSON.stringify(result.audit));安装 SDK 并运行会向你配置的模型发起真实请求,可能计费;以下命令供你在已授权的测试环境执行,本次离线检查没有运行它们:
npm install openai
node responses-fulfillment.mjs程序保留了全部输出项参与重放,没有仅凭 response.output_text 续接历史。output_text 是 SDK 聚合文本的便利属性,不能当作完整状态,也不能假定每个原始 REST 响应都有这个顶层字段。状态管理示例
首轮强制查询,后续允许模型继续调用或作答;parallel_tool_calls: false 限制一轮零个或一个调用,不保证调用成功。解析器仍扫描整个数组,异常 fixture 中的多个不同调用也各自配对;这不等于开启了并行执行。未知类型明确停止,示例没有实现托管工具、异步工具或多代理编排。
六轮、30 秒是演示的应用限制。时间检查发生在请求和函数之间,不会强制中断已经发出的网络请求;生产接口还应设置网络超时、取消信号和下游超时。max_output_tokens 包含推理与可见输出;max_tool_calls 限制内置工具调用,不能替代自定义函数的轮数、并发、时间或费用控制。Responses 请求参数
多调用与幂等:先确定业务语义
每个模型调用都用自己的原 call_id 关联结果。应用可以限制独立只读查询的并发;有顺序依赖或会产生副作用的动作,应按业务规则排队、授权和核验,不让 Promise 的完成顺序决定结果身份。
上面的 Map 只防止同一次程序执行中、同一 response.id + call_id 的重复处理。相同 ID 内容冲突会停止,而不是复用另一组参数的结果;同一输出数组里的完全相同调用只回传一次。这是本地去重设计,进程重启会丢失记录,新一轮模型请求也可能产生新 ID,不能保证业务动作只执行一次。
若把查询改为“重新发货”,另设由服务端批准的业务操作键,例如订单、动作和已批准请求 ID 的组合。不要只用订单号,否则会误拦住以后合法的新操作。恢复流程至少要区分:
| 持久记录 | 下一动作 |
|---|---|
| 已有成功回执 | 复用结果,继续回传和生成答案 |
| 明确失败且尚未执行动作 | 根据失败类别决定是否重试 |
pending 或超时,动作结果未知 | 向业务系统查询回执或人工对账,确认前不重做 |
| 没有记录 | 原子地登记操作键,再执行已批准动作 |
唯一约束、事务、下游幂等键和对账属于应用工程设计。只在数据库插入 pending,无法消除“下游动作完成、写回账本前进程崩溃”的窗口。模型请求重试与业务动作重试必须分开;前者拿到新调用,不代表后者获准再执行。
状态不是 SDK 帮你自动选的
选择一种明确的续接方式,然后对它验证恢复和保留策略:
| 方式 | 下一轮带什么 | 不能漏掉的条件 |
|---|---|---|
| 手动重放 | 历史输入、相关完整输出项、新函数结果 | 保留 reasoning、调用和 assistant phase;示例采用此方式 |
previous_response_id | 上一响应 ID 与新增结果或输入 | 每轮重发顶层 instructions;不要再重复附上同一完整历史 |
| Conversation | 会话标识与新增输入 | 完成后自动追加输入和输出;须定义访问、删除与恢复策略 |
previous_response_id 与 conversation 不能在同一请求中使用。响应 ID 串联也适用于 HTTP,不限于 WebSocket;第三方网关若报不支持,应先核对它的实际实现。串联减少请求载荷,但历史输入 token 仍计费,不是免费上下文。会话状态指南
当前指南说明 Responses 默认返回加密推理项;include: ["reasoning.encrypted_content"] 仍是显式请求此信息的可用写法。不要因为内容加密就丢掉它,也不要把它当成能公开阅读的业务执行日志。支持 phase 的 assistant 消息也要保留原值,不能把中途说明重建成最终答案。
store: false 关闭普通 Response 存储,不等于项目已获 Zero Data Retention,也不会删除应用或网关日志。默认存储的 Responses 应用状态有 30 天保留规则及例外,Conversation 项没有同样的 30 天 TTL;滥用监控、经批准的 MAM/ZDR、后台模式及托管工具另有规定。要满足保留要求,核对项目、服务和启用功能的具体政策。OpenAI 数据使用与保留说明
Streaming 的安全边界
Responses HTTP streaming 使用带类型的 SSE 事件,旧 Chat 文本 chunk 累加器不能直接承担函数调度。参数“拼完了”和整次 Response “成功完成了”是两件事:此处采用保守设计,等完整 Response 成功后才交给上面的执行循环。
| 事件 | 应用保存或判断什么 | 能否开始执行函数 |
|---|---|---|
response.output_item.added | 输出项自身的 id、类型及原 call_id | 不能 |
response.function_call_arguments.delta | 按 item_id 或 output_index 累积参数片段 | 不能解析半截 JSON 后执行 |
response.function_call_arguments.done | 完整 arguments,关联原 item | 这个事件不证明整次响应完成 |
response.output_item.done | 完整 item,其中函数项带原 call_id | 先暂存,等待响应终态 |
response.completed | 完整 Response 状态与输出 | 此设计仅此时交给校验和执行 |
response.failed、response.incomplete、取消或提前 EOF | 错误、部分输出与已完成动作记录 | 停止本轮,不能把 EOF 当成功 |

特别注意:当前 response.function_call_arguments.done schema 有 arguments、item_id、output_index、sequence_number、type,没有完整 item,也没有 call_id。应从真正的函数输出项取得 call_id,不要从事件序号编造。参数完成事件 schema、输出项完成事件 schema
下面是可独立保存的 HTTP 事件收集函数。events 必须是传输层已解析 JSON 的异步事件迭代器;它不含 SSE 网络解析,也不启动业务函数。返回的 Response 可以交给相同的非流式校验流程,sequence_number 去重只覆盖这条事件流,不承担业务幂等。
export async function collectCompletedResponse(events) {
const seen = new Map();
const items = new Map();
const argumentsByItem = new Map();
let completed;
for await (const event of events) {
if (!Number.isInteger(event.sequence_number)) throw new Error("事件缺少序号");
const fingerprint = JSON.stringify(event);
if (seen.has(event.sequence_number)) {
if (seen.get(event.sequence_number) !== fingerprint) throw new Error("事件序号冲突");
continue;
}
seen.set(event.sequence_number, fingerprint);
if (["response.failed", "response.incomplete", "response.cancelled"].includes(event.type)) {
throw new Error(`流式响应失败: ${event.type}`);
}
if (event.type === "error") throw new Error("流式传输返回错误");
if (event.type === "response.output_item.added" ||
event.type === "response.output_item.done") {
items.set(event.output_index, event.item);
} else if (event.type === "response.function_call_arguments.delta") {
argumentsByItem.set(event.item_id,
(argumentsByItem.get(event.item_id) ?? "") + event.delta);
} else if (event.type === "response.function_call_arguments.done") {
argumentsByItem.set(event.item_id, event.arguments);
} else if (event.type === "response.completed") {
completed = event.response;
if (completed.status !== "completed" || !Array.isArray(completed.output)) {
throw new Error("completed 事件缺少完整 Response");
}
for (const [index, item] of items) {
if (item.type !== "function_call") continue;
const finalItem = completed.output[index];
if (!finalItem || finalItem.id !== item.id || finalItem.call_id !== item.call_id) {
throw new Error("函数输出项身份不匹配");
}
const args = argumentsByItem.get(item.id);
if (args !== undefined && args !== finalItem.arguments) {
throw new Error("函数参数完成值不匹配");
}
}
return completed;
}
}
throw new Error("事件流提前结束,未收到 response.completed");
}此收集器的释放策略是应用选择,不是服务端“工具恰好执行一次”的保证。合成事件可以验证断线前不会释放调用、重复事件不会重复累计参数;真实连接的重连、取消和代理缓冲仍需在你的环境验收。Streaming Responses 指南
怎样判断失败,怎样恢复到旧路径
遇到 failed、incomplete、cancelled 或仍在 queued/in_progress 的响应,停止本轮业务调度。incomplete_details 可能指向输出预算、内容过滤等原因;先保存状态与已获得结果,再决定扩大预算、调整输入还是交给人工处理,不能把部分文本当最终成功。失败事件、不完整事件
未知函数、坏 JSON、数组或额外字段、越权订单在示例中返回结构化工具错误。缺失 call_id、相同 ID 参数冲突、未实现的输出类型、空最终答案及拒绝则明确停止。错误结果可以帮助模型说明失败,但不应让“说明得很通顺”升级为业务成功;程序还要求至少有一次成功查询。这个条件只证明应用读到了演示结果,最终文本是否忠实表达订单状态仍要对照业务结果核验。
发布前以同一份脱敏测试数据比较旧、新适配层:
- 正常查询:新旧得到同样的发货状态;新路径能留下原
call_id、工具结果及完整答案。 - 多调用和重复:各不同调用分别配对;同 ID 同参数不重复处理,冲突参数明确停止。
- 输入与权限:坏 JSON、
null、额外字段、未知函数及非授权订单不会读取业务数据。 - 响应失败:
incomplete、failed、拒绝、空回答、轮数耗尽各有预期失败结果。 - 状态恢复:手动重放保持推理项和
phase;串联重新发送指令;恢复时复用已成功业务回执。 - 流式中断:参数
.done后但 Response 完成前断线仍不执行;成功动作之后文本中断,恢复只补回传和回答。 - 回滚:切换开关指向已验证的模型、端点、参数与状态适配器;保留操作账本,不重做已完成动作。
本次离线检查覆盖程序语法、假响应下的调用与失败分支、历史项保存,以及合成流事件的完成与断线处理。它们不证明 SDK helper 可加载、当前账户能访问模型、接口接受所有字段或生产权限有效。真实上线验收仍需你的授权环境、去敏执行记录与业务回执;先开放内部只读流量,再逐项增加并行、streaming 或写工具。
常见问题
只改成 responses.create() 为什么还没有执行函数?
模型返回的是调用请求。你的应用必须遍历 output,把允许的 function_call 交给已实现的函数,再用同一 call_id 回传 function_call_output 并请求下一轮。只显示模型文本或只保存调用 JSON,都没有完成业务动作。函数调用流程
call_id 能作为发货操作的幂等键吗?
它能关联一次协议调用,不能单独保证同一发货动作跨请求、跨进程只发生一次。模型重试可能产生新 ID;写操作须使用后端批准的业务键、持久回执和结果未知时的对账流程。parallel_tool_calls: false 也不替代这些机制。
关掉 store 就能保证零保留吗?
不能。store: false 影响普通 Response 存储,ZDR 资格、滥用监控、应用与网关日志以及额外功能有各自规则。若使用 Conversation,还须单独处理没有 30 天 TTL 的会话项。数据保留政策
出错时能直接切回 Chat Completions 吗?
只有旧路径的模型、工具、参数和状态转换仍可用时才行。GPT-6 Astra 和 GPT-6.1 Sol 的工具调用需要 Responses,不能以它们的 Chat 工具请求充当回滚方案。恢复时还须沿用已完成业务动作的回执,避免重做。当前 API 与模型兼容要求
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月8日。
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月8日。
- 1.OpenAI 迁移指南developers.openai.com/api/docs/guides/migrate-to-responses
- 2.Microsoft 的迁移说明learn.microsoft.com/es-es/azure/developer/ai/how-to/azure-openai-to-responses
- 3.百炼 Responses 兼容说明help.aliyun.com/zh/model-studio/compatibility-with-openai-responses-api
- 4.当前模型兼容说明developers.openai.com/api/docs/guides/latest-model
- 5.函数调用指南developers.openai.com/api/docs/guides/function-calling
- 6.状态管理示例developers.openai.com/api/docs/guides/conversation-state
- 7.Responses 请求参数developers.openai.com/api/reference/resources/responses/methods/create
- 8.OpenAI 数据使用与保留说明developers.openai.com/api/docs/guides/your-data
- 9.参数完成事件 schemadevelopers.openai.com/api/reference/resources/responses/streaming-events
- 10.Streaming Responses 指南developers.openai.com/api/docs/guides/streaming-responses





