工具调用的完整闭环
实现模型提议、参数校验、权限检查、执行和结果回传的完整工具循环。
建议先读:结构化输出与校验
本页内容
工具调用把语言连接到真实系统#
用户问“订单 A100 到哪了”,模型参数里没有这笔订单的实时状态。工具调用让模型提出查询请求,应用执行已注册的函数,再把结果回传给模型组织回答。本章用于完成这一整圈,而不是只展示一个 tools 数组。达标后,你应能跟踪一次调用从提议到执行到反馈的关联关系,并解释任何一步失败时如何停止。
前置是结构化输出与可靠 HTTP 调用。工具可以是本地函数,也可以包装数据库或远程 API。它不是让模型直接运行任意 JavaScript;模型输出的是名称和参数,应用决定是否执行以及执行哪个受控实现。
五个阶段各有责任#
第一步,应用提供工具定义,写清名称、用途和输入 schema。第二步,模型输出一个或多个函数调用项。第三步,应用解析参数,检查工具名、参数结构、用户身份和业务范围。第四步,应用执行工具,产生结构化结果。第五步,把结果与原调用关联后送回模型,直到模型给出最终回复或应用决定停止。
OpenAI Responses 的调用项使用 type=function_call,含 name、arguments 和 call_id;arguments 是 JSON 字符串。回传项使用 type=function_call_output,带相同 call_id 和 output。这个关联 ID 不是业务订单号,也不应拿消息 item 的 id 代替。多轮无状态调用时要保留必要的响应项目,不能只保留可见文本。官方函数调用指南
工具定义越清楚,模型越容易选对。get_order_status 比 do_task 更具体;orderId 的格式和“只返回当前用户可访问订单状态”的说明能减少误用。但这些描述仍不是权限实施。实际代码必须从登录态取得 userId,不能接受模型传一个 userId 就代表其他用户查询。
完整示例:离线订单查询闭环#
环境:Node.js 22;文件 tool-loop.mjs;无需依赖;执行 node tool-loop.mjs。模型由固定 fixture 代替,订单库是内存对象;示例不请求网络。它保留真实 Responses 的调用项形状,但 fixture 不是一个兼容接口服务。
const tools = [{
type: 'function',
name: 'get_order_status',
description: '查询当前登录用户的一笔订单状态。',
strict: true,
parameters: {
type: 'object',
properties: { orderId: { type: 'string' } },
required: ['orderId'],
additionalProperties: false,
},
}];
const orders = new Map([
['A100', { ownerId: 'u1', status: '已发货', carrier: '示例快递' }],
]);
function execute(call, userId) {
if (call.name !== 'get_order_status') return { ok: false, code: 'UNKNOWN_TOOL' };
let args;
try { args = JSON.parse(call.arguments); }
catch { return { ok: false, code: 'BAD_JSON' }; }
if (!args || typeof args !== 'object' || Array.isArray(args) ||
Object.keys(args).length !== 1 || typeof args.orderId !== 'string' ||
!/^A\d{3}$/.test(args.orderId)) {
return { ok: false, code: 'BAD_ARGUMENTS' };
}
const order = orders.get(args.orderId);
// 将不存在与无权限合并,避免泄露其他用户的订单是否存在。
if (!order || order.ownerId !== userId) return { ok: false, code: 'NOT_AVAILABLE' };
return { ok: true, orderId: args.orderId, status: order.status, carrier: order.carrier };
}
async function mockCreateResponse({ input, tools: availableTools }) {
const result = input.findLast(item => item.type === 'function_call_output');
if (!result) {
return { status: 'completed', output: [{
type: 'function_call', id: 'fc_fixture', call_id: 'call_1',
name: availableTools[0].name, arguments: '{"orderId":"A100"}',
}] };
}
const value = JSON.parse(result.output);
return { status: 'completed', output: [{
type: 'message', content: [{
type: 'output_text',
text: value.ok ? value.orderId + ' 当前状态:' + value.status : '当前无法查询该订单。',
}],
}] };
}
const input = [{ role: 'user', content: '帮我查 A100 的物流状态。' }];
let finished = false;
for (let turn = 0; turn < 3; turn += 1) {
const response = await mockCreateResponse({ input, tools });
if (response.status !== 'completed' || !Array.isArray(response.output)) {
throw new Error('模型响应未完成或 output 无效');
}
input.push(...response.output); // 保留调用项目,再追加其结果。
const calls = response.output.filter(item => item.type === 'function_call');
if (calls.length === 0) {
const text = response.output.filter(item => item.type === 'message')
.flatMap(item => item.content ?? [])
.filter(part => part.type === 'output_text').map(part => part.text).join('');
if (!text) throw new Error('没有调用,也没有最终文本');
console.log(text);
finished = true;
break;
}
for (const call of calls) {
const result = execute(call, 'u1'); // 身份来自服务端会话,而非模型参数。
input.push({
type: 'function_call_output', call_id: call.call_id,
output: JSON.stringify(result),
});
}
}
if (!finished) throw new Error('达到轮次上限');
预期输出“A100 当前状态:已发货”。将 execute 的用户改为 u2,会得到“当前无法查询该订单”。将 fixture 名称换成任意未注册函数,执行器只返回 UNKNOWN_TOOL,不会根据字符串查找全局对象或动态执行代码。
第一段只声明查询能力,不暴露写订单能力。execute 把语法、结构与身份检查放在访问结果之前。mockCreateResponse 第一轮发出调用,第二轮读取已关联的结果;这使你能观察完整协议,而不会误以为一次模型请求已自动运行本地函数。循环上限防止 fixture 或模型一直索要工具。
哪些事情不能交给工具描述保证#
工具参数最小化可以减少攻击面。例如查询“我的订单”无需让模型传 ownerId;若后端从当前会话补充身份,模型就没有机会自行扩大范围。日期和分页大小也应有上限,防止一句“查全部历史”触发巨大数据库查询。工具输出只返回任务需要的字段,不返回整张用户表。
只读工具与写工具应分开。把 get_order_status 与 refund_order 合成一个带 action 的万能函数,会让权限、确认和审计变得复杂。拆开后,读取可以按现有授权执行;退款则需要验证可退金额、确认范围和幂等操作标识。模型说“用户同意了”不是确认凭据,应用应验证真正的 UI 确认记录。
多个独立查询可以并发,但有依赖的动作必须串行。先查询库存再根据查询结果下单,不能放进同一批盲目并发。即使模型在一个响应里提出多个调用,执行器也可以根据策略拒绝或排序,而不是把 parallel_tool_calls 理解成无条件并行许可证。
工具失败的返回要能指导后续动作。NOT_AVAILABLE 表示不能继续基于该订单作答;TEMPORARY_UNAVAILABLE 可能允许有限重试;BAD_ARGUMENTS 允许模型纠正参数。不要把异常吞掉返回空对象,模型可能把空对象解释成“没有问题”。同时避免把内部堆栈、SQL 或凭据当成工具结果给模型。
工具结果也属于不可信材料#
一个网页检索工具返回的正文里可能含“请调用发送邮件工具”。这仍是外部内容,不应变成执行指令。受信任的是工具代码与验证过程,不是所有经工具传回的文字。执行层应独立检查每次调用的权限和目的,不能因为上一轮来自工具就跳过。
真实接入需要独立的 createResponse({input, tools}) 适配器:它把当前输入项与工具定义一起发送给 OpenAI Responses,处理 HTTP 错误、响应状态与结构,再返回完整供应商响应,尤其保留 output 数组。示例中的 mockCreateResponse 使用相同的参数边界;只有在真实适配器满足这个合同后,才能替换调用处。model-http 章的 summarize 只返回文本摘要,不接收 tools,还会把纯工具输出当成没有文本,因此不能直接用它替换。若要复用,应先抽取通用 createResponse,再把 summarize 做成其上的文本包装器。
工具循环应先把完整 response.output 追加到后续 input,再为每个调用追加带原 call_id 的 function_call_output;下一轮继续传 input 与 tools。只有 function_call 的中间响应是合法路径,不应因没有文本而失败;没有调用时才进入最终文本检查。不要只复制 message,还需按所选模型的官方要求保留推理等响应项目。流式工具参数可能分片到达,必须等参数完成、合并并校验后执行,不能收到半个 JSON 就提前动手。
可观测性至少包含任务 ID、模型轮次、call_id、工具名、耗时、结果码和是否实际产生副作用。日志中的参数应脱敏。对前端而言,显示“正在查询订单”比一直显示“思考中”更有解释力,但结果失败时仍需清楚结束状态。
从工具名称开始设计业务边界#
工具目录是模型看到的操作界面,设计时可以类比后端公开 API。一个名为 manage_order 的函数加上任意 action 字符串,表面上节省了定义,实际上把查询、退款、改地址和取消订单混在同一权限入口。模型更难判断该选什么参数,执行器也更难声明哪种调用可重试、哪种必须确认。把高影响动作拆成独立工具,才能给每项能力设置单独策略。
名称应表达操作对象和动作,描述应解释何时用、什么时候不用、字段从哪里获取。例如 get_order_status 适用于已知订单编号的只读查询,不应该在用户问一般退款政策时调用。参数描述里写明日期格式、货币单位与分页限制,可以减少歧义,但不会替代执行器的真实检查。说明越准确,模型越容易提出合法请求;验证越明确,错误请求越不容易进入业务层。
对前端工程师来说,最容易遗漏的是“哪些参数根本不应由模型决定”。当前租户、登录用户、可访问仓库范围、应用环境和密钥,应从服务端上下文获得。工具参数只表达用户任务需要选择的内容,例如订单号、检索词与允许范围内的日期。这样,模型即使生成了 tenantId,也会因额外字段而被拒绝,而不是获得跨租户查询入口。
默认参数也要谨慎。查询 pageSize 可以由执行器默认设为二十并限制最大一百,这是可解释的性能策略;退款 amount 缺失则不应自动取订单全额,因为缺少信息可能意味着用户还没确定金额。把“未传字段”转换成业务动作前,先判断这个默认值是否会扩大操作范围。模型辅助的便利不能以隐式扩大授权为代价。
工具选择、调用次数与完成状态#
向 OpenAI Responses 提供 tools 后,模型可以根据任务决定是否调用。tool_choice 控制选择策略,parallel_tool_calls 控制是否允许一轮提出多个函数调用;当明确设为 false 时,相关官方指南说明可将该轮限制为零或一个函数调用。不同工具类型和型号的支持范围需要单独确认,不能把某个函数批次行为推广到所有内置工具。官方函数选择与并行调用
强制调用工具并不等于强制得到正确参数。对于“必须查实时库存才能回答”的任务,可以在工作流中要求读取步骤,而不是只相信提示词“有必要时查询”。另一方面,让每个寒暄都调用数据库会增加时延和暴露面。选择策略应该依据完成任务需要的事实,区别“必须获取的证据”和“模型可能有用的辅助能力”。
协议响应的 completed 也不表示用户目标已经完成。第一轮模型已经完整输出一个 function_call,这一轮生成是 completed,但应用还没执行查询。工具返回后第二轮才可能生成答案。应用应分别记录模型轮次状态、工具执行状态和业务任务状态,避免前端在第一轮结束时就显示“查询完成”。
call_id 的作用是把一个函数调用与它的结果对应起来。一次响应里有两个同名工具调用时,不能按工具名匹配结果;并发查询完成顺序也可能不同。业务 operationId 则解决副作用重复执行问题,不能用每次模型新生成的 call_id 代替。模型重试可能换一个 call_id,但用户仍然只想提交同一笔业务操作。
示例二:一个能保留完整输出的模型适配接口#
保存为 tool-adapter.mjs,Node.js 22.22+,无需依赖,执行 node tool-adapter.mjs。程序只调用本地 transport fixture,模拟两轮官方响应项目。相较第一个实验,它把模型接口与订单执行分开,并检查下一轮确实带回原工具调用及对应结果。readTool 是只读演示函数,不接入真实工单服务。
import assert from 'node:assert/strict';
const definitions = [{
type: 'function', name: 'read_ticket', description: '读取当前用户的一张工单',
strict: true,
parameters: {
type: 'object', properties: { id: { type: 'string' } },
required: ['id'], additionalProperties: false,
},
}];
const sentRequests = [];
async function fixtureTransport(_url, options) {
const body = JSON.parse(options.body);
sentRequests.push(structuredClone(body));
const returned = body.input.find(x => x.type === 'function_call_output');
const output = returned
? [{ type: 'message', content: [{ type: 'output_text', text: '工单正在处理中。' }] }]
: [{ type: 'function_call', id: 'fc_1', call_id: 'call_1',
name: 'read_ticket', arguments: '{"id":"T1"}' }];
return new Response(JSON.stringify({ id: 'r' + sentRequests.length, status: 'completed', output }));
}
async function createResponse({ input, tools, transport }) {
const response = await transport('https://api.openai.com/v1/responses', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'fixture-only', input, tools, store: false }),
});
if (!response.ok) throw new Error('MODEL_HTTP_' + response.status);
let body;
try { body = await response.json(); } catch { throw new Error('MODEL_INVALID_JSON'); }
if (body?.status !== 'completed' || !Array.isArray(body.output)) {
throw new Error('MODEL_INVALID_ENVELOPE');
}
return body; // 工具中间响应没有文本也完全合法。
}
function readTool(call) {
if (call.name !== 'read_ticket') return { ok: false, code: 'UNKNOWN_TOOL' };
let args;
try { args = JSON.parse(call.arguments); } catch { return { ok: false, code: 'BAD_JSON' }; }
if (!args || Object.keys(args).length !== 1 || args.id !== 'T1') {
return { ok: false, code: 'NOT_AVAILABLE' };
}
return { ok: true, id: 'T1', status: 'processing' };
}
const input = [{ role: 'user', content: '查工单 T1' }];
let completed = false;
for (let round = 0; round < 3; round++) {
const response = await createResponse({ input, tools: definitions, transport: fixtureTransport });
input.push(...response.output);
const calls = response.output.filter(x => x.type === 'function_call');
if (!calls.length) {
const finalText = response.output.filter(x => x.type === 'message')
.flatMap(x => x.content ?? []).filter(x => x.type === 'output_text').map(x => x.text).join('');
assert.ok(finalText);
console.log(finalText);
completed = true;
break;
}
for (const call of calls) input.push({
type: 'function_call_output', call_id: call.call_id, output: JSON.stringify(readTool(call)),
});
}
if (!completed) throw new Error('ROUND_LIMIT');
const second = sentRequests[1];
assert.equal(second.tools[0].name, 'read_ticket');
assert.ok(second.input.some(x => x.type === 'function_call' && x.call_id === 'call_1'));
assert.ok(second.input.some(x => x.type === 'function_call_output' && x.call_id === 'call_1'));
console.log('通过:工具定义、原调用和结果都进入下一轮');
适配器的输入合同是包含 input、tools 和 transport 的对象,输出合同是完整响应对象。它没有把 output_text 作为成功条件,因此不会拒绝第一轮纯工具输出。fixture 保存结构化克隆后的请求,而不是保存原数组引用;否则循环继续 push 时,测试记录也会跟着变化,可能掩盖“发送时到底包含什么”的错误。
这里的 createResponse 有意只展示协议适配,不再次复制完整重试层。真实接入要组合上一章的状态分类、响应体限制和取消策略,并填入受控密钥与有效型号。不要把示例里的 fixture-only 发给服务端。也不要只将函数名改成 summarize,因为返回文本的小包装器已经丢失了继续工具循环所需的信息。
若所选模型返回额外推理项目,真实适配器应按该型号要求保存可回传信息。应用可以为界面生成一个简化视图,但内部协议记录与 UI 文本应分开存储。把内部所有项目都塞进聊天气泡既不必要,也可能暴露不应展示的执行信息;把所有非文本项目都删掉则会破坏后续轮次。
参数校验要落到真实业务约束#
JSON.parse 只证明参数字符串符合 JSON 语法。它可能得到 null、数组、数字或带未知字段的对象,因此下一步要验证根类型、必填字段、额外字段和值域。金额用分为单位的安全整数,日期说明时区和边界,分页大小设置上限。工具必须拒绝它无法解释的值,而不是依靠隐式类型转换得到“差不多能用”的参数。
业务事实应在执行时重查。用户五分钟前确认的订单状态可能已经改变,库存可能被其他请求消耗,工单可能转给另一团队。对需要前置版本的写入,参数或确认记录里保存 expectedVersion,业务服务使用条件更新;版本不符时返回冲突,让应用重新展示差异。仅在模型调用前查一次状态,不能保护实际写入时的一致性。
错误返回也应形成小合同。BAD_ARGUMENTS 表示模型可能纠正输入;FORBIDDEN 表示不能通过换参数探测其他对象;CONFLICT 表示事实已变,应重新读取;TEMPORARY_UNAVAILABLE 表示有预算时可以重试。把这些情况全部变成字符串“失败”,模型就不知道下一步该澄清、停止还是重试,用户也无法理解系统为什么停住。
错误内容应既有帮助又有限制。告诉模型“日期必须是将来日期”通常可以帮助修正;返回数据库连接串、完整 SQL 和账户信息则没有必要。对于不存在与无权访问的对象,有时需要合并成 NOT_AVAILABLE,避免通过结果差异枚举他人的资源。是否合并由业务安全策略决定,而不是为了让错误看起来统一。
并发工具为什么不能盲目 Promise.all#
两个独立只读查询可以并行,例如查物流和查发票,但先读取订单再根据结果申请退款具有顺序依赖。模型一次提出多个调用,并不能证明它们互不影响。执行器应根据工具元数据和业务类型决定并行、串行或拒绝。只读与写入分组是起点,仍要考虑对同一对象的共享限制。
Promise.all 在其中一项拒绝时立即返回失败,但其他任务不会因此自动停止。假设一项写入成功、另一项查询失败,如果只把整批标成失败再全部重试,就可能重复写入。可以使用逐项结果记录或 allSettled 收集状态,并为每一项保留 call_id;是否继续等待、取消剩余任务或向模型回传部分结果,需要工作流明确规定。
并发还需要上限。模型可能在一轮提出很多查询,每一项又有自己的重试,最终放大数据库与上游压力。限制每轮工具数、同时执行数、单项耗时和总调用预算,比单纯设置模型输出长度更直接。工具参数中的 pageSize 和检索 topK 同样需要限制,否则一次“只调用一个工具”也可能读取海量数据。
工具输出应尽量结构化且精简。查询一百条记录后,不一定要把整张表回传给模型;可以返回汇总、前几条以及一个受权限约束的后续分页标识。输出太长会占用下一轮上下文,还可能让关键错误被淹没。分页标识只是一种资源句柄,服务端仍需按当前用户重新鉴权,不能把猜中标识当作访问许可。
练习:做一个真正有幂等约束的备注工具#
现在把只读查询升级为给工单追加备注。要求当前用户有权操作、确认绑定具体内容、相同操作重复提交只写一次、同一个幂等键换内容必须报冲突。确认记录由可信 UI 流程建立;本实验用普通对象模拟这一步,不表示模型可以自己填写 approved 字段。保存为 note-tool.mjs,Node.js 22.22+,无依赖,执行 node note-tool.mjs。
完整参考实现:确认、权限、幂等与失败验证
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
const ticket = { id: 'T1', ownerId: 'u1', version: 1, notes: [] };
const operations = new Map();
const fingerprint = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
function appendNote({ operationId, payload, approval, actorId }) {
if (actorId !== ticket.ownerId || payload.ticketId !== ticket.id) throw new Error('FORBIDDEN');
if (typeof operationId !== 'string' || !operationId ||
typeof payload.text !== 'string' || !payload.text.trim() || payload.text.length > 120) {
throw new Error('BAD_ARGUMENTS');
}
const key = actorId + ':' + operationId;
const digest = fingerprint(payload);
const saved = operations.get(key);
if (saved) {
if (saved.digest !== digest) throw new Error('IDEMPOTENCY_CONFLICT');
return saved.result;
}
if (approval?.actorId !== actorId || approval?.operationId !== operationId ||
approval?.digest !== digest || !Number.isFinite(approval?.expiresAt) ||
approval.expiresAt <= Date.now()) {
throw new Error('CONFIRMATION_REQUIRED');
}
if (payload.expectedVersion !== ticket.version) throw new Error('VERSION_CONFLICT');
// 这是单进程同步临界段;真实数据库要用事务和唯一约束。
ticket.notes.push(payload.text);
ticket.version++;
const result = { noteId: 'N' + ticket.notes.length, version: ticket.version };
operations.set(key, { digest, result });
return result;
}
const payload = { ticketId: 'T1', expectedVersion: 1, text: '已联系用户补充凭证。' };
const approval = {
actorId: 'u1', operationId: 'op1', digest: fingerprint(payload), expiresAt: Date.now() + 60000,
};
const command = { operationId: 'op1', payload, approval, actorId: 'u1' };
const first = appendNote(command);
assert.deepEqual(appendNote(command), first);
assert.equal(ticket.notes.length, 1);
assert.throws(() => appendNote({ ...command, actorId: 'u2' }), /FORBIDDEN/);
assert.throws(() => appendNote({
...command, payload: { ...payload, text: '不同内容' },
}), /IDEMPOTENCY_CONFLICT/);
assert.throws(() => appendNote({
...command, operationId: 'op2', approval: undefined,
}), /CONFIRMATION_REQUIRED/);
const nextPayload = { ...payload, expectedVersion: ticket.version };
for (const expiresAt of [undefined, NaN, Date.now() - 1]) {
assert.throws(() => appendNote({ actorId: 'u1', operationId: 'op3',
payload: nextPayload, approval: { actorId: 'u1', operationId: 'op3',
digest: fingerprint(nextPayload), expiresAt } }), /CONFIRMATION_REQUIRED/);
}
assert.equal(ticket.notes.length, 1);
console.log('通过:重复只写一次、越权拒绝、参数冲突、缺少及过期确认');
这段实现先验证当前权限,再读取幂等结果,意味着过去成功的操作也不能在权限撤销后随意查询。随后检查确认摘要与版本,在真正写入前建立条件。重复相同操作不再次检查已经过时的 expectedVersion,而是返回原结果;如果把版本检查放在幂等命中之前,正常重试会因为自己第一次写入已增加版本而被错误拒绝。
这里的摘要依赖固定构造顺序的 payload,教学对象的字段顺序可控。真实系统若接收语义相同但字段顺序不同的 JSON,应使用明确的规范化编码后再计算摘要,或逐字段构造待确认对象。不要直接对任意用户原始 JSON 字符串取哈希就宣称“相同操作一定得到相同键”。
内存 Map 只能证明本进程里的行为。两个服务实例同时处理相同键时,各自的 Map 都可能为空,因此必须把唯一约束放到共同存储,并把业务写入与结果记录放在可靠的一致性边界内。若下游服务无法参与同一事务,就需要它自己的幂等 API 或可查询的操作标识;这也是为何发送邮件、支付和普通数据库备注不能共用一套轻率重试策略。
让工具闭环能够被人接手#
一个可维护工具系统应记录“模型提议了什么”和“应用实际执行了什么”的差别。例如模型提议修改两张工单,权限检查只允许其中一张,最终回复必须根据真实结果说明部分完成。不要把模型最初的计划直接渲染成完成清单,也不要因为它最后说“已经处理”就更新本地业务状态。
人工接手时需要看到对象、操作参数、确认范围、执行结果和失败原因,而不是完整提示词与所有日志堆栈。保留 call_id、operationId 和业务对象 ID 的关联,可以从用户反馈定位到真实写入。对于敏感动作,审计日志还应说明谁授予授权、何时确认以及当时对象版本;这些信息不应由模型自由生成。
本章的三个实验分别验证协议闭环、完整响应适配和受控写操作。它们可以独立运行,但不是把代码拼在一起就得到生产系统。真正集成时还要统一错误合同、会话身份、持久存储和取消传播,并按工具逐项做失败注入。先把一个业务动作的成功与失败都解释清楚,再扩充工具目录,维护成本会明显更低。
验收与三个自测#
验收要覆盖正常查询、越权、未知工具、非法 JSON、非法参数和循环超限,并能在记录中把每条结果对应到原调用。真实服务并发、流式参数、写操作幂等留到后续工程实现,不能凭此例宣称已覆盖。
- 问:模型返回函数调用是否代表函数已经运行?答:没有,本地自定义函数由应用执行。
- 问:为什么不能信任模型传来的 userId?答:身份由登录会话决定,模型只是生成参数,不能授予权限。
- 问:call_id 与业务幂等键相同吗?答:不同;前者关联一次模型协议调用,后者识别可重复提交的同一业务操作。
本章示例验证记录#
已离线运行工具闭环、完整输出适配和幂等备注三个程序,验证原调用与结果沿同一 call_id 续接、轮次上限、重复写入、越权、参数冲突、缺失与过期确认。未调用真实模型或工单服务,数据库并发事务尚未联调。
官方资料#
阅读 OpenAI 函数调用 和 Responses 接口。本例的订单数据和回复为教学 fixture,未连接任何真实物流系统。