Чтобы перейти с 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 API | Responses API | Кто теперь отвечает | Проверка паритета |
|---|---|---|---|
| Assistant: instructions, model, tools | responses.create() + конфигурация приложения | Репозиторий и deploy config | Та же политика ответа и тот же набор разрешенных tools |
| Thread | Conversation, previous_response_id или история в вашей БД | Вы выбираете один вариант на маршрут | Повторный запрос видит нужный контекст, но не чужой |
| Message | input и output items | Приложение или Conversation | Роли, текст, изображения и tool items не потеряны |
| Run | Один Response и, при необходимости, следующий вызов | Явная функция-оркестратор | Есть timeout, лимит раундов и наблюдаемая ошибка |
requires_action | function_call → function_call_output | Ваш backend исполняет custom function | call_id возвращается без изменения |
| Retrieval / File Search | Hosted file_search tool + vector store | OpenAI выполняет поиск; вы проверяете корпус | Есть file_search_call, цитаты и ожидаемые документы |
| Run polling | Streaming или background mode для долгих задач | Клиент/worker | Обрыв соединения и повторный запрос не дублируют действие |
Официальная инструкция по миграции Assistants полезна как карта объектов, а журнал deprecations Assistants API задает предельную дату. Отдельно проверьте deprecation reusable prompts, чтобы не закончить один перенос началом следующего.
Сначала зафиксируйте старый контракт
До изменения кода выгрузите не сами секреты, а наблюдаемое поведение:
- Версию инструкций и список tools с JSON Schema.
- Набор тестовых диалогов: обычный ответ, несколько ходов, один и несколько tool calls, отказ инструмента, пустой поиск и длинная задача.
- Ожидаемые обязательные факты и запрещенные действия.
- P50/P95 latency, количество model calls, input/output tokens и стоимость на сценарий.
- Правила хранения, удаления, tenant isolation и журналирования.
Такой golden set важнее сравнения текста слово в слово. Модель может сформулировать правильный ответ иначе. Проверяйте факты, вызванный tool, аргументы, side effect, цитаты, состояние и границы доступа.
Выберите владельца состояния
Responses API предлагает три рабочих схемы. Не смешивайте их случайно в одном маршруте.
| Схема | Когда подходит | Цена и хранение | Главный риск |
|---|---|---|---|
| Durable Conversation | Нужен серверный идентификатор диалога между сессиями и устройствами | Items сохраняются в Conversation; они не ограничены обычным 30-дневным TTL Response | Нужно привязать conversation id к tenant и политике удаления |
previous_response_id | Короткая последовательная цепочка, где удобна ссылка на прошлый Response | Response 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.
pythonimport 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.
pythonimport 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_search | OpenAI внутри Responses | Специфические output items, citations, доступ к данным, ошибки и поддержку выбранной моделью |
| Remote MCP | Responses обращается к указанному MCP server; результат виден как mcp_call | server_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.
pythonimport 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. Безопасная очередность такая:
- Заморозить создание новых Assistants и зафиксировать конфигурацию.
- Включить Responses только для новых сессий через feature flag.
- Старые Threads оставить на чтение в прежнем пути до завершения активных сессий.
- Переносить историю старого Thread только когда пользователь реально его открывает.
- Преобразовывать сообщения и 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.
| Сигнал | Go | No-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, находит правильные документы и может быть безопасно остановлен по заранее заданному сигналу.



