为什么我放弃了传统方案,改用 OpenAI Responses API
起因:一个让我崩溃的 Bug
上个月我在维护公司的客服机器人时,遇到了一个经典的诡异问题:对话进行到第 15 轮左右,机器人开始“失忆”,把用户前面说过的订单号忘得一干二净。
排查了一整晚,问题根源很典型:我们的 Chat Completions 方案需要手动维护 messages 数组,而有个同事在某个分支里用切片截断历史时,把 system 消息也一起切掉了。就这么一行代码,让我加班到凌晨两点。
那天我决定认真研究一下 OpenAI 的 Responses API——它宣称能自动管理对话状态。用了一周后,我把项目里的旧方案全部迁移了过去。这篇文章记录我踩过的坑和真实的迁移过程。
传统方案的痛点到底在哪
先说说我们原来的架构。基于 Chat Completions,每次请求都要传完整的对话历史:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是客服助手"},
{"role": "user", "content": "我上周下单的订单在哪"},
{"role": "assistant", "content": "请提供订单号"},
# ... 手动维护,越滚越长
]
)
这套方案有三个让人头疼的地方:
- 状态管理全靠手动。数据库里要存一份完整的 messages JSON,每次请求全量上传,第 50 轮对话时 token 消耗和延迟都很难看。
- 工具调用结果拼接繁琐。函数调用后要手动构造
role: "tool"的消息塞回数组,格式稍有差错就报 400。 - 多步骤任务(比如网页浏览、文件生成)需要自己编排循环,代码里全是状态机。
迁移实录:三步走
第一步:最简单的替换
Responses API 的基本调用长这样:
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-4o",
input="帮我查一下北京今天的天气"
)
print(response.output_text)
第一个惊喜:response.output_text 是个便捷属性,直接拿到最终文本,不用再遍历 choices[0].message.content。虽然是小事,但代码可读性确实好了不少。
第二步:状态管理(这是我要的核心)
之前那个“失忆” Bug 的解法在这里变得极其简单。第一次请求后拿到 response.id,后续请求直接引用:
# 第一轮
response = client.responses.create(
model="gpt-4o",
instructions="你是客服助手", # system prompt 有了专属字段,永远不会被误删
input="我的订单号是 A12345"
)
# 第二轮,只传新输入 + previous_response_id
response2 = client.responses.create(
model="gpt-4o",
previous_response_id=response.id,
input="刚才那个订单到哪了?"
)
服务端会自动把上下文拼好。我特意测了 30 轮长对话,记忆完好无损。之前那类 Bug 直接从代码里消失了。
注意:服务端默认保留状态 30 天。如果想完全不落盘,创建时加 store: false,但同时也就不能用 previous_response_id 了——这是个取舍。
第三步:工具调用的简化
这是第二个大惊喜。用内置工具时,连自己写执行循环都省了:
response = client.responses.create(
model="gpt-4o",
tools=[{"type": "web_search_preview"}],
input="OpenAI 最近发布了什么新模型?"
)
模型自己搜索、自己整合,一次调用搞定。以前用 Chat Completions 做网页浏览,我得自己写 fetch、截断网页内容、再拼回消息——大概 80 行代码。
自定义函数工具当然还是需要你自己执行,但返回结果的格式简化了:
response = client.responses.create(
model="gpt-4o",
input="北京现在几度?",
tools=[{
"type": "function",
"name": "get_weather",
"description": "查询城市天气",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}]
)
# 执行函数后,把结果送回去
response2 = client.responses.create(
model="gpt-4o",
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": response.output[0].call_id,
"output": '{"temp": "25°C"}'
}]
)
迁移过程中踩的坑
诚实地说,迁移不是一帆风顺的:
坑 1:max_tokens 改名了。 新参数叫 max_output_tokens。我直接照搬旧参数,收到报错才反应过来。同样,temperature 等参数基本兼容,但一些细节行为有差异。
坑 2:响应结构完全不同。 response.output 是个列表,里面可能混合 message、function_call 等多种类型的 item。我在解析时一开始假设了固定顺序,结果遇到工具调用和文本混合输出的场景就崩了。正确做法是按 type 字段过滤:
for item in response.output:
if item.type == "message":
print(item.content[0].text)
坑 3:状态存储的隐私问题。 我们是医疗相关业务,法务团队对“对话内容存在 OpenAI 服务端 30 天”很有顾虑。最后的方案是敏感会话用 store: false + 自己加密存储,普通会话用 previous_response_id。不是所有场景都适合全盘迁移。
实用建议
- 不要一次性全量迁移。我先用一个低风险的内部工具验证了两周,才动核心客服系统。
- 给
previous_response_id链加监控。如果用户清理了历史会话,引用失效的 ID 会报错,要做好降级(重新开一段对话)。 instructions字段好好利用。把系统提示词从 messages 里挪出来,语义更清晰,也避免了被截断逻辑误伤。- 长对话注意 token 上限。服务端拼上下文不等于无限上下文,超长链路依然要设计摘要或分段机制。
诚实评估
Responses API 不是银弹。它的状态管理把你锁在了 OpenAI 的存储上,多云架构的团队会犹豫;生态和社区资料目前也少于成熟的 Chat Completions,遇到冷门报错时 Google 出来的结果不多。
但对于我这种中小团队、单供应商、被手动状态管理折磨够了的人来说,这次迁移让核心编排代码减少了大约 40%,最难缠的那类 Bug 直接根除。这个收益,值得。