LLM-интеграция — это включение вероятностной языковой модели в детерминированную или почти детерминированную программную систему так, чтобы итоговый продукт оставался полезным, управляемым, наблюдаемым и экономически оправданным.
Ключевое слово здесь не «языковая». И даже не «модель». Ключевое слово — «система».
Если вы отправили текст в API и получили ответ, вы ещё ничего не интегрировали. Вы сделали сетевой вызов. Это полезно для демо, ноутбука, внутреннего эксперимента или вечернего восторга в Telegram. Но production начинается там, где появляются:
- пользователи с разными входными данными;
- SLA или хотя бы ожидание доступности;
- стоимость каждого запроса;
- приватные данные;
- ошибки формата;
- повторные запуски;
- ограничения провайдера;
- необходимость объяснить, почему система ответила именно так;
- последствия неправильного ответа.
Обычная backend-функция работает примерно так:
def calculate_discount(user, cart):
if user.is_vip:
return cart.total * 0.15
return cart.total * 0.05Она скучная. Скука — достоинство. При одинаковом входе вы получите одинаковый выход. Ошибки можно воспроизвести. Тесты можно написать один раз и доверять им.
LLM-вызов выглядит иначе:
response = client.responses.create(
model="some-model",
input="Определи намерение пользователя: ..."
)На поверхности это тоже функция. Внутри — вероятностная генерация, зависящая от модели, параметров, контекста, системных инструкций, формата, скрытых обновлений провайдера и качества входных данных.
Поэтому первая ошибка новичка — обращаться с LLM как с обычной функцией.
Правильнее думать о модели как о внешнем исполнителе с сильными языковыми способностями, но без вашей ответственности за продукт. Исполнитель может быть блестящим. Может быть усталым. Может неправильно понять контекст. Может уверенно сказать чушь. Может вернуть почти правильный JSON с одной лишней запятой, и ваша очередь задач остановится ночью.
Инженерная задача — не верить исполнителю. Инженерная задача — построить вокруг него контур управления.
Минимальная production-обвязка LLM-вызова
Даже самый простой вызов модели в backend должен иметь:
- Явный use case.
- Версионированный prompt/template.
- Строгую схему входа.
- Строгую схему выхода.
- Timeout.
- Retry policy.
- Error mapping.
- Логирование без утечки секретов и PII.
- Метрики стоимости и latency.
- Тестовый набор примеров.
- Fallback или понятное поведение при отказе.
Если этого нет, у вас не production-интеграция. У вас удачный скрипт.
Это не снобизм. Это цена реальности.
Пример: классификация обращения в поддержку
Задача: пользователь пишет сообщение в поддержку. Нужно определить категорию:
- billing;
- technical_issue;
- account_access;
- refund;
- other.
Наивный вариант:
prompt = f"Определи категорию обращения: {message}"
category = call_llm(prompt)Проблемы:
- модель может вернуть «Похоже, это вопрос по оплате» вместо
billing; - может вернуть две категории;
- может объяснить решение;
- может ошибиться на коротком сообщении;
- может потратить слишком много токенов;
- невозможно нормально построить аналитику;
- backend должен парсить человеческую фразу.
Production-вариант требует контракта:
{
"category": "billing",
"confidence": 0.82,
"needs_human": false,
"reason": "Пользователь спрашивает о списании средств"
}И схемы:
from enum import Enum
from pydantic import BaseModel, Field
class TicketCategory(str, Enum):
billing = "billing"
technical_issue = "technical_issue"
account_access = "account_access"
refund = "refund"
other = "other"
class TicketClassification(BaseModel):
category: TicketCategory
confidence: float = Field(ge=0, le=1)
needs_human: bool
reason: str = Field(max_length=300)Теперь модель не «разговаривает». Она заполняет контракт. Если контракт нарушен — это не философская проблема, а обычная ошибка интеграции: retry, repair, fallback, ручная обработка.
Главный инженерный сдвиг
Плохой вопрос: «Какой промпт заставит модель отвечать идеально?»
Хороший вопрос: «Какую систему надо построить, чтобы несовершенный ответ модели не разрушал продукт?»
Этот сдвиг отделяет любителя от инженера.
Чеклист главы
- Для каждого LLM-вызова в проекте можно назвать use case одним предложением.
- Ответ модели описан схемой, а не «ну там текст приходит».
- Известно, что произойдёт при таймауте, rate limit и невалидном ответе.
- Стоимость и latency вызова видны в логах или метриках.
- Есть хотя бы 10 примеров входа/выхода, на которых проверяется поведение.
- Понятно, кто и как обрабатывает случай «модель не справилась».
Практикум
Задача 1.1 — аудит наивной интеграции. ⭑
Возьмите любой свой скрипт или прототип с LLM-вызовом (или напишите наивный классификатор тикетов из этой главы за 15 минут). Пройдитесь по списку из 11 пунктов минимальной production-обвязки и составьте таблицу: пункт → есть/нет → чем грозит отсутствие в проде.
Критерии приёмки:
- По каждому из 11 пунктов есть честный ответ «есть/нет».
- Для каждого «нет» описан конкретный сценарий инцидента, а не абстрактное «будет плохо».
- Выбраны три пункта, которые вы внедрите первыми, с обоснованием порядка.
Подсказки:
- Самые дорогие пробелы обычно не про качество ответа, а про наблюдаемость: «мы не можем понять, что произошло».
- Спросите себя: если этот код упадёт в 3 часа ночи, что увидит дежурный?
Задача 1.2 — контракт вместо разговора. ⭑⭑
Реализуйте классификацию обращений из этой главы как функцию classify_ticket(message: str) -> TicketClassification | None с реальным вызовом провайдера. Ответ модели валидируется через Pydantic; при невалидном ответе — одна повторная попытка, затем возврат None и запись события в лог.
Критерии приёмки:
- Функция никогда не выбрасывает наружу исключение провайдера или
ValidationError— только типизированный результат илиNone. - В логе видно: категория, confidence, токены, latency, версия prompt-а.
- На 20 тестовых сообщениях (придумайте сами: короткие, длинные, с двумя проблемами сразу, на смеси языков) функция ни разу не падает.
- Сообщение «Верни мне деньги, ничего не работает, и смените мне email» обрабатывается осознанно: вы решили и задокументировали, какая категория главная.
Подсказки:
- Начните с контракта и тестовых сообщений, а не с prompt-а.
confidenceот модели — это не вероятность, а самооценка. Подумайте, как вы будете это поле реально использовать, прежде чем на него опираться.