Assistants API 八月下线:别只换接口,五层状态迁移才是 Responses API 最容易翻车的地方
上个月一个做知识库问答的朋友把一段报错甩给我用户刚上传完合同模型却像失忆一样重新追问文件在哪更糟的是客服在后台补了一条人工备注下一轮回答又把它当成从没发生过。代码里没有明显异常Assistants API 的 thread、run、file search 都还在业务却开始出现一种最难排查的“半失忆”。我们把问题追到最后发现他为了赶进度把业务会话、模型会话、工具执行状态全塞进了 thread。平时确实省事新消息往 thread 里追加创建 run等它完成再从 messages 里取答案。可一旦要做权限切换、审计、重放或者换一套接口这个看似整齐的抽象就会把所有边界缠在一起。Assistants API 将在 2026 年 8 月 26 日关闭。真正需要迁走的并不是几个 endpoint也不是把assistant_id改成一个新的模型名。麻烦在于过去由 thread 和 run 悄悄代管的状态现在要被团队重新看见、重新命名、重新放回自己的系统里。这篇不写“十分钟迁移教程”。那类教程最容易让人误判工作量请求能返回 200不代表应用已经迁完。下面我按真实工程里最容易漏掉的五层状态来拆对话、指令、工具、文件、运行记录。最后给一套能逐步替换、又不拿真实用户当测试数据的迁移骨架。别把旧接口的对象名原样搬过去Assistants API 的诱惑在于它替开发者打包了很多东西。你创建一个 assistant配置指令、模型和工具为每个用户建立 thread把消息塞进去创建 run平台再帮你推进模型调用、工具调用和结果写回。原型阶段这比自己维护一套状态机轻松得多。可它也会制造一个错觉好像“对话”天然就应该归模型供应商保存。实际上业务系统里至少同时存在三种不同的连续性。用户连续性这个人是谁权限是什么历史订单能看哪些。任务连续性这次是在改合同、查库存还是在跑一次数据修复。模型连续性上一轮模型看过什么、调用过什么、准备接着推理什么。旧架构里这三件事常常被一个thread_id勉强绑住。Responses API 的迁移价值不只是换成一个更现代的调用形式而是逼着你把它们拆开用户与任务属于你的数据库模型上下文只是一段可以续接、也可以丢弃的计算状态。这一步听起来像架构洁癖到了故障现场才知道它有多实际。比如某个用户从普通员工升成管理员你不应该因为一个旧 thread 还在就让新权限自动继承旧上下文里的敏感检索结果。再比如客服希望重放一条回答真正需要保存的是“当时的输入、工具返回、模型版本、提示词版本”不是笼统地记一句“这个 thread 跑过”。迁移的第一条原则先把你真正拥有的状态列出来再决定哪些交给 API 保存。先做资产清单别先改 SDK我建议在写第一行新代码之前先把线上所有 assistant 拉成一张清单。不要只抄 ID。每一行至少标出它服务哪个产品、日均请求、是否带文件检索、有哪些函数、函数里是否存在写操作、是否保存过 thread、是否和外部工作流相连。许多团队到了这一步才发现看似一个聊天机器人背后其实混着客服问答、内部报表、工单创建和付款提醒四种风险完全不同的应用。然后再列 thread 的用途。有人拿它做用户长期聊天有人一张订单一个 thread有人甚至把一次批处理任务也塞进 thread。后两种在迁移时最容易出错因为业务任务结束后模型状态其实不一定要继续保留。把所有 thread 一股脑映射为previous_response_id只会把旧系统的混乱原封不动带过去。清单里还要留一列叫“可删除性”。用户注销后哪些业务记录需要删除哪些只需要脱敏哪些模型响应还在供应商侧存储期内团队必须在迁移前对齐。过去不清楚并不代表不存在只是旧接口把它藏得更深。迁移是补齐数据边界的好时机而不是把历史债务压缩成一段兼容代码。这份清单不会直接让产品更酷却会决定你是否能在八月前完成。没有它工程师只能在每个报错出现时临时问“这个 assistant 是谁在用”时间会被沟通耗掉真正的代码反而不是瓶颈。第一层对话状态不要再只剩一个 thread_idResponses API 支持用previous_response_id接续上下文。这个字段很方便但不要把它误当成业务会话主键。更稳妥的做法是在自己的表里维护一个“业务会话 → 最近模型响应”的映射同时保留自己的消息副本和版本信息。一个最小的会话记录至少应有这些字段conversation_id、user_id、task_type、latest_response_id、prompt_version、policy_version、updated_at。前面三个解决业务归属后四个是以后排错、灰度和审计时能救命的东西。很多人迁移时会直接把每一轮完整聊天记录再次拼进input。小流量时没问题量一大就会出现两笔成本token 一轮轮膨胀提示词里的旧规则也越来越难清理。用previous_response_id能让模型保持连续但它不替你做权限校验也不替你保证历史指令仍然适用。这里有个很容易被忽略的细节当你用previous_response_id续接时上一轮的 instructions 不会自动当作永久系统规则继承。换句话说团队必须明确选择每轮都带上当前的开发者指令还是把规则版本固定在业务会话上。我的建议是前者——每轮携带当前生效的关键规则并把版本写进日志。规则变了下一轮才有机会真的变。这不是多写几行代码而是把“模型记得什么”从黑盒改成了可管理的资产。你可以在用户注销时删除业务会话映射可以在发现提示词污染时从一个干净的响应重新开始也可以把高风险任务切到不保存上下文的短会话而不用改动整个产品。更重要的是业务会话和模型连续性应该允许一对多。一个客服工单可能先经过检索问答再进入人工补充最后由模型生成回复。业务侧它始终是同一张工单模型侧却可以因为权限变更、上下文过长或提示词升级而重新起一段 response 链。把这层关系设计清楚后面换模型、做 A/B 对照都不会牵一发动全身。第二层指令是产品配置不是散落在代码里的长字符串迁移中最容易被低估的是 instructions。旧 assistant 往往在创建时绑了一段很长的系统提示词里面既有语气、格式也混着合规边界、工具使用条件和业务例外。新调用如果只迁一个模型名和用户输入表面当然能出答案质量却会像突然换了一个客服。先把指令拆成三类稳定的角色约束、随用户权限变化的业务规则、随任务变化的上下文。稳定规则可以按版本管理权限规则应该由服务端实时拼装任务上下文只在本次调用传入。这样某项政策更新时你不会因为还有五十个历史 assistant 存着旧指令而漏改。不要把提示词版本号只写在 Git 提交信息里。每次请求把prompt_version放进结构化日志才有可能回答“为什么上周同一个问题还能查到今天查不到”。很多所谓模型波动后来追出来其实只是一个隐藏分支改了工具描述或者权限提示词没有跟着产品配置更新。模型输出可以有随机性产品规则不能靠回忆。这就是把指令从一段文本升级为可发布配置的原因。先加一层迁移适配不要把新旧逻辑搅在同一处实际落地时我会在业务服务和 SDK 之间放一个很薄的适配层。它不负责重新发明 Agent 框架只做四件事把业务会话解析成当前模型上下文统一写入调用记录把工具定义交给现有业务服务把最终 response 映射回产品需要的消息格式。旧 Assistants 路径和新 Responses 路径都经过这层产品页面、权限系统和工单系统就不用同时改两次。这样做的价值是让迁移变成可比较的替换而不是大规模重构。相同的用户问题可以先经过两条实现适配层记录两边的耗时、工具调用和引用文件。差异出现时工程师不必在前端、数据库、提示词、SDK 四个地方同时猜先看适配层的统一事件就能知道是输入被改了、工具结果不同还是模型输出不同。适配层还应该明确“谁有资格续接上下文”。例如用户换了租户、后台客服转交了工单、权限组被收紧业务服务可以主动不给previous_response_id重新创建干净的模型链而不是把历史记忆默认带到新的边界里。这种主动切断在旧 thread 模型里往往被忽略在新接口下反而更容易成为可读的业务规则。上图表达的不是“旧接口和新接口谁更高级”而是责任边界的变化旧的 assistant、thread、run 把连续对话和执行编排包进一个黑箱新的 response loop 要求你把用户、权限、工具和日志放在外圈。刚开始代码更多出问题时却少得多。第三层工具调用从 run 生命周期回到你的业务事务Assistants API 的 run 有一个很强的心理暗示模型开始运行、卡在requires_action、工具提交输出、最终 completed流程看上去像平台已经替你托管了。迁移后函数调用会更直接地出现在 response 输出项里。表面上少了一个对象实际上是把工具调度权交回给了你。这恰恰是应该接住的部分。订单查询、退款、发券、写库这类工具不能只因为模型给了一个合法 JSON 就立即执行。它们本质上都是业务事务应该经过参数校验、权限判定、幂等键、审计记录再把结果作为function_call_output送回模型。下面这段骨架省略了数据库实现但把边界留清楚了。重点不在 SDK 写法而在于工具调用和业务执行之间必须有你自己的服务层。import OpenAI from openai; const client new OpenAI(); const tools [{ type: function, name: lookup_order, description: 查询当前用户有权限查看的订单, parameters: { type: object, properties: { order_no: { type: string } }, required: [order_no], additionalProperties: false } }]; async function ask(conversation, userText) { const response await client.responses.create({ model: gpt-5, instructions: currentInstructions(conversation.policy_version), previous_response_id: conversation.latest_response_id || undefined, input: userText, tools }); const calls response.output.filter(x x.type function_call); const outputs []; for (const call of calls) { const args JSON.parse(call.arguments); const result await lookupOrderWithPolicy({ userId: conversation.user_id, orderNo: args.order_no, idempotencyKey: call.call_id }); outputs.push({ type: function_call_output, call_id: call.call_id, output: JSON.stringify(result) }); } if (outputs.length 0) return response; return client.responses.create({ model: gpt-5, previous_response_id: response.id, instructions: currentInstructions(conversation.policy_version), input: outputs, tools }); }这个循环里call.call_id很适合作为一次工具执行的关联标识但不要把它当成天然的数据库幂等键就结束了。真正写库的操作还应当绑定业务侧的请求 ID同一个用户连续点两次“确认退款”模型即使生成两个调用也应该由业务规则决定能不能发生两次。很多迁移事故都发生在这里。测试环境只跑了查询工具于是大家以为 function calling 已经通了上线后第一次遇到扣款、库存锁定、邮件发送才发现 run 时代隐藏的重试语义不见了业务服务也没有自己的去重和审计。工具 schema 也不要只放在一个常量文件里。它和数据库字段、接口权限一样会演化参数改名、枚举新增、描述调整都会影响模型选择。为 schema 加版本号在日志中保留调用时的版本当你需要比较新旧模型的工具选择质量时才能避免把 schema 变化误判为模型退化。第四层文件与检索不是附件而是一套可追溯的数据产品知识库应用最容易在迁移时丢东西。旧项目往往只有一个 assistant 配置里面挂着 file search文件是谁上传的、何时生效、覆盖了哪个版本、被哪次回答引用过信息散在后台页面、thread 附件和业务库的不同角落。迁移前先做一次文件资产盘点不要急着上传。按“原始文件、解析版本、向量库归属、业务权限、有效期”五列导出清单。文件本身和可检索性不是一回事同一份 PDF 可能仍要保留原件但旧版本不应再进入新的向量检索同一个向量库也不能默认对所有租户开放。Responses API 可以通过file_search连接向量库但你仍然需要一张自己的映射表tenant_id、knowledge_base_id、vector_store_id、document_version、active。模型请求只拿到经过业务层筛过的 vector store。这样用户问“合同第七条”时检索范围不是由模型猜而是由产品权限确定。还有一个常被忽视的指标命中并不等于正确。迁移期间要抽样保存检索结果的文件 ID、片段、相似度或引用信息再和最终回答绑定。以后业务方说“AI 编的”工程师才能判断问题出在文件没同步、召回错了还是模型误读了正确片段。同步策略也该单独验收。给文件上传一套可见状态原件已接收、解析完成、索引完成、权限生效、旧版本下线。别让“上传成功”成为唯一状态它只说明二进制到了某个地方不说明用户已经能在正确权限下被检索到。实际产品里最危险的不是文件不存在而是文件半可用时模型已经开始回答。文件迁移最怕“数据搬过去了所以功能等价”的幻觉。真正等价的判断是同一个用户、同一个权限、同一个问题在新旧路径上是否检到同一批有效材料并给出可接受的答案。第五层run 消失后运行记录反而要更完整以前打开一个 run就能看到 queued、in_progress、requires_action、completed 或 failed。很多团队把这当成了观测能力。其实那只是平台层状态一旦请求跨越网关、队列、多个业务工具和人工审批你需要的是一条自己的端到端轨迹。建议为每次用户任务生成task_id再让它贯穿 API 调用、工具执行和业务日志。每一轮至少保存请求时间、模型与参数、输入摘要、response ID、工具名、工具参数摘要、工具耗时、工具结果摘要、最终输出、错误类别。敏感原文不一定要全部落日志但关联关系不能没有。这样做的直接收益是你不必靠猜来回答三个问题这次回答慢到底慢在模型还是慢在库存系统同一条工具为什么执行两次某个答案引用的是今天更新的制度还是三个月前的旧文件如果这些问题只能去供应商控制台和自家日志各翻一遍迁移根本没有完成。Responses API 本身提供了更直接的输出项和追踪能力但不要把“看得到 response ID”误当成可运营。运营需要能按用户、任务、版本和工具聚合工程需要能从一个投诉反查到一轮具体调用安全团队需要知道一条敏感数据究竟经过了哪个远程工具。这些索引只能由应用自己建立。为了避免日志变成另一座垃圾山我通常会把“可检索的事件字段”和“受控保存的原始内容”分开。前者用于日常看板和告警例如耗时、模型、工具、状态码后者只在有权限的排障场景读取。这样既不会因为完全不留证据而无法复盘也不会因为把所有输入输出无限堆积而制造新的数据治理问题。一套不打断业务的迁移顺序真正迁的时候我更推荐按能力拆而不是找一个周末把所有 endpoint 替换掉。顺序的核心是先让新路径看见旧路径的真实输入和结果再让少量低风险任务经过新路径最后才把旧对象退场。先盘点列出所有 assistant、thread、工具、文件库与调用量。先建表补齐业务会话、响应映射、工具审计和文件映射。先做纯问答不带写操作让新旧路径对同一输入做对照。再接查询工具把权限、参数校验和幂等放进业务服务。最后接写操作退款、发信、写库必须单独验收每条事务。对照时别只看“最终文案像不像”。至少要同时比较四件事首 token 和总耗时、工具调用次数、检索命中的文件版本、业务错误率。新模型可能回答更好却多调了两次高成本工具新检索可能更快却漏掉了被权限正确允许的附件。没有这些数据所谓迁移成功只是一种感觉。验证样本也要从真实问题里来。挑二十条生产中出现过的典型请求简单问答、带文件检索、多轮追问、工具查询、权限边界、异常输入各占一些。把期望结果写成可观察条件而不是“回答看起来不错”。例如订单号不存在时不能编造越权用户不能看到订单字段文件更新后旧条款不能再被引用一个写操作在网络抖动下不能重复提交。这批样本以后就是你的回归集。模型升级、工具 schema 改动、提示词调整都能再跑一遍。没有回归集团队每次发版都只能靠人工在聊天框里试两句某个边缘任务悄悄坏掉也没人知道。还有一个工程上的小建议给每个提示词、工具 schema、知识库配置都加版本号。Responses API 让组合方式更灵活也意味着上线后变动更频繁。没有版本号三周后你会面对一条无法复现的投诉同一句问题当时到底用了哪个系统提示词、哪版函数参数、哪组文件我见过最省时间的团队不是写代码最快的团队而是一开始就承认“迁移的是一个运行中的产品不是一段 SDK 示例”。他们先把边界画清楚后来加一个工具、换一个模型、收紧一条权限都不用再去猜 thread 里藏了什么历史。真正该带走的不是旧对象而是控制权Assistants API 的退场并不意味着过去的设计一无是处。它让很多团队快速验证了 Agent 产品的可能性也替早期项目省掉了不少状态管理工作。但当应用开始接入真实用户、真实文件和真实事务状态总会回到你手里——只是早一点正视和晚一点被事故逼着正视的区别。所以这次迁移最值得保留的成果不是一个新的responses.create调用而是一张能说清楚责任的架构图用户状态在业务库模型上下文有明确映射工具执行走业务事务文件检索受权限约束运行记录可以完整追溯。接口会继续更新模型会继续换代。可只要这五件事还在自己手上下一次变更就不会再是一场把所有状态塞进黑盒、然后祈祷它们自己长好的迁移。资料OpenAI Assistants API deep dive、OpenAI Responses API quickstart。