Попробуйте отправить модели два запроса подряд: «Меня зовут Анна» и «Как меня зовут?». На второй вопрос модель честно ответит, что не знает. Каждый вызов 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("Как меня зовут?"))
Что происходит:
store=False— отключаем сохранение ответа для последующего получения через Responses API.history— самостоятельно храним историю диалога.input=history— при каждом запросе передаём всю историю.- Модель помнит имя Анны благодаря переданным сообщениям, а не серверному хранению.
Главное отличие: при 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 вместо текста.