Урок 2. Формат messages: как устроен диалог

В первом уроке вы отправили первый запрос и получили ответ. Теперь разберём главный параметр этого запроса — messages. Он выглядит как обычный список словарей, но именно в нём живёт вся память разговора, приёмы вроде префилла и примеров, и большинство ошибок новичка.

Все примеры — на Python с пакетом 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 не существует — системный промпт передаётся отдельным параметром, к нему вернёмся ниже.

Квиз 1

Какие два ключа обязательны в каждом сообщении?

Модель ничего не помнит между запросами

Это главная мысль урока, и её стоит перечитать дважды. Каждый вызов 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.

И обратное замечание: истории требует далеко не всё. Если вы переводите слово или классифицируете отзыв, список из одного сообщения — нормально и правильно.

Квиз 2

Вы отправили два запроса подряд. Что модель знает о первом, когда обрабатывает второй?

Объект ответа

Метод 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по каким правилам он идёт. Постоянные правила поведения не нужно дублировать в каждой реплике пользователя, их место в системном промпте.

Квиз 3

Вы хотите, чтобы ответ модели начинался со слов «Итого по кварталу:». Как это сделать через 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 — Чат-бот в консоли

Соберите простейший многоходовый чат. Алгоритм ровно такой:

  1. Завести список для истории разговора.
  2. Спросить у пользователя реплику через input() и добавить её в список с ролью user.
  3. Отправить весь список в API.
  4. Напечатать ответ модели.
  5. Добавить ответ в список с ролью assistant.
  6. Вернуться к шагу 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.

Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.

Перевод и адаптация урока «The Messages Format» курса Anthropic API Fundamentals © Anthropic, лицензия CC BY-NC 4.0. Перевод: Дарья Воронкина (@aishipuchka). Материал изменён: переведён на русский, примеры и названия моделей актуализированы, добавлены квизы. Используется некоммерчески.