Перейти к основному содержанию

Миграция с OpenAI Assistants API на Responses API: состояние, tools и откат

12 мин чтенияAPI Гайды

Безопасный переход — это не замена endpoint. Нужно вынести инструкции в версионируемый код, выбрать владельца состояния, реализовать явный цикл tools, проверить File Search и перевести новые сессии через feature flag.

Схема миграции: Assistant, Thread, Run и Tool переходят в код приложения, Responses, Conversation и явный tool loop с контрольной точкой отката

Чтобы перейти с Assistants API на Responses API без потери поведения, не пытайтесь механически преобразовать каждый Assistant и Thread в новый серверный объект. Зафиксируйте текущий контракт, перенесите инструкции в код приложения, выберите одну модель состояния, реализуйте явный цикл custom tools и сначала направьте в новый путь только новые диалоги.

Критический срок для Assistants API — 26 августа 2026 года: после этой даты API будет отключен. При этом текущая страница миграции еще показывает соответствие Assistant → Prompt, но reusable prompt objects уже отдельно deprecated с 3 июня 2026 года, а /v1/prompts будет отключен 30 ноября 2026 года. Поэтому новый долгоживущий код не должен зависеть от этого промежуточного слоя: храните системные инструкции в репозитории и отправляйте их через instructions и input.

Граница статьи — миграция официального OpenAI Assistants API. Она не покрывает переход с Chat Completions, а также Azure OpenAI, Yandex Cloud и сторонние OpenAI-compatible gateways: у них другие endpoints, версии, хранение и набор поддерживаемых tools.

Что именно заменить

Assistants APIResponses APIКто теперь отвечаетПроверка паритета
Assistant: instructions, model, toolsresponses.create() + конфигурация приложенияРепозиторий и deploy configТа же политика ответа и тот же набор разрешенных tools
ThreadConversation, previous_response_id или история в вашей БДВы выбираете один вариант на маршрутПовторный запрос видит нужный контекст, но не чужой
Messageinput и output itemsПриложение или ConversationРоли, текст, изображения и tool items не потеряны
RunОдин Response и, при необходимости, следующий вызовЯвная функция-оркестраторЕсть timeout, лимит раундов и наблюдаемая ошибка
requires_actionfunction_callfunction_call_outputВаш backend исполняет custom functioncall_id возвращается без изменения
Retrieval / File SearchHosted file_search tool + vector storeOpenAI выполняет поиск; вы проверяете корпусЕсть file_search_call, цитаты и ожидаемые документы
Run pollingStreaming или background mode для долгих задачКлиент/workerОбрыв соединения и повторный запрос не дублируют действие

Официальная инструкция по миграции Assistants полезна как карта объектов, а журнал deprecations Assistants API задает предельную дату. Отдельно проверьте deprecation reusable prompts, чтобы не закончить один перенос началом следующего.

Сначала зафиксируйте старый контракт

До изменения кода выгрузите не сами секреты, а наблюдаемое поведение:

  1. Версию инструкций и список tools с JSON Schema.
  2. Набор тестовых диалогов: обычный ответ, несколько ходов, один и несколько tool calls, отказ инструмента, пустой поиск и длинная задача.
  3. Ожидаемые обязательные факты и запрещенные действия.
  4. P50/P95 latency, количество model calls, input/output tokens и стоимость на сценарий.
  5. Правила хранения, удаления, tenant isolation и журналирования.

Такой golden set важнее сравнения текста слово в слово. Модель может сформулировать правильный ответ иначе. Проверяйте факты, вызванный tool, аргументы, side effect, цитаты, состояние и границы доступа.

Выберите владельца состояния

Responses API предлагает три рабочих схемы. Не смешивайте их случайно в одном маршруте.

СхемаКогда подходитЦена и хранениеГлавный риск
Durable ConversationНужен серверный идентификатор диалога между сессиями и устройствамиItems сохраняются в Conversation; они не ограничены обычным 30-дневным TTL ResponseНужно привязать conversation id к tenant и политике удаления
previous_response_idКороткая последовательная цепочка, где удобна ссылка на прошлый ResponseResponse objects по умолчанию хранятся 30 дней; предыдущие input tokens в цепочке все равно тарифицируютсяinstructions прошлой операции не переносятся автоматически
История в вашей БД + store=FalseНужен собственный retention, аудит или переносимостьВы отправляете нужные items каждый раз и управляете хранениемНельзя терять output items reasoning-моделей и результаты tools

Параметры conversation и previous_response_id взаимоисключающие. Если используете цепочку через previous_response_id, повторно передавайте instructions: они относятся к текущему Response, а не наследуются как скрытая конфигурация Assistant. Подробные гарантии описаны в руководстве OpenAI по conversation state.

Для регулируемых данных чаще удобнее собственная БД и store=False. Для многоканального продукта — Conversation с явной таблицей tenant_id → conversation_id. Для короткого workflow без отдельной истории — previous_response_id. Выбор должен пройти review по privacy, стоимости и удалению, а не только по количеству строк кода.

Один сценарий до и после миграции

Возьмем один и тот же контракт: пользователь спрашивает Где мой заказ A-104?, tool lookup_order принимает обязательный строковый order_id, а приложение должно вернуть подтвержденный статус. Ожидаемый результат — ровно один разрешенный вызов с A-104, возврат результата с исходным call_id и финальный ответ только на основании tool output. Миграция провалена при неизвестном tool, неверных аргументах, повторном side effect, потере call_id, смешении tenant или отсутствии финального ответа после MAX_TOOL_ROUNDS.

До: Assistant, Thread, Run и requires_action

Этот legacy-фрагмент нужен как parity harness до отключения Assistants. Тот же JSON Schema должен быть уже сохранен в Assistant с ID из OPENAI_ASSISTANT_ID.

python
import json import os from openai import OpenAI client = OpenAI() ASSISTANT_ID = os.environ["OPENAI_ASSISTANT_ID"] MAX_TOOL_ROUNDS = 5 LEGACY_TOOL_SCHEMA = { "type": "function", "function": { "name": "lookup_order", "description": "Return the current status of an order visible to this tenant.", "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": False, }, "strict": True, }, } def lookup_order(order_id: str, tenant_id: str) -> dict: return {"order_id": order_id, "status": "in_transit", "tenant_id": tenant_id} assistant = client.beta.assistants.retrieve(ASSISTANT_ID) matching_tools = [ tool for tool in assistant.tools if tool.type == "function" and tool.function.name == "lookup_order" ] if ( len(matching_tools) != 1 or matching_tools[0].function.parameters != LEGACY_TOOL_SCHEMA["function"]["parameters"] ): raise RuntimeError("Stored Assistant tool schema differs from migration contract") thread = client.beta.threads.create() client.beta.threads.messages.create( thread_id=thread.id, role="user", content="Где мой заказ A-104?", ) run = client.beta.threads.runs.create_and_poll( thread_id=thread.id, assistant_id=ASSISTANT_ID, ) for _ in range(MAX_TOOL_ROUNDS): if run.status == "completed": break if run.status != "requires_action": raise RuntimeError(f"Legacy Run failed with status={run.status}") outputs = [] for call in run.required_action.submit_tool_outputs.tool_calls: if call.function.name != "lookup_order": raise RuntimeError(f"Unsupported tool: {call.function.name}") args = json.loads(call.function.arguments) result = lookup_order(args["order_id"], tenant_id="tenant_demo") outputs.append({ "tool_call_id": call.id, "output": json.dumps(result, ensure_ascii=False), }) run = client.beta.threads.runs.submit_tool_outputs_and_poll( thread_id=thread.id, run_id=run.id, tool_outputs=outputs, ) else: raise RuntimeError("Legacy Run exceeded MAX_TOOL_ROUNDS") messages = client.beta.threads.messages.list(thread_id=thread.id, limit=1) print(messages.data[0].content[0].text.value)

До запуска сравните LEGACY_TOOL_SCHEMA с реально сохраненной конфигурацией Assistant. Этот фрагмент не обновляет production Assistant и не доказывает совпадение схемы сам по себе.

После: конфигурация приложения, Responses и application tool loop

Ниже минимальный, но полный цикл для application-managed history. Он использует переменную OPENAI_MODEL, сохраняет все output items и возвращает результат custom function с исходным call_id.

Код ниже согласован с текущими официальными примерами OpenAI, но в этом run не запускался с целевыми credentials и установленной у читателя версией SDK. Перед cutover выполните оба фрагмента в staging, проверьте доступность ресурсов и сравните один и тот же golden contract.

python
import json import os from openai import OpenAI client = OpenAI() MODEL = os.environ["OPENAI_MODEL"] MAX_TOOL_ROUNDS = 5 tools = [ { "type": "function", "name": "lookup_order", "description": "Return the current status of an order visible to this tenant.", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"} }, "required": ["order_id"], "additionalProperties": False, }, "strict": True, } ] def lookup_order(order_id: str, tenant_id: str) -> dict: # Replace with a tenant-scoped, read-only database query. return {"order_id": order_id, "status": "in_transit", "tenant_id": tenant_id} def run_agent(user_text: str, tenant_id: str) -> str: input_items = [{"role": "user", "content": user_text}] completed_call_ids: set[str] = set() for _ in range(MAX_TOOL_ROUNDS): response = client.responses.create( model=MODEL, instructions=( "Answer in Russian. Never invent order data. " "Use lookup_order when the user asks about an order." ), input=input_items, tools=tools, store=False, ) # Keep the complete output, including reasoning-related items. input_items.extend(response.output) calls = [item for item in response.output if item.type == "function_call"] if not calls: return response.output_text for call in calls: if call.call_id in completed_call_ids: raise RuntimeError(f"Duplicate side effect blocked: {call.call_id}") args = json.loads(call.arguments) if call.name != "lookup_order": result = {"error": f"Unsupported tool: {call.name}"} else: result = lookup_order( order_id=args["order_id"], tenant_id=tenant_id, ) completed_call_ids.add(call.call_id) input_items.append( { "type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result, ensure_ascii=False), } ) raise RuntimeError("Tool loop exceeded MAX_TOOL_ROUNDS") print(run_agent("Где мой заказ A-104?", tenant_id="tenant_demo"))

Custom function выполняет ваше приложение, не OpenAI. Поэтому авторизация, idempotency, timeout и audit log должны находиться вокруг фактического действия. Для write-инструмента храните call_id как idempotency key в БД, а не только в памяти процесса. Параметр ограничения встроенных tools не заменяет ваш MAX_TOOL_ROUNDS.

Официальное руководство по function calling показывает тот же протокол: модель возвращает function_call, приложение выполняет код, затем отправляет строковый function_call_output. Если модель вернула несколько calls, обработайте каждый или явно запретите параллельность в своем контракте.

Не смешивайте три контракта tools

Тип toolКто выполняет действиеЧто проверять при миграции
Hosted/built-in, например file_searchOpenAI внутри ResponsesСпецифические output items, citations, доступ к данным, ошибки и поддержку выбранной моделью
Remote MCPResponses обращается к указанному MCP server; результат виден как mcp_callserver_url/authorization, список разрешенных tools, передаваемые данные, mcp_approval_request и политика approvals
Custom functionВаше приложениеАргументы, tenant authorization, idempotency, выполнение кода и function_call_output с исходным call_id

По текущему руководству OpenAI по MCP и Connectors remote MCP использует tool type mcp; передача данных на remote server по умолчанию требует approval. Не переносите custom function loop на MCP механически и не отключайте approvals, пока не проверены server, данные и конкретные действия.

Перенесите File Search отдельно от custom tools

file_search — hosted tool: OpenAI выполняет поиск по vector store и возвращает результат в Response. Для него не нужен ваш function_call_output.

python
import os from openai import OpenAI client = OpenAI() response = client.responses.create( model=os.environ["OPENAI_MODEL"], instructions="Answer only from the indexed documents and cite the source.", input="Какой срок возврата указан в политике?", tools=[ { "type": "file_search", "vector_store_ids": [os.environ["OPENAI_VECTOR_STORE_ID"]], "max_num_results": 8, } ], include=["file_search_call.results"], ) print(response.output_text) for item in response.output: if item.type == "file_search_call": print(item.results)

До cutover проверьте не только наличие ответа:

  • новый vector store содержит все активные файлы, а удаленные документы действительно исключены;
  • фильтры metadata не смешивают tenants и версии;
  • в output есть file_search_call, а в тексте — корректные file citations;
  • минимум 20–50 контрольных вопросов находят тот же или лучший источник;
  • пустой поиск честно сообщает об отсутствии данных;
  • latency и стоимость укладываются в бюджет.

Точные параметры и формат результатов сверяйте с документацией File Search.

Новые диалоги сначала, старые Threads — по требованию

OpenAI не предоставляет автоматический перенос Threads в Conversations. Безопасная очередность такая:

  1. Заморозить создание новых Assistants и зафиксировать конфигурацию.
  2. Включить Responses только для новых сессий через feature flag.
  3. Старые Threads оставить на чтение в прежнем пути до завершения активных сессий.
  4. Переносить историю старого Thread только когда пользователь реально его открывает.
  5. Преобразовывать сообщения и tool items явно, сохраняя связь со старым thread_id.

Не импортируйте весь архив заранее без требования продукта: это увеличивает объем персональных данных, стоимость проверки и радиус ошибки. Если старый Thread содержит уже выполненные действия, не воспроизводите их как новые tool calls. Переносите их как историю или краткое проверенное резюме.

Streaming и долгие задачи

Run polling не нужно копировать буквально. Для интерактивного ответа используйте streaming и считайте завершением финальное событие, а не первый текстовый delta. Для действительно долгой операции можно включить background mode, сохранить response id и опрашивать статус. Background mode — отдельное решение, а не обязательная замена каждому Run.

При обрыве клиента backend должен знать, был ли tool уже выполнен. Сначала записывайте idempotency key и результат действия, затем безопасно продолжайте генерацию. Иначе повтор подключения может создать второй платеж, тикет или заказ.

Cutover: измеримый go/no-go

Перед переключением соберите отчет по одному и тому же golden set.

СигналGoNo-go / rollback
Функциональный паритетВсе обязательные факты и tools проходятПропущен tool, неверные аргументы или неподтвержденный факт
Side effectsНи одного дубля; tenant checks проходятЛюбое повторное или межтенантное действие
File SearchЦитаты ведут к ожидаемым файламИсточник отсутствует, устарел или принадлежит другому tenant
СостояниеКонтекст сохраняется в выбранном режимеПотеря инструкций, смешение сессий, невозможность удаления
НадежностьError rate и P95 не хуже согласованного бюджетаРост timeout/5xx выше stop threshold
ЭкономикаСтоимость на успешный сценарий в пределах бюджетаНеконтролируемый рост input tokens или tool rounds

Практичный rollout: 1% внутренних запросов, затем 5%, 25%, 50% и 100%. На каждом этапе сравнивайте Responses и Assistants в shadow-режиме только для read-only сценариев; write tools нельзя исполнять дважды.

Rollback должен быть подготовлен до первого процента:

  • один feature flag выбирает оркестратор для новой сессии;
  • выбранный путь закрепляется за сессией, чтобы не смешивать форматы состояния;
  • новый код пишет correlation id, старый thread_id и новый response_id/conversation_id;
  • отключение флага возвращает новые сессии на старый путь до 26 августа 2026 года;
  • данные Responses не удаляются автоматически в момент отката и доступны для расследования согласно вашей retention policy.

После отключения Assistants откат возможен только на ваш заранее сохраненный внутренний workflow, а не на закрытый endpoint. Поэтому финальный production cutover должен завершиться с запасом, а не вечером 25 августа.

Где проходит граница провайдера

Эта инструкция относится к официальному OpenAI API. OpenAI-совместимый JSON у gateway не доказывает совместимость Conversations, hosted File Search, background mode, retention или streaming events. Azure OpenAI, Yandex Cloud и другие платформы имеют собственные endpoints, версии, регионы, хранение и сроки — проверяйте их документацию и делайте отдельный parity run.

Для прямого OpenAI route также сначала проверьте страну, project key и отдельный API billing. Это разобрано в руководстве как получить OpenAI API key для русскоязычного пользователя. Если официальный route недоступен в фактической стране, остановитесь на границе контракта, а не маскируйте другого поставщика под OpenAI.

Итоговый чек-лист

  • В production больше не создаются Assistants, Threads и Runs.
  • Инструкции и tool schemas версионируются вместе с кодом.
  • Reusable Prompts не стали новой долгосрочной зависимостью.
  • Для каждого маршрута выбран один режим состояния.
  • Custom tool loop возвращает исходный call_id, ограничивает раунды и защищает side effects.
  • File Search проверен по документам, citations, filters и пустым результатам.
  • Новые сессии переключаются отдельно от старой истории.
  • Golden set, latency, tokens, стоимость и error rate сравниваются до и после.
  • Feature flag, session pinning, correlation ids и rollback runbook проверены на staging.
  • Финальное отключение Assistants запланировано заметно раньше 26 августа 2026 года.

Миграция завершена не тогда, когда первый responses.create() вернул HTTP 200, а когда новый путь сохраняет нужное состояние, вызывает ровно те tools, не повторяет side effects, находит правильные документы и может быть безопасно остановлен по заранее заданному сигналу.

#Assistants API#Responses API#OpenAI API#миграция API#function calling#File Search
Поделиться: