OpenAI API в Python: как подключить ChatGPT к приложению

От первого запроса до Telegram-бота, который не забывает контекст

OpenAI API в Python: как подключить ChatGPT к приложению

Задача подключить 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.

ChatGPTOpenAI 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, ограничения, тестирование и переход от чат-бота к агентному циклу.

Бонус для читателей

Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, безлимит на маркетплейсах и поможет с льготной ипотекой. Ладно, окей, это просто скидка, без остального, но хорошая.

Вам может быть интересно
medium