Первый вызов API
В этом уроке вы напишете и запустите первый запрос к Claude из TypeScript. Разберём, как устроены запрос и ответ, куда девать системный промпт, как читать ответ по частям и что делать с ошибками. В конце соберём классификатор обращений в поддержку, который возвращает не текст, а проверенный JSON.
Установка и ключ
Понадобятся Node.js, npm и API-ключ из консоли на platform.claude.com. Ключ кладём в переменную окружения, не в код.
mkdir support-classifier && cd support-classifier
npm init -y
npm install @anthropic-ai/sdk zod
export ANTHROPIC_API_KEY="sk-ant-..."Для запуска TypeScript-файлов удобно использовать npx tsx file.ts или любой привычный вам способ.
Минимальный запрос
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // ключ берётся из ANTHROPIC_API_KEY
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "Клиент пишет: «Не могу войти в личный кабинет, кнопка не реагирует». Это баг или вопрос?" },
],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}Три обязательных параметра: model (используем алиас claude-opus-5, о выборе модели — следующий урок), max_tokens (верхняя граница длины ответа) и messages (история диалога). Первое сообщение всегда от user.
Обратите внимание на цикл по response.content. Ответ — не строка, а массив блоков. Обычно там один текстовый блок, но могут быть и другие: блок рассуждения, запрос на вызов инструмента. Поэтому TypeScript не даст написать response.content[0].text без проверки типа, и это правильно.
Что ещё лежит в ответе
Помимо content, пригодятся два поля.
stop_reason объясняет, почему модель остановилась. end_turn — закончила сама. max_tokens — упёрлась в лимит, ответ обрезан, лимит нужно поднять. tool_use — хочет вызвать инструмент (об этом в модуле про агентов). refusal — отказалась отвечать по соображениям безопасности; в этом случае в stop_details есть категория и объяснение. Проверять stop_reason перед чтением текста — хорошая привычка с первого дня.
usage содержит input_tokens и output_tokens. Это то, за что вы платите. Логируйте их с самого начала: через месяц вопрос «сколько стоит наш классификатор» будет решаться одним запросом к логам, а не гаданием.
Системный промпт
Правила поведения задают не в сообщении пользователя, а в отдельном параметре system. Туда идёт всё, что не меняется от запроса к запросу: роль, категории, формат ответа.
const SYSTEM = `Ты сортируешь обращения в поддержку продуктовой студии.
Категории: bug, question, feature_request, billing, other.
Срочность: low, normal, high. high — если клиент не может работать.
Отвечай одним словом категории, затем через пробел срочность.`;
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 256,
system: SYSTEM,
messages: [{ role: "user", content: ticketText }],
});API не хранит состояние. Каждый запрос — с чистого листа, и если нужен диалог, вы отправляете всю историю целиком: массив messages с чередованием user и assistant. Для классификатора это не нужно, для бота клиентского портала — будет, и мы вернёмся к этому в уроке про контекст.
Потоковый вывод
Для классификатора ответ короткий, но бот в портале будет писать абзацы, и пользователь не должен смотреть на пустой экран. Метод stream отдаёт текст по мере генерации.
const stream = client.messages.stream({
model: "claude-opus-5",
max_tokens: 4096,
system: SYSTEM,
messages: [{ role: "user", content: question }],
});
stream.on("text", (delta) => process.stdout.write(delta));
const finalMessage = await stream.finalMessage();
console.log("\n", finalMessage.usage);finalMessage() возвращает тот же объект, что и create, со stop_reason и usage. Не собирайте его вручную из событий: SDK делает это сам и корректно обрабатывает обрывы. Для длинных ответов потоковая передача обязательна: SDK требует её при большом max_tokens, чтобы не упереться в таймаут HTTP.
Ошибки
SDK кидает типизированные исключения, и ловить их надо от частного к общему.
try {
const response = await client.messages.create({ /* ... */ });
} catch (error) {
if (error instanceof Anthropic.AuthenticationError) {
console.error("Ключ неверный или отозван");
} else if (error instanceof Anthropic.RateLimitError) {
console.error("Лимит запросов, попробуйте позже");
} else if (error instanceof Anthropic.APIError) {
console.error(`Ошибка API ${error.status}: ${error.message}`);
} else {
throw error;
}
}SDK сам повторяет запрос при сетевых ошибках, 429 и 5xx (по умолчанию два раза), поэтому свою логику повторов поверх писать не нужно. Не сравнивайте текст ошибки со строками: классы для этого и существуют.
Структурированный ответ
Классификатор, который отвечает «bug high», хрупок: однажды модель напишет «Категория: bug, срочность высокая», и парсер сломается. Правильный путь — попросить JSON по схеме и получить гарантию, что ответ ей соответствует. Для этого есть messages.parse и схема на Zod.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
const Ticket = z.object({
category: z.enum(["bug", "question", "feature_request", "billing", "other"]),
urgency: z.enum(["low", "normal", "high"]),
summary: z.string().describe("Суть обращения одной фразой"),
});
const client = new Anthropic();
export async function classify(ticketText: string) {
const response = await client.messages.parse({
model: "claude-opus-5",
max_tokens: 1024,
system: "Ты сортируешь обращения в поддержку продуктовой студии.",
messages: [{ role: "user", content: ticketText }],
output_config: { format: zodOutputFormat(Ticket) },
});
if (!response.parsed_output) {
throw new Error(`Ответ не разобран, stop_reason: ${response.stop_reason}`);
}
return response.parsed_output; // типизирован как z.infer<typeof Ticket>
}parsed_output будет null, если модель отказалась отвечать или ответ обрезался по max_tokens, поэтому проверка обязательна. В остальных случаях вы получаете объект с правильными типами, и в коде дальше нет ни одного JSON.parse. Схема кэшируется на стороне API: первый запрос с новой схемой чуть медленнее, следующие — нет.
Это уже рабочий классификатор. Прикрутить к нему очередь писем и запись в трекер — обычная бэкенд-задача, в которой нет ничего специфичного для ИИ.
Попробуйте сами
10–15 мин на рабочем месте- Запустите минимальный запрос из урока. Выведите
stop_reasonиusage. Потом уменьшитеmax_tokensдо 5 и посмотрите, что изменится. - Соберите классификатор с
messages.parse. Прогоните через него пять реальных (обезличенных) обращений из вашего проекта. Где модель ошиблась? Попробуйте уточнить системный промпт: часто помогает описать, что считать срочным. - Перепишите один из запросов на
streamи убедитесь, чтоfinalMessage().usageсовпадает с тем, что показывает консоль на platform.claude.com.
Коротко
client.messages.createсmodel,max_tokensиmessages— весь минимум для запроса.- Ответ — массив блоков
content, а не строка; проверяйтеblock.typeиstop_reason. - Постоянные правила — в
system, история диалога — вmessages; API не хранит состояние. client.messages.streamплюсfinalMessage()— для длинных ответов и живого интерфейса.- Ошибки ловим по классам SDK от частного к общему; повторы SDK делает сам.
- Для машиночитаемого ответа используйте
messages.parseс Zod-схемой и проверяйтеparsed_output.
Видеоверсия
Сценарий озвучки · 478 слов, ≈ 4 мин
В этом уроке мы сделаем первый запрос к Claude из TypeScript и соберём классификатор обращений в поддержку. Ничего сложного: одна библиотека, один ключ, несколько строк кода.
Сначала установка. Ставим официальный SDK через npm и кладём API-ключ из консоли в переменную окружения. Ключ в коде не появляется никогда: библиотека сама найдёт его в окружении.
Минимальный запрос выглядит так. Создаём клиент. Вызываем метод «создать сообщение» и передаём три вещи: модель, максимальную длину ответа в токенах и список сообщений. В списке одно сообщение от пользователя: текст обращения клиента и вопрос, баг это или просто вопрос. Модель отвечает.
Важная деталь: ответ — это не строка, а массив блоков. Обычно там один текстовый блок, но могут быть и другие, например запрос на вызов инструмента. Поэтому мы проходим по массиву и печатаем только текстовые блоки. TypeScript не даст вам обратиться к тексту без проверки типа, и это правильно.
Кроме содержимого в ответе есть два полезных поля. Причина остановки: модель закончила сама, упёрлась в лимит длины, хочет вызвать инструмент или отказалась отвечать. Проверять её нужно всегда. И расход токенов на входе и выходе — это то, за что вы платите. Логируйте его с первого дня.
Правила поведения задаются не в сообщении, а в отдельном системном промпте. Туда идёт всё постоянное: роль, список категорий, формат ответа. Для нашего классификатора это пять категорий и три уровня срочности. Помните: API не хранит состояние. Каждый запрос начинается с чистого листа, и если нужен диалог, вы каждый раз отправляете всю историю.
Для длинных ответов есть потоковый режим. Текст приходит по мере генерации, и пользователь видит его сразу, а не смотрит на пустой экран. В конце вызываем метод «финальное сообщение» и получаем тот же объект, что и в обычном режиме, с причиной остановки и расходом. Собирать его из событий вручную не нужно.
Ошибки SDK отдаёт типизированными классами. Ловим их от частного к общему: неверный ключ, превышен лимит запросов, любая другая ошибка API. Сетевые сбои и перегрузку библиотека повторяет сама.
И главное для классификатора — структурированный ответ. Если просить модель ответить словами, однажды она напишет чуть иначе, и парсер сломается. Вместо этого описываем схему на Zod: категория из списка, срочность из списка, краткое резюме. Передаём схему через метод «разобрать», и API гарантирует, что ответ ей соответствует. На выходе — типизированный объект, и в коде нет ни одного ручного разбора JSON. Единственная проверка: результат может быть пустым, если модель отказалась или ответ обрезался.
Это уже рабочий классификатор. Дальше — очередь писем и запись в трекер, обычная бэкенд-работа.
Пара практических советов напоследок. Не занижайте лимит длины ответа: если модель упрётся в него, ответ обрежется посреди фразы, и вы получите причину остановки «лимит», а не готовый результат. Для коротких классификаций хватит пары сотен токенов, для обычных ответов ставьте запас в тысячи. И не отправляйте ключ на фронтенд ни в каком виде: любой запрос к модели из браузера должен идти через ваш сервер, иначе ключ утечёт в первый же день.
В следующем уроке разберёмся, какую модель выбирать и как не переплачивать.
