OpenAI Responses API:告别胶水代码的原生工具编排范式
1. 项目概述:这不是另一个API,而是一次开发范式的迁移
我第一次在内部测试环境里跑通 client.responses.create() 这行代码时,手是悬在键盘上方停了三秒的。不是因为报错,而是因为——它真的就“成了”。没有反复调试的 system prompt 拼接,没有手动拆解 message 数组的嵌套逻辑,没有为 streaming 写七八层 try-except 去兜底 event 类型,更没有为一个简单的货币换算,硬生生搭起一套 function calling 的状态机。那一刻我意识到,OpenAI 不是在发布一个新 API,而是在把过去三年我们写废的几千行胶水代码,直接编译进了它的协议栈里。
这个 Responses API,不是 Chat Completions 的升级版,也不是 Assistants 的平替。它是 OpenAI 对“开发者真实工作流”的一次逆向工程:我们真正卡住的地方从来不是模型能力,而是 如何让模型能力与业务逻辑之间不产生语义断层 。以前你得自己当翻译官——把用户说的“查下昨天的汇率”翻译成函数名 get_exchange_rate ,再把函数返回的 {"rate": 153.2} 翻译成“1欧元≈153.2日元”。Responses API 把这个翻译过程收编了,它自己就是那个双语同传,而且带上下文记忆、带错误回滚、带多模态输入理解。
关键词里虽然写着“None”,但实际贯穿全文的隐性关键词有三个: orchestration(编排)、tool-native(原生工具)、output-contract(输出契约) 。这三点才是它区别于所有旧接口的核心。orchestration 不再是你用 while 循环 + if 判断写的那堆状态管理;tool-native 意味着 web search、file search、computer use 不是插件,而是模型推理流程中天然存在的“运算符”,就像加减乘除一样被调度;output-contract 则彻底终结了正则提取、关键词匹配、LLM 自己写 JSON 还经常少个逗号的痛苦时代。
适合谁看?如果你还在用 messages=[{"role":"system","content":...}] 手动拼接提示词,如果你的 stream=True 代码里还藏着 if "delta" in chunk 的字符串判断,如果你每次加个新功能就得重写一遍 function calling 的调用链——那你不是在用 API,你是在给 API 当人肉编译器。这篇指南就是给你卸下这套编译器的。它不假设你熟悉 Assistants 的 thread 和 run 概念,也不要求你读过 Chat Completions 的全部参数文档。它只假设一件事:你想用最少的代码,做最稳的事。接下来的所有内容,都围绕这个目标展开。
2. 核心设计哲学:为什么 Responses API 能砍掉70%的胶水代码
2.1 从“分层调用”到“单点入口”的架构跃迁
我们先看一张真实的对比图——不是概念图,是我在上个项目里截下来的生产环境日志片段:
| 场景 | 旧方案(Chat Completions + Assistants 混用) | 新方案(Responses API) | 胶水代码减少量 |
|---|---|---|---|
| 生成产品描述 | client.chat.completions.create() → 提取 response.choices[0].message.content → 清洗换行符 → 截断到150字 → 存入DB |
client.responses.create(..., max_output_tokens=200) → 直接取 .output_text |
83行 → 1行 |
| 分析商品图+文本 | client.chat.completions.create() 调用两次(先OCR后分析)→ 手动合并结果 → 写容错逻辑防图片加载失败 |
input=[{...}, {"content": [{"type":"input_image", "image_url":...}]}] → 一次请求完成 |
142行 → 9行 |
| 实时客服反馈分析 | stream=True → 自己解析 event.data → 区分 content , error , done → UI层做防抖和追加 → 失败时重试逻辑 |
stream=True → 遍历 for event in stream: → 只处理 response.output_text.delta 和 response.error |
67行 → 12行 |
这个数字差异背后,是根本性的设计哲学切换。旧方案是“乐高式组装”:你买来 Chat Completions 这块积木、Assistants 那块积木、Function Calling 又一块积木,然后花大力气设计连接件(adapter)、缓冲区(buffer)、状态同步器(state sync)。Responses API 是“一体压铸成型”:它把所有积木的接口标准、供电电压、通信协议,全在出厂时就统一了。你拿到的不是零件包,而是一个能直接拧上螺丝就运转的电机。
提示:这不是“简化”,而是“归一化”。OpenAI 把过去分散在三个独立 API 中的抽象层(message、thread、function_call),全部折叠进
responses.create()的单一参数结构里。input字段既能接收字符串,也能接收 message 数组,还能接收带 image_url 的 multimodal 对象;tools字段既能塞内置工具(web_search_preview),也能塞自定义函数(type: "function");text.format字段则直接接管了输出形态的定义权。这种归一化带来的最大好处,是你的代码不再需要为不同场景写不同分支——同一个create()调用,靠参数组合就能覆盖 90% 的业务需求。
2.2 “Orchestration 自动化”的底层实现机制
很多开发者看到文档里“自动处理 orchestration logic”这句话,第一反应是:“又一个营销话术吧?” 我也这么想,直到我扒开了它的响应体结构。下面这段是我在调试时抓到的真实 response 对象(已脱敏):
{
"id": "resp_abc123",
"object": "response",
"created": 1712345678,
"model": "gpt-4o-2025-03-28",
"output_text": "100欧元≈16,473.12日元...",
"output": [
{
"type": "function_call",
"name": "convert_currency",
"arguments": "{\"amount\":100,\"from_currency\":\"EUR\",\"to_currency\":\"JPY\"}",
"call_id": "call_def456"
}
],
"usage": {
"prompt_tokens": 128,
"completion_tokens": 42,
"total_tokens": 170
}
}
注意 output 字段——它不再是旧 API 里那个扁平的 choices[0].message.content 。它是一个 类型化的操作指令数组 。当模型决定要调用函数时,它不返回一段文字让你去 json.loads() ,而是直接返回一个 type: "function_call" 的结构化对象,里面连 call_id 都帮你生成好了。这意味着什么?
- 你不用再写正则去匹配
{"name": "xxx", "arguments": "yyy"} - 你不用自己生成唯一 call_id 去关联请求和响应
- 你不用手动校验 arguments 是否符合 schema(strict: true 已在服务端强制)
更关键的是,这个 output 数组是 可扩展的 。今天它只有 function_call ,明天如果 OpenAI 加了 web_search_result 或 file_search_excerpt 类型,你的现有代码完全不用改——只要在 if/elif 里加一行新分支就行。这就是 orchestration 自动化的本质:它把原本由 SDK 或业务代码承担的“决策-执行-反馈”闭环,下沉到了 API 协议层。你的角色,从交响乐团指挥(协调每个乐手何时奏响),变成了唱片制作人(只关心最终混音效果)。
2.3 “Tool-Native” 与 “Plugin-Style” 的生死线
这里必须划清一条技术红线:Responses API 的内置工具(web_search_preview、file_search、computer_use)和传统意义上的“插件”有本质区别。插件是运行在你服务器上的独立进程,API 调用它,它再调用第三方服务,整个链路是你可控但也是你担责的。而 Responses API 的工具是 模型推理图中的原生算子 。
举个具体例子:当你传 tools=[{"type": "web_search_preview"}] 时,模型在生成 token 的过程中,会动态插入一个“搜索算子节点”。这个节点的输入是当前上下文的 embedding,输出是经过重排序、去重、摘要后的网页片段集合。整个过程发生在 OpenAI 的 GPU 集群上,和你的文本生成共享同一个 attention mask 和 KV cache。它不是先搜完再生成,而是边搜边生成,搜索结果会实时参与后续 token 的概率计算。
这就解释了为什么 web_search_preview 返回的结果里,会有 (reuters.com) 这样的来源标注——这不是后处理加的水印,而是模型在生成时,就把引用来源作为输出约束的一部分。同样, file_search 工具返回的引用位置(如 "page": 12, "section": "3.2" ),也不是 OCR 后的坐标映射,而是模型对文档语义结构的理解结果。
注意:这种原生集成带来两个硬性约束。第一,你无法像调用自定义函数那样,对 web search 的结果做预处理(比如过滤掉某域名);第二,搜索行为本身会消耗额外的 token(通常比纯文本生成多 30%-50%)。所以别把它当成免费搜索引擎用——它的定位是“为生成服务的上下文增强”,而不是“替代你自己的搜索服务”。
3. 实操核心环节:从零搭建一个抗压的 Responses API 应用
3.1 环境初始化:安全不是选项,是默认配置
很多人栽在第一步:API key 硬编码。这不是小问题,是生产事故的定时炸弹。我见过最惨的一次,是某电商团队把 key 写死在前端 JS 里,被爬虫扫出后,三天内刷了 200 万 token,账单直接破十万。Responses API 的初始化,必须遵循“最小权限+环境隔离”原则。
# ✅ 正确做法:四层防护
import os
from openai import OpenAI
from dotenv import load_dotenv
# 第一层:环境变量加载(.env 文件应加入 .gitignore)
load_dotenv()
# 第二层:key 有效性校验(防止空值导致后续静默失败)
api_key = os.getenv("OPENAI_API_KEY")
if not api_key or len(api_key) < 30:
raise ValueError("Invalid OPENAI_API_KEY: check your .env file")
# 第三层:客户端初始化时指定超时和重试(这是很多人忽略的关键!)
client = OpenAI(
api_key=api_key,
timeout=30.0, # 连接+读取总超时,避免卡死
max_retries=2, # 自动重试,但不过度消耗资源
# 第四层:生产环境强制启用 base_url(便于未来切流量或加网关)
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
)
# ✅ 额外加固:为敏感操作单独建 client(如文件上传)
file_client = OpenAI(
api_key=api_key,
timeout=120.0, # 文件上传需更长超时
base_url="https://api.openai.com/v1" # 文件 API 必须走官方地址
)
实操心得: .env 文件里永远不要写 OPENAI_API_KEY=sk-xxx 这种明文。正确姿势是 OPENAI_API_KEY_FILE=./secrets/api_key.txt ,然后在代码里读取文件内容。这样即使 .env 被误提交,key 也不会泄露。另外, base_url 参数看似多余,但在灰度发布时极其关键——你可以先让 5% 的流量走 https://staging-api.openai.com/v1 ,验证无误后再全量切。
3.2 文本生成:告别“Prompt Engineering 炼丹术”
旧时代,写 prompt 是玄学。你要反复调整 temperature、top_p、presence_penalty,还要在 system prompt 里埋各种“请务必”、“严禁”、“若未遵守将...”的恐吓式指令。Responses API 把这些全干掉了,因为它把 prompt 的控制权,交给了更可靠的结构化参数。
# ❌ 旧写法(脆弱且难维护)
system_prompt = """你是一个专业的产品文案,必须:
1. 严格控制在150字以内
2. 用第二人称“你”来写
3. 每句话以动词开头
4. 禁止出现“这款”、“该产品”等指代词
5. 结尾必须带emoji"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"产品名:{name},特性:{features}"}
]
response = client.chat.completions.create(model="gpt-4o", messages=messages)
# ✅ 新写法(稳定且可验证)
response = client.responses.create(
model="gpt-4o",
instructions="你是一个专业的产品文案,专注为商务人士撰写简洁有力的描述", # 行为定义,非规则罗列
input=f"""生成 {name} 的产品描述,突出以下特性:
- {features[0]}
- {features[1]}
- {features[2]}
目标用户:{audience}""",
temperature=0.7, # 创意度,0.7 是多数文案的黄金值
max_output_tokens=200, # 硬性长度限制,比字符数更精准
# 关键新增:输出格式契约(见 4.3 节详解)
text={"format": {"type": "text"}} # 明确告诉模型:只要纯文本,不要 markdown
)
为什么这样更稳?因为 instructions 字段只定义角色和目标(what),不规定实现细节(how)。模型知道“我是文案专家”,而不是“我必须每句以动词开头”。后者是反人类的,模型会为了满足规则而牺牲语义连贯性。而 max_output_tokens 是基于 token 计数的硬限制,比 max_length=150 (按字符)可靠十倍——中文里一个 emoji 就占 4 个 token,你按字符数截,可能刚好看不见句号。
实操心得:
temperature参数我建议永远设为 0.7±0.2。低于 0.3 时输出过于刻板,像机器人念说明书;高于 0.9 时又容易胡说。我们做过 A/B 测试:在电商文案场景,0.7 的点击率比 0.3 高 22%,比 0.9 高 15%。这不是玄学,是模型在创意与可控性之间的最佳平衡点。
3.3 多模态图像分析:一次请求,三重价值
图像分析是 Responses API 最被低估的能力。很多人以为它只是“能看图”,其实它解决了三个层次的问题: 识别(what)、理解(why)、建议(how) 。我们拿电商商品图分析来实测:
def analyze_product_image(image_url: str) -> dict:
"""
一次请求完成:品类识别 + 质量诊断 + 摄影优化建议
返回结构化结果,直接喂给运营后台
"""
response = client.responses.create(
model="gpt-4o",
instructions="你是资深电商视觉顾问,专精于提升商品转化率",
input=[
{
"role": "user",
"content": "请按以下顺序分析图片:\n1. 产品所属一级品类(如服装、电子、家居)\n2. 当前图片暴露的3个主要质量问题\n3. 给出2条具体摄影改进建议(需可执行)"
},
{
"role": "user",
"content": [
{
"type": "input_image",
"image_url": image_url
}
]
}
],
temperature=0.2, # 图像分析需高确定性
# 关键:强制结构化输出,避免自由发挥
text={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"category": {"type": "string"},
"quality_issues": {
"type": "array",
"items": {"type": "string"}
},
"photography_tips": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["category", "quality_issues", "photography_tips"]
}
}
}
)
# 安全解析:即使模型没严格按 schema 输出,也尝试修复
try:
return json.loads(response.output_text)
except json.JSONDecodeError:
# fallback:用正则提取关键字段(兜底策略)
import re
result = {}
result["category"] = re.search(r"1\.\s*产品所属一级品类:(.+?)\n", response.output_text).group(1).strip()
result["quality_issues"] = re.findall(r"2\.\s*主要质量问题:\n(.+?)\n3\.", response.output_text, re.DOTALL)
result["photography_tips"] = re.findall(r"3\.\s*摄影改进建议:\n(.+?)$", response.output_text, re.DOTALL)
return result
# ✅ 实际调用(带错误重试)
for attempt in range(3):
try:
analysis = analyze_product_image("https://example.com/product.jpg")
break
except Exception as e:
if attempt == 2:
raise e
time.sleep(1) # 指数退避
这个函数的价值在于,它把原本需要三个人干的活(视觉设计师看图分类、品控专员找瑕疵、摄影指导提建议),压缩进一次 API 调用。更重要的是,返回的是结构化 JSON,可以直接存入数据库的 product_analysis 表,触发自动化工单(如“图片质量分<60,通知商家重拍”)。
注意:
input字段传数组时,第二个元素的content必须是列表(即使只有一个 image),这是 Responses API 的硬性要求。如果写成{"content": {"type":"input_image", ...}}会直接报 400 错误。这个细节文档里没强调,但我们踩过坑——模型返回的错误信息是invalid input format,非常误导。
3.4 流式响应:让用户感觉“AI 在思考”,而不是“在加载”
流式响应不是炫技,是用户体验的生死线。我们的数据表明:在客服对话场景,响应延迟超过 1.2 秒,用户放弃率上升 47%。Responses API 的 streaming 设计,精准切中了这个痛点。
def stream_customer_feedback_analysis(feedback: str, ui_callback=None):
"""
ui_callback: (chunk: str, is_final: bool) -> None
用于更新前端UI,如:追加到聊天框、显示“正在分析...”
"""
stream = client.responses.create(
model="gpt-4o",
instructions="提取客户反馈中的核心问题、情绪倾向、解决优先级",
input=feedback,
stream=True,
temperature=0.3, # 保持分析客观性
max_output_tokens=500
)
full_text = ""
for event in stream:
# ✅ 只处理两种事件类型,其他一律忽略(防御性编程)
if event.type == "response.output_text.delta":
chunk = event.delta
full_text += chunk
if ui_callback:
ui_callback(chunk, is_final=False)
elif event.type == "response.error":
error_msg = f"分析失败:{event.error.message}"
if ui_callback:
ui_callback(error_msg, is_final=True)
raise RuntimeError(error_msg)
# ✅ 最终确认:确保流已结束,且有完整输出
if not full_text.strip():
raise RuntimeError("Streaming completed but no output received")
if ui_callback:
ui_callback("", is_final=True) # 通知UI:流结束
return full_text
# ✅ 前端调用示例(伪代码)
def on_ui_update(chunk: str, is_final: bool):
if is_final:
show_loading_indicator(False)
append_to_chat("AI", full_analysis_result)
else:
append_to_chat_streaming("AI", chunk) # 追加到流式显示区域
这个实现的关键,在于 事件类型的精确过滤 。Responses API 的 stream 事件类型有十几种( response.created , response.done , response.usage 等),但你真正需要处理的只有 response.output_text.delta (内容块)和 response.error (错误)。其他事件要么是元信息(如 response.created ),要么是调试用(如 response.usage ),在生产环境里处理它们只会增加复杂度和出错概率。
实操心得:永远在
stream循环外加full_text累加器,并在循环结束后校验full_text.strip()。我们遇到过最诡异的 bug:某次网络抖动,response.output_text.delta事件发了一半就断了,但 stream 对象却正常结束了。没有这个校验,你的函数会静默返回空字符串,前端显示一片空白,用户以为功能坏了。
4. 内置工具深度解析:Web Search、File Search、Computer Use 的实战边界
4.1 Web Search Preview:不是搜索引擎,是“事实锚定器”
很多人把 web_search_preview 当成免费 Google 用,结果发现返回结果不准、延迟高、还限频。这是根本性误解。它的设计定位是: 当模型对自己的知识不确定时,自动触发一次可信源检索,把结果作为生成依据,而非独立答案 。
我们做了个对照实验:问“2025年4月5日美股三大指数涨跌幅”,分别用三种方式:
| 方式 | 响应时间 | 准确率 | 典型问题 |
|---|---|---|---|
| 纯模型(无 tools) | 0.8s | 33% | 编造日期、捏造指数名称 |
web_search_preview |
3.2s | 98% | 引用 Reuters 等信源,带时间戳 |
| 自建 Google Custom Search | 2.1s | 92% | 需自己清洗 HTML、提取摘要、处理反爬 |
差距在哪? web_search_preview 的检索不是关键词匹配,而是 语义检索 。它把你的问题转成向量,在 OpenAI 的索引库中找最相关的网页片段,再用模型重写摘要。所以它返回的不是原始网页,而是“模型理解后的事实快照”。
# ✅ 正确用法:用作生成的“事实校验开关”
response = client.responses.create(
model="gpt-4o",
instructions="回答必须基于最新公开信息,若不确定,请使用 web_search_preview 工具",
input="特斯拉2025年第一季度财报何时发布?",
tools=[{"type": "web_search_preview"}],
# 关键:设置低 temperature,确保事实不被“创意”污染
temperature=0.1
)
注意:
web_search_preview有严格的调用条件。模型不会为所有问题触发它——只有当问题涉及时效性强、模型训练数据外、或存在多个矛盾说法的信息时,才会调用。你不能强制它搜索,也不能预测它是否搜索。这是它的优点(智能),也是局限(不可控)。
4.2 File Search:文档知识库的“无感接入”
File Search 工具的价值,不在于它能搜 PDF,而在于它 消除了文档预处理的黑盒 。旧方案里,你要用 PyPDF2 提取文本,用 LangChain 分块,用 Chroma 建向量库,最后再 query。Responses API 把这整条链路封装了,你只管传文件 ID。
但这里有三个致命陷阱,必须避开:
- 文件上传不是“存进去就完事”
上传后必须等待status: "processed",否则查询会返回空。我们封装了一个健壮的上传函数:
def upload_and_wait(file_path: str, client: OpenAI) -> str:
"""上传文件并轮询至处理完成,返回 file_id"""
with open(file_path, "rb") as f:
file_obj = client.files.create(
file=f,
purpose="assistants" # 必须是 assistants,file_search 专用
)
# 轮询状态(最大10次,每次2秒)
for _ in range(10):
file_info = client.files.retrieve(file_obj.id)
if file_info.status == "processed":
return file_obj.id
time.sleep(2)
raise RuntimeError(f"File {file_path} processing timeout")
-
查询时必须显式传入 file_ids
不能指望模型“记得”你上传过什么。每次responses.create()都要带上file_ids=["file_abc123"]。 -
结果引用是“语义锚点”,不是页码
返回的"citation": {"file_id": "file_abc123", "page": 12}中的page是模型估算的,不是 PDF 的物理页码。它表示“相关内容大概在文档的第12页附近”,用于快速定位,不能用于精确跳转。
4.3 Computer Use:界面自动化的“最后一公里”
Computer Use 工具是 Responses API 的王炸,但它目前(2025年4月)仍处于 beta 阶段,有明确的使用边界:
- ✅ 支持:网页表单填写、按钮点击、下拉选择、文本框输入、页面导航(
go to url) - ❌ 不支持:桌面应用(如 Excel)、移动端 App、需要鼠标拖拽的操作、验证码识别
它的核心价值,是解决“ 重复性界面操作 ”。比如我们给某 SaaS 客服系统做的自动化:当用户说“帮我查订单 #12345 的物流”,AI 会自动:
- 打开物流查询页
- 在订单号框输入
12345 - 点击“查询”按钮
- 截取物流状态区域
- 生成自然语言回复
# ✅ 启用 computer use 的最小配置
response = client.responses.create(
model="gpt-4o",
instructions="你是一名客服助手,可操作网页完成用户请求",
input="查询订单号 12345 的最新物流状态",
tools=[
{
"type": "computer_use",
"name": "browser_control", # 工具别名,便于日志追踪
"parameters": {
"allowed_domains": ["logistics.example.com"] # 白名单,安全强制
}
}
],
# ⚠️ 关键:必须开启 screenshot(否则无法理解界面)
computer_use={"screenshot": True}
)
注意:
allowed_domains是硬性安全策略,不配置会报错。它确保 AI 只能在你授权的域名内操作,杜绝越界风险。这也是为什么它不能用于通用浏览器自动化——你必须明确告诉它“只能去哪几个网站”。
5. Function Calling:从“胶水代码”到“声明式编程”的跨越
5.1 函数定义:用 Schema 代替注释
旧式 function calling 的最大痛点,是函数描述靠自然语言,模型理解靠猜。Responses API 强制使用 JSON Schema,把模糊的“意图”变成精确的“契约”。
# ❌ 旧写法(模型常忽略 description)
{
"name": "get_weather",
"description": "获取指定城市的天气,支持城市名或经纬度",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}}
}
}
# ✅ 新写法(strict: true + 精确 schema)
{
"type": "function",
"name": "get_weather",
"description": "获取指定城市的实时天气和未来24小时预报",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市中文名,如'北京'、'上海市',不接受英文或坐标"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["location"],
"additionalProperties": False
},
"strict": True # 强制模型严格遵守 schema
}
strict: true 是分水岭。开启后,模型绝不会传 {"city": "Beijing"} 这种非法参数,也不会漏传 location 。它要么传完全合规的 JSON,要么不调用函数。这让你的后端函数可以放心地 def get_weather(location: str, unit: str = "celsius") ,不用再写 if "location" not in args: raise ValueError 。
5.2 调用链闭环:一个函数,三次交互
Function Calling 不是单次调用,而是 三次交互的闭环 :
- Query → Model Decide :用户问,模型决定调哪个函数、传什么参数
- Model → Your Code :你执行函数,得到结果
- Your Code → Model → User :你把结果喂回模型,它生成最终回复
Responses API 把这三次交互的协议标准化了。关键在于 call_id 的传递:
# 第一次:获取模型的调用指令
response = client.responses.create(..., tools=tools)
if response.output and response.output[0].type == "function_call":
tool_call = response.output[0]
# ✅ 提取 call_id,这是关联三次交互的唯一钥匙
call_id = tool_call.call_id
# 第二次:执行你的函数
args = json.loads(tool_call.arguments)
result = get_weather(**args)
# 第三次:把结果和 call_id 一起送回去
next_input = [
{"role": "user", "content": user_query},
tool_call, # 原样传回模型的调用指令
{
"type": "function_call_output", # 固定类型
"call_id": call_id, # 必须和第一次的 call_id 一致
"output": json.dumps(result) # 字符串化结果
}
]
final_response = client.responses.create(input=next_input, tools=tools)
实操心得:
call_id是 UUID 格式(如call_abc123-def456),但你 绝不能 自己生成它。必须用模型返回的原始值。我们曾因前端 JS 里用Math.random()伪造 call_id,导致第二次请求被拒绝,错误信息是call_id mismatch,排查了两天。
5.3 多函数协同:构建你的“AI 微服务总线”
一个应用往往需要多个函数。Responses API 支持在一个请求里定义多个工具,模型会自动选择最合适的:
tools = [
{
"type": "function",
"name": "get_weather",
"description": "获取天气",
"parameters": { ... }
},
{
"type": "function",
"name": "book_flight",
"description": "预订航班",
"parameters": { ... }
},
{
"type": "function",
"name": "calculate_tax",
"description": "计算税费",
"parameters": { ... }
}
]
# 用户问:“上海明天天气怎么样?顺便查下飞北京的 cheapest 航班”
# 模型会自动触发 get_weather 和 book_flight 两个函数
# 你的代码需遍历 response.output 处理所有 function_call
for tool_call in [o for o in response.output if o.type == "function_call"]:
if tool_call.name == "get_weather":
# 处理天气
elif tool_call.name == "book_flight":
# 处理航班
这就是“AI 微服务总线”的雏形。你不用管调用顺序(模型会根据依赖关系自动排序),不用管并发(API 内部处理),你只负责把每个函数做成原子化的、无状态的服务。当业务变化时,你只需增删工具定义,不用重构整个调用链。
6. 结构化输出:让 AI 成为你数据库的“免写入接口”
6.1 JSON Schema:从“尽力而为”到“强制合约”
Structured Outputs 的革命性,在于它把 AI 的输出从“尽力而为”变成了“强制合约”。旧方案里,你让模型输出 JSON,它可能返回:
{ "name": "iPhone", "price": 999 }✅{"name":"iPhone","price":"$999"}❌(price 类型错){"product_name":"iPhone","price":999}❌(字段名错){"name":"iPhone","price":999,"desc":"great phone"}❌(多了字段)
Responses API 的 text.format 用 JSON Schema 强制约束,以上四种情况都会被拦截,模型会重试直到输出合规。
# ✅ 定义一个严苛的 schema(电商商品入库)
schema = {
"type": "object",
"properties": {
"sku": {"type": "string", "minLength": 6, "maxLength": 20},
"name": {"type": "string", "minLength": 2},
"price": {"type": "number", "minimum": 0.01, "multipleOf": 0.01},
"category": {"type": "string", "enum": ["electronics", "clothing", "home"]},
"in_stock": {"type": "boolean"}
},
"required": ["sku", "name", "price", "category", "in_stock"],
"additionalProperties": False, # 禁止任何额外字段
"strict": True
}
response = client.responses.create(
model="gpt-4o",
input="从以下文本提取商品信息:iPhone 15 Pro 256GB,售价¥8,999,有货",
text={"format": {"type": "json_schema", "schema": schema}}
)
# 模型返回的一定是 { "sku": "...更多推荐


所有评论(0)