ZeroPost
Все статьи

Claude API: как я делал чат-бота и что из этого вышло

ZeroPost AI12 июня 2026 г. 3 мин чтения
Claude API: как я делал чат-бота и что из этого вышло

Несколько месяцев назад мне понадобился чат-бот, который держит контекст разговора и не начинает галлюцинировать на третьем сообщении. Начал с OpenAI, застрял на странных ответах, и в итоге переехал на Anthropic. Вот что узнал в процессе.

Как устроен Claude API

Документация Anthropic читается проще, чем у большинства конкурентов. Основная точка входа — /v1/messages. Ты отправляешь массив сообщений с ролями (user и assistant), модель возвращает ответ. Никакой магии.

Выглядит это так:

import anthropic

client = anthropic.Anthropic(api_key="sk-ant-...")

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Привет, как дела?"}
    ]
)

print(response.content[0].text)

Минут двадцать у меня ушло, пока не понял: response.content — это список объектов, а не строка. Звучит очевидно, но я полез распечатывать весь объект и завис над структурой ответа.

Память разговора: где я напортачил

Главное в чат-боте — контекст. Claude не хранит историю сам по себе, каждый запрос начинается с чистого листа. Значит, историю нужно носить с собой и передавать при каждом вызове.

Первые полчаса я делал так:

history = []

def chat(user_message):
    history.append({"role": "user", "content": user_message})
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=history
    )
    
    assistant_message = response.content[0].text
    history.append({"role": "assistant", "content": assistant_message})
    
    return assistant_message

Работает. Но история растёт бесконечно — после пятидесяти сообщений ты передаёшь огромный контекст, платишь за все входящие токены и рискуешь упереться в лимит. Решил обрезанием: оставлял последние N сообщений плюс системный промпт:

MAX_HISTORY = 20

def chat(user_message, system_prompt="Ты полезный ассистент."):
    history.append({"role": "user", "content": user_message})
    
    trimmed = history[-MAX_HISTORY:]
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        system=system_prompt,
        messages=trimmed
    )
    
    assistant_message = response.content[0].text
    history.append({"role": "assistant", "content": assistant_message})
    
    return assistant_message

Обратите внимание на параметр system — он идёт отдельно, не внутри массива сообщений. Это не очевидно, если переехал с OpenAI, где системное сообщение пихается первым элементом в тот же список.

Стриминг — когда важно не ждать

Первый прототип отвечал целиком: пишешь вопрос, пауза секунд пять, потом весь текст разом. Для демо сойдёт. Для реального использования — нет.

Включить стриминг просто:

with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=history
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

В веб-приложении это обычно делается через Server-Sent Events или WebSocket. Я делал через SSE на FastAPI — пользователь видит текст по мере генерации, ощущение живого диалога появляется сразу.

Системный промпт решает больше, чем кажется

Поначалу я писал промпт на три строчки: "Ты помощник. Отвечай по делу. Будь вежлив." Технически работает, но плохо.

Дело в том, что модель не знает контекст твоего продукта — она знает только то, что ты ей написал. Начал объяснять подробнее: ограничения, формат ответов, что делать в нестандартных ситуациях. Разница ощутимая. Вот пример для саппорт-бота:

Ты саппорт-агент компании [X]. Твоя задача — помогать пользователям с вопросами 
по продукту. Если не знаешь ответа — говори об этом прямо и предлагай обратиться 
к живому специалисту. Не придумывай информацию о продукте. Отвечай на том языке, 
на котором пишет пользователь.

"Не придумывай информацию" — строчка, без которой не обойтись. Без неё модель иногда генерирует правдоподобные, но несуществующие детали о твоём продукте, особенно если вопрос пользователя выходит за рамки контекста.

Что я бы сделал иначе с самого начала

Сразу настроил бы нормальное логирование запросов и ответов. Когда бот начинает вести себя странно, ты хочешь видеть, что именно передавалось в каждом вызове. Без логов это детектив без улик.

Обработку ошибок тоже добавил бы с первого коммита, а не через неделю. Claude API возвращает разные типы: rate limit, перегрузка серверов, проблемы с контентом. Без try/except бот просто падает вместо того, чтобы нормально сообщить пользователю о проблеме:

from anthropic import RateLimitError, APIStatusError

try:
    response = client.messages.create(...)
except RateLimitError:
    return "Слишком много запросов, попробуй через минуту."
except APIStatusError as e:
    return f"Что-то пошло не так: {e.status_code}"

И не тянул бы с переходом на асинхронный клиент. Синхронная версия хороша для прототипа, но в приложении с несколькими одновременными пользователями нужен AsyncAnthropic — это не сложно, просто лучше сделать сразу.


Claude API достаточно предсказуем, и это, как ни странно, его главное достоинство. После нескольких проектов у меня появилось ощущение, что я примерно знаю, что получу. Галлюцинаций меньше, чем ожидал, инструкции в системном промпте выполняются честнее. Но контекст всё равно нужно держать руками — никто за тебя это не сделает.

Зеро
Понравилась заметка?
Зеро публикует новые материалы каждый день в Telegram. Подпишитесь — следующая уже завтра.
✈️ В канал