Assistants API将停止服务:Python迁移Responses API实战
凌晨两点,线上客服机器人仍在不断创建 Thread、启动 Run、轮询状态。日志没有报错,接口也能正常返回,但这套代码已经进入倒计时。
OpenAI 已明确宣布:Assistants API 将于 2026年8月26日停止服务。它不是一次普通的 SDK 方法改名,而是把原来的 Assistant、Thread、Run 模型改成 Prompt、Conversation、Response 和 Item。
官方迁移指南:
https://developers.openai.com/api/docs/assistants/migration
如果项目里还能搜到下面这些调用,现在就应该开始处理:
client.beta.assistants client.beta.threads client.beta.threads.messages client.beta.threads.runs一、先理解变化:迁移的不是一个接口
新旧对象可以这样对应:
| Assistants API | Responses API体系 | 主要变化 |
|---|---|---|
| Assistant | Prompt或请求配置 | 模型、指令、工具配置不再依赖Assistant对象 |
| Thread | Conversation | 不只保存消息,还可以保存工具调用和输出等Item |
| Run | Response | 输入与输出结构更直接,不再依赖Run轮询读取结果 |
| Run step | Item | 消息、函数调用、工具输出都以不同Item类型存在 |
官方迁移指南将其概括为“输入Items,返回输出Items”。其中最容易踩坑的是:Thread不能直接当作Conversation ID继续使用。
官方目前没有提供自动迁移全部Thread的工具,建议让新会话直接进入Conversations,旧会话再按实际访问需求回填,而不是一次性迁移所有历史数据。
二、旧代码为什么不能只改方法名
典型的Assistants API调用通常需要四步:
importosimporttimefromopenaiimportOpenAI client=OpenAI()thread=client.beta.threads.create()client.beta.threads.messages.create(thread_id=thread.id,role="user",content="帮我检查这段代码中的并发问题",)run=client.beta.threads.runs.create(thread_id=thread.id,assistant_id=os.environ["OPENAI_ASSISTANT_ID"],)whilerun.statusin("queued","in_progress"):time.sleep(1)run=client.beta.threads.runs.retrieve(thread_id=thread.id,run_id=run.id,)messages=client.beta.threads.messages.list(thread_id=thread.id,order="desc",limit=1,)print(messages.data[0].content)这段代码把“存储消息”“启动任务”“查询任务状态”“读取结果”拆成了多个对象。
迁移到Responses API后,同一个同步请求可以直接返回Response;SDK还提供了output_text辅助属性,不必假定文本一定在output[0].content[0]中。
Responses API迁移说明:
https://developers.openai.com/api/docs/guides/migrate-to-responses
三、先跑通最小Responses调用
升级Python SDK:
python-mpipinstall-Uopenai配置环境变量:
exportOPENAI_API_KEY="你的API Key"exportOPENAI_MODEL="你的项目准备使用的模型ID"最小调用代码:
importosfromopenaiimportOpenAI client=OpenAI()response=client.responses.create(model=os.environ["OPENAI_MODEL"],instructions="你是一名代码审查助手,只指出可以复现的问题。",input=[{"role":"user","content":"请检查这段Python代码是否存在并发安全问题。",}],)print(response.output_text)第一轮改造建议保持原模型、原指令和原业务输入不变,只替换API编排方式。否则同时更换模型、提示词和接口,一旦输出发生变化,很难判断是哪项修改造成的。
四、多轮会话:用Conversation替代Thread
创建Conversation:
conversation=client.conversations.create(metadata={"user_id":"user-42"})print(conversation.id)发送第一轮消息:
response=client.responses.create(model=os.environ["OPENAI_MODEL"],conversation=conversation.id,instructions="你是一名Python代码审查助手。",input=[{"role":"user","content":"解释一下什么是竞态条件。",}],)print(response.output_text)第二次请求继续传入同一个conversation.id:
response=client.responses.create(model=os.environ["OPENAI_MODEL"],conversation=conversation.id,instructions="你是一名Python代码审查助手。",input=[{"role":"user","content":"结合刚才的解释,再给一个线程安全的修改示例。",}],)print(response.output_text)Conversation可以保存消息、工具调用和工具输出等Item,其定位比只保存消息的Thread更宽。
Conversations API文档:
https://developers.openai.com/api/reference/resources/conversations/methods/create
如果项目只是短链式对话,也可以使用previous_response_id:
first=client.responses.create(model=os.environ["OPENAI_MODEL"],input="解释Python中的竞态条件。",store=True,)second=client.responses.create(model=os.environ["OPENAI_MODEL"],input="给一个线程锁修复示例。",previous_response_id=first.id,store=True,)不过需要注意:使用previous_response_id并不代表旧输入不再计费,官方说明响应链中的历史输入Token仍会作为输入计算。对于需要长期绑定用户会话的系统,Conversation通常更容易管理。
五、不要把SDK调用散落在业务代码里
比较稳妥的做法是增加一层适配器,让业务代码只认识start()和ask()。
fromdataclassesimportdataclassfromtypingimportAny@dataclass(frozen=True)classReply:text:strresponse_id:strconversation_id:strclassResponsesChat:def__init__(self,client:Any,model:str,instructions:str,)->None:self.client=client self.model=model self.instructions=instructionsdefstart(self,user_id:str)->str:conversation=self.client.conversations.create(metadata={"user_id":user_id})returnconversation.iddefask(self,conversation_id:str,user_text:str,)->Reply:response=self.client.responses.create(model=self.model,conversation=conversation_id,instructions=self.instructions,input=[{"role":"user","content":user_text,}],)returnReply(text=response.output_text,response_id=response.id,conversation_id=conversation_id,)业务层只保存自己的session_id与OpenAI conversation_id之间的关系:
importosfromopenaiimportOpenAI client=OpenAI()chat=ResponsesChat(client=client,model=os.environ["OPENAI_MODEL"],instructions="你是一名严谨的技术支持助手。",)conversation_id=chat.start(user_id="user-42")reply=chat.ask(conversation_id=conversation_id,user_text="为什么我的异步任务会重复执行?",)print(reply.text)生产环境不要使用内存字典保存映射。应该写入数据库或Redis,并至少保留以下字段:
business_session_id provider conversation_id created_at updated_at migration_status这样既能避免应用重启后丢失会话,也方便灰度期间同时识别旧Thread和新Conversation。
六、旧Thread怎么回填
官方建议优先让新会话使用Conversation,旧Thread按需回填。下面是只迁移纯文本消息的简化版本:
fromopenaiimportOpenAI client=OpenAI()defbackfill_text_thread(thread_id:str)->str:messages=[]forpageinclient.beta.threads.messages.list(thread_id=thread_id,order="asc",).iter_pages():messages.extend(page.data)items=[]formessageinmessages:text_parts=[content.text.valueforcontentinmessage.contentifcontent.type=="text"]ifnottext_parts:continuecontent_type=("input_text"ifmessage.role=="user"else"output_text")items.append({"role":message.role,"content":[{"type":content_type,"text":"\n".join(text_parts),}],})conversation=client.conversations.create(items=items)returnconversation.id这段代码只处理文本。旧Thread中如果含有图片、文件引用、函数调用、工具输出或引用标注,必须分别转换,不能静默丢弃。
更稳妥的策略是:
- 新用户会话全部创建Conversation。
- 最近仍然活跃的Thread按需回填。
- 长期未访问的历史Thread只归档,不主动迁移。
- 首次访问旧会话时执行迁移并记录新旧ID。
- 对迁移失败的数据保留原始Thread ID和错误日志。
七、给适配层补一个不消耗Token的单元测试
下面的测试使用伪客户端,不需要真实API Key,主要验证请求参数和返回值映射:
fromtypesimportSimpleNamespaceimportunittestclassRecorder:def__init__(self,result):self.result=result self.calls=[]defcreate(self,**kwargs):self.calls.append(kwargs)returnself.resultclassFakeClient:def__init__(self):self.conversations=Recorder(SimpleNamespace(id="conv_test"))self.responses=Recorder(SimpleNamespace(id="resp_test",output_text="迁移成功",))classResponsesChatTest(unittest.TestCase):deftest_start_and_ask(self):client=FakeClient()chat=ResponsesChat(client,"model-from-env","只回答技术问题",)conversation_id=chat.start("user-42")reply=chat.ask(conversation_id,"如何迁移?")self.assertEqual(conversation_id,"conv_test")self.assertEqual(reply.text,"迁移成功")self.assertEqual(client.responses.calls[0]["conversation"],"conv_test",)self.assertEqual(client.responses.calls[0]["input"],[{"role":"user","content":"如何迁移?",}],)if__name__=="__main__":unittest.main()运行命令:
python-munittest-v预期结果:
test_start_and_ask ... ok Ran 1 test OK我对上面的适配器与测试做了本地验证,测试可以通过。验证范围是参数构造、会话ID传递和文本结果映射;真实网络调用仍需要项目自己的API Key、可用模型、工具配置及账户权限。
八、Prompt迁移不能只复制一段instructions
旧Assistant通常还包含:
- 模型ID;
- Instructions;
- File Search或Code Interpreter;
- 自定义函数Schema;
- 响应格式;
- 温度等生成配置。
官方迁移指南支持在控制台中把Assistant转换为Prompt,并通过下面的形式调用:
response=client.responses.create(prompt={"id":os.environ["OPENAI_PROMPT_ID"],},conversation=conversation_id,input=[{"role":"user","content":"检查这段代码。",}],)不过,当前迁移文档同时提醒开发者关注可复用Prompt对象的弃用时间线。长期项目不要把全部业务逻辑绑定在单一配置对象上,建议继续保留自己的适配层和配置版本号,以便后续替换。
九、上线时用双实现和开关控制
不要在一次发布中删除旧实现。可以保留两个后端:
importosdefbuild_chat_backend(client):backend=os.getenv("AI_BACKEND","assistants",)ifbackend=="responses":returnResponsesChat(client=client,model=os.environ["OPENAI_MODEL"],instructions="你是一名技术支持助手。",)returnAssistantsChat(client=client,assistant_id=os.environ["OPENAI_ASSISTANT_ID"],)推荐的切换顺序:
- 盘点所有assistant_id、Thread存储位置和工具调用。
- 用统一适配层包住新旧实现。
- 内部测试账号先切到Responses。
- 对固定测试集做影子请求,不把影子结果返回用户。
- 比较答案正确性、工具调用参数、延迟、Token用量和错误率。
- 按1%、10%、30%、100%逐步扩大流量。
- 在确认稳定前保留旧实现和回滚开关。
- 完成切换后再停止创建新Thread。
如果新接口异常,只需调整环境变量并重新部署:
exportAI_BACKEND="assistants"回滚只能作为迁移期间的临时方案,因为Assistants API到期后旧接口将不再是有效退路。
十、回归测试不要只比较文案是否一样
模型输出具有非确定性,逐字比较很容易误报。更有价值的测试指标包括:
| 测试项 | 检查方法 |
|---|---|
| 基础问答 | 是否包含必要事实与结论 |
| 多轮上下文 | 第二轮能否引用第一轮信息 |
| 工具调用 | 函数名称、参数和调用次数是否正确 |
| 结构化输出 | JSON是否符合Schema |
| 文件检索 | 是否引用正确文件及片段 |
| 异常处理 | 超时、限流、工具报错是否可恢复 |
| 数据隔离 | 不同用户是否绑定不同Conversation |
| 成本变化 | 记录输入、输出和总Token |
| 延迟变化 | 对比P50、P95与超时率 |
尤其要检查工具循环。Assistants API中的Run会替应用管理一部分执行过程,而Responses体系下,开发者需要更明确地处理工具调用与工具输出。只验证普通聊天成功,并不能证明业务已经迁移完成。
十一、别混淆API迁移和ChatGPT会员
这次改造解决的是API项目兼容性,不等同于ChatGPT Plus订阅。团队如果还长期使用ChatGPT Plus、Claude Pro、Gemini Advanced等工具,可以把gpt985作为第三方AI会员充值平台了解;使用前仍要看清套餐说明、账号要求、到账说明和售后规则。工程迁移本身则应直接依据API官方文档完成。
十二、迁移检查清单
- 代码中已定位全部client.beta调用;
- 新会话不再创建Thread;
- 业务会话已映射到Conversation;
- 输出读取改为response.output_text或遍历typed output;
- 自定义函数调用完成适配;
- File Search、Code Interpreter等工具单独验收;
- 旧Thread制定了按需回填方案;
- Conversation ID持久化到数据库或Redis;
- 完成多用户数据隔离测试;
- 监控错误率、延迟和Token用量;
- 保留灰度开关和迁移期回滚方案;
- 团队已记录2026年8月26日停用节点。
Assistants API迁移最危险的做法,是看到旧代码还能运行,就继续把改造排到以后。
真正稳妥的处理方式,是先建立适配层,让新会话进入Responses API,再迁移工具和必要的历史数据,最后用回归测试、灰度流量与监控完成切换。这样即使输出结构或会话逻辑出现差异,也能在影响全部用户之前发现问题。