Архитектура приложения с LLM

Как организовать код, чтобы LLM был надёжной частью приложения, а не источником хаоса.

LLM-слой — изолированная часть приложения, отвечающая за общение с моделью: сборку промптов, вызов API, повторы, фолбэки и парсинг ответов.

Изолируйте работу с LLM

Не разбрасывайте вызовы messages.create по всему коду. Соберите их в один модуль/сервис. Тогда смена провайдера, модели или формата промпта — это правка в одном месте, а не по всему проекту. Бизнес-логика вызывает ваш сервис, а не SDK напрямую.

Где хранить промпты

Промпты — это не «магические строки» внутри функций. Их версионируют как код или конфиг: отдельные файлы/шаблоны, с понятными именами. Тогда их можно ревьюить, тестировать, A/B-сравнивать и откатывать, не трогая логику.

# prompts/support_system.txt — отдельный файл, а не строка в коде
# llm_service.py
def answer_support(question, history):
    system = load_prompt("support_system.txt")
    return call_llm(system=system, messages=history + [user(question)])

Ретраи и таймауты — на уровне слоя

Повторы временных ошибок и разумные таймауты задают один раз в LLM-слое (через настройки SDK), а не дублируют в каждом месте вызова. Так поведение единообразно.

Фолбэки

Продумайте, что делать, когда модель недоступна или вернула мусор:

  • Фолбэк на другую модель: если основная перегружена (529), пробуем запасную (например, более дешёвую/быструю).
  • Фолбэк на правило: если LLM не ответил, показать заранее заготовленный ответ или передать оператору.
  • Деградация, а не падение: сбой LLM не должен ронять всё приложение.
def call_with_fallback(messages):
    try:
        return call_model("claude-opus-4-8", messages)
    except (RateLimitError, OverloadedError):
        return call_model("claude-haiku-4-5", messages)   # запасная
    except Exception:
        return DEFAULT_ANSWER                              # мягкая деградация

Разделяйте детерминированное и недетерминированное

LLM непредсказуем. Всё, что можно сделать обычным кодом (валидация, маршрутизация, вычисления), делайте кодом, а модели оставляйте то, что требует понимания языка. Так система предсказуемее и дешевле.

Типичная форма LLM-функции

Большинство «умных» функций укладываются в один и тот же конвейер: собрать промпт из входных данных и шаблона → вызвать модель (с ретраями и таймаутом) → распарсить и провалидировать ответ → при необходимости выполнить инструмент и дозапросить → вернуть результат или фолбэк. Если держать эту структуру в голове, любая новая фича становится подстановкой в знакомый каркас, а не изобретением с нуля. И каждый шаг этого конвейера — отдельная точка, которую удобно логировать и тестировать.

Итог

  • Изолируйте общение с LLM в один слой; бизнес-логика не зовёт SDK напрямую.
  • Промпты храните как версионируемые артефакты, не как строки в коде.
  • Ретраи, таймауты и фолбэки задавайте на уровне слоя; сбой LLM = деградация, не падение.
Проверьте себя
1. Почему стоит изолировать вызовы LLM в отдельный слой?
AЧтобы код был длиннее
BЧтобы смена провайдера, модели или промпта требовала правки в одном месте, а не по всему проекту
CЭто требование SDK
DЧтобы обойти rate limits
2. Как правильно хранить промпты в продакшене?
AКак магические строки внутри бизнес-функций
BКак версионируемые артефакты (файлы/шаблоны), которые можно ревьюить и откатывать
CВ базе данных пользователей
DВ переменных окружения вместе с ключом
3. Что такое фолбэк на уровне приложения с LLM?
AПовтор того же запроса без изменений
BЗапасной путь при сбое: другая модель, заготовленный ответ или передача оператору — вместо падения
CУвеличение max_tokens
DОтключение логирования
Поддержать проект