Первый чат-бот на Claude API у меня заработал примерно за час. Потом я потратил ещё три, чтобы понять, почему он отвечает невпопад — и это оказалось не баг в коде, а я неправильно понял, как работает контекст.
Расскажу по порядку, без приукрашивания.
Что нужно до первой строчки кода
Начинается всё с console.anthropic.com — регистрируешься, получаешь API-ключ, кладёшь немного денег на счёт. Бесплатного тира нет, но цены вменяемые: claude-haiku стоит копейки, claude-sonnet подороже, claude-opus — когда качество важнее экономии.
Ключ сразу прячу в переменную окружения, не в код. Звучит банально, но первый раз я именно так и закоммитил ключ в репозиторий. Полчаса отзывал, перевыпускал, проверял логи. Больше не повторял.
Дальше одна команда:
pip install anthropic
Никаких дополнительных зависимостей, никаких танцев с настройкой. Можно работать.
Как устроен запрос — и где я споткнулся
Базовый вызов выглядит так:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Привет, как дела?"}
]
)
print(response.content[0].text)
Просто. Работает с первого раза. Но вот где я облажался — решил, что API сам хранит историю разговора. Отправил второй вопрос, и бот понятия не имел, о чём мы только что говорили.
Дело в том, что Claude API stateless. Каждый запрос — с чистого листа. Историю диалога нужно передавать самому, руками, в каждом новом запросе. Это не недостаток, а архитектурное решение, которое даёт контроль. Но когда не знаешь — час уходит на отладку того, что на самом деле работает правильно.
Правильная история диалога выглядит так:
messages = []
def chat(user_input):
messages.append({"role": "user", "content": user_input})
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=messages
)
assistant_reply = response.content[0].text
messages.append({"role": "assistant", "content": assistant_reply})
return assistant_reply
Список messages растёт с каждым обменом репликами. Передаёшь его целиком — бот помнит контекст. Не передаёшь — не помнит. Просто, но неочевидно с первого взгляда.
System prompt — это половина работы
Без system prompt бот отвечает как универсальный ассистент. Иногда это то что нужно, но чаще хочется конкретики: поддержка для сервиса, помощник по документации, бот для обработки заявок.
System prompt передаётся отдельным параметром:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="Ты помощник интернет-магазина электроники. Отвечай только на вопросы о товарах, доставке и возврате. Если вопрос не по теме — вежливо объясни, чем можешь помочь.",
messages=messages
)
На написание хорошего system prompt у меня ушло больше времени, чем на весь остальной код. Это нормально. Плохо написанный промпт — и бот начинает галлюцинировать, выходить за рамки или, наоборот, отказывается отвечать на нормальные вопросы.
Я тестирую промпт вручную в claude.ai, прежде чем вставлять в код. Так быстрее находишь углы, где поведение ломается.
Стриминг и почему он важен для UX
По умолчанию API возвращает ответ целиком — после того как сгенерировал всё. Для длинных ответов это означает паузу в несколько секунд: пользователь смотрит в пустой экран и не понимает, работает ли вообще что-то.
Стриминг решает это. Текст появляется по мере генерации, как в claude.ai:
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
messages=messages
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
Разница в ощущениях принципиальная. Бот с задержкой пять секунд кажется сломанным. Бот, который начинает отвечать через полсекунды и печатает на глазах, — кажется живым.
Что я бы сделал иначе
Три вещи, которые я понял не сразу.
С max_tokens я поначалу ставил слишком маленькое значение — бот обрывал ответы на полуслове. Слишком большое — платишь за токены, которые не нужны. Для обычного чата 1024–2048 обычно хватает, для технических объяснений беру больше.
Отдельная история с контекстным окном. Если история диалога растёт бесконечно, в какой-то момент запрос превышает лимит токенов и падает с ошибкой. Нужно либо обрезать старые сообщения, либо делать суммаризацию. Я добавил простое ограничение: храню последние 20 сообщений, остальное отбрасываю. Для большинства сценариев хватает.
На практике больнее всего ударила обработка ошибок — точнее, её отсутствие. API возвращает разные коды: rate limit, overload, invalid request. Первое время я их игнорировал и получал необработанные исключения в продакшне. Сейчас оборачиваю вызовы в try/except и добавляю retry с экспоненциальной задержкой для временных ошибок.
Работающий чат-бот на Claude API — это несложно. Сложнее сделать его полезным: написать хороший промпт, правильно управлять контекстом, не дать ему уйти в сторону от задачи. Код — меньшая часть работы. Большая — понять, чего ты вообще хочешь от бота, и объяснить это Claude достаточно точно, чтобы он не импровизировал там, где не надо.
