AmigaОбучение ИИ
Модуль 2 · Первый вызов API · урок 2 из 14

Первый вызов API

11 мин чтения▶ есть видеоверсия

В этом уроке вы напишете и запустите первый запрос к Claude из TypeScript. Разберём, как устроены запрос и ответ, куда девать системный промпт, как читать ответ по частям и что делать с ошибками. В конце соберём классификатор обращений в поддержку, который возвращает не текст, а проверенный JSON.

Установка и ключ

Понадобятся Node.js, npm и API-ключ из консоли на platform.claude.com. Ключ кладём в переменную окружения, не в код.

bash
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 или любой привычный вам способ.

Минимальный запрос

typescript
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. Туда идёт всё, что не меняется от запроса к запросу: роль, категории, формат ответа.

typescript
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 отдаёт текст по мере генерации.

typescript
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 кидает типизированные исключения, и ловить их надо от частного к общему.

typescript
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.

typescript
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 мин на рабочем месте
  1. Запустите минимальный запрос из урока. Выведите stop_reason и usage. Потом уменьшите max_tokens до 5 и посмотрите, что изменится.
  2. Соберите классификатор с messages.parse. Прогоните через него пять реальных (обезличенных) обращений из вашего проекта. Где модель ошиблась? Попробуйте уточнить системный промпт: часто помогает описать, что считать срочным.
  3. Перепишите один из запросов на 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. Единственная проверка: результат может быть пустым, если модель отказалась или ответ обрезался.

Это уже рабочий классификатор. Дальше — очередь писем и запись в трекер, обычная бэкенд-работа.

Пара практических советов напоследок. Не занижайте лимит длины ответа: если модель упрётся в него, ответ обрежется посреди фразы, и вы получите причину остановки «лимит», а не готовый результат. Для коротких классификаций хватит пары сотен токенов, для обычных ответов ставьте запас в тысячи. И не отправляйте ключ на фронтенд ни в каком виде: любой запрос к модели из браузера должен идти через ваш сервер, иначе ключ утечёт в первый же день.

В следующем уроке разберёмся, какую модель выбирать и как не переплачивать.

Отметка хранится только в вашем браузере