Интеграция AI API: OpenAI, Anthropic и локальные модели
Перейти к разделу
Три экосистемы, одна архитектура
В продакшене вы будете работать как минимум с двумя провайдерами. OpenAI имеет наиболее широкую экосистему, Anthropic обеспечивает лучшую производительность на сложных задачах, а локальные модели (Ollama, vLLM) дают контроль над данными и задержкой. Ключ — абстракция: ваш код не должен быть привязан к конкретному провайдеру.
Базовый паттерн прост: определите интерфейс для LLM-вызовов, реализуйте его для каждого провайдера и переключайтесь через конфигурацию. В TypeScript это означает общий тип ChatMessage и функцию complete(), возвращающую AsyncIterable<string> для стриминга.
// Provider-agnostic interface
interface LLMProvider {
complete(messages: ChatMessage[], opts: CompletionOpts): AsyncIterable<string>;
countTokens(text: string): Promise<number>;
}
// OpenAI implementation
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
stream: true,
});
for await (const chunk of stream) {
yield chunk.choices[0]?.delta?.content ?? '';
}Стриминг: зачем и как
Без стриминга пользователь ждёт ответа 5–30 секунд. Со стримингом он видит первый токен через 200–500 мс. С точки зрения UX это огромная разница. Но стриминг добавляет сложность — нужно обрабатывать частичный парсинг JSON, обратное давление и переподключение.
Anthropic SDK использует Server-Sent Events (SSE) нативно. На клиенте вы открываете собственный SSE-эндпоинт или используете ReadableStream в Route Handler Next.js. Важно: никогда не отправляйте сырой стрим провайдера напрямую клиенту — всегда трансформируйте в собственный формат, чтобы можно было сменить провайдера без изменения фронтенда.
// Anthropic streaming with proper error handling
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const stream = client.messages.stream({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages,
});
stream.on('text', (text) => {
// Transform to your own event format
res.write(`data: ${JSON.stringify({ type: 'delta', text })}\n\n`);
});
stream.on('error', (err) => {
res.write(`data: ${JSON.stringify({ type: 'error', code: err.status })}\n\n`);
});Обработка ошибок и стратегии повторных запросов
Продакшен API-вызовы падают. Ограничения по запросам (429), серверные ошибки (500, 503), таймауты, сетевые ошибки. Без логики повторных запросов ваша система упадёт при первом сбое. При наивном повторе (немедленная попытка) вы устроите DDoS провайдеру и получите более длительный бан.
Экспоненциальная задержка с джиттером — стандарт: первая попытка через 1 с, вторая через 2 с, третья через 4 с — плюс случайный джиттер 0–500 мс, чтобы запросы от разных клиентов не пересекались. Для 429 соблюдайте заголовок Retry-After. Для 500/503 повторяйте. Для 400 (неверный запрос) не повторяйте — это баг в вашем коде.
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
if (attempt === maxRetries) throw err;
if (err.status === 400 || err.status === 401) throw err;
const base = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 500;
await new Promise(r => setTimeout(r, base + jitter));
}
}
throw new Error('Unreachable');
}Локальные модели: когда и как
Локальные модели (через Ollama, vLLM или TGI) оправданы в трёх сценариях: данные не должны покидать вашу инфраструктуру, вам нужна предсказуемая задержка без ограничений по запросам, или вы выполняете высокообъёмный инференс, где затраты на API превышают затраты на GPU. Для прототипа Ollama с OpenAI-совместимым эндпоинтом достаточно — существующий код работает без изменений.
// Ollama with OpenAI-compatible endpoint
const localLLM = new OpenAI({
baseURL: 'http://localhost:11434/v1',
apiKey: 'ollama', // required but unused
});
const response = await localLLM.chat.completions.create({
model: 'llama3.1:8b',
messages,
stream: true,
});Структурированные выходные данные и вызов функций
В продакшене вам нужны структурированные данные от модели — JSON, а не свободный текст. OpenAI предлагает Structured Outputs с валидацией по JSON Schema. Anthropic использует tool_use, где вы определяете входную схему. Оба работают, но реализации различаются. Ваша обёртка должна скрывать это различие за единым интерфейсом.
Критическое правило: никогда не парсите сырой вывод модели напрямую. Всегда валидируйте через Zod или аналогичную библиотеку. Модель может вернуть синтаксически корректный JSON, нарушающий ваши бизнес-правила — отсутствующие поля, неверные типы, значения вне диапазона. Валидация на границе системы дешевле, чем отладка в продакшене.
Никогда не привязывайте код приложения к конкретному провайдеру. Абстракция через интерфейсы сэкономит вам недели работы при смене модели — а этот момент наступит раньше, чем вы ожидаете.
Начните с одного провайдера и слоя абстракции. Добавляйте второго, когда он реально понадобится. Избыточная инженерия вначале вредит больше, чем рефакторинг позже.
Build & Deploy AI Apps
Никогда не отправляйте сырой стрим провайдера напрямую клиенту. Всегда трансформируйте в собственный формат событий — так вы сможете сменить провайдера без изменения фронтенда.
Реализуйте интерфейс LLMProvider для OpenAI и Anthropic. Напишите функцию complete(), принимающую массив сообщений и возвращающую AsyncIterable<string>. Добавьте логику повторных запросов с экспоненциальной задержкой. Проверьте переключение провайдеров через переменную окружения без изменения кода вызывающей стороны.
Подсказка
Используйте паттерн фабрики: createProvider(name: string) возвращает нужную реализацию. Для тестов создайте мок-провайдера, возвращающего заданные ответы.
Реализуйте простую AI-функцию с API: 1) Выберите провайдера (OpenAI, Anthropic или open-source), 2) Настройте API-ключ, 3) Напишите функцию, принимающую текст и возвращающую резюме (макс. 3 предложения), 4) Добавьте обработку ошибок, 5) Измерьте задержку и стоимость для 100 запросов. Всё это должно занять максимум 1 час.
Подсказка
Документируйте процесс и результаты — они послужат справочником для похожих задач в будущем.
Реализуйте AI-функцию, извлекающую структурированные данные из текста. 1) Определите схему Zod для вывода (например, ExtractedContact с полями name, email, phone, company). 2) Напишите промпт, инструктирующий модель вернуть JSON, соответствующий схеме. 3) Валидируйте ответ модели через Zod. 4) Обрабатывайте случай, когда модель возвращает невалидный JSON — повторный запрос с уточнённым промптом. Протестируйте на 10 разных входных данных.
Подсказка
Схема Zod служит контрактом между вашим кодом и моделью. Сначала определите её и выведите TypeScript-типы из неё — не наоборот.
- Абстрагируйте LLM-вызовы через интерфейс — привязка к провайдеру является реальным риском
- Стриминг улучшает UX на порядок, но требует собственного формата событий
- Экспоненциальная задержка с джиттером обязательна для логики повторных запросов в продакшене
- Всегда валидируйте структурированные выходные данные через схему — модель не является базой данных