Урок 2. Формат messages: как устроен диалог
В первом уроке вы отправили первый запрос и получили ответ. Теперь разберём главный параметр этого запроса — messages. Он выглядит как обычный список словарей, но именно в нём живёт вся память разговора, приёмы вроде префилла и примеров, и большинство ошибок новичка.
anthropic и клиентом из урока 1. Если ключ лежит в переменной окружения ANTHROPIC_API_KEY, достаточно написать client = Anthropic().
Список messages: role и content
Параметр messages ждёт список словарей. У каждого словаря ровно два обязательных ключа:
role— кто говорит:"user"(вы) или"assistant"(Claude).content— что сказано. Либо просто строка, либо список блоков контента (у каждого свойtype: текст, изображение и так далее). Пока нам хватит строки.
Самый короткий диалог — одно сообщение:
messages = [
{"role": "user", "content": "Привет, Claude! Как дела?"}
]
А вот разговор из четырёх реплик:
messages = [
{"role": "user", "content": "Привет, Claude! Как дела?"},
{"role": "assistant", "content": "Привет! Всё хорошо. Чем помочь?"},
{"role": "user", "content": "Расскажи забавный факт про хорьков."},
{"role": "assistant", "content": "Возбуждённые хорьки издают квохчущий звук — его называют «дукинг»."},
]
Роли всегда чередуются: user → assistant → user → assistant. Список открывает user. Никакой роли system внутри messages не существует — системный промпт передаётся отдельным параметром, к нему вернёмся ниже.
Какие два ключа обязательны в каждом сообщении?
Модель ничего не помнит между запросами
Это главная мысль урока, и её стоит перечитать дважды. Каждый вызов API полностью независим. Claude не хранит вашу переписку на своей стороне: он видит ровно тот список messages, который вы прислали, и ничего больше.
Проверьте сами:
client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
messages=[{"role": "user", "content": "Меня зовут Даша."}]
)
# Новый запрос — модель уже не знает, как вас зовут
client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
messages=[{"role": "user", "content": "Как меня зовут?"}]
)
Чтобы разговор стал разговором, историю ведёте вы: складываете свои реплики и ответы модели в один список и отправляете его целиком при каждом запросе.
messages = [{"role": "user", "content": "Меня зовут Даша."}]
response = client.messages.create(
model="claude-haiku-4-5", max_tokens=200, messages=messages
)
# кладём ответ модели обратно в историю
messages.append({"role": "assistant", "content": response.content[0].text})
messages.append({"role": "user", "content": "Как меня зовут?"})
response = client.messages.create(
model="claude-haiku-4-5", max_tokens=200, messages=messages
)
print(response.content[0].text) # → Вас зовут Даша.
Отсюда два практических следствия. Первое: «память» чат-бота — это ваш список в оперативке или в базе, а не магия внутри модели. Второе: история отправляется заново каждый раз, поэтому длинный диалог — это каждый раз много входных токенов. Что с этим делать (кэширование, обрезка истории), обсудим в следующих уроках; актуальные цифры по стоимости и лимитам всегда смотрите в документации Anthropic.
И обратное замечание: истории требует далеко не всё. Если вы переводите слово или классифицируете отзыв, список из одного сообщения — нормально и правильно.
Вы отправили два запроса подряд. Что модель знает о первом, когда обрабатывает второй?
Объект ответа
Метод client.messages.create() возвращает не строку, а объект Message. Выглядит он примерно так:
Message(
id='msg_01Mq5gDnUmDESukTgwPV8xtG',
content=[TextBlock(text='Bonjour', type='text')],
model='claude-haiku-4-5',
role='assistant',
stop_reason='end_turn',
stop_sequence=None,
type='message',
usage=Usage(input_tokens=19, output_tokens=8)
)
Что здесь есть:
content— самое важное. Это список блоков контента, у каждого свой тип. Текст первого блока достаём так:response.content[0].text.id— идентификатор ответа.type— всегда"message".role— всегда"assistant": это реплика модели.model— какая модель реально обработала запрос.stop_reason— почему генерация остановилась (договорил сам, упёрся вmax_tokens, встретил стоп-последовательность).stop_sequence— какая именно стоп-последовательность сработала, если сработала.usage— сколько токенов ушло на вход и вышло на выходе. По этим числам считается расход.
Если запомнить из всего объекта одну вещь — пусть это будет: текст ответа лежит в content, и content — это список. Обратите внимание, что role ответа равна "assistant" — ровно то значение, с которым вы кладёте ответ обратно в историю.
Две типовые ошибки
Ошибка 1 — начать с assistant. Список обязан открываться репликой user:
# Ошибка валидации
messages=[
{"role": "assistant", "content": "Привет!"}
]
Ошибка 2 — сломать чередование. Два assistant подряд (или два user) не пройдут:
# Ошибка валидации
messages=[
{"role": "user", "content": "Привет!"},
{"role": "assistant", "content": "Здравствуйте!"},
{"role": "assistant", "content": "Чем помочь?"}
]
Чаще всего это вылезает, когда историю дописывают в двух местах кода и где-то забывают добавить ответ модели. Правило простое: положили user — дождитесь ответа и положите assistant.
Префилл и примеры
Раз в messages можно класть реплики assistant, туда можно положить и начало ответа, которого ещё не было. Модель продолжит с этого места. Это называют префиллом, или «вложить слова в уста Claude».
Допустим, нужно хайку, начинающееся строкой «горный воздух тих»:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=500,
messages=[
{"role": "user", "content": "Напиши красивое хайку"},
{"role": "assistant", "content": "горный воздух тих"}
]
)
print("горный воздух тих" + response.content[0].text)
Два нюанса. Первый: префилл — это последнее сообщение в списке, и роль у него assistant. Второй: в ответе придёт только продолжение, ваше начало модель обратно не повторит — склеивать строки придётся руками.
Тот же механизм даёт few-shot — обучение примерами. Вместо длинной инструкции «отвечай одним словом POSITIVE или NEGATIVE» вы просто показываете несколько пар «реплика → ответ» и в конце оставляете открытый вопрос:
messages=[
{"role": "user", "content": "Непопулярное мнение: солёные огурцы — гадость."},
{"role": "assistant", "content": "NEGATIVE"},
{"role": "user", "content": "Кажется, моя любовь к огурцам вышла из-под контроля: купила надувной круг в виде огурца."},
{"role": "assistant", "content": "POSITIVE"},
{"role": "user", "content": "Серьёзно, зачем вообще это есть?"},
{"role": "assistant", "content": "NEGATIVE"},
{"role": "user", "content": "Попробовала острые огурчики от @PickleCo — вкусовые рецепторы пляшут! 🌶️"},
]
Без примеров Claude на такой запрос напишет абзац с разбором и списком признаков — красиво, но бесполезно, если вы обрабатываете десять тысяч отзывов и ждёте одно слово. С примерами ответ будет «POSITIVE». Формат вы задали не описанием, а демонстрацией.
Системный промпт — отдельный параметр
Всё, что описано выше, — это messages. Но у запроса есть ещё один канал: system. Это инструкция «над разговором» — роль, правила, ограничения. Она передаётся отдельным аргументом и внутрь списка сообщений не кладётся:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=500,
system="Ты — редактор. Отвечай сухо и по делу, без вступлений.",
messages=[
{"role": "user", "content": "Сократи этот абзац вдвое: ..."}
]
)
Разница в назначении: messages — это что происходит в разговоре, system — по каким правилам он идёт. Постоянные правила поведения не нужно дублировать в каждой реплике пользователя, их место в системном промпте.
Вы хотите, чтобы ответ модели начинался со слов «Итого по кварталу:». Как это сделать через messages?
Упражнения
Дальше — руками. Оба упражнения делаются в обычном Python-файле рядом с вашим клиентом из урока 1.
Упражнение 2.1 — Функция перевода
Напишите функцию translate(word, language), которая возвращает перевод слова на указанный язык. Базовый уровень — вернуть что-то вроде «Слово "hello" на испанском: Hola». Уровень посложнее: добиться, чтобы translate("chicken", "итальянский") вернуло ровно pollo — без вступлений и пояснений.
Решение упражненияСначала попробуйте сами — потом сверьтесь
def translate(word, language):
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
messages=[
{
"role": "user",
"content": f"Переведи слово «{word}» на {language}. "
f"В ответе — только переведённое слово, ничего больше.",
}
],
)
return response.content[0].text
print(translate("chicken", "итальянский")) # pollo
Ключевая фраза — «только переведённое слово, ничего больше». Заметьте: истории здесь нет и не нужно, каждый вызов самостоятельный. Если модель всё равно упрямо добавляет вступление, добавьте префилл: последним сообщением положите {"role": "assistant", "content": ""} с началом нужного формата.
Упражнение 2.2 — Чат-бот в консоли
Соберите простейший многоходовый чат. Алгоритм ровно такой:
- Завести список для истории разговора.
- Спросить у пользователя реплику через
input()и добавить её в список с рольюuser. - Отправить весь список в API.
- Напечатать ответ модели.
- Добавить ответ в список с ролью
assistant. - Вернуться к шагу 2 — и предусмотреть способ выйти.
Решение упражненияСначала попробуйте сами — потом сверьтесь
conversation = []
while True:
user_input = input("Вы: ")
if user_input.lower() in ("выход", "quit"):
print("Разговор окончен.")
break
conversation.append({"role": "user", "content": user_input})
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
messages=conversation,
)
reply = response.content[0].text
print(f"Claude: {reply}")
conversation.append({"role": "assistant", "content": reply})
Двадцать строк — и у вас чат с памятью. Вся «память» здесь — список conversation: уберите строку с append ответа, и бот перестанет помнить предыдущие реплики. Попробуйте это сломать намеренно, так лучше запоминается.
Хотите задать боту характер — добавьте в вызов параметр system="...". Он передаётся при каждом запросе и в список conversation не попадает.
Что запомнить
messages— список словарей с ключамиroleиcontent. Роль —userилиassistant.- Список открывает
user, дальше роли строго чередуются. Нарушение — ошибка валидации. - Модель не помнит ничего между запросами. Историю копите и отправляете вы.
- Ответ — объект
Message. Текст лежит вresponse.content[0].text, расход токенов — вusage. - Последнее
assistant-сообщение работает как префилл: модель продолжит текст с него. - Несколько пар «вопрос → ответ» в истории задают формат ответа лучше, чем описание словами.
- Системный промпт — отдельный параметр
system, а не элементmessages.
Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.