跳转到主要内容

Structured Outputs 与 Function Calling:怎么选、怎么验收

11 分钟阅读OpenAI API

Structured Outputs 约束模型最终返回的数据形状;Function Calling 让模型提出工具调用,由应用执行真实查询或操作。需要先查数据再返回固定 UI 对象时,才使用两者组合。

订单客服场景中直接结构化响应、函数调用和混合流程的决策关系图

只需要模型返回符合固定 JSON Schema 的最终答案,选 Structured Outputs;需要查询数据库、调用内部 API 或改变外部状态,选 Function Calling。 如果必须先取得实时数据,再把结果整理成固定 UI 对象,就先走工具循环,最后再用 Structured Outputs。这三条路径解决的是不同问题,strict: true 也不能替代权限、业务语义和真实执行结果的验证。

你的任务正确路径订单客服例子完成标准
把已有文本整理成固定对象Structured Outputs(text.format根据用户已提供的工单文本输出分类卡片返回值通过 schema 与业务规则
查询或改变系统中的真实数据Function Calling用订单号查询履约系统应用实际执行工具,并核验查询或写入结果
先执行工具,再返回固定对象工具调用 + Structured Outputs查到实时物流后生成固定客服卡片工具结果真实,最终对象也通过 schema 与语义验证

先问一个问题就够了:如果删掉工具执行代码,用户的任务还能真实完成吗? 能,通常是最终输出格式问题;不能,就是工具问题。不要为了看起来“像 Agent”而虚构一个工具,也不要把一个格式正确的 JSON 当成已经查询或修改过业务系统。

Structured Outputs 和 Function Calling 的差别不在 JSON

两者都能使用 JSON Schema,但 schema 约束的对象不同:

  • Structured Outputs 约束的是模型给用户或下游 UI 的最终响应。在 Responses API 中使用 text.format
  • Function Calling 约束的是模型提交给应用的工具参数。模型提出调用,真正的数据库查询、HTTP 请求或写操作仍由你的代码执行。

OpenAI 的结构化输出指南也采用这条边界:连接模型与工具、函数或数据时使用 function calling;约束模型最终响应时使用结构化 text.format

因此,“两者都返回 JSON”不是有效的选型依据。真正的分界是:这一步是在表达答案,还是在请求应用做事?

路径一:直接返回结构化客服卡片

假设工单原文已经包含订单号和用户诉求,你只想让模型输出前端可渲染的分类结果,不需要查询实时订单。Python SDK 可以直接用 Pydantic 定义最终类型:

python
import os from typing import Literal from openai import OpenAI from pydantic import BaseModel client = OpenAI() class TicketCard(BaseModel): order_id: str category: Literal["delivery_delay", "refund", "other"] needs_live_lookup: bool customer_message: str response = client.responses.parse( model=os.environ["OPENAI_MODEL"], input=[ { "role": "system", "content": ( "把工单整理成客服卡片。只根据输入判断;" "没有实时订单数据时,不得声称已经查单。" ), }, { "role": "user", "content": "订单 A1024 三天没更新物流,我想知道是否延误。", }, ], text_format=TicketCard, ) for output in response.output: if output.type != "message": continue for item in output.content: if item.type == "refusal": raise RuntimeError(f"模型拒绝处理:{item.refusal}") if not item.parsed: raise RuntimeError("没有得到可解析的结构化结果") card = item.parsed print(card.model_dump())

JavaScript 使用 Zod 时,当前 Responses API 的格式入口在 text.format,不是旧版 Chat Completions 示例中的 response_format

javascript
import OpenAI from "openai"; import { z } from "zod"; import { zodTextFormat } from "openai/helpers/zod"; const openai = new OpenAI(); const TicketCard = z.object({ order_id: z.string(), category: z.enum(["delivery_delay", "refund", "other"]), needs_live_lookup: z.boolean(), customer_message: z.string(), }); const response = await openai.responses.parse({ model: process.env.OPENAI_MODEL, input: [ { role: "system", content: "只根据工单生成客服卡片,不得声称已查单。" }, { role: "user", content: "订单 A1024 三天没更新物流。" }, ], text: { format: zodTextFormat(TicketCard, "ticket_card"), }, }); for (const output of response.output) { if (output.type !== "message") continue; for (const item of output.content) { if (item.type === "refusal") throw new Error(item.refusal); if (!item.parsed) throw new Error("没有得到可解析的结构化结果"); console.log(item.parsed); } }

这条路径只能得出“需要实时查询”。它不能证明订单真的延误,因为输入里没有履约系统的最新记录。

路径二:让应用查询真实订单

现在用户问“订单 A1024 当前到哪了”。答案依赖实时系统,应该定义工具。下面的循环保留 response.output,解析函数参数,完成应用侧授权与执行,再用相同 call_id 回传结果:

python
import json import os from openai import OpenAI client = OpenAI() tools = [ { "type": "function", "name": "get_order_status", "description": "查询当前账号有权访问的订单状态。", "strict": True, "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "业务订单号,例如 A1024", } }, "required": ["order_id"], "additionalProperties": False, }, } ] def get_order_status(order_id: str, authenticated_user_id: str) -> dict: # 示例:这里必须在服务端验证订单归属,再查询真实业务系统。 return order_service.lookup( order_id=order_id, user_id=authenticated_user_id, ) input_items = [ {"role": "user", "content": "请查订单 A1024 当前到哪了。"} ] response = client.responses.create( model=os.environ["OPENAI_MODEL"], input=input_items, tools=tools, tool_choice="auto", ) input_items += response.output for item in response.output: if item.type != "function_call": continue if item.name != "get_order_status": raise RuntimeError(f"未允许的工具:{item.name}") arguments = json.loads(item.arguments) order_id = arguments["order_id"] # schema 通过后仍要做权限、业务格式和资源归属验证。 result = get_order_status( order_id=order_id, authenticated_user_id=current_user.id, ) input_items.append( { "type": "function_call_output", "call_id": item.call_id, "output": json.dumps(result, ensure_ascii=False), } ) final_response = client.responses.create( model=os.environ["OPENAI_MODEL"], input=input_items, tools=tools, instructions="只根据工具返回值回答,并说明数据更新时间。", ) print(final_response.output_text)

这里有两个独立事实:模型生成了合法的 get_order_status 参数;应用查到了有权限访问的订单记录。前者不能推出后者。即使 HTTP 请求和 JSON 解析都成功,工具仍可能遇到订单不存在、越权、超时或上游返回旧数据。

tool_choice: "auto" 允许模型不调用、调用一个或多个工具;"required" 要求至少调用一个;也可以指定某个函数或把可用工具限制为子集。强制调用只能控制选择,不能替代应用侧授权。具体形状可对照 OpenAI 的工具选择说明

路径三:先查订单,再输出固定 UI 对象

混合流程不是“给一次请求同时加两个 schema”这么简单,而是两个验收点:

  1. 模型提出工具调用,应用验证并执行;
  2. 工具结果回传后,模型生成固定结构的最终响应。

可以复用上一节的 input_items,在已经追加 function_call_output 后,发起一个不再提供工具的结构化请求:

python
from typing import Literal from pydantic import BaseModel class LiveOrderCard(BaseModel): order_id: str status: Literal["processing", "shipped", "delivered", "exception"] last_updated_at: str next_action: Literal["wait", "contact_support", "request_refund"] customer_message: str card_response = client.responses.parse( model=os.environ["OPENAI_MODEL"], input=input_items, instructions=( "只使用工具结果生成订单卡片。不得补造物流节点;" "工具没有返回的事实不要写入 customer_message。" ), text_format=LiveOrderCard, ) card = card_response.output_parsed if card is None: raise RuntimeError("最终卡片被拒绝、未完成或无法解析") print(card.model_dump())

最后一轮不再暴露工具,避免模型在你期待最终对象时再次进入调用分支。生产代码还应遍历 output 区分拒绝、未完成和已解析消息,而不是只对 output_text 做一次 json.loads

strict: true 究竟保证什么

严格模式保证函数参数遵守受支持的 JSON Schema。官方严格模式规则要求:

  • 每个对象都设置 additionalProperties: false
  • properties 中的所有字段都列入 required
  • 业务上可选的字段用包含 null 的类型表达,例如 ["string", "null"]

它不保证订单号真实存在,不保证模型选对工具,也不保证调用者有权访问订单。对于退款、发信、删除和付款等写操作,还必须由服务端重新校验参数,绑定当前身份,并使用幂等键防止重试造成重复副作用。

Structured Outputs 也只保证形状,不保证事实正确。输入与 schema 完全不相干时,模型仍可能为了满足 schema 填出看似完整的值。因此要在提示中定义“不适用”路径,必要时加入可空字段或明确状态,而不是逼模型编造。

用五层验收代替“JSON 能解析就成功”

验收层要检查什么典型失败
1. 请求与传输HTTP、SDK、响应状态是否完成超时、限流、响应未完成
2. SchemaJSON 可解析,字段、枚举和类型符合约束缺字段、额外字段、错误枚举
3. 业务语义值是否符合订单格式、状态机和时间规则合法字符串却不是有效订单号
4. 授权与幂等当前用户能否执行;写操作能否安全重试越权查询、重复退款
5. 真实查询或效果是否查到源记录;写入是否产生可核验结果工具返回成功文案,但数据库未变

读取类工具应记录数据来源和更新时间,并把“未找到”作为显式结果。写入类工具应返回业务系统生成的操作 ID,再按该 ID 查询最终状态。模型生成的“已退款”句子不是支付系统的回执。

拒绝和 JSON mode 也要单独分支

安全拒绝不一定符合你提供的业务 schema。Responses API 会在消息内容中返回 refusal 项;先处理拒绝,再读取 parsed,不要把拒绝当成 schema 错误无限重试。

JSON mode 只保证输出是有效 JSON,不保证符合指定 schema。新实现能使用 Structured Outputs 时,不应再用“提示模型严格输出 JSON + 手写解析重试”模拟类型安全。如果你正在从 Chat Completions 迁移,先按函数调用迁移指南调整 text.format、工具定义、输出项解析和 call_id 循环。

上线前的最短决策清单

  1. 最终结果是否只依赖输入中已有的信息?是,就从 text.format 开始。
  2. 是否必须读取实时数据或改变外部状态?是,就实现 Function Calling 与应用侧执行器。
  3. 是否还需要固定的前端对象?在工具结果完成后,再增加 Structured Outputs。
  4. schema 是否开启 strict,并满足 required、additionalProperties: false 和 nullable 规则?
  5. 是否把拒绝、未完成、无工具调用、多个工具调用和上游失败设为明确分支?
  6. 是否验证了语义、权限、幂等性以及真实查询或写入效果?

如果还没有可用于本地验证的项目密钥,先完成 OpenAI API Key 创建与首次 Responses 请求验收。代码跑通后,不要以“返回 200”收口:分别保存结构化解析结果、工具执行日志和业务系统回执,你才能知道究竟是哪一层真的成功了。

#OpenAI#Structured Outputs#Function Calling#Responses API#JSON Schema
分享文章: