Курс Anthropic: основы API (Anthropic API fundamentals)
Перевод первого курса из anthropics/courses: ключ API и первый запрос, формат messages, выбор модели, параметры max_tokens, stop_sequences и temperature, стриминг и запросы с картинками. Код запускается и на Claude, и на локальной модели через Ollama.
Anthropic API fundamentals — первый из пяти курсов-ноутбуков в репозитории anthropics/courses. Авторы советуют проходить их по порядку: основы API → туториал по промптам → Real world prompting → Prompt evaluations → Tool use. Этот курс учит работать с Claude через Python SDK: получить ключ, отправить запрос, разобрать ответ, выбрать модель, настроить параметры генерации, получать ответ потоком и отправлять картинки.
Как читать вместе с курсом
Заголовок раздела «Как читать вместе с курсом»- Когда. Вместе с уроком 8.1.1 «OpenAI-совместимые API»: там те же
идеи (роли,
max_tokens,temperature, стриминг, токены и цена) на примере OpenAI-совместимых API. Этот курс показывает второй большой формат — Anthropic Messages API — и помогает увидеть, что у разных провайдеров одинаково, а что нет. Урок 6 о картинках — практика к уроку 9.1.1. - Сколько времени. Три-четыре часа с упражнениями.
- Что можно пропустить. Если урок 8.1.1 пройден, уроки 1–2 читаются за 15 минут. Не пропускайте уроки 4 (параметры) и 5 (стриминг): события стрима устроены у Anthropic иначе, чем у OpenAI.
- Чем отличается от OpenAI-формата (подробно — в примечаниях по ходу):
| Anthropic Messages API | OpenAI Chat Completions | |
|---|---|---|
| Системный промпт | отдельный параметр system | сообщение с ролью system |
max_tokens | обязателен | необязателен |
| Ответ | response.content — список блоков (text, tool_use, thinking…) | response.choices[0].message.content — строка |
| Причина остановки | stop_reason: end_turn, max_tokens, stop_sequence, tool_use | finish_reason: stop, length, tool_calls |
| Токены | usage.input_tokens, usage.output_tokens | usage.prompt_tokens, usage.completion_tokens |
| Стриминг | типизированные события message_start, content_block_delta… | куски chunk.choices[0].delta |
Подготовка
Заголовок раздела «Подготовка»Этого раздела в оригинале нет: он заменяет ключ Anthropic и платный API на выбор «Claude или локальная модель».
-
Поставьте SDK версии 0.x. Курс написан под SDK 2024 года. В версии 1.x из
messages.create()убраны параметрыtemperature,top_pиtop_k(их вызов даётTypeError), а в уроке 4 они нужны. Последняя версия ветки 0.x работает со всем кодом курса:Окно терминала pip install "anthropic<1" python-dotenv httpx jupyter -
Поднимите модель. Без ключа — Ollama (урок 7.2.2). Для уроков 1–5 подойдёт
qwen3:8b, для урока 6 о картинках нужна модель со зрением, напримерqwen2.5vl:7b:Окно терминала ollama pull qwen3:8bollama pull qwen2.5vl:7b # для урока 6 -
Сохраните помощник
claude_local.pyрядом с ноутбуками. Если задана переменная окруженияANTHROPIC_API_KEY, он создаёт обычный клиент Claude; если нет — клиент, который смотрит на Ollama.
# claude_local.py — запуск кода курсов Anthropic на Claude или на локальной модели (адаптация ChinaAI).# Есть ключ ANTHROPIC_API_KEY — работаем с настоящим Claude (или с другим сервером из ANTHROPIC_BASE_URL);# нет ключа — с Ollama на этом компьютере.import osfrom functools import partial
from anthropic import Anthropic, AsyncAnthropic
if os.environ.get("ANTHROPIC_API_KEY"): client = Anthropic() # ключ и адрес SDK возьмёт из переменных окружения сам async_client = AsyncAnthropic() MODEL_NAME = os.environ.get("MODEL_NAME", "claude-haiku-4-5")else: # Ollama понимает формат Anthropic Messages API (адрес — без /v1); # ключ нужен SDK для порядка, локальный сервер его не проверяет base_url = os.environ.get("ANTHROPIC_BASE_URL", "http://localhost:11434") client = Anthropic(base_url=base_url, api_key="ollama") async_client = AsyncAnthropic(base_url=base_url, api_key="ollama") MODEL_NAME = os.environ.get("MODEL_NAME", "qwen3:8b")
if not MODEL_NAME.startswith("claude"): # Qwen3 и DeepSeek по умолчанию «думают»: первым блоком ответа приходит thinking, а код курсов # ждёт text в content[0]. Выключаем размышления во всех вызовах (в отдельном вызове можно передать # thinking=... явно — он перекроет значение по умолчанию). for c in (client, async_client): c.messages.create = partial(c.messages.create, thinking={"type": "disabled"}) c.messages.stream = partial(c.messages.stream, thinking={"type": "disabled"})Где работает модель — выбирается переменными окружения перед запуском Jupyter:
| Где модель | Что задать |
|---|---|
| Ollama на этом компьютере | ничего; для урока 6 — export MODEL_NAME=qwen2.5vl:7b |
| Claude | export ANTHROPIC_API_KEY=... (модель по умолчанию — claude-haiku-4-5) |
| DeepSeek (Anthropic-совместимый адрес) | export ANTHROPIC_API_KEY=<ключ DeepSeek> ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic MODEL_NAME=deepseek-flash |
Имена облачных моделей меняются; актуальные смотрите в уроке 8.1.1 и в документации провайдера.
Урок 1. Начало работы с Claude SDK
Заголовок раздела «Урок 1. Начало работы с Claude SDK»Цели урока
Заголовок раздела «Цели урока»В этом первом уроке вы научитесь:
- устанавливать нужные пакеты и проходить аутентификацию в API;
- отправлять первый запрос ИИ-ассистенту Claude.
Установка SDK
Заголовок раздела «Установка SDK»Прежде чем погружаться в SDK, убедитесь, что у вас установлен Python. Python SDK для Claude требует Python 3.7.1 или новее.
Проверить текущую версию Python можно командой в терминале:
python --versionЕсли Python не установлен или версия старше нужной, зайдите на официальный сайт Python и следуйте инструкции для вашей операционной системы.
Когда Python готов, поставьте пакет anthropic через pip:
# Эта команда — для установки из ноутбука%pip install anthropic# Эта — для установки из командной строки# pip install anthropicПолучаем ключ API
Заголовок раздела «Получаем ключ API»Чтобы API узнавал вас, нужен ключ API. Получить его можно так:
- Если ещё не зарегистрированы, создайте аккаунт Anthropic на https://console.anthropic.com.
- Войдите и откройте страницу настроек API. Её можно найти, нажав на значок профиля в правом верхнем углу и выбрав в меню «API Keys», или через меню «API Keys» на вкладке Settings.
- На странице нажмите кнопку «Create Key». Появится окно, где ключу нужно дать понятное имя — такое, чтобы было ясно, для какого проекта он нужен. Ключей в аккаунте может быть сколько угодно (учтите, что лимиты запросов и сообщений действуют на уровне аккаунта, а не ключа).
- Введите имя и нажмите «Create». Новый ключ появится на экране.
Обязательно скопируйте ключ: уйдя со страницы, вы больше не сможете его посмотреть.
(В оригинале здесь скриншот страницы регистрации.)
Помните: ключ API — секретные данные, которые открывают доступ к вашему аккаунту Anthropic. Обращайтесь с ним как с паролем: никогда не публикуйте его и не добавляйте в системы контроля версий вроде Git.
Храним ключ безопасно
Заголовок раздела «Храним ключ безопасно»Ключ можно вписать прямо в код Python, но обычно лучше держать секретные данные вроде ключей отдельно от кода. Один
из распространённых способов — хранить ключ в файле .env и загружать его пакетом python-dotenv. Вот как это
настроить.
Создайте файл .env в той же папке, где лежит ноутбук. Впишите в него ключ в таком формате:
ANTHROPIC_API_KEY=put-your-api-key-hereСохраните файл .env. Поставьте пакет python-dotenv командой в терминале или в ноутбуке:
# Установка из ноутбука:%pip install python-dotenv
# Установка из терминала:# pip install python-dotenvТеперь можно загрузить ключ из файла .env функцией load_dotenv() из модуля dotenv:
from dotenv import load_dotenvimport os
load_dotenv() # читает .env и кладёт значения в переменные окруженияmy_api_key = os.getenv("ANTHROPIC_API_KEY")Простые запросы через клиент
Заголовок раздела «Простые запросы через клиент»Пакет anthropic установлен, ключ загружен — можно отправлять запросы к Claude API. Сначала создадим объект
клиента: это главная точка входа для работы с API.
from anthropic import Anthropic
client = Anthropic( api_key=my_api_key)SDK anthropic сам ищет переменную окружения ANTHROPIC_API_KEY, поэтому передавать ключ вручную не обязательно,
достаточно так:
from anthropic import Anthropic
client = Anthropic()Клиент создан — пора сделать первый запрос. Чтобы отправить Claude сообщение и получить ответ, используем метод
messages.create() объекта client. Параметры и формат ответа подробно разберём в следующем уроке, а пока просто
запустите код ниже — и вы получите первое сообщение от Claude!
from claude_local import client, MODEL_NAME # Claude или локальная модель — см. «Подготовку»
our_first_message = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-haiku-20240307" max_tokens=1000, messages=[ {"role": "user", "content": "Привет! Напиши, пожалуйста, хокку о домашней курице"} ])
print(our_first_message.content[0].text)our_first_message = client.messages.create( model="claude-3-haiku-20240307", max_tokens=1000, messages=[ {"role": "user", "content": "Hi there! Please write me a haiku about a pet chicken"} ])
print(our_first_message.content[0].text)В оригинале Claude ответила таким хокку (в переводе): «Пернатый друг квохчет, / Весь день копается в земле, / Верная курица».
Упражнение
Заголовок раздела «Упражнение»Мы только начали, так что упражнение может показаться скромным. Но потренироваться в основах всегда полезно. Сделайте следующее:
- Создайте новый ноутбук или скрипт на Python.
- Импортируйте нужные пакеты.
- Загрузите ключ API (или подключитесь к локальной модели через
claude_local.py). - Попросите модель рассказать анекдот и напечатайте результат (можно скопировать код выше и подправить его).
Урок 2. Работа с сообщениями
Заголовок раздела «Урок 2. Работа с сообщениями»Цели урока
Заголовок раздела «Цели урока»- Понять формат Messages API.
- Научиться работать с объектом ответа модели и понимать его.
- Написать простого многоходового чат-бота.
Базовая настройка
Заголовок раздела «Базовая настройка»Начнём с импорта нужных пакетов и создания клиента. Как получить ключ API и где его хранить — в прошлом уроке.
from dotenv import load_dotenvload_dotenv() # загружаем переменные окружения из .env (если файла нет — ничего страшного)
# В оригинале: from anthropic import Anthropic; client = Anthropic()# Клиент сам ищет переменную окружения ANTHROPIC_API_KEY; без неё помощник подключится к Ollamafrom claude_local import client, MODEL_NAMEФормат сообщений
Заголовок раздела «Формат сообщений»Как мы видели в прошлом уроке, client.messages.create() отправляет Claude сообщение и возвращает ответ:
response = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=[ {"role": "user", "content": "Какие вкусы используются в Dr. Pepper?"} ])
print(response)В оригинале печатается объект примерно такого вида (текст ответа сокращён):
Message(id='msg_013wVsHLHRjuDM2WgvVJ8RNm', content=[ContentBlock(text='The exact flavor formula for Dr Pepper is a closely guarded trade secret, ...', type='text')], model='claude-3-haiku-20240307', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=18, output_tokens=225))Присмотримся к этому фрагменту:
messages=[ {"role": "user", "content": "Какие вкусы используются в Dr. Pepper?"}]Параметр messages — ключевая часть работы с Claude API. Через него передаются история диалога и контекст, чтобы
модель сгенерировала уместный ответ.
messages — это список словарей, где каждый словарь — одно сообщение диалога. У каждого словаря должны быть ключи:
role— строка, обозначающая, кто отправил сообщение:"user"(пользователь) или"assistant"(Claude);content— строка или список словарей с содержимым сообщения. Строка считается одним текстовым блоком содержимого. Если передан список, у каждого словаря должен быть"type"(например,"text"или"image") и соответствующее содержимое. Пока будем передавать вcontentодну строку.
Пример списка с одним сообщением пользователя:
messages = [ {"role": "user", "content": "Привет, Claude! Как дела?"}]А вот пример с несколькими сообщениями, составляющими диалог:
messages = [ {"role": "user", "content": "Привет, Claude! Как дела?"}, {"role": "assistant", "content": "Привет! У меня всё хорошо, спасибо. Чем могу помочь?"}, {"role": "user", "content": "Расскажи забавный факт о хорьках."}, {"role": "assistant", "content": "Конечно! Знаете ли вы, что возбуждённые хорьки издают кудахтающие звуки, которые называют «доукингом»?"},]Помните, что сообщения всегда чередуются: пользователь, ассистент, пользователь… (В оригинале здесь схема чередования ролей.)
Формат сообщений позволяет строить вызовы API как диалог и сохранять контекст: можно хранить всю историю разговора, включая сообщения и пользователя, и ассистента. Так у модели при генерации ответа есть весь контекст диалога, и ответы получаются связнее и уместнее.
Обратите внимание: во многих задачах история диалога не нужна, и нет ничего плохого в том, чтобы передать список из одного сообщения!
Какие два ключа обязательны в каждом сообщении?
- a)
"sender"и"text" - b)
"role"и"content" - c)
"user"и"assistant" - d)
"input"и"output"
Ответ
Правильный ответ — b. У каждого сообщения должны быть "role" и "content".
Разбираем ответ
Заголовок раздела «Разбираем ответ»Теперь посмотрим, как устроен ответ Claude. Попросим модель о чём-нибудь простом:
response = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=[ {"role": "user", "content": "Переведи «привет» на французский. Ответь одним словом"} ])И посмотрим, что лежит в response:
responseМы получаем объект Message с набором свойств. Например:
Message(id='msg_01Mq5gDnUmDESukTgwPV8xtG', content=[TextBlock(text='Bonjour.', type='text')], model='claude-3-haiku-20240307', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=19, output_tokens=8))Самое важное здесь — свойство content: в нём лежит то, что модель для нас сгенерировала. Это список блоков
содержимого, и у каждого есть тип, который определяет его форму. (В оригинале — схема объекта content.)
Чтобы получить сам текст ответа, нужно сделать так:
print(response.content[0].text) # Bonjour.Кроме content, в объекте Message есть и другие сведения:
id— уникальный идентификатор объекта;type— тип объекта, всегда"message";role— роль сгенерированного сообщения в диалоге, всегда"assistant";model— модель, которая обработала запрос и сгенерировала ответ;stop_reason— причина, по которой модель остановила генерацию (подробнее — позже);stop_sequence— об этом тоже чуть позже;usage— сведения для оплаты и лимитов:input_tokens— сколько входных токенов использовано;output_tokens— сколько выходных токенов использовано.
Важно знать, что всё это доступно, но если запомнить только одно, пусть это будет: в content лежит то, что
сгенерировала модель.
Упражнение
Заголовок раздела «Упражнение»Напишите функцию translate, которая принимает два аргумента:
- слово;
- язык.
При вызове translate должна возвращать результат просьбы к модели перевести word на язык language. Например:
translate("привет", "испанский")# 'Слово «привет» по-испански: Hola'
translate("курица", "итальянский")# 'По-итальянски «курица» — pollo'Дополнительные баллы, если напишете промпт так, чтобы модель отвечала только переведённым словом, без вступления:
translate("курица", "итальянский")# 'pollo'Решение
Одно из возможных решений:
def translate(word, language): response = client.messages.create( model=MODEL_NAME, # в оригинале здесь "claude-3-opus-20240229" max_tokens=1000, messages=[ {"role": "user", "content": f"Переведи слово «{word}» на {language} язык. Ответь только переведённым словом, больше ничего"} ] ) return response.content[0].textОшибки в списке сообщений
Заголовок раздела «Ошибки в списке сообщений»Ошибка № 1: начинать с сообщения ассистента
Заголовок раздела «Ошибка № 1: начинать с сообщения ассистента»Поначалу с messages легко ошибиться. Список сообщений должен начинаться с сообщения user. Этот код выдаёт
ошибку, потому что список начинается с сообщения ассистента:
response = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=[ {"role": "assistant", "content": "Привет!"} ])print(response.content[0].text)Ошибка № 2: неправильное чередование
Заголовок раздела «Ошибка № 2: неправильное чередование»Сообщения должны чередоваться: user, assistant, user… Если нарушить правило, будет ошибка:
response = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=[ {"role": "user", "content": "Привет!"}, {"role": "assistant", "content": "Здравствуйте!"}, {"role": "assistant", "content": "Чем могу помочь??"} ])print(response.content[0].text)Зачем ещё нужен список сообщений
Заголовок раздела «Зачем ещё нужен список сообщений»Вкладываем слова в уста Claude
Заголовок раздела «Вкладываем слова в уста Claude»Ещё одна распространённая стратегия, когда нужен очень конкретный ответ, — «вложить слова в уста Claude». Можно
передавать не только сообщения user, но и сообщение assistant, которое Claude использует при генерации.
В API Anthropic вы не ограничены сообщениями user. Если передать сообщение assistant, Claude продолжит диалог с
последнего токена ассистента. Только помните, что начинать нужно с сообщения user.
Допустим, я хочу, чтобы Claude написала хокку, которое начинается строкой «горный воздух свеж». Можно передать такую историю диалога:
messages=[ {"role": "user", "content": "Сочини красивое хокку"}, {"role": "assistant", "content": "горный воздух свеж"}]Мы говорим модели, что хотим хокку, И вкладываем ей в уста первую строку.
response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[ {"role": "user", "content": "Сочини красивое хокку"}, {"role": "assistant", "content": "горный воздух свеж"} ])print(response.content[0].text)Модель вернёт только продолжение — две оставшиеся строки. Чтобы получить хокку целиком, начиная с нашей строки:
print("горный воздух свеж" + response.content[0].text)В оригинале получилось: «calming mountain air, / dancing sunlight on still waters, / nature’s gentle grace» («горный воздух свеж, / солнце пляшет на тихой воде, / мягкая благодать природы»).
Few-shot: примеры в промпте
Заголовок раздела «Few-shot: примеры в промпте»Одна из самых полезных стратегий промптинга — few-shot prompting: дать модели несколько примеров. Примеры направляют то, что генерирует модель, и история сообщений — простой способ их передать.
Допустим, мы хотим анализировать тональность твитов. Начнём с простого вопроса «проанализируй тональность этого твита» и посмотрим, что выйдет:
response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[ {"role": "user", "content": "Проанализируй тональность этого твита: Только что попробовал новые острые огурчики от @PickleCo — мои вкусовые рецепторы пляшут от счастья! 🌶️🥒 #любовькогурцам #остраяеда"}, ])print(response.content[0].text)В первый раз Claude выдала длинный разбор: тональность «исключительно положительная», а дальше по пунктам — фраза о «пляшущих рецепторах», эмодзи перца и огурца, хэштеги, восклицательный знак и общий вывод.
Ответ отличный, но, наверное, гораздо подробнее, чем нужно, особенно если мы хотим автоматически разметить тональность множества твитов.
Скорее всего, нам удобнее стандартный формат ответа: одно слово (POSITIVE, NEUTRAL, NEGATIVE) или число (1, 0, -1). Для простоты пусть модель отвечает «POSITIVE» или «NEGATIVE». Один из способов — few-shot: передать модели историю диалога, которая показывает, как именно отвечать:
response = client.messages.create( model=MODEL_NAME, max_tokens=500, 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 — мои вкусовые рецепторы пляшут от счастья! 🌶️🥒 #любовькогурцам #остраяеда"}, ])print(response.content[0].text) # POSITIVEУпражнение: чат-бот
Заголовок раздела «Упражнение: чат-бот»Напишите простой многоходовой чат-бот для командной строки. Формат сообщений прямо создан для чат-приложений. Чтобы сделать чат-бота на Claude, достаточно:
- Завести список для истории диалога.
- Спросить у пользователя сообщение через
input()и добавить его в список. - Отправить историю сообщений модели.
- Напечатать ответ модели.
- Добавить ответ ассистента в историю.
- Вернуться к шагу 2 и повторить! (Используйте цикл и дайте пользователю способ выйти.)
Решение
conversation_history = []
while True: user_input = input("Вы: ")
if user_input.lower() == "выход": print("Разговор окончен.") break
conversation_history.append({"role": "user", "content": user_input})
response = client.messages.create( model=MODEL_NAME, messages=conversation_history, max_tokens=500 )
assistant_response = response.content[0].text print(f"Ассистент: {assistant_response}") conversation_history.append({"role": "assistant", "content": assistant_response})Урок 3. Модели
Заголовок раздела «Урок 3. Модели»Цели урока
Заголовок раздела «Цели урока»- Разобраться в моделях Claude.
- Сравнить скорость и возможности моделей.
from dotenv import load_dotenvload_dotenv()
from claude_local import client, MODEL_NAMEМодели Claude
Заголовок раздела «Модели Claude»Python SDK для Claude поддерживает несколько моделей с разными возможностями и характеристиками. (В оригинале здесь диаграмма «стоимость — скорость» для моделей Claude 3 и 3.5, показывающая разброс компромиссов между ценой и интеллектом.)
Выбирая модель, стоит учитывать несколько факторов:
- задержку (насколько модель быстрая);
- возможности (насколько она умная);
- стоимость (насколько она дорогая).
Сравнение ключевых характеристик всех моделей семейства Claude — в таблице в документации.
Сравниваем скорость моделей
Заголовок раздела «Сравниваем скорость моделей»Ниже простая функция: она отправляет один и тот же промпт всем четырём моделям и печатает ответ каждой и время запроса.
import time
def compare_model_speeds(): # В оригинале: ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229", # "claude-3-sonnet-20240229", "claude-3-haiku-20240307"] # Для Claude сейчас: ["claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-6"] # Для Ollama — модели Qwen3 разного размера (скачайте их заранее командой ollama pull): models = ["qwen3:0.6b", "qwen3:1.7b", "qwen3:4b", "qwen3:8b"] task = "Кратко, одним абзацем, объясни, что такое фотосинтез."
for model in models: start_time = time.time()
response = client.messages.create( model=model, max_tokens=500, messages=[{"role": "user", "content": task}] )
end_time = time.time() execution_time = end_time - start_time tokens = response.usage.output_tokens time_per_token = execution_time / tokens
print(f"Модель: {model}") print(f"Ответ: {response.content[0].text}") print(f"Сгенерировано токенов: {tokens}") print(f"Время выполнения: {execution_time:.2f} с") print(f"Время на токен: {time_per_token:.2f} с\n")
compare_model_speeds()Ответы у вас будут другими. Вот сводка того, что получилось у авторов:
| Модель | Сгенерировано токенов | Время, с | Время на токен, с |
|---|---|---|---|
| claude-3-5-sonnet-20240620 | 146 | 2,56 | 0,02 |
| claude-3-opus-20240229 | 146 | 7,32 | 0,05 |
| claude-3-sonnet-20240229 | 108 | 2,64 | 0,02 |
| claude-3-haiku-20240307 | 126 | 1,09 | 0,01 |
Важно и то, что с простым промптом вроде «Кратко, одним абзацем, объясни, что такое фотосинтез» все модели справляются хорошо. В таком случае, скорее всего, разумно выбрать самый быстрый и дешёвый вариант.
Пример выше — простая иллюстрация разницы в скорости, но не строгое доказательство. Авторы построили и более честный
график: дали моделям один и тот же промпт по 50 раз и усреднили время ответа. Чтобы сравнение было «справедливым»,
моделей просили генерировать очень длинные ответы, а затем обрезали все ответы на одном и том же числе токенов с
помощью max_tokens (о нём — в следующем уроке). На графике Haiku заметно быстрее остальных.
Сравниваем возможности моделей
Заголовок раздела «Сравниваем возможности моделей»Haiku явно самая быстрая модель — зачем тогда другие? Всё сводится к компромиссу между скоростью, стоимостью и общими возможностями. Haiku быстрее всех, но в некоторых ситуациях её ответы могут уступать по качеству ответам Opus. При этом во многих случаях Haiku справляется не хуже более сильных моделей. Единственный способ узнать, какая модель «лучшая» для вашей задачи, — попробовать их и оценить результат.
В целом авторы рекомендовали самую сильную на тот момент модель, Claude 3.5 Sonnet, для таких задач:
- Программирование: Claude 3.5 Sonnet сама пишет, правит и запускает код, ускоряя перевод кода между языками и миграции.
- Поддержка клиентов: понимает контекст пользователя и ведёт многошаговые процессы — поддержка круглые сутки, быстрые ответы, довольные клиенты.
- Анализ данных: разбирается в неструктурированных данных, находит закономерности, строит визуализации и прогнозы.
- Обработка изображений: хорошо читает диаграммы, графики и картинки, точно переписывает текст с них и делает выводы не только из текста.
- Тексты: заметно лучше понимает нюансы и юмор и пишет качественные, живые тексты.
Сравнение моделей на бенчмарках — в карточке моделей Claude 3.
Демонстрация возможностей
Заголовок раздела «Демонстрация возможностей»Показать все возможности моделей одной демонстрацией трудно, но функция ниже пытается. Мы просим каждую модель решить такую задачу:
Каково среднее геометрическое за месяц для фекальных колиформ в распределительной сети при такихзначениях FC: 24, 15, 7, 16, 31 и 23? Результат будет внесён в отчёт NPDES DMR, поэтому округлидо ближайшего целого.Правильный ответ — 18. Каждая модель решает задачу 7 раз, и мы записываем ответы:
def compare_model_capabilities(): # В оригинале — те же четыре модели Claude 3 и 3.5, что и выше models = ["qwen3:0.6b", "qwen3:1.7b", "qwen3:4b", "qwen3:8b"] task = """ Каково среднее геометрическое за месяц для фекальных колиформ в распределительной сети при таких значениях FC: 24, 15, 7, 16, 31 и 23? Результат будет внесён в отчёт NPDES DMR, поэтому округли до ближайшего целого. Ответь только числом и больше ничего не пиши. """
for model in models: answers = [] for attempt in range(7): response = client.messages.create( model=model, max_tokens=1000, messages=[{"role": "user", "content": task}] ) answers.append(response.content[0].text)
print(f"Модель: {model}") print("Ответы: ", answers)
compare_model_capabilities()Результаты одного прогона у авторов:
claude-3-5-sonnet-20240620— верный ответ 7 из 7 раз;claude-3-opus-20240229— 7 из 7;claude-3-sonnet-20240229— 3 из 7 (ответы 18, 17, 16, 17, 19, 18, 18);claude-3-haiku-20240307— 2 из 7 (17, 17, 18, 17, 17, 17, 18).
Очевидно, что с этой задачей лучше всех справляются Claude 3.5 Sonnet и Claude 3 Opus.
Примечание: это очень упрощённая демонстрация возможностей моделей. Это совсем не строгое сравнение, а лишь наглядный учебный пример. Строгое количественное сравнение на стандартных бенчмарках — в карточке моделей Claude 3.
Как выбрать модель
Заголовок раздела «Как выбрать модель»Следующий логичный вопрос: какую модель использовать? Не зная задач и требований конкретного приложения, ответить трудно. Выбор модели сильно влияет на качество, удобство для пользователя и экономику приложения:
- Возможности. Первое и главное — справляется ли модель с задачами и сценариями вашего приложения. Разные модели по-разному сильны в разных областях: общее понимание языка, знания в предметной области, рассуждения, качество текста. Важно, чтобы сильные стороны модели совпадали с требованиями приложения.
- Скорость. Скорость обработки и генерации — ещё один важнейший фактор, особенно в приложениях, где нужен ответ в реальном или почти реальном времени. Быстрые модели делают работу с приложением отзывчивее и удобнее. Но нужен баланс между скоростью и возможностями: самая быстрая модель не всегда подходит лучше всего.
- Стоимость. Цена модели — практическое соображение, от которого зависят жизнеспособность и масштабируемость приложения. Модели с бо́льшими возможностями обычно дороже — и по цене API, и по нужным вычислительным ресурсам. Важно оценить затраты и найти самый выгодный вариант, который всё ещё отвечает требованиям.
Один из подходов: начать с Haiku
Заголовок раздела «Один из подходов: начать с Haiku»Для экспериментов авторы часто советуют начинать с Haiku. Это лёгкая и быстрая модель, отличная отправная точка для многих приложений: скорость и низкая цена делают её удобной для первых опытов и прототипов. Во многих задачах Haiku вполне способна давать качественные ответы. Начав с неё, можно быстро перебирать промпты и настройки и оценивать качество без больших затрат и задержек. Если ответы не устраивают, легко «перейти» на модель вроде Claude 3.5 Sonnet.
Оценка и переход на модель сильнее
Заголовок раздела «Оценка и переход на модель сильнее»По мере развития приложения важно завести полноценный набор оценок (эвалов) под вашу задачу и ваши промпты. Он станет эталоном, по которому вы измерите качество выбранной модели и примете обоснованное решение о переходе.
Если ответы Haiku не отвечают требованиям или нужны бо́льшая сложность и точность, можно легко перейти на более сильные модели вроде Sonnet или Opus: они справляются со сложными задачами и тонкостями языка лучше.
Строгая система оценок позволяет объективно сравнить модели на вашей задаче. Эти данные и подскажут, какая модель подходит приложению лучше всего.
Урок 4. Параметры модели
Заголовок раздела «Урок 4. Параметры модели»Цели урока
Заголовок раздела «Цели урока»- Понять роль параметра
max_tokens. - Управлять ответами модели параметром
temperature. - Объяснить, зачем нужен
stop_sequence.
from dotenv import load_dotenvload_dotenv()
from claude_local import client, MODEL_NAMEМаксимум токенов (max_tokens)
Заголовок раздела «Максимум токенов (max_tokens)»В каждом запросе к Claude обязательно указывать три параметра:
modelmax_tokensmessages
До сих пор мы ставили max_tokens в каждый запрос, но ни разу не остановились, чтобы разобраться, что это такое.
Вот самый первый наш запрос:
our_first_message = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[ {"role": "user", "content": "Привет! Напиши, пожалуйста, хокку о домашней курице"} ])Так зачем нужен max_tokens?
Коротко: max_tokens задаёт максимальное число токенов, которое Claude может сгенерировать в ответе. Но сначала
поговорим о самих токенах.
Большинство больших языковых моделей «думают» не целыми словами, а последовательностью кусочков слов — токенов. Токены — это маленькие строительные блоки текста, которые Claude обрабатывает, понимает и генерирует. Когда мы даём Claude промпт, он сначала превращается в токены и передаётся модели. Затем модель генерирует ответ по одному токену за раз.
У Claude токен в среднем соответствует примерно 3,5 символа английского текста, но точное число зависит от языка.
Как работать с max_tokens
Заголовок раздела «Как работать с max_tokens»Параметр max_tokens задаёт верхний предел числа токенов, которые сгенерирует Claude. Допустим, мы просим написать
стихотворение и ставим max_tokens = 10. Claude начнёт генерировать токены и остановится, как только дойдёт до
десяти. Часто это даёт обрезанный, незаконченный ответ. Попробуем!
truncated_response = client.messages.create( model=MODEL_NAME, max_tokens=10, messages=[ {"role": "user", "content": "Напиши мне стихотворение"} ])print(truncated_response.content[0].text)У авторов Claude успела написать только «Here is a poem for you: The» («Вот стихотворение для вас: The»). У вас результат, скорее всего, будет другим, но таким же обрезанным: модель начала писать и остановилась на десятом токене.
Посмотреть, ПОЧЕМУ модель остановилась, можно в свойстве stop_reason объекта ответа. Здесь в нём значение
"max_tokens": модель остановилась, потому что дошла до нашего лимита!
truncated_response.stop_reason # 'max_tokens'Конечно, если повторить запрос с большим max_tokens, мы, скорее всего, получим стихотворение целиком:
longer_poem_response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[ {"role": "user", "content": "Напиши мне стихотворение"} ])print(longer_poem_response.content[0].text)С max_tokens = 500 у авторов получилось законченное стихотворение «Whispers of the Wind» («Шёпот ветра») из трёх
строф. Если посмотреть на stop_reason этого ответа, там будет "end_turn": так модель сообщает, что закончила
естественным образом. Она написала стихотворение, больше ей сказать нечего — и она остановилась.
longer_poem_response.stop_reason # 'end_turn'Важно понимать, что модель «не знает» о max_tokens, когда генерирует текст. Значение max_tokens не меняет того,
как Claude пишет ответ: большой лимит просто оставляет модели место, маленький — обрезает ответ.
Также важно знать, что большой max_tokens не гарантирует ответа определённой длины. Если попросить анекдот и
поставить max_tokens = 1000, ответ почти наверняка будет гораздо короче 1000 токенов.
response = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=[{"role": "user", "content": "Расскажи анекдот"}])print(response.content[0].text)print(response.usage.output_tokens)У авторов Claude рассказала классическую шутку («Почему учёные не доверяют атомам? Потому что те всё выдумывают!» — по-английски это игра слов: atoms make up everything — «составляют всё» и «всё выдумывают») и спросила, рассказать ли ещё. Ответ занял всего 55 токенов: мы дали потолок в 1000, но это не значит, что модель их сгенерирует.
Зачем менять max_tokens
Заголовок раздела «Зачем менять max_tokens»Понимать токены важно при работе с Claude по таким причинам:
- Лимиты API. Токены входного текста и сгенерированного ответа учитываются в лимитах использования API. У каждого запроса есть предельное число токенов, которое он может обработать. Понимание токенов помогает укладываться в лимиты и расходовать их разумно.
- Производительность. Число сгенерированных токенов прямо влияет на время обработки и память. Длинные входы и
большие
max_tokensтребуют больше вычислений. Понимание токенов помогает оптимизировать запросы. - Качество ответа. Подходящее значение
max_tokensгарантирует, что ответ будет достаточно длинным и вместит всё нужное. Еслиmax_tokensслишком мал, ответ может оборваться. Поэкспериментируйте, чтобы найти баланс для своей задачи.
Посмотрим, как число сгенерированных токенов влияет на скорость. Функция ниже трижды просит Claude сочинить очень
длинный диалог двух персонажей, каждый раз с другим max_tokens. Затем печатает, сколько токенов реально
сгенерировано и сколько времени это заняло.
import time
def compare_num_tokens_speed(): token_counts = [100, 1000, 4096] task = """ Сочини длинный, подробный диалог — не меньше 5000 слов — двух персонажей, которые обсуждают влияние соцсетей на психическое здоровье. У персонажей должны быть разные мнения, и они должны вести уважительный и обстоятельный спор. """
for num_tokens in token_counts: start_time = time.time()
response = client.messages.create( model=MODEL_NAME, max_tokens=num_tokens, messages=[{"role": "user", "content": task}] )
end_time = time.time() execution_time = end_time - start_time
print(f"Сгенерировано токенов: {response.usage.output_tokens}") print(f"Время выполнения: {execution_time:.2f} с\n")
compare_num_tokens_speed()Один из прогонов у авторов:
Сгенерировано токенов: 100Время выполнения: 1.51 с
Сгенерировано токенов: 1000Время выполнения: 8.33 с
Сгенерировано токенов: 3433Время выполнения: 28.80 сКак видите, чем больше токенов генерирует Claude, тем дольше это занимает!
Ещё нагляднее другой опыт авторов: они просили Claude повторить очень длинный текст и обрезали генерацию с помощью
max_tokens на разной длине, по 50 раз для каждой длины, и усредняли время. На графике время растёт вместе с длиной
ответа почти линейно.
Стоп-последовательности (stop_sequences)
Заголовок раздела «Стоп-последовательности (stop_sequences)»Ещё один важный параметр, который мы пока не видели, — stop_sequences. Это набор строк: если какая-то из них
появится в генерируемом ответе, генерация остановится. По сути, мы говорим Claude: «Если сгенерируешь эту
последовательность — больше ничего не пиши!»
Вот запрос без стоп-последовательности:
response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[{"role": "user", "content": "Сгенерируй JSON-объект, описывающий человека: имя, email и телефон."}],)print(response.content[0].text)Код просит модель сгенерировать JSON-объект с описанием человека. Claude сгенерировала объект
{"name": "John Doe", "email": "johndoe@example.com", "phoneNumber": "123-456-7890"}, а после него — объяснение
каждого поля и совет заменить значения на свои.
Объект модель выдала, но добавила объяснение. Если мы хотим, чтобы Claude остановилась сразу после закрывающей
скобки } JSON-объекта, добавим в код параметр stop_sequences.
response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[{"role": "user", "content": "Сгенерируй JSON-объект, описывающий человека: имя, email и телефон."}], stop_sequences=["}"])print(response.content[0].text)Модель сгенерировала:
Here's a JSON object representing a person with a name, email, and phone number:{ "name": "John Doe", "email": "john.doe@example.com", "phone": "555-1234"ВАЖНО: обратите внимание, что в ответе нет самой стоп-последовательности }. Если бы мы хотели разобрать
ответ как JSON, закрывающую } пришлось бы дописать.
Почему модель остановилась, можно проверить в свойстве stop_reason. Предыдущий ответ остановился по причине
'stop_sequence': модель сгенерировала одну из заданных стоп-последовательностей и сразу остановилась.
response.stop_reason # 'stop_sequence'Какая именно последовательность сработала, показывает свойство stop_sequence:
response.stop_sequence # '}'Стоп-последовательностей может быть несколько. Тогда модель остановится, как только встретит любую из них, а
свойство stop_sequence в ответе покажет, какую именно.
Функция ниже трижды просит Claude написать стихотворение и остановиться, если появится буква «b» или «c»:
def generate_random_letters_3_times(): for i in range(3): response = client.messages.create( model=MODEL_NAME, max_tokens=500, messages=[{"role": "user", "content": "Сочини стихотворение"}], stop_sequences=["б", "в"] # в оригинале — латинские "b" и "c" в английском стихотворении ) print(f"Ответ {i+1} остановился по причине {response.stop_reason}. Стоп-последовательность: {response.stop_sequence}")
generate_random_letters_3_times()Пример вывода у авторов: в первый раз Claude остановилась на букве «c», во второй и третий — на «b». Стали бы вы так делать на практике? Вряд ли!
Температура (temperature)
Заголовок раздела «Температура (temperature)»Параметр temperature управляет «случайностью» и «творческостью» ответов. Он принимает значения от 0 до 1: чем
выше, тем разнообразнее и непредсказуемее ответы и тем больше вариантов формулировок. Низкая температура даёт более
детерминированный результат — самые вероятные формулировки и ответы. По умолчанию температура равна 1.
Генерируя текст, Claude предсказывает распределение вероятностей следующего токена (слова или части слова). Температура изменяет это распределение перед тем, как выбрать следующий токен. При низкой температуре (близкой к 0,0) распределение становится острее: самым вероятным токенам достаются ещё большие вероятности. Модель становится детерминированнее и выбирает самые вероятные, «безопасные» варианты. При высокой температуре (ближе к 1,0) распределение становится площе: вероятности менее вероятных токенов растут. Модель становится случайнее и «исследует» больше, ответы получаются разнообразнее и творческее. (В оригинале — схема того, как температура меняет распределение.)
Зачем менять температуру? Для аналитических задач ставьте температуру ближе к 0,0, для творческих и генеративных — ближе к 1,0.
Небольшая демонстрация. Функция ниже трижды отправляет запрос с температурой 0, а затем трижды с температурой 1, прося «Придумай название для инопланетной планеты. Ответь одним словом».
def demonstrate_temperature(): temperatures = [0, 1] for temperature in temperatures: print(f"Три запроса с температурой {temperature}") print("================") for i in range(3): response = client.messages.create( model=MODEL_NAME, max_tokens=100, messages=[{"role": "user", "content": "Придумай название для инопланетной планеты. Ответь одним словом."}], temperature=temperature ) print(f"Ответ {i+1}: {response.content[0].text}")
demonstrate_temperature()Результат у авторов (у вас будет другим):
Три запроса с температурой 0================Ответ 1: Xendor.Ответ 2: Xendor.Ответ 3: Xendor.Три запроса с температурой 1================Ответ 1: Xyron.Ответ 2: Xandar.Ответ 3: Zyrcon.При температуре 0 все три ответа одинаковые. Учтите, что даже при температуре 0,0 результат не полностью детерминирован. Но разница с температурой 1 очевидна: там каждый ответ — совсем другое название планеты.
Авторы показали влияние температуры и на графике. С промптом «Выбери любое животное в мире. Ответь одним словом — названием животного» они спросили Claude 100 раз при температуре 0 и ещё 100 раз при температуре 1 и посчитали, как часто встречается каждое животное. При температуре 0 Claude каждый раз отвечала «Жираф». Помните: температура 0 не гарантирует детерминированности, но делает похожие ответы гораздо вероятнее. При температуре 1 Claude всё равно выбирала жирафа больше чем в половине случаев, но среди ответов появилось и много других животных!
Системный промпт
Заголовок раздела «Системный промпт»Системный промпт — необязательный параметр system, который можно передать вместе с сообщениями. Он задаёт рамку
разговора: даёт Claude общие инструкции, определяет роль или сообщает фоновые сведения, которые должны влиять на
ответы.
Главное о системном промпте:
- Он необязателен, но полезен, чтобы задать тон и контекст разговора.
- Он действует на уровне всего разговора и влияет на все ответы Claude в нём.
- Он помогает направлять поведение Claude, не повторяя инструкции в каждом сообщении пользователя.
Учтите, что в системный промпт в основном стоит класть только тон, контекст и роль. Подробные инструкции, внешние
данные (например, документы) и примеры для лучшего результата помещайте в первый ход user. В последующих ходах
user их повторять не нужно.
Попробуем:
message = client.messages.create( model=MODEL_NAME, max_tokens=1000, system="Ты — услужливый репетитор иностранного языка и всегда отвечаешь по-французски.", messages=[ {"role": "user", "content": "Привет, как дела?!"} ])
print(message.content[0].text) # у авторов: «Bonjour ! Je suis ravi de vous rencontrer. Comment allez-vous aujourd'hui ?»Упражнение
Заголовок раздела «Упражнение»Напишите функцию generate_questions, которая:
- принимает два параметра:
topic(тема) иnum_questions(число вопросов); - генерирует
num_questionsвопросов на размышление по темеtopicв виде нумерованного списка; - печатает сгенерированные вопросы.
Например, вызов generate_questions(topic="свобода воли", num_questions=3) может напечатать три вопроса: насколько
наши решения действительно исходят из свободной воли, а не из генов, воспитания и общества; совместима ли свобода
воли с детерминизмом; что меняется в представлениях об ответственности и наказании, если принять или отвергнуть
свободу воли.
В решении используйте параметры:
max_tokens— чтобы ограничить ответ 1000 токенов;system— системный промпт, который говорит модели, что она эксперт по темеtopicи должна выдать нумерованный список;stop_sequences— чтобы модель остановилась ровно после нужного числа вопросов. (Если просим 3 вопроса, модель должна остановиться, как только напишет «4.»; если 5 — как только напишет «6.».)
Возможное решение
def generate_questions(topic, num_questions=3): response = client.messages.create( model=MODEL_NAME, max_tokens=500, system=f"Ты — эксперт по теме «{topic}». Придумывай вопросы на размышление по этой теме.", messages=[ {"role": "user", "content": f"Придумай {num_questions} вопроса(ов) по теме «{topic}» в виде нумерованного списка."} ], stop_sequences=[f"{num_questions+1}."] # остановиться, как только начнётся лишний пункт ) print(response.content[0].text)
generate_questions(topic="свобода воли", num_questions=3)Урок 5. Стриминг
Заголовок раздела «Урок 5. Стриминг»Цели урока
Заголовок раздела «Цели урока»- Понять, как работает стриминг (потоковая выдача).
- Научиться работать с событиями стрима.
from dotenv import load_dotenvload_dotenv()
from claude_local import client, MODEL_NAMEДо сих пор мы отправляли сообщения Claude так:
response = client.messages.create( messages=[ { "role": "user", "content": "Напиши мне эссе об ара и глиняных солонцах в Амазонии", } ], model=MODEL_NAME, max_tokens=800, temperature=0,)print("Ответ получен!")print("========================")print(response.content[0].text)Это работает, но важно помнить: при таком подходе мы получаем ответ от API только когда всё содержимое сгенерировано. Запустите ячейку ещё раз — и увидите, что ничего не печатается, пока весь ответ не появится разом. Во многих случаях это нормально, но если вы делаете приложение, где пользователь должен ждать весь ответ, ему будет неудобно.
Здесь на сцену выходит стриминг!
Стриминг позволяет писать приложения, которые получают содержимое по мере того, как модель его генерирует, а не ждут весь ответ. Так работают приложения вроде claude.ai: пока модель генерирует ответ, текст потоком идёт в браузер пользователя и сразу отображается. (В оригинале здесь анимация интерфейса claude.ai.)
Работаем со стримом
Заголовок раздела «Работаем со стримом»Чтобы получить ответ потоком, достаточно передать stream=True в client.messages.create. Это простая часть. Сложнее
то, как потом работать с потоковым ответом и обрабатывать приходящие данные.
stream = client.messages.create( messages=[ { "role": "user", "content": "Напиши предложение из трёх слов, без вступления. Только три слова", } ], model=MODEL_NAME, max_tokens=100, temperature=0, stream=True,)Посмотрим на переменную stream:
stream # <anthropic.Stream at 0x111e5ec10>Смотреть особенно не на что! Сам по себе объект стрима мало что даёт. Это генератор, который выдаёт отдельные события, отправленные сервером (server-sent events, SSE), по мере их получения от API. Нам нужно перебрать его и обработать каждое событие: данные больше не приходят одним готовым куском. Попробуем перебрать потоковый ответ:
for event in stream: print(event)Мы получили от API много событий. Разберёмся, что они значат. (В оригинале — цветная схема с пояснением событий.)
Каждый стрим содержит события в таком порядке:
- MessageStartEvent — сообщение с пустым содержимым;
- серия блоков содержимого, каждый из которых состоит из:
- события ContentBlockStartEvent;
- одного или нескольких событий ContentBlockDeltaEvent;
- события ContentBlockStopEvent;
- одно или несколько событий MessageDeltaEvent — изменения верхнего уровня в итоговом сообщении;
- финальное событие MessageStopEvent.
В нашем ответе был только один блок содержимого. (В оригинале — схема всех событий, связанных с ним.)
Всё содержимое, которое сгенерировала модель и которое нам интересно, приходит в событиях ContentBlockDeltaEvent; их
тип — "content_block_delta". Сам текст лежит в свойстве text внутри delta. Напечатаем только
сгенерированный текст:
stream = client.messages.create( messages=[ { "role": "user", "content": "Напиши предложение из трёх слов, без вступления. Только три слова", } ], model=MODEL_NAME, max_tokens=100, temperature=0, stream=True,)for event in stream: if event.type == "content_block_delta": print(event.delta.text)У авторов вывод выглядел так — по кусочку на строке:
Cats meow loudly.Содержимое печатается, но читать его неудобно. Печатая поток функцией print(), полезно передать два
дополнительных аргумента:
end="". По умолчаниюprint()добавляет в конце символ новой строки (\n). Сend=""новая строка не добавляется, и следующийprint()продолжит печатать на той же строке.flush=True. Заставляет сразу выводить текст в консоль, не дожидаясь символа новой строки или заполнения буфера. Так текст появляется в реальном времени, по мере поступления.
Внесём эти изменения:
stream = client.messages.create( messages=[ { "role": "user", "content": "Напиши предложение из трёх слов, без вступления. Только три слова", } ], model=MODEL_NAME, max_tokens=100, temperature=0, stream=True,)for event in stream: if event.type == "content_block_delta": print(event.delta.text, flush=True, end="") # у авторов: Cats meow loudly.На таком коротком тексте стриминг почти незаметен. Попросим модель написать что-нибудь подлиннее:
stream = client.messages.create( messages=[ { "role": "user", "content": "Как работают большие языковые модели?", } ], model=MODEL_NAME, max_tokens=1000, temperature=0, stream=True,)for event in stream: if event.type == "content_block_delta": print(event.delta.text, flush=True, end="")Запустите ячейку, если ещё не запускали: текст будет появляться постепенно, по мере поступления!
Итак, события ContentBlockDeltaEvent содержат сгенерированный текст. Есть и много других событий — нужны ли они нам? Да! Вот один быстрый пример. Если нужны сведения об использовании токенов, придётся заглянуть в два места:
MessageStartEventсодержит число входных токенов (токенов промпта);MessageDeltaEventсодержит число сгенерированных выходных токенов.
Изменим код, чтобы печатать, сколько токенов ушло на промпт и сколько сгенерировала модель:
stream = client.messages.create( messages=[ { "role": "user", "content": "Как работают большие языковые модели?", } ], model=MODEL_NAME, max_tokens=1000, temperature=0, stream=True,)for event in stream: if event.type == "message_start": input_tokens = event.message.usage.input_tokens print("СОБЫТИЕ MESSAGE START", flush=True) print(f"Входных токенов: {input_tokens}", flush=True) print("========================") elif event.type == "content_block_delta": print(event.delta.text, flush=True, end="") elif event.type == "message_delta": output_tokens = event.usage.output_tokens print("\n========================", flush=True) print("СОБЫТИЕ MESSAGE DELTA", flush=True) print(f"Выходных токенов: {output_tokens}", flush=True)Другие типы событий
Заголовок раздела «Другие типы событий»Работая со стримами, вы можете встретить и другие типы событий:
- ping — в стриме может быть сколько угодно событий ping;
- error — иногда в стриме появляются события ошибки. Например, в периоды высокой нагрузки может прийти
overloaded_error, что в обычном (непотоковом) режиме соответствовало бы HTTP 529.
Пример события ошибки:
event: errordata: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}Время до первого токена (TTFT)
Заголовок раздела «Время до первого токена (TTFT)»Главная причина использовать стриминг — сократить время до первого токена (time to first token, TTFT): сколько проходит, прежде чем вы или пользователь получите первый кусочек сгенерированного текста. Покажем, как стриминг влияет на TTFT.
Начнём с обычного подхода без стриминга. Попросим модель написать очень длинный текст, но обрежем его на 500 токенах:
import time
def measure_non_streaming_ttft(): start_time = time.time()
response = client.messages.create( max_tokens=500, messages=[ { "role": "user", "content": "Напиши мне длинное эссе об истории Американской революции", } ], temperature=0, model=MODEL_NAME, )
response_time = time.time() - start_time
print(f"Время до первого токена: {response_time:.3f} с") print(f"Время до полного ответа: {response_time:.3f} с") print(f"Всего сгенерировано токенов: {response.usage.output_tokens}")
print(response.content[0].text)
measure_non_streaming_ttft()Теперь то же самое со стримингом:
def measure_streaming_ttft(): start_time = time.time()
stream = client.messages.create( max_tokens=500, messages=[ { "role": "user", "content": "Напиши мне длинное эссе об истории Американской революции", } ], temperature=0, model=MODEL_NAME, stream=True ) have_received_first_token = False for event in stream: if event.type == "content_block_delta": if not have_received_first_token: ttft = time.time() - start_time # запоминаем момент первого куска текста have_received_first_token = True print(event.delta.text, flush=True, end="") elif event.type == "message_delta": output_tokens = event.usage.output_tokens total_time = time.time() - start_time
print(f"\nВремя до первого токена: {ttft:.3f} с", flush=True) print(f"Время до полного ответа: {total_time:.3f} с", flush=True) print(f"Всего сгенерировано токенов: {output_tokens}", flush=True)
measure_streaming_ttft()Сравним результаты авторов:
| Без стриминга | Со стримингом | |
|---|---|---|
| Время до первого токена | 4,194 с | 0,492 с |
| Время до полного ответа | 4,194 с | 4,274 с |
| Сгенерировано токенов | 500 | 500 |
Разница в TTFT огромная! А ведь демонстрация генерирует всего 500 токенов, и это Haiku — самая быстрая модель. Если
сгенерировать 1000 токенов моделью Opus, цифры будут совсем другими. Функцию compare_ttft(), которая делает такое
сравнение для claude-3-opus-20240229, авторы сопроводили предупреждением «НЕ ЗАПУСКАЙТЕ: работает больше минуты и
генерирует около 2000 токенов Opus». Их результат: со стримингом первый токен пришёл через 1,9 секунды, без
стриминга — через 47 секунд (сгенерировано 997 и 998 токенов).
Когда Opus генерирует длинный текст, влияние стриминга на TTFT ещё очевиднее: без стриминга первый токен пришёл через 47 секунд, со стримингом — через 1,8.
Помните: стриминг не ускоряет волшебным образом генерацию ответа в целом. Первые данные приходят гораздо раньше, но от начала запроса до последнего сгенерированного токена проходит столько же времени.
Помощники для стриминга
Заголовок раздела «Помощники для стриминга»Python SDK даёт несколько удобств для стриминга. Вместо client.messages.create с stream=True можно вызвать
client.messages.stream — тогда доступны полезные вспомогательные методы. client.messages.stream() возвращает
MessageStreamManager — контекстный менеджер, который выдаёт MessageStream: его можно перебирать, он генерирует
события и накапливает сообщение.
Код ниже использует client.messages.stream, что позволяет брать помощники вроде stream.text_stream: так текст
удобно получать по мере поступления, не проверяя вручную тип события. stream.text_stream — итератор только по
текстовым дельтам стрима.
Есть и другие полезные методы, например get_final_message: когда стрим прочитан до конца, он возвращает итоговое
накопленное сообщение. Это удобно, если нужен и стриминг, и весь готовый текст после окончания генерации. Можно,
конечно, написать код, который сам собирает сообщение, но с этим методом проще.
Пример ниже печатает каждый кусок текста по мере поступления, а когда стрим закончится — итоговое сообщение целиком:
# В оригинале: from anthropic import AsyncAnthropic; client = AsyncAnthropic()from claude_local import async_client as client, MODEL_NAME
async def streaming_with_helpers(): async with client.messages.stream( max_tokens=1024, messages=[ { "role": "user", "content": "Напиши мне сонет об орхидеях", } ], model=MODEL_NAME, # в оригинале — "claude-3-opus-20240229" ) as stream: async for text in stream.text_stream: print(text, end="", flush=True)
final_message = await stream.get_final_message() print("\n\nСТРИМ ЗАКОНЧЕН. ВОТ ИТОГОВОЕ НАКОПЛЕННОЕ СООБЩЕНИЕ: ") print(final_message.to_json())
await streaming_with_helpers() # в Jupyter await можно писать прямо в ячейке; в скрипте — asyncio.run(...)С client.messages.stream() можно было также задать свои обработчики, которые срабатывают на любое событие стрима,
только на генерацию текста и т. д. В оригинале следующий пример определяет класс MyStream (наследник
AsyncMessageStream) с двумя обработчиками: on_text (срабатывает на каждую текстовую дельту и печатает текст
зелёным) и on_stream_event (срабатывает на любое событие и печатает его тип) — и передаёт его в
client.messages.stream(..., event_handler=MyStream). Там же перечислены другие обработчики: on_message (собрано
целое сообщение, событие message_stop), on_content_block (собран целый блок, content_block_stop),
on_exception (исключение во время стрима), on_timeout (таймаут запроса) и on_end (последнее событие стрима).
green = '\033[32m'reset = '\033[0m'
async def streaming_events_demo(): async with client.messages.stream( max_tokens=1024, messages=[ { "role": "user", "content": "Сочини стихотворение из пяти слов", } ], model=MODEL_NAME, ) as stream: async for event in stream: print("событие:", event.type) # как on_stream_event: любое событие if event.type == "text": print(green + event.text + reset, flush=True) # как on_text: только текст, зелёным # Итоговое накопленное сообщение, когда стрим исчерпан message = await stream.get_final_message() print("итоговое сообщение: ", message.to_json())
await streaming_events_demo()Упражнение
Заголовок раздела «Упражнение»Напишите простой чат-бот со стримингом. В оригинале работа бота показана анимацией: реплики пользователя синим, ответы модели зелёным; раскраска необязательна и нужна в основном для наглядности.
Возможное решение
Одна из простых реализаций. Лучше всего запускать её отдельным скриптом, а не ячейкой ноутбука:
from claude_local import client, MODEL_NAME # в оригинале: from anthropic import Anthropic; client = Anthropic()
# Цветовые коды ANSIBLUE = "\033[94m"GREEN = "\033[92m"RESET = "\033[0m"
def chat_with_claude(): print("Добро пожаловать в чат-бот!") print("Напишите «выход», чтобы закончить.")
conversation = []
while True: user_input = input(f"{BLUE}Вы: {RESET}")
if user_input.lower() == "выход": print("До свидания!") break
conversation.append({"role": "user", "content": user_input})
print(f"{GREEN}Модель: {RESET}", end="", flush=True)
stream = client.messages.create( model=MODEL_NAME, max_tokens=1000, messages=conversation, stream=True )
assistant_response = "" for chunk in stream: if chunk.type == "content_block_delta": content = chunk.delta.text print(f"{GREEN}{content}{RESET}", end="", flush=True) assistant_response += content # копим ответ, чтобы добавить его в историю
print() # перевод строки после полного ответа
conversation.append({"role": "assistant", "content": assistant_response})
if __name__ == "__main__": chat_with_claude()Урок 6. Промпты с картинками
Заголовок раздела «Урок 6. Промпты с картинками»Возможности зрения
Заголовок раздела «Возможности зрения»Модели семейства Claude 3 умеют понимать и анализировать изображения. В разговор можно передавать и текст, и картинки — это открывает новые сценарии. С изображениями работают все три модели: Opus, Sonnet и Haiku. У Claude 3.5 Sonnet самое сильное зрение, поэтому в этом уроке используется она.
# Скачиваем картинки урока из репозитория anthropics/courses (один раз)import osimport urllib.request
BASE = "https://raw.githubusercontent.com/anthropics/courses/master/anthropic_api_fundamentals/"files = [f"prompting_images/{name}.png" for name in ["uh_oh", "animal1", "animal2", "animal3", "people", "slide1", "slide2", "slide3"]]files += [f"images/research_paper/page{i}.png" for i in range(1, 6)]
for f in files: os.makedirs(os.path.dirname(f), exist_ok=True) # создаём папки, если их нет if not os.path.exists(f): urllib.request.urlretrieve(BASE + f, f)print("Готово:", len(files), "файлов")Чтобы дать Claude картинку, используется тот же формат messages, что и для текстовых разговоров. Обычное текстовое
сообщение пользователя выглядит так:
messages = [ { "role": "user", "content": "расскажи анекдот" }]from dotenv import load_dotenvload_dotenv()
from claude_local import client, MODEL_NAME
messages = [ {"role": "user", "content": "расскажи анекдот"}]
response = client.messages.create( messages=messages, model=MODEL_NAME, max_tokens=200)print(response.content[0].text)Чего мы ещё не видели: content в сообщении можно задать списком блоков содержимого. Вместо
messages = [ {"role": "user", "content": "расскажи анекдот"}]можно написать так:
messages = [ { "role": "user", "content": [ {"type": "text", "text": "расскажи анекдот"}, ] }]Эти два сообщения равнозначны:
{"role": "user", "content": "Расскажи историю"}{"role": "user", "content": [{"type": "text", "text": "Расскажи историю"}]}Попробуем:
messages = [ { "role": "user", "content": [ {"type": "text", "text": "расскажи анекдот"}, ] }]
response = client.messages.create( messages=messages, model=MODEL_NAME, max_tokens=200)print(response.content[0].text)Работает! В список можно добавить сколько угодно блоков, как в этом примере:
messages = [ { "role": "user", "content": [ {"type": "text", "text": "кто"}, {"type": "text", "text": "тебя"}, {"type": "text", "text": "создал?"}, ] }]
response = client.messages.create( messages=messages, model=MODEL_NAME, max_tokens=200)print(response.content[0].text)Зачем так делать? С чисто текстовым промптом, наверное, незачем, но этот формат обязателен для мультимодальных промптов!
Чтобы передать Claude картинку, нужно написать блок содержимого типа image. Пример:
messages = [ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": "/9j/4AAQSkZJRg..." } } ] }](В оригинале — схема, поясняющая обязательные поля блока картинки.) В блоке source такие свойства:
type— способ кодирования картинки. Пока это должен бытьbase64;media_type— тип картинки. Поддерживаютсяimage/jpeg,image/png,image/gifиimage/webp;data— сами данные картинки.
Промпт из одной картинки
Заголовок раздела «Промпт из одной картинки»Чаще всего вместе с картинкой мы передаём текст, но можно отправить и одну картинку. Попробуем! Картинки для урока
лежат в папке prompting_images. Сначала посмотрим на одну из них средствами Python:
from IPython.display import ImageImage(filename='./prompting_images/uh_oh.png')(На картинке — сильно обгоревший на солнце человек на пляже; источник — Wikimedia Commons, CC BY-SA.)
Теперь передадим эту картинку Claude. Первый шаг — получить строку с картинкой в кодировке base64, которую мы отправим модели. Код может показаться сложным, но шагов всего четыре:
- Открыть файл в режиме «чтение двоичных данных».
- Прочитать всё содержимое файла как объект
bytes. - Закодировать двоичные данные в base64.
- Превратить base64-байты в строку.
import base64
# открываем файл картинки в режиме «чтение двоичных данных»with open("./prompting_images/uh_oh.png", "rb") as image_file:
# читаем содержимое картинки как объект bytes binary_data = image_file.read()
# кодируем двоичные данные в Base64 base_64_encoded_data = base64.b64encode(binary_data)
# превращаем base_64_encoded_data из bytes в строку base64_string = base_64_encoded_data.decode('utf-8')На переменную base64_string можно посмотреть, но человеку она мало что скажет. Прочитаем первые 100 символов:
base64_string[:100]# 'iVBORw0KGgoAAAANSUhEUgAAB4AAAAQ4CAYAAADo08FDAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAE8mlUWHRYTUw6Y29tLmFkb2Jl'Данные картинки теперь в строке. Следующий шаг — правильно оформить список сообщений для Claude:
messages = [ { "role": "user", "content": [{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": base64_string }, }] }]Последний шаг — отправить список сообщений и посмотреть, что ответит Claude!
response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-5-sonnet-20240620" max_tokens=2048, messages=messages)print(response.content[0].text)Claude описала картинку: мужчина на пляже, по-видимому, сильно страдает — кожа ярко-красная от солнечного ожога, на лице боль; на заднем плане песок, отдыхающие и океан; сцена — наглядное напоминание о солнцезащитном креме.
Claude начинает описывать картинку, потому что других явных указаний мы не дали. Добавим к картинке текст:
messages = [ { "role": "user", "content": [{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": base64_string }, }, { "type": "text", "text": "Что этот человек мог сделать, чтобы такого не случилось?" }] }](В оригинале — схема, выделяющая блок картинки и текстовый блок.) Отправим запрос:
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude ответила списком мер: крем с высоким SPF, нанесённый щедро и регулярно; шляпа и одежда с защитой от ультрафиолета; тень в часы самого сильного солнца (обычно с 10 до 16 часов); пляжный зонт; вода и ограничение времени на солнце; повторное нанесение крема после купания.
Несколько картинок
Заголовок раздела «Несколько картинок»Можно передать Claude несколько картинок, добавив несколько блоков image в content сообщения пользователя. Пример:
messages = [ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": image1_media_type, "data": image1_data, }, }, { "type": "image", "source": { "type": "base64", "media_type": image2_media_type, "data": image2_data, }, }, { "type": "image", "source": { "type": "base64", "media_type": image3_media_type, "data": image3_data, }, }, {"type": "text", "text": "Чем различаются эти картинки?"}, ], }]Помощник для картинок
Заголовок раздела «Помощник для картинок»Когда работаешь с картинками, особенно в динамических скриптах, создавать блоки вручную надоедает. Напишем небольшую функцию, которая создаёт правильно оформленные блоки картинок:
import base64import mimetypes
def create_image_message(image_path): # Открываем файл картинки в режиме «чтение двоичных данных» with open(image_path, "rb") as image_file: # Читаем содержимое картинки как объект bytes binary_data = image_file.read()
# Кодируем двоичные данные в Base64 base64_encoded_data = base64.b64encode(binary_data)
# Превращаем base64_encoded_data из bytes в строку base64_string = base64_encoded_data.decode('utf-8')
# Определяем MIME-тип картинки по расширению файла mime_type, _ = mimetypes.guess_type(image_path)
# Собираем блок картинки image_block = { "type": "image", "source": { "type": "base64", "media_type": mime_type, "data": base64_string } }
return image_blockФункция принимает путь к картинке и возвращает словарь, готовый к отправке Claude. Она даже сама определяет MIME-тип картинки.
Попробуем новую картинку:
Image("./prompting_images/animal1.png")С помощью нашей функции отправим запрос:
messages = [ { "role": "user", "content": [ create_image_message("./prompting_images/animal1.png") ] }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude описала белоголового орлана, сидящего на бетонной или каменной конструкции у воды: белая голова, жёлтый клюв, тёмное оперение.
Теперь объединим в промпте текст и картинку:
messages = [ { "role": "user", "content": [ create_image_message("./prompting_images/animal1.png"), {"type": "text", "text": "Где в мире можно встретить это животное?"} ] }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude ответила, что белоголовый орлан живёт в основном в Северной Америке, чаще всего на Аляске и в Канаде, и перечислила места, где его легко увидеть: Аляска, северо-запад США, район Великих озёр, Флорида, Чесапикский залив, Скалистые горы — обычно у воды, потому что орланы питаются рыбой.
Теперь попробуем передать Claude несколько картинок. У нас есть три картинки с разными животными:
from IPython.display import displaydisplay(Image("./prompting_images/animal1.png", width=300))display(Image("./prompting_images/animal2.png", width=300))display(Image("./prompting_images/animal3.png", width=300))Передадим все три картинки одним сообщением вместе с вопросом «Что это за животные?»:
messages = [ { "role": "user", "content": [ create_image_message('./prompting_images/animal1.png'), create_image_message('./prompting_images/animal2.png'), create_image_message('./prompting_images/animal3.png'), {"type": "text", "text": "что это за животные?"} ] }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude 3.5 Sonnet правильно назвала всех троих: белоголовый орлан, бурый медведь (вероятно, гризли), плывущий в воде, и дикобраз крупным планом.
Работает отлично! Но важно знать, что менее способная модель, например Claude 3 Haiku, может справиться хуже. С тем же запросом она ответила: «На картинке дикобраз…» — и описала только третье животное.
Ответ оставляет желать лучшего. Напомним: мы отправили картинку белоголового орлана, гризли в воде и дикобраза крупным планом, а затем текст «Что это за животные?». А где орлан и медведь? С Claude 3.5 Sonnet такой проблемы не было, но с другими моделями полезно подписывать каждую картинку текстовым блоком. Даже простые подписи «Картинка 1», «Картинка 2» и т. д. могут сильно изменить результат.
Попробуем:
messages = [ { "role": "user", "content": [ {"type": "text", "text": "Картинка 1:"}, create_image_message('./prompting_images/animal1.png'), {"type": "text", "text": "Картинка 2:"}, create_image_message('./prompting_images/animal2.png'), {"type": "text", "text": "Картинка 3:"}, create_image_message('./prompting_images/animal3.png'), {"type": "text", "text": "что это за животные?"} ] }]
response = client.messages.create( model=MODEL_NAME, # в оригинале здесь нарочно "claude-3-haiku-20240307" max_tokens=2048, messages=messages)print(response.content[0].text)Теперь Claude 3 Haiku перечислила всех: «Картинка 1: белоголовый орлан… Картинка 2: бурый медведь… Картинка 3: дикобраз…». Гораздо лучше!
Картинки не с диска (по ссылке)
Заголовок раздела «Картинки не с диска (по ссылке)»Иногда нужно дать Claude картинку, которой у вас нет локально. Способов много, но все сводятся к одному рецепту:
- получить данные картинки какой-нибудь библиотекой для запросов;
- закодировать двоичное содержимое картинки в Base64;
- превратить закодированные данные из байтов в строку в кодировке UTF-8.
Для запроса данных картинки по ссылке используем httpx. По ссылке в примере — церковь, над которой в небе
северное сияние.
import base64import httpx
image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Church_of_light.jpg/1599px-Church_of_light.jpg"image_media_type = "image/jpeg"image_data = base64.b64encode(httpx.get(image_url).content).decode("utf-8")
messages = [ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": image_media_type, "data": image_data, }, }, { "type": "text", "text": "Опиши эту картинку." } ], }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude описала северное сияние над маленькой белой церковью с красной крышей колокольни в заснеженной местности: зелёные всполохи на звёздном небе, горы на заднем плане, ощущение уединения.
Как и раньше, можно написать функцию, которая создаёт блок картинки по ссылке. Ниже очень простая реализация: она принимает URL и:
- запрашивает данные картинки через
httpx; - определяет MIME-тип простейшей обработкой строки — берёт часть после последней точки; это не самое надёжное решение;
- кодирует данные в base64 и превращает байты в строку UTF-8;
- возвращает правильно оформленный блок картинки, готовый для промпта!
Вызов get_image_dict_from_url("https://somewebsite.com/cat.png") вернёт такой словарь:
{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": <данные картинки> },}def get_image_dict_from_url(image_url): # Отправляем GET-запрос по адресу картинки и получаем содержимое response = httpx.get(image_url) image_content = response.content
# Определяем тип картинки по расширению в адресе # Способ не безупречный, но обычно работает image_extension = image_url.split(".")[-1].lower() if image_extension == "jpg" or image_extension == "jpeg": image_media_type = "image/jpeg" elif image_extension == "png": image_media_type = "image/png" elif image_extension == "gif": image_media_type = "image/gif" else: raise ValueError("Неподдерживаемый формат картинки")
# Кодируем содержимое картинки в base64 image_data = base64.b64encode(image_content).decode("utf-8")
# Собираем словарь в форме блока картинки image_dict = { "type": "image", "source": { "type": "base64", "media_type": image_media_type, "data": image_data, }, }
return image_dictПопробуем! В примере две ссылки на картинки с Wikimedia Commons (CC BY-SA): PNG с пожарной машиной и JPG с вертолётом скорой помощи. Передадим обе вместе с вопросом «Что общего у этих картинок?»:
url1 = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Rincon_fire_truck.png/1600px-Rincon_fire_truck.png"url2 = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Ornge_C-GYNP.jpg/1600px-Ornge_C-GYNP.jpg"
messages = [ { "role": "user", "content": [ {"type": "text", "text": "Картинка 1:"}, get_image_dict_from_url(url1), {"type": "text", "text": "Картинка 2:"}, get_image_dict_from_url(url2), {"type": "text", "text": "Что общего у этих картинок?"} ], }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Claude правильно определила, что на обеих картинках транспорт экстренных служб: пожарная машина и оранжевый вертолёт санавиации. Главное же — мы увидели, как передать Claude картинки, скачанные по ссылке.
Советы по промптам с картинками
Заголовок раздела «Советы по промптам с картинками»Будьте конкретны
Заголовок раздела «Будьте конкретны»Как и в текстовых промптах, конкретный и подробный мультимодальный промпт даёт лучший результат. Разберём пример.
Вот фотография компании друзей. На ней 8 человек, но двое обрезаны краем кадра.
Image(filename='./prompting_images/people.png')Если просто спросить Claude «сколько людей на этой картинке?», скорее всего, услышим, что их 7:
messages = [ { "role": "user", "content": [ create_image_message("./prompting_images/people.png"), {"type": "text", "text": "Сколько людей на этой картинке?"} ], }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)Если же применить простые приёмы промпт-инжиниринга — попросить думать по шагам, сказать, что модель эксперт по подсчёту людей и что нужно учитывать «частичных» людей, обрезанных краем кадра, — результат будет лучше:
messages = [ { "role": "user", "content": [ create_image_message("./prompting_images/people.png"), {"type": "text", "text": "У тебя идеальное зрение и большое внимание к деталям, поэтому ты эксперт по подсчёту объектов на картинках. Сколько людей на этой фотографии? Некоторые люди могут быть частично закрыты или обрезаны краем кадра, от кого-то может быть видна только рука. Считай человека, даже если видна лишь одна часть тела. Прежде чем дать ответ в тегах <answer>, подумай по шагам в тегах <thinking> и разбери каждую часть картинки."} ], }]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages)print(response.content[0].text)В <thinking> Claude прошла по картинке слева направо, перечислила семь человек в основной группе, отдельно
отметила руку у левого края кадра как восьмого человека и дала ответ 8.
Используйте примеры
Заголовок раздела «Используйте примеры»Примеры в промпте улучшают качество ответов и с текстом, и с картинками.
Для демонстрации возьмём несколько слайдов презентации. Цель — чтобы Claude генерировала JSON-описание содержимого слайда. Посмотрите на первый слайд:
display(Image("./prompting_images/slide1.png", width=800))Нам нужен ответ в формате JSON: цвет фона, заголовок, основной текст и описание картинки слайда. Для первого слайда JSON может выглядеть так:
{ "background": "#F2E0BD", "title": "Haiku", "body": "Our most powerful model, delivering state-of-the-art performance on highly complex tasks and demonstrating fluency and human-like understanding", "image": "The image shows a simple line drawing of a human head in profile view, facing to the right. ..."}Это отличный случай, чтобы с помощью примеров показать Claude, какой именно ответ нам нужен. Для сравнения — ещё два слайда:
display(Image("./prompting_images/slide2.png", width=800))display(Image("./prompting_images/slide3.png", width=800))Воспользуемся форматом диалога: передадим Claude пример прошлого запроса и правильного ответа на него.
def generate_slide_json(image_path):
# Эталонный ответ для первого слайда (текст слайда английский, поэтому и значения английские) slide1_response = """{ "background": "#F2E0BD", "title": "Haiku", "body": "Our most powerful model, delivering state-of-the-art performance on highly complex tasks and demonstrating fluency and human-like understanding", "image": "The image shows a simple line drawing of a human head in profile view, facing to the right. The head is depicted using thick black lines against a pale yellow background. Inside the outline of the head, there appears to be a white, spoked wheel or starburst pattern, suggesting a visualization of mental activity or thought processes. The overall style is minimalist and symbolic rather than realistic." }"""
messages = [ { "role": "user", "content": [ create_image_message("./prompting_images/slide1.png"), {"type": "text", "text": "Сгенерируй JSON-описание этого слайда. В нём должны быть цвет фона, заголовок, основной текст и описание картинки"} ], }, { "role": "assistant", "content": slide1_response # пример: так выглядит правильный ответ }, { "role": "user", "content": [ create_image_message(image_path), {"type": "text", "text": "Сгенерируй JSON-описание этого слайда. В нём должны быть цвет фона, заголовок, основной текст и описание картинки"} ], }, ]
response = client.messages.create( model=MODEL_NAME, max_tokens=2048, messages=messages ) print(response.content[0].text)
generate_slide_json("./prompting_images/slide2.png")generate_slide_json("./prompting_images/slide3.png")Для второго слайда Claude вернула JSON с фоном #F2E0BD, заголовком «Sonnet», текстом о «самом сбалансированном»
по интеллекту и скорости варианте и описанием пяти шестерёнок; для третьего — фон #D2957B, заголовок «Opus» и
описание двух рук, соединяющих кусочки пазла.
Упражнение
Заголовок раздела «Упражнение»В этом упражнении с помощью модели нужно переписать и кратко пересказать научную статью Anthropic. В папке
images/research_paper лежат пять скриншотов страниц статьи. Чтобы было проще, пути ко всем пяти уже собраны в
список:
research_paper_pages = [ "./images/research_paper/page1.png", "./images/research_paper/page2.png", "./images/research_paper/page3.png", "./images/research_paper/page4.png", "./images/research_paper/page5.png"]Посмотрим на первую страницу:
Image(research_paper_pages[0])Ваша задача
Заголовок раздела «Ваша задача»С помощью модели:
- перепишите текст с каждой из 5 страниц;
- объедините текст всех страниц в одну большую расшифровку;
- передайте всю расшифровку модели и попросите пересказать статью целиком простым, нетехническим языком.
Ответ может выглядеть примерно так: «Статья исследует новый вид атаки на большие языковые модели вроде ChatGPT — “Many-shot Jailbreaking” (MSJ). Модели недавно научились обрабатывать гораздо более длинные входы, и атака этим пользуется: показывает ИИ сотни примеров вредного или нежелательного поведения…»
Возможное решение
import base64import mimetypes
research_paper_pages = [ "./images/research_paper/page1.png", "./images/research_paper/page2.png", "./images/research_paper/page3.png", "./images/research_paper/page4.png", "./images/research_paper/page5.png"]
# create_image_message — та же функция, что выше в уроке
def transcribe_single_page(page_url): messages = [ { "role": "user", "content": [ create_image_message(page_url), {"type": "text", "text": "перепиши текст с этой страницы научной статьи как можно точнее."} ] } ]
response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-5-sonnet-20240620" max_tokens=5000, messages=messages ) return response.content[0].text
def summarize_paper(pages): complete_paper_text = "" for page in pages: print("переписываю страницу ", page) transcribed_text = transcribe_single_page(page) print(transcribed_text[:200]) # начало расшифровки — для контроля complete_paper_text += transcribed_text response = client.messages.create( model=MODEL_NAME, max_tokens=5000, messages=[ { "role": "user", "content": f"Вот расшифровка научной статьи <paper>{complete_paper_text}</paper>. Перескажи статью для читателей без научной подготовки, не меньше чем в 3 абзацах. Обязательно объясни все сокращения и технический жаргон и, где можно, используй аналогии." } ] ) print(response.content[0].text)
summarize_paper(research_paper_pages)Что сделать руками
Заголовок раздела «Что сделать руками»- Один код — два формата. Перепишите чат-бота из урока 2 на OpenAI-совместимый API (урок 8.1.1) и запустите оба варианта на одной локальной модели. Выпишите различия: где системный промпт, где текст ответа, как называются поля токенов и причины остановки.
- Температура на графике. Повторите опыт авторов с животными: 30 запросов при
temperature=0и 30 при1(«Выбери любое животное. Ответь одним словом»). Постройте столбчатую диаграмму частот. Сколько разных животных вышло в каждом случае? - TTFT и длина промпта. Измерьте
measure_streaming_ttft()для промптов длиной примерно 100, 2000 и 8000 токенов (возьмите отчёт Matterport из части 3 туториала). Как растёт время до первого токена и меняется ли скорость генерации? - Подписи к картинкам. Повторите опыт «три животных без подписей и с подписями» на
qwen2.5vl:7b. Помогают ли подписи локальной модели так же, как помогли Claude 3 Haiku?
Словарик
Заголовок раздела «Словарик»| English | Русский |
|---|---|
| API key | ключ API |
| SDK (software development kit) | SDK, библиотека для работы с API |
| client | клиент (объект для запросов к API) |
| Messages API | API сообщений (формат Anthropic) |
| content block | блок содержимого |
| stop reason / end turn | причина остановки / естественное завершение хода |
| stop sequence | стоп-последовательность |
| token / max tokens | токен / максимум токенов |
| temperature | температура (сэмплирования) |
| system prompt | системный промпт |
| putting words in Claude’s mouth (prefill) | вложить слова в уста модели, prefill |
| few-shot prompting | few-shot, примеры в промпте |
| streaming / server-sent events (SSE) | стриминг, потоковая выдача / события, отправляемые сервером |
| time to first token (TTFT) | время до первого токена |
| vision / multimodal prompt | зрение / мультимодальный промпт |
| base64 / MIME type | base64 (кодирование двоичных данных текстом) / MIME-тип |