$ sudo teach IT

Попробуйте отправить модели два запроса подряд: «Меня зовут Анна» и «Как меня зовут?». На второй вопрос модель честно ответит, что не знает. Каждый вызов client.responses.create независим: модель не хранит ничего между запросами. Но пользователи ждут от ассистента нормального разговора, где можно сказать «а теперь сделай короче» или «а что насчёт второго пункта», и агенту тоже нужно помнить, что он уже сделал на прошлых шагах.

После урока вы сможете вести с моделью многоходовой диалог двумя способами: хранить историю сообщений в своей программе или поручить это серверу OpenAI через previous_response_id. Вы поймёте, чем эти способы различаются, сколько стоит длинный диалог и как не дать истории разрастись до бесконечности.

Что уже умеем

В прошлом уроке вы отправили первый запрос через Responses API, разделили instructions и input, ограничили длину ответа и посчитали стоимость по response.usage. Каждый такой запрос был одиночным. Теперь свяжем запросы в разговор.

Что вы узнаете в рамках статьи

  • почему модель «забывает» прошлые сообщения и что такое контекст диалога;
  • как передать в input список сообщений с ролями user и assistant;
  • как написать консольный чат с историей в обычном списке Python;
  • как продолжить диалог через previous_response_id и что делает параметр store;
  • почему длинный диалог дорожает и как ограничить историю.

Ключевые термины

  • Контекст диалога — всё, что модель видит в текущем запросе: инструкции, прошлые сообщения и новый вопрос.
  • Сообщение — словарь с ролью и текстом, например {"role": "user", "content": "Привет"}.
  • Роль — кто автор сообщения: user — пользователь, assistant — модель.
  • История сообщений — список всех сообщений диалога, который программа отправляет модели.
  • previous_response_id — параметр, который говорит API продолжить диалог с указанного прошлого ответа.
  • store — параметр, разрешающий OpenAI сохранить ответ на сервере, чтобы на него можно было сослаться позже.

Почему модель не помнит разговор

Ощущение памяти в чат-ботах создаёт не модель, а программа вокруг неё. Убедимся в этом:

from openai import OpenAI

client = OpenAI()

r1 = client.responses.create(model="gpt-6-luna", input="Меня зовут Анна.")
r2 = client.responses.create(model="gpt-6-luna", input="Как меня зовут?")
print(r2.output_text)

Что увидим в консоли: что-то вроде «Я не знаю вашего имени, вы его не называли». Второй запрос ничего не знает о первом. Чтобы модель ответила «Анна», первое сообщение нужно передать вместе со вторым — то есть отправить контекст.

История сообщений в списке Python

Параметр input принимает не только строку, но и список сообщений. Каждое сообщение — словарь с ключами role и content. Вы складываете в список вопросы пользователя и ответы модели и при каждом запросе отправляете весь список.

from openai import OpenAI

client = OpenAI()
INSTRUCTIONS = "Ты дружелюбный помощник. Отвечай кратко."
history = []

def chat(user_text: str) -> str:
    history.append({"role": "user", "content": user_text})
    response = client.responses.create(
        model="gpt-6-luna",
        instructions=INSTRUCTIONS,
        input=history,
    )
    history.append({"role": "assistant", "content": response.output_text})
    return response.output_text

print(chat("Меня зовут Анна."))
print(chat("Как меня зовут?"))
print(f"Сообщений в истории: {len(history)}")

Что увидим:

Приятно познакомиться, Анна!
Вас зовут Анна.
Сообщений в истории: 4

Разберём, почему это работает. При втором вызове в input уходят три сообщения: вопрос, ответ модели и новый вопрос. Модель видит весь разговор и отвечает с учётом него. Обратите внимание на две детали. Ответ модели обязательно добавляется в историю с ролью assistant, иначе модель не увидит, что сама сказала. А инструкции передаются отдельно при каждом запросе и в историю не попадают — их удобно менять, не трогая диалог.

На основе этой функции легко собрать консольный чат:

while True:
    text = input("Вы: ").strip()
    if text == "/exit":
        break
    print("Бот:", chat(text))

Плюс этого способа — полный контроль: история лежит у вас, её можно сохранить в файл, отредактировать или сократить.

Диалог на стороне сервера: previous_response_id

Responses API умеет хранить диалог сам. По умолчанию ответы сохраняются на сервере OpenAI (параметр store=True), и у каждого ответа есть идентификатор response.id. Если в следующем запросе указать previous_response_id, модель получит весь предыдущий контекст, а вам достаточно отправить только новое сообщение.

r1 = client.responses.create(
    model="gpt-6-luna",
    instructions="Ты помощник по путешествиям. Отвечай кратко.",
    input="Я лечу в Казань на три дня.",
)

r2 = client.responses.create(
    model="gpt-6-luna",
    instructions="Ты помощник по путешествиям. Отвечай кратко.",
    input="Что мне взять с собой в октябре?",
    previous_response_id=r1.id,
)

print(r2.output_text)
print("Входных токенов во втором запросе:", r2.usage.input_tokens)

Что увидим: совет учитывает и Казань, и три дня, хотя во втором запросе о них ни слова. А число входных токенов окажется больше, чем длина одного второго вопроса: сервер подставил прошлый контекст, и он оплачивается как вход. previous_response_id экономит вам код, но не деньги.

Важная деталь: инструкции из прошлого ответа не переносятся автоматически, поэтому передавайте instructions в каждом запросе. Если указать store=False, ответ не сохранится на сервере, и сослаться на него через previous_response_id будет нельзя. Так поступают, когда данные нельзя хранить у провайдера, — тогда историю ведут сами, как в предыдущем разделе.

Какой способ выбрать? Собственная история даёт контроль и прозрачность: вы видите, что отправляется. previous_response_id удобен для быстрого прототипа и коротких диалогов. В этом курсе пригодятся оба.

Пример использования store=False

Иногда мы не хотим, чтобы OpenAI сохранял ответы модели для последующего получения через API. Для этого используется параметр store=False. При этом многоходовой диалог по-прежнему возможен — просто историю сообщений придётся хранить самостоятельно.

from openai import OpenAI

client = OpenAI()
history = []

def chat(user_text):
    history.append({"role": "user", "content": user_text})

    response = client.responses.create(
        model="gpt-6-luna",
        instructions="Отвечай кратко.",
        input=history,
        store=False
    )

    history.append({
        "role": "assistant",
        "content": response.output_text
    })

    return response.output_text


print(chat("Меня зовут Анна"))
print(chat("Как меня зовут?"))

Что происходит:

  1. store=False — отключаем сохранение ответа для последующего получения через Responses API.
  2. history — самостоятельно храним историю диалога.
  3. input=history — при каждом запросе передаём всю историю.
  4. Модель помнит имя Анны благодаря переданным сообщениям, а не серверному хранению.

Главное отличие: при store=True можно продолжить диалог через previous_response_id. При store=False в нашем примере мы передаём историю вручную.

Таким образом, store=False не отключает память диалога — он меняет способ управления ею. Важно также понимать, что этот параметр не означает полного отсутствия обработки или временного хранения данных провайдером.

Длинный диалог дорожает

С каждым обменом история растёт, а значит, растут входные токены каждого следующего запроса. В десятом сообщении вы платите за все девять предыдущих. Рано или поздно история упрётся и в контекстное окно. Простейшая защита — хранить только последние сообщения:

MAX_MESSAGES = 11

def trim(history: list) -> list:
    return history[-MAX_MESSAGES:]

Вызывайте trim перед отправкой, то есть когда новый вопрос уже добавлен в историю: модель увидит последние пять обменов и новый вопрос, а самые старые сообщения забудет. Значение берите нечётным: история в этот момент заканчивается вопросом, и тогда срез тоже начнётся с вопроса пользователя, а не с ответа модели. Это грубый, но рабочий приём; более аккуратные способы управления памятью вы изучите в модуле про LangChain.

Частые ошибки

  • Не добавлять ответ модели в историю. Тогда модель видит только вопросы пользователя и теряет нить разговора. После каждого запроса добавляйте сообщение с ролью assistant.
  • Класть инструкции в историю на каждом шаге. Инструкции дублируются и съедают токены. Передавайте их параметром instructions.
  • Считать, что previous_response_id бесплатен. Прошлый контекст всё равно попадает на вход модели и оплачивается.
  • Использовать previous_response_id при store=False. Несохранённый ответ нельзя продолжить, API вернёт ошибку.

Наиболее частые вопросы

  • Сколько сообщений можно хранить в истории? — Столько, сколько помещается в контекстное окно вместе с инструкциями и ответом. Но из-за стоимости обычно хранят меньше и обрезают старые сообщения.
  • Можно ли смешивать оба способа? — Технически да, но в одном диалоге лучше выбрать один, иначе легко отправить одни и те же сообщения дважды.
  • Как сохранить диалог между запусками программы? — Историю-список можно записать в JSON-файл через модуль json и загрузить при старте. При previous_response_id достаточно сохранить идентификатор последнего ответа.
  • Почему бы не хранить всю историю всегда? — Каждое старое сообщение оплачивается в каждом новом запросе, а слишком длинный контекст ещё и снижает внимание модели к деталям.

Итоги

Здорово, ваш код научился вести разговор! Теперь вы понимаете, что память диалога — это контекст, который программа передаёт модели в каждом запросе, и умеете делать это двумя способами: собственным списком сообщений с ролями и через previous_response_id. Вы знаете, почему длинный диалог дорожает и как его подрезать. Эти же приёмы лежат в основе цикла агента: история его шагов — это тот же список сообщений.

Пока модель отвечает свободным текстом, и чтобы достать из него дату или число, пришлось бы разбирать строку вручную. В следующем уроке вы научите модель отвечать строго по схеме Pydantic и получать готовый объект Python вместо текста.

На следующий урок