Задача подключить OpenAI API часто выглядит в бэклоге как небольшая интеграция на пару часов: отправить текст из Python, получить ответ модели и показать его пользователю. На локалке всё и правда собирается за вечер. Проблемы начинаются после запуска: пользователи задают несколько вопросов подряд, отправляют длинные сообщения и не хотят смотреть на пустой экран, пока модель готовит ответ. Стоит нескольким людям написать одновременно, и вместо ответов в логах уже лежит пачка 429 Too Many Requests. Заодно выясняется, что модель сама не помнит предыдущие запросы, JSON не всегда получается валидным, а расходы лучше отслеживать заранее, чтобы в конце месяца команда не обнаружила неожиданную сумму в биллинге.
В статье разберём, как подключить OpenAI API к Python через Responses API: безопасно настроим ключ, передадим контекст диалога, включим стриминг, получим типизированный ответ и соберём Telegram-бота, который не разваливается сразу после первого сообщения пользователя.
ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
Что даёт OpenAI API и чем отличается от ChatGPT
ChatGPT — готовое приложение с интерфейсом, историей переписки и пользовательскими настройками. OpenAI API — программный доступ к моделям: ваш код отправляет запрос, получает результат и решает, что делать дальше. В поиске эту интеграцию часто называют ChatGPT API, хотя в документации и коде используется название OpenAI API.
Подписка ChatGPT и биллинг API живут отдельно. Даже если у вас есть Plus, Pro или корпоративный тариф, запросы из Python оплачиваются в аккаунте API по фактическому использованию. Cookie браузера и подписка ChatGPT для работы с API не подходят. Нужен отдельный API-ключ, созданный на платформе OpenAI.
| ChatGPT | OpenAI API |
|---|---|
| Готовый чат с интерфейсом | Доступ к моделям из кода |
| Подходит для ручной работы | Подходит для приложений, ботов и автоматизации |
| Оплата по тарифу ChatGPT | Оплата по токенам и вызовам инструментов |
| Контекстом и интерфейсом управляет ChatGPT | Контекст, ошибки, лимиты и UX остаются на стороне вашего приложения |
В июле 2026 года текстовая линейка GPT-5.6 состоит из Sol, Terra и Luna. Имя модели gpt-5.6 работает как алиас: сейчас запросы с ним отправляются в GPT-5.6 Sol. В проде нужно явно выбирать уровень модели — gpt-5.6-sol, gpt-5.6-terra или gpt-5.6-luna, — чтобы алиас gpt-5.6 не переключил приложение на другой уровень, так как однажды алиас обновится, а вместе с ним могут измениться качество, задержка и чек. Такие сюрпризы веселят только на демо.
| Модель | Цена за 1 млн входных токенов | Цена за 1 млн выходных токенов | Где использовать |
|---|---|---|---|
| GPT-5.6 Sol | $5 | $30 | Сложный код, анализ, агентные сценарии и задачи, где ошибка дороже запроса |
| GPT-5.6 Terra | $2,50 | $15 | Рабочий баланс качества, скорости и цены |
| GPT-5.6 Luna | $1 | $6 | Массовые классификации, черновики, простые боты и недорогие пет-проекты |
Важно: в таблице указаны стандартные цены для короткого контекста на июль 2026 года. Кэшированный вход дешевле, а запросы длиннее 272 тысяч входных токенов тарифицируются по повышенной ставке. Перед расчётом экономики откройте актуальную pricing-страницу.
Для учебных примеров начнём с gpt-5.6-terra: она даёт нормальный баланс качества и цены. Для стриминга и Telegram-бота возьмём gpt-5.6-luna, где важнее быстрый и недорогой ответ. Sol оставим для задач, в которых дополнительное качество окупает более дорогой запрос.
Читайте также: если данные нельзя отправлять в облако или хочется крутить модель на своём железе, посмотрите обзор локальных нейросетей для ПК. Внутри: Ollama, LM Studio и другие варианты без облачного API.
API-ключ OpenAI и окружение проекта
Сначала нужен аккаунт на платформе OpenAI, отдельный проект и API-ключ. В проекте задаются лимиты расходов и доступ участников команды. Для рабочего сервиса полезно развести dev, staging и prod: тогда тестовый скрипт не съест бюджет продакшена и не упрётся в его rate limit.
После создания ключ показывается полностью один раз. Скопируйте его в менеджер секретов или локальный .env. В исходники строку sk-… не кладём, даже «временно на пять минут». У временных костылей подозрительно длинная жизнь.
Правило безопасности: ключ из публичного GitHub находят автоматические сканеры. Иногда между git push и первым чужим запросом проходит несколько минут. Если секрет утёк, удалите его в кабинете, создайте новый и проверьте Usage. Разбор похожих историй есть в материале о вайбкодинге и дырах в продакшене.
Примеры ниже рассчитаны на Python 3.11 или новее. Сам SDK поддерживает и более старые версии, но в коде статьи используются современные аннотации типов вроде int | None и list[str].
Создадим папку проекта и виртуальное окружение:
# создаём папку проекта
mkdir openai-python-demo
# переходим в папку проекта
cd openai-python-demo
# создаём виртуальное окружение
python -m venv .venv
# активируем окружение в macOS или Linux
source .venv/bin/activate
# для Windows PowerShell используем эту команду
# .venv\Scripts\Activate.ps1
# ставим актуальный SDK и загрузчик .env-файла
pip install --upgrade openai python-dotenv
В корне проекта создаём файл .env:
# ключ проекта OpenAI
OPENAI_API_KEY=sk-proj-ваш_ключ
Рядом создаём .gitignore, чтобы секрет и виртуальное окружение не попали в репозиторий:
# локальные секреты
.env
# виртуальное окружение Python
.venv/
# служебные файлы Python
__pycache__/
*.pyc
Файл .env удобен локально. В Docker, Kubernetes и облаке секрет обычно приходит через переменную окружения или secret manager. Официальный Python SDK сам ищет OPENAI_API_KEY, поэтому передавать api_key в OpenAI(…) не требуется. Так ключ реже светится в логах и на ревью.
Про оплату: доступность API и способов оплаты зависит от страны и платёжного профиля. Для пользователей из России обычно нужна зарубежная банковская карта. Обходные схемы и прокси-сервисы в этой статье не рассматриваем и в шпионов не играем.
OpenAI Python: первый запрос через Responses API
Создадим файл first_request.py. В актуальном SDK текст генерируется через client.responses.create(): метод принимает модель, input и настройки запроса, а собранный текст лежит в response.output_text. Такой же паттерн показан в официальном quickstart.
# загружаем переменные из локального .env-файла
from dotenv import load_dotenv
# импортируем клиент OpenAI
from openai import OpenAI
# добавляем переменные из .env в окружение процесса
load_dotenv()
# создаём клиент; OPENAI_API_KEY подхватится автоматически
client = OpenAI()
# отправляем запрос через Responses API
response = client.responses.create(
# используем Terra как баланс качества и цены
model="gpt-5.6-terra",
# передаём пользовательский запрос обычной строкой
input="Объясни в двух предложениях, зачем Python-проекту виртуальное окружение.",
)
# печатаем собранный текст ответа
print(response.output_text)
Запускаем файл:
python first_request.py
После запуска в терминале появится короткое объяснение про изоляцию зависимостей и разные версии пакетов. Формулировка может меняться, но сам скрипт должен завершиться без traceback.
В примере четыре важных сущности. OpenAI() создаёт клиент и настраивает HTTP-соединение. model выбирает модель, input содержит пользовательский запрос, а responses.create() возвращает объект Response с текстом, идентификатором, usage и служебными элементами.
Поле output сложнее обычной строки: там могут лежать сообщения, вызовы функций, результаты инструментов и reasoning-элементы. Вместо обращения к response.output[0].content[0] проще использовать response.output_text.
Почему в старых туториалах другой код
В старых гайдах по ChatGPT API до сих пор встречается client.chat.completions.create() и массив messages. Этот интерфейс поддерживается, но для новых текстовых интеграций OpenAI рекомендует Responses API: там есть единый формат items, состояние между ходами, output_text и новые инструменты.
# предыдущий поддерживаемый интерфейс; новые примеры пишем на Responses API
completion = client.chat.completions.create(
# модель
model="gpt-5.6-terra",
# история в формате Chat Completions
messages=[
# сообщение пользователя
{"role": "user", "content": "Привет!"},
],
)
Этот фрагмент нужен, чтобы вы узнавали код из старых туториалов. Он не сломан, но дальше все рабочие примеры будут на Responses API.
Читайте также: как устроены AI-скиллы для Codex и других агентов. Это следующий уровень после одиночного запроса к модели.
Полезный блок со скидкой
Отправить первый запрос к модели несложно. Дальше появляются контекст, асинхронный код, обработка ошибок и другие детали, из которых уже складывается настоящее приложение. Разобраться с этой частью можно на курсах Практикума: «Python-разработчик» поможет освоить бэкенд и работу с API, а «Инженер по искусственному интеллекту» — научиться создавать сервисы с нейросетями.
По промокоду: KOD (можно просто нажать) на платное обучение действует скидка.
Бесплатная вводная часть тоже есть — карту привязывать не нужно.
Как модель помнит переписку
API не ведёт дневник переписки сам по себе. Если отправить два независимых запроса, второй не узнает, что было в первом. Контекст должен передать ваш код. В Responses API для этого можно переслать историю вручную или связать ответы через previous_response_id.
Способ 1. Передаём историю вручную
Ручная история подходит, когда приложение само хранит диалог, шифрует его, обрезает старые сообщения или собирает контекст из нескольких источников. Для reasoning-моделей лучше сохранять все элементы response.output, а не только видимый текст: там могут лежать служебные элементы, нужные следующему запросу.
# импортируем клиент
from openai import OpenAI
# создаём клиент
client = OpenAI()
# начинаем историю с первого сообщения пользователя
history = [
# первый запрос
{"role": "user", "content": "Придумай имя для сервиса мониторинга логов."},
]
# отправляем первый запрос без серверного хранения состояния
first_response = client.responses.create(
# выбираем модель
model="gpt-5.6-terra",
# передаём накопленную историю
input=history,
# отключаем хранение ответа на стороне API
store=False,
)
# показываем первый ответ
print(first_response.output_text)
# добавляем в историю все элементы ответа модели
history += first_response.output
# добавляем уточнение пользователя
history.append(
# просим продолжить с учётом первого ответа
{"role": "user", "content": "Оставь лучший вариант и придумай к нему слоган."}
)
# отправляем всю историю повторно
second_response = client.responses.create(
# используем ту же модель
model="gpt-5.6-terra",
# передаём обновлённый список
input=history,
# по-прежнему управляем состоянием сами
store=False,
)
# печатаем ответ с учётом первого хода
print(second_response.output_text)
Минус ручной истории видно сразу: с каждым ходом payload толстеет. Зато вы контролируете, что именно уходит модели. Можно убрать флуд, оставить последние сообщения, добавить свежие данные из базы и не пересылать весь чат со времён динозавров.
Способ 2. Связываем ответы по ID
Для короткого чата проще сохранить response.id и передать его в previous_response_id. Сервер найдёт предыдущий ответ и продолжит цепочку. В коде остаётся один идентификатор вместо массива сообщений.
# импортируем клиент
from openai import OpenAI
# создаём клиент
client = OpenAI()
# отправляем первое сообщение
first_response = client.responses.create(
# выбираем модель
model="gpt-5.6-terra",
# задаём тему диалога
input="Придумай имя для сервиса мониторинга логов.",
)
# выводим первый ответ
print(first_response.output_text)
# отправляем уточнение и ссылаемся на предыдущий ответ
second_response = client.responses.create(
# используем ту же модель
model="gpt-5.6-terra",
# передаём ID предыдущего ответа
previous_response_id=first_response.id,
# задаём новый вопрос
input="Оставь лучший вариант и придумай к нему слоган.",
)
# выводим продолжение диалога
print(second_response.output_text)
Под капотом: previous_response_id упрощает код, но не обнуляет счётчик. Токены прошлых ходов всё равно учитываются как входные. По умолчанию объекты Response сохраняются 30 дней; хранение можно отключить через store=False. Подробности и исключения собраны в гайде по состоянию диалога.
Для простого бота возьмём previous_response_id. Для корпоративного чата с аудитом, своей политикой retention и переносом между устройствами лучше хранить контекст в базе или использовать Conversations API.
Задаём модели роль и правила через instructions
Пользовательский input меняется на каждом ходе. Правила приложения обычно стабильны: язык, тон, формат, границы компетенции. В Responses API их удобно передавать через instructions. Этот параметр имеет более высокий приоритет, чем пользовательский запрос.
Соберём бота поддержки, который отвечает по-русски и не дописывает продукту фичи, которых нет в документации:
# импортируем клиент
from openai import OpenAI
# создаём клиент
client = OpenAI()
# задаём постоянные правила приложения
support_rules = """
Ты — бот технической поддержки.
Отвечай только на русском языке.
Используй только факты из переданного описания продукта.
Если данных не хватает, попроси пользователя уточнить вопрос или обратиться к оператору.
Пиши короткими абзацами и не придумывай функции продукта.
"""
# описываем продукт и вопрос пользователя
user_input = """
Документация: экспорт отчёта доступен в CSV и XLSX. PDF пока не поддерживается.
Вопрос: можно скачать отчёт в PDF?
"""
# отправляем запрос с правилами и пользовательскими данными
response = client.responses.create(
# для поддержки берём модель с балансом цены и качества
model="gpt-5.6-terra",
# передаём правила поведения
instructions=support_rules,
# передаём данные конкретного запроса
input=user_input,
)
# показываем ответ
print(response.output_text)
Ожидаемое поведение простое: бот сообщает, что PDF пока не поддерживается, и предлагает CSV или XLSX. Если он начинает рассказывать о «скрытой кнопке экспорта», значит, где-то промпт свернул не туда.
Есть нюанс: instructions действует только в текущем запросе. При продолжении через previous_response_id правила прошлого хода автоматически не становятся инструкциями нового вызова. Поэтому постоянные правила передают заново — нагляднее ниже в Telegram-боте.
Параметр temperature можно использовать для управления вариативностью: низкие значения полезны для извлечения данных и поддержки, высокие — для задач, где нужны разные формулировки. У GPT-5.6 на качество и задержку также влияет reasoning.effort. Совместимость sampling-параметров зависит от модели и режима reasoning, поэтому при смене настроек не стесняйтесь сверяться с карточкой модели и прогоняйте smoke-тест на своём наборе запросов.
Стримим ответ по мере генерации
Без стриминга приложение ждёт, пока модель закончит весь ответ. Пользователь отправляет сообщение и видит в чате тишину. Через несколько секунд начинает казаться, что запрос где-то застрял между фронтом и бэкендом. При stream=True API присылает события по мере готовности, а новые куски текста приходят в response.output_text.delta.
# импортируем клиент
from openai import OpenAI
# создаём клиент
client = OpenAI()
# открываем поток событий
stream = client.responses.create(
# выбираем быструю и недорогую модель
model="gpt-5.6-luna",
# просим короткое объяснение
input="Объясни разработчику, зачем нужен health check сервиса.",
# включаем потоковую передачу
stream=True,
)
# читаем события до завершения ответа
for event in stream:
# оставляем только события с новой порцией текста
if event.type == "response.output_text.delta":
# печатаем текст без перевода строки и сразу сбрасываем буфер
print(event.delta, end="", flush=True)
# переводим курсор на новую строку после завершения потока
print()
При запуске текст начнёт печататься в терминале постепенно. Это и есть стриминг: первый символ появляется раньше, чем модель закончит весь ответ.
Стрим состоит из типизированных событий. Кроме дельт текста, там встречаются response.created, response.completed и error. На фронте обычно открывают SSE или WebSocket до своего бэкенда, а бэкенд уже проксирует дельты от OpenAI. Отдавать API-ключ прямо в браузер нельзя.
Для прода: поток усложняет модерацию, пользователь видит часть ответа до того, как генерация закончилась. Для особо чувствительных сценариев продумайте буферизацию, проверку входа и возможность остановить поток.
Structured Outputs: получаем JSON по схеме
Допустим, мы вытаскиваем из текста вакансии название, зарплату и стек. Если просто попросить модель вернуть JSON, всё держится на договорённости: перед фигурной скобкой может появиться пояснение, одно из полей пропадёт, а зарплата вместо числа приедет строкой. Structured Outputs привязывает ответ к JSON Schema. В Python SDK схему удобно описать Pydantic-моделью, после чего SDK вернёт типизированный объект.
Возьмём текст вакансии и вытащим из него название, вилку зарплаты и стек:
# импортируем клиент OpenAI
from openai import OpenAI
# импортируем базовый класс Pydantic
from typing import Literal
from pydantic import BaseModel
# описываем структуру зарплаты
class Salary(BaseModel):
# нижняя граница вилки
minimum: int | None
# верхняя граница вилки
maximum: int | None
# валюта из текста вакансии
currency: Literal["RUB"] | None
# описываем итоговую структуру вакансии
class Vacancy(BaseModel):
# название позиции
title: str
# типизированная зарплата
salary: Salary
# технологии из описания
stack: list[str]
# формат работы
work_format: str | None
# создаём клиент
client = OpenAI()
# сохраняем исходный неструктурированный текст
vacancy_text = """
Ищем Python-разработчика в продуктовую команду. Вилка 250–320 тысяч рублей.
Стек: Python 3.12, FastAPI, PostgreSQL, Redis, Docker. Работа удалённая.
"""
# просим SDK распарсить ответ по Pydantic-схеме
response = client.responses.parse(
# используем модель с хорошим балансом качества и цены
model="gpt-5.6-terra",
# объясняем задачу извлечения
instructions="Извлеки факты из вакансии. Не додумывай отсутствующие значения.",
# передаём исходный текст
input=vacancy_text,
# задаём класс ожидаемого результата
text_format=Vacancy,
)
# получаем готовый объект Vacancy
vacancy = response.output_parsed
if vacancy is None:
raise RuntimeError("Не удалось получить структурированный ответ")
# печатаем валидированный JSON с отступами
print(vacancy.model_dump_json(indent=2))
В результате получится валидированный объект Vacancy: название позиции, зарплата от 250 000 до 320 000 RUB, список технологий и удалённый формат работы.
Structured Outputs гарантирует соответствие ответа заданной схеме, а Literal ограничивает допустимое значение валюты. При отказе модели или незавершённой генерации output_parsed может отсутствовать. Метод responses.parse() создаёт JSON Schema из Pydantic-модели и возвращает распарсенный объект. Если следующему сервису нужен JSON, используйте model_dump_json(). В Python-коде можно напрямую обращаться к полям vacancy.salary.minimum и vacancy.stack, без ручного json.loads().
Structured Outputs подходит для извлечения сущностей, маршрутизации тикетов, генерации конфигов и контрактов между сервисами. Когда модель должна выбрать действие и вызвать ваш код, вместо текстовой схемы лучше использовать function calling. Здесь нам нужен именно результат, поэтому Pydantic проще.
OpenAI API: цена запроса, токены и контроль расходов
Токены — расчётный юнит API. Один токен не равен одному слову: русское слово может занять несколько токенов, а код режется по своим правилам. В биллинг попадает обычный и кэшированный input, output модели и служебные reasoning-токены.
Точная статистика приезжает в каждом завершённом Response:
# отправляем запрос
response = client.responses.create(
# выбираем модель
model="gpt-5.6-luna",
# передаём текст
input="Суммируй: сервис получил 1200 запросов, 18 завершились ошибкой.",
)
# выводим входные токены
print("input:", response.usage.input_tokens)
# выводим количество токенов, взятых из кэша
print("cached input:", response.usage.input_tokens_details.cached_tokens)
# выводим токены видимого ответа и рассуждения
print("output:", response.usage.output_tokens)
# выводим общий объём запроса
print("total:", response.usage.total_tokens)
После запроса вы увидите четыре числа: входные токены, кэшированную часть входа, выход и общий объём. Эти значения лучше логировать вместе с моделью и типом операции — потом будет проще объяснить, почему один эндпоинт внезапно подорожал.
До отправки запроса есть два варианта оценки. Для простой строки можно использовать tiktoken локально. Для сообщений, картинок, файлов и схем инструментов точнее будет специальный endpoint подсчёта входных токенов: он принимает тот же payload, что и Responses API.
# просим API точно посчитать вход до генерации
count = client.responses.input_tokens.count(
# указываем модель, потому что токенизация зависит от неё
model="gpt-5.6-terra",
# передаём тот же input, который собираемся отправить
input="Проверь этот текст перед дорогим запросом.",
)
# выводим точное количество входных токенов
print(count.input_tokens)
Локальный tiktoken не делает сетевой запрос и подходит для грубого pre-check. Кодировка o200k_base здесь используется как совместимая оценка, а не как гарантия точного совпадения с серверным подсчётом GPT-5.6. Для сообщений, изображений, файлов и больших схем ориентируйтесь на endpoint подсчёта и итоговый response.usage.
# импортируем локальный токенизатор
import tiktoken
# берём актуальную базовую кодировку для современных моделей
encoding = tiktoken.get_encoding("o200k_base")
# кодируем строку и считаем элементы
estimated_tokens = len(encoding.encode("Пример текста для предварительной оценки."))
# выводим приблизительный результат
print(estimated_tokens)
Сколько стоит один запрос
Допустим, GPT-5.6 Luna получила 10 тысяч входных токенов и вернула 2 тысячи выходных. Без записи в кэш, инструментов и дополнительных режимов такой запрос стоит около $0,022: $0,01 за input и $0,012 за output. Запись токенов в кэш у GPT-5.6 стоит в 1,25 раза дороже обычного input, а повторное чтение — в 10 раз дешевле. Ставки меняются, поэтому расчёты сверяйте с официальной таблицей цен.
- Ставьте spend limits в проекте и алерты до того, как выкатите фичу пользователям.
- Логируйте response.usage рядом с типом операции, моделью и идентификатором пользователя.
- Ограничивайте max_output_tokens там, где ответ должен быть коротким.
- Не тащите в каждый ход всю базу знаний; сначала найдите релевантный фрагмент, затем отправьте его модели.
Кэш помогает, когда начало промпта повторяется: длинные instructions, описание формата или набор примеров. На GPT-5.6 кэшированный input стоит в десять раз дешевле обычного, но структуру промпта нужно держать стабильной.
Ошибки, rate limits и ретраи
На локалке traceback полезен: он сразу показывает, где именно всё пошло не так. В проде необработанное исключение не должно доходить до пользователя вместе со стеком вызовов. SDK даёт отдельные классы ошибок, поэтому неверный API-ключ, сетевой сбой и 429 Too Many Requests можно обрабатывать по-разному.
В продовых логах иногда встречается такое, что вам, людям, и не снилось: 429, таймауты, оборванные соединения и повторные запросы, которые только усиливают нагрузку.
| Исключение | Что могло случиться | Что же делать |
|---|---|---|
| AuthenticationError | Ключ неверный, отозван или не имеет доступа | Не ретраить. Проверить секрет и конфигурацию проекта |
| RateLimitError | Превышен лимит запросов или токенов либо закончилась квота | Временный лимит повторить с задержкой; при insufficient_quota проверить баланс и spend limits, не ретраить |
| APIConnectionError | Сеть, DNS, TLS, прокси или firewall | Повторить запрос, проверить инфраструктуру |
| APITimeoutError | Ответ не успел прийти до timeout | Повторить идемпотентный запрос и настроить timeout |
| BadRequestError | Неверный параметр или слишком большой payload | Исправить запрос; ретрай без изменения бесполезен |
SDK автоматически повторяет сетевые ошибки, 408, 409, 429 и ответы 5xx два раза с короткой экспоненциальной задержкой. Для приложения под нагрузкой этого часто мало: несколько воркеров могут одновременно получить 429 и так же одновременно пойти на повтор. Добавим внешний retry с джиттером.
# импортируем генератор случайной задержки
import random
# импортируем sleep
import time
# импортируем клиент и классы ошибок
from openai import (
APIConnectionError,
APITimeoutError,
AuthenticationError,
OpenAI,
RateLimitError,
)
# создаём клиент со стандартными внутренними ретраями SDK
client = OpenAI(timeout=30.0, max_retries=2)
# объявляем функцию с прикладным retry
def ask_with_retry(prompt: str, attempts: int = 4) -> str:
# перебираем попытки от нуля до attempts - 1
for attempt in range(attempts):
try:
# отправляем запрос
response = client.responses.create(
# выбираем недорогую модель
model="gpt-5.6-luna",
# передаём пользовательский текст
input=prompt,
)
# возвращаем ответ при успехе
return response.output_text
except AuthenticationError:
# ошибка ключа не исчезнет после ожидания
raise
except (RateLimitError, APIConnectionError, APITimeoutError):
# пробрасываем последнюю ошибку после исчерпания попыток
if attempt == attempts - 1:
raise
# считаем экспоненциальную паузу и добавляем джиттер
delay = min(2 ** attempt + random.random(), 30)
# ждём перед новой попыткой
time.sleep(delay)
# эта строка нужна для статического анализатора типов
raise RuntimeError("Retry loop finished unexpectedly")
Джиттер добавляет небольшую случайность, чтобы воркеры не просыпались в одну миллисекунду. Для очередей и фоновых задач retry лучше вынести на уровень Celery, RabbitMQ или другого брокера: тогда процесс не спит внутри веб-запроса, а задача возвращается в очередь.
Когда нужен AsyncOpenAI
Синхронный клиент проще и подходит скриптам, CLI и обычным воркерам. В FastAPI, aiohttp или асинхронном Telegram-боте блокирующий HTTP-вызов тормозит event loop. Там используйте AsyncOpenAI и await. Сам по себе async не ускоряет один запрос, зато позволяет приложению обслуживать другие соединения, пока API думает.
# импортируем асинхронный клиент
import asyncio
from openai import AsyncOpenAI
async def main():
client = AsyncOpenAI()
response = await client.responses.create(
model="gpt-5.6-luna",
input="Коротко объясни, что такое event loop.",
)
print(response.output_text)
if __name__ == "__main__":
asyncio.run(main())
Мини-проект: как подключить ChatGPT к коду Telegram-бота
Hello world мы уже пережили, поэтому соберём проект, которым можно потыкать руками. Бот принимает сообщение из Telegram, отправляет его в OpenAI API и возвращает ответ. Контекст каждого пользователя хранится через previous_response_id, команда /reset начинает новую цепочку, а ошибки превращаются в нормальные сообщения вместо простыни traceback. Если бота ещё нет, сначала зарегистрируйте его по инструкции «Телеграм-бот на Python».
Что будет в проекте
- Асинхронный клиент OpenAI, потому что python-telegram-bot работает на asyncio.
- Отдельный previous_response_id в context.user_data для каждого пользователя.
- Повторная передача instructions на каждом ходе.
- Команды /start и /reset, обработка лимитов, сети и слишком длинных сообщений Telegram.
Устанавливаем зависимости
# ставим проверенные версии библиотек
pip install openai==2.46.0 python-telegram-bot==22.8 python-dotenv
Добавляем в .env второй секрет:
# ключ проекта OpenAI
OPENAI_API_KEY=sk-proj-ваш_ключ
# токен Telegram-бота от BotFather
TELEGRAM_BOT_TOKEN=ваш_telegram_токен
Полный код bot.py
# импортируем логирование
import logging
# импортируем доступ к переменным окружения
import os
# загружаем локальный .env-файл
from dotenv import load_dotenv
# импортируем асинхронный клиент и ошибки OpenAI
from openai import (
APIConnectionError,
APITimeoutError,
AsyncOpenAI,
AuthenticationError,
RateLimitError,
)
# импортируем тип входящего обновления Telegram
from telegram import Update
# импортируем компоненты фреймворка бота
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
# загружаем значения из .env в окружение процесса
load_dotenv()
# настраиваем формат логов
logging.basicConfig(
# добавляем время, имя логгера, уровень и сообщение
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
# выводим сообщения уровня INFO и выше
level=logging.INFO,
)
# создаём логгер текущего модуля
logger = logging.getLogger(__name__)
# читаем токен Telegram из окружения
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
# завершаем запуск, если токен Telegram не настроен
if not TELEGRAM_BOT_TOKEN:
# сообщаем, какой секрет отсутствует
raise RuntimeError("Переменная TELEGRAM_BOT_TOKEN не задана")
# задаём постоянные правила поведения модели
BOT_INSTRUCTIONS = """
Ты — технический помощник для разработчиков.
Отвечай на русском языке.
Пиши по существу, но поясняй неочевидные решения.
Если приводишь код, отделяй его пустыми строками и не используй Markdown-разметку.
Если данных недостаточно, задай один уточняющий вопрос.
Не утверждай, что запускал код или проверял внешнюю систему, если этого не было.
"""
# создаём один асинхронный HTTP-клиент на всё приложение
openai_client = AsyncOpenAI(
# ограничиваем время одного запроса
timeout=45.0,
# оставляем встроенные ретраи SDK
max_retries=2,
)
# объявляем максимальную длину одного сообщения Telegram
TELEGRAM_MESSAGE_LIMIT = 4096
# разбиваем длинный ответ на допустимые куски
def split_message(text: str, limit: int = TELEGRAM_MESSAGE_LIMIT) -> list[str]:
# возвращаем исходный текст, если он уже помещается
if len(text) <= limit:
# оборачиваем строку в список для единого интерфейса
return [text]
# создаём список готовых частей
chunks: list[str] = []
# сохраняем ещё не отправленный хвост
remaining = text
# продолжаем, пока хвост длиннее лимита
while len(remaining) > limit:
# ищем последний перевод строки внутри допустимого окна
split_at = remaining.rfind("\n", 0, limit)
# используем жёсткую границу, если подходящего перевода строки нет
if split_at == -1:
# ставим границу ровно по лимиту
split_at = limit
# добавляем очередную часть без лишних пробелов по краям
chunks.append(remaining[:split_at].strip())
# оставляем необработанный хвост
remaining = remaining[split_at:].strip()
# добавляем последний непустой фрагмент
if remaining:
# сохраняем остаток
chunks.append(remaining)
# возвращаем все части
return chunks
# обрабатываем команду /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# очищаем старый контекст пользователя при новом старте
context.user_data.pop("previous_response_id", None)
# проверяем, что обновление содержит сообщение
if update.message:
# отправляем инструкцию пользователю
await update.message.reply_text(
"Привет! Пришлите технический вопрос. "
"Я сохраню контекст диалога. Команда /reset начнёт разговор заново."
)
# обрабатываем команду /reset
async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# удаляем ID предыдущего ответа, если он был
context.user_data.pop("previous_response_id", None)
# проверяем наличие сообщения
if update.message:
# подтверждаем очистку контекста
await update.message.reply_text("Контекст очищен. Можно начать новую тему.")
# обрабатываем обычное текстовое сообщение
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# завершаем обработку, если Telegram не прислал сообщение или текст
if not update.message or not update.message.text:
# ничего не отвечаем на неподдерживаемый update
return
# сохраняем текст пользователя
user_text = update.message.text.strip()
# игнорируем пустую строку после trim
if not user_text:
# просим прислать содержательный текст
await update.message.reply_text("Пришлите вопрос текстом.")
# завершаем хендлер
return
# читаем ID предыдущего ответа этого пользователя
previous_response_id = context.user_data.get("previous_response_id")
try:
# отправляем сообщение в Responses API
response = await openai_client.responses.create(
# используем недорогую модель для чат-сценария
model="gpt-5.6-luna",
# повторяем инструкции на каждом ходе
instructions=BOT_INSTRUCTIONS,
# включаем небольшое reasoning для технических вопросов
reasoning={"effort": "low"},
# передаём пользовательский текст
input=user_text,
# продолжаем цепочку, если ID уже сохранён
previous_response_id=previous_response_id,
# ограничиваем слишком длинный ответ модели
max_output_tokens=1200,
)
# сохраняем новый ID для следующего сообщения
context.user_data["previous_response_id"] = response.id
# берём готовый текст или подставляем запасное сообщение
answer = response.output_text.strip() or "Модель вернула пустой ответ. Попробуйте ещё раз."
# делим ответ на части, которые принимает Telegram
for chunk in split_message(answer):
# отправляем очередной кусок пользователю
await update.message.reply_text(chunk)
except AuthenticationError:
# пишем подробность в серверный лог
logger.exception("OpenAI API key rejected")
# сообщаем пользователю о конфигурационной ошибке
await update.message.reply_text(
"Сервис сейчас неправильно настроен. Администратору нужно проверить API-ключ."
)
except RateLimitError:
# логируем превышение лимита
logger.exception("OpenAI rate limit reached")
# просим повторить запрос позже
await update.message.reply_text(
"Сейчас слишком много запросов. Повторите сообщение через несколько секунд."
)
except (APIConnectionError, APITimeoutError):
# логируем сетевую ошибку или таймаут
logger.exception("OpenAI connection failed")
# не раскрываем пользователю внутренние детали
await update.message.reply_text(
"Не удалось дождаться ответа модели. Попробуйте ещё раз."
)
except Exception:
# ловим неожиданный баг, чтобы хендлер не упал молча
logger.exception("Unexpected bot error")
# отправляем нейтральное сообщение
await update.message.reply_text(
"Что-то пошло не так. Ошибка уже попала в лог."
)
# собираем и запускаем приложение
def main() -> None:
# создаём Telegram-приложение с токеном бота
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# регистрируем команду старта
application.add_handler(CommandHandler("start", start))
# регистрируем команду очистки контекста
application.add_handler(CommandHandler("reset", reset))
# принимаем текстовые сообщения, кроме команд
application.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
)
# запускаем long polling до остановки процесса
application.run_polling(allowed_updates=Update.ALL_TYPES)
# запускаем main только при прямом старте файла
if __name__ == "__main__":
# передаём управление точке входа
main()
Запускаем бота
python bot.py
Проверка простая: задайте вопрос, затем отправьте уточнение «а покажи пример». Бот должен продолжить ту же тему. После /reset такое же уточнение уже не связано с предыдущим диалогом.
Как здесь устроен контекст
python-telegram-bot хранит context.user_data отдельно для каждого пользователя. После ответа мы кладём туда response.id. На следующем сообщении этот ID уходит в previous_response_id, и модель видит предыдущую цепочку. Команда /reset удаляет ID и начинает новый тред.
В памяти процесса такой контекст переживает только текущий запуск. После рестарта всё забудется. Для прода добавьте persistence, Redis или свою базу. Если один пользователь пишет боту с нескольких устройств и должен продолжать один диалог, храните ID по идентификатору пользователя, а не внутри конкретного воркера.
BOT_INSTRUCTIONS передаётся на каждом ходу. Это специально: при previous_response_id старое значение instructions не становится частью нового запроса автоматически. Если забыть эту строку, бот может постепенно съехать с нужного языка и ограничений — классический «на локалке всё было ок».
Что стоит добавить перед продом
- Персистентное хранилище контекста и TTL для старых диалогов.
- Метрики latency, ошибок, токенов и стоимости по типам запросов.
- Очередь или семафор, который ограничит параллельные обращения к модели.
- Модерацию и бизнес-правила для сценариев, где пользовательский ввод может запускать действия.
- Набор eval-тестов: реальные вопросы, ожидаемые свойства ответа и сравнение моделей перед обновлением.
Теперь OpenAI API подключён к приложению без магии: ключ лежит вне кода, запросы идут через Responses API, контекст сохраняется между сообщениями, а ошибки не пробивают пользователю сырой traceback. Под выражением ChatGPT API обычно и имеют в виду такую обвязку вокруг моделей OpenAI. Дальше можно подключить поиск по документации, function calling, файлы или агентный цикл. Базовое правило останется тем же: контролировать input, проверять output, считать usage и не поручать модели работу инфраструктуры.
Советуем дополнительно почитать по теме
15 скиллов для AI-агентов: один раз настроил — агент запомнил — как устроены повторно используемые инструкции для агентов, SKILL.md, автозапуск навыков и подключение к Claude Code, Cursor и другим инструментам.
Запускаем Python-скрипт на сервере, чтобы он работал всё время — переносим бота или другой Python-процесс на сервер и настраиваем постоянный запуск через systemd.
RAG-система для своих данных — подключаем языковую модель к внутренней документации и базе знаний, чтобы ответы опирались на найденные данные.
Код-ревью с ИИ: 20 промптов для безопасности, рефакторинга и PR — запросы для проверки кода, архитектуры, уязвимостей и производительности до того, как изменения попадут в продакшен.
Как создать AI-агента: пошаговое руководство — память, инструменты, Function Calling, ограничения, тестирование и переход от чат-бота к агентному циклу.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, безлимит на маркетплейсах и поможет с льготной ипотекой. Ладно, окей, это просто скидка, без остального, но хорошая.
