Гибридная AI-архитектура: почему мы перестали доверять LLM все вычисления
Как SCADA.AI прошла путь от 'пусть LLM всё посчитает' до трёхслойной архитектуры, где Python отвечает за вычисления, а YandexGPT — только за нарратив. Реальный кейс, метрики и антипаттерны из production.
Провокация: 95% — не преувеличение
Давайте сразу к делу. Если вы разрабатываете AI-продукт и отправляете в LLM запрос типа «посчитай среднюю температуру за последние 24 часа» — вы делаете что-то не так. Если ваш health score вычисляется через GPT-4 — вы сидите на пороховой бочке. Если ваша бизнес-логика живёт в промпте — готовьтесь к сюрпризам в продакшене.
«Любой достаточно продвинутый баг неотличим от фичи» Энди Келли, перефразируя Кларка
В этой статье я расскажу, как мы в SCADA.AI прошли путь от «давайте LLM всё посчитает» до гибридной архитектуры, где LLM отвечает только за narrative (текст для пользователя), а 95% вычислений происходят детерминированно на Python. Покажу реальный код, метрики, подводные камни и кейс, когда LLM вернул пустой JSON и чуть не обрушил систему.
Часть 1: Иллюзия «умного» AI
Как это начинается
Вы — разработчик. Заказчик хочет «умную» систему. Вы слышите:
- «AI должен анализировать данные»
- «Система должна понимать, что происходит»
- «Пусть нейросеть решает»
И вы, вооружённые API, начинаете писать промпты:
Ты — инженер-аналитик SCADA-системы.
Вот данные за последние 24 часа:
- Температура: avg 22.5°C, min 18°C, max 26°C
- CO2: avg 650 ppm, min 400 ppm, max 1200 ppm
- Аварии: 3 high, 5 medium, 12 low
Посчитай индекс здоровья системы от 0 до 100.
Верни JSON: {"score": число, "status": "CRITICAL/WARNING/GOOD/EXCELLENT"} Кажется элегантно: одна строчка API, одна модель, один ответ. Но это ловушка.
Три фундаментальные проблемы
1. Скорость
LLM-запрос занимает 5-10 секунд. Детерминированная функция на Python — 50 миллисекунд. Разница в 100-200 раз.
Представьте: оператор на промышленном объекте открывает дашборд. Ждёт 10 секунд. Обновляет страницу. Снова ждёт. В критической ситуации (авария, утечка газа) эти 10 секунд — вечность.
2. Стоимость
Каждый запрос к LLM стоит денег. Звучит недорого. Но:
- 100 операторов × 20 запросов в день × 30 дней = 60 000 запросов
- 60 000 × $0.03 = $1 800 в месяц только на health score
- Добавьте narrative, рекомендации, анализ логов — счёт растёт экспоненциально
Детерминированная функция: $0 навсегда.
3. Нестабильность
LLM — это вероятностная модель. Один и тот же запрос может вернуть:
{"score": 65, "status": "GOOD"}{"score": 70, "status": "GOOD"}{"score": 60, "status": "WARNING"}- А иногда — битый JSON, который ломает ваш парсер
«Определение безумия — делать одно и то же снова и снова, ожидая разных результатов» — Рита Мей Браун
В промышленной автоматизации (SCADA) нестабильность — это не просто неудобство. Это риск для безопасности людей и оборудования.
Часть 2: Анатомия ошибки
Кейс: пустой sub_scores
Это реальная история из нашего проекта SCADA.AI v3.1.0.
Задача: Показать под статусом Health Score детализацию расчёта:
55 = Аварии 18 + Среда 22 + Оборудование 12 + Энергия 3
Наивное решение: Попросить LLM вернуть sub_scores в JSON:
{
"score": "число",
"status": "строка",
"sub_scores": {
"alarms": {"score": "число", "weight": 35},
"environmental": {"score": "число", "weight": 30},
"equipment": {"score": "число", "weight": 25},
"energy": {"score": "число", "weight": 10}
}
} Что произошло:
- LLM возвращал
sub_scores: {}(пустой объект) - Frontend пытался отрендерить детализацию
- Получал
undefinedвместо данных - Детализация не появлялась
- Пользователь видел только «55» без объяснений
Почему LLM возвращал пустоту:
- Мы не дали few-shot examples с
sub_scores - LLM «забыл» про это поле
- Температура в промпте (0.05) иногда приводила к «лени» модели
- Даже если LLM заполнял
sub_scores, значения были выдуманными — модель не знала реальных весов
Сколько времени мы потратили на дебаг: 3 часа чтения логов, просмотра API-ответов, добавления console.log во frontend.
Решение: 5 строк кода.
Часть 3: Трёхслойная архитектура
После серии подобных инцидентов мы пришли к гибридной архитектуре, где каждый слой делает то, что умеет лучше всего.
┌─────────────────────────────────────┐
│ Слой 3: LLM Narrative │
│ (только текст для пользователя) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Слой 2: Deterministic Analyzers │
│ (формулы, расчёты, валидация) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Слой 1: Data Collectors │
│ (SQL queries к SCADA БД) │
└─────────────────────────────────────┘ Слой 1: Data Collectors
Задача: Собрать сырые данные из PostgreSQL (SCADA-база).
Принцип: Минимум логики, максимум I/O.
# modules/health/data_collectors.py
async def collect_all_health_data(period_hours: int = 24) -- dict:
"""
Собирает сырые данные из SCADA БД.
Никаких вычислений — только SQL queries.
"""
pool = await get_db_pool()
async with pool.acquire() as conn:
# Аварии за период
alarms = await conn.fetch("""
SELECT priority, COUNT(*) as count
FROM alarm_events_history
WHERE timestamp - NOW() - INTERVAL '$1 hours'
GROUP BY priority
""", period_hours)
# Параметры среды (температура, CO2, влажность, давление, VOC)
environmental = await collect_environmental_data(conn, period_hours)
# Оборудование (битые/офлайн/дребезжащие теги)
equipment = await collect_equipment_status(conn)
# Энергоэффективность (освещение)
lighting = await collect_lighting_data(conn)
return {
"alarms": format_alarms(alarms),
"environmental": environmental,
"equipment": equipment,
"lighting": lighting,
"period_hours": period_hours,
"collected_at": datetime.now().isoformat()
} Почему это важно:
- Данные собираются один раз за запрос
- SQL queries оптимизированы (индексы, партиционирование)
- Если БД недоступна — мы узнаем об этом сразу, а не через 5 секунд LLM-обработки
- Данные можно кэшировать в Redis на 5 минут
Слой 2: Deterministic Analyzers
Задача: Превратить сырые данные в метрики, индексы, статусы.
Принцип: Чистые функции. Один вход — один выход. Никаких LLM.
# modules/health/analysis.py
from dataclasses import dataclass
from typing import Literal
@dataclass
class HealthReport:
score: int
status: Literal["CRITICAL", "WARNING", "GOOD", "EXCELLENT"]
sub_scores: dict
issues: list[dict]
def compute_health_report(data: dict) -- HealthReport:
"""
Детерминированный расчёт здоровья системы.
50 миллисекунд. 0 рублей. 100% стабильность.
"""
# === Под-индексы ===
alarm_idx = _compute_alarm_index(data["alarms"]["by_priority"])
env_idx = _compute_environmental_index(data["environmental"])
equip_idx = _compute_equipment_index(data["equipment"])
energy_idx = _compute_energy_index(data["lighting"])
# === Композитный score ===
score = round(
0.40 * alarm_idx +
0.35 * env_idx +
0.25 * equip_idx
)
# === Статус (детерминированно, не от LLM!) ===
if score < 30:
status = "CRITICAL"
elif score < 60:
status = "WARNING"
elif score < 85:
status = "GOOD"
else:
status = "EXCELLENT"
# === Sub-scores (для детализации в UI) ===
sub_scores = {
"alarms": {"score": alarm_idx, "weight": 40},
"environmental": {"score": env_idx, "weight": 35},
"equipment": {"score": equip_idx, "weight": 25},
}
# === Issues (топ проблем) ===
issues = _extract_top_issues(data, sub_scores)
return HealthReport(
score=score,
status=status,
sub_scores=sub_scores,
issues=issues,
) Реальные формулы (из production-кода):
def _compute_alarm_index(by_priority: dict) -- int:
"""
score = 100 - min(high * 15, 50) - min(medium * 4, 25) - min(low * 0.5, 10)
"""
high = by_priority.get("high", 0)
medium = by_priority.get("medium", 0)
low = by_priority.get("low", 0)
score = 100
score -= min(high * 15, 50) # Макс -50 за критические аварии
score -= min(medium * 4, 25) # Макс -25 за средние
score -= min(low * 0.5, 10) # Макс -10 за информационные
return max(0, round(score))
def _compute_environmental_index(env: dict) -- int:
"""
Взвешенная сумма статусов 5 параметров.
OK=100, WARNING=55, CRITICAL=15
"""
weights = {
"co2": 0.30,
"temperature": 0.25,
"voc": 0.20,
"humidity": 0.15,
"pressure": 0.10,
}
status_scores = {"OK": 100, "WARNING": 55, "CRITICAL": 15, "NO_DATA": 50}
total = 0
for param, weight in weights.items():
param_data = env.get(param, {})
status = param_data.get("status", "NO_DATA")
total += status_scores[status] * weight
return round(total) Почему это лучше LLM:
- 50 мс вместо 5-10 секунд
- 0 рублей вместо $0.03 за запрос
- 100% стабильность — один вход всегда даёт один выход
- Тестируемо — можно написать unit tests
- Интерпретируемо — инженер видит формулу и понимает логику
- Версионируемо — изменил формулу, закоммитил, задеплоил
Слой 3: LLM Narrative
Задача: Превратить сухие цифры в понятный текст для оператора.
Принцип: LLM получает уже посчитанные данные и генерирует narrative.
# modules/health/prompts.py
HEALTH_SYSTEM_PROMPT = """
Ты — инженер-аналитик SCADA-системы промышленного здания.
ВХОДНЫЕ ДАННЫЕ (уже посчитаны детерминированно):
- score: {score} (из 100)
- status: {status}
- sub_scores: {sub_scores}
- issues: {issues}
- alarms: {alarms}
- environmental: {environmental}
ЗАДАЧА:
1. Напиши краткое резюме состояния (2-3 предложения)
2. Выдели главную проблему
3. Предложи 1-2 конкретных действия
4. Используй деловой, но не формальный тон
ВАЖНО:
- НЕ пересчитывай score — он уже правильный
- НЕ выдумывай данные, которых нет в input
- Используй только предоставленные числа
- Отвечай на русском языке
""" Что получает LLM:
{
"score": 55,
"status": "WARNING",
"sub_scores": {
"alarms": {"score": 45, "weight": 40},
"environmental": {"score": 70, "weight": 35},
"equipment": {"score": 60, "weight": 25}
},
"issues": [
{"param": "co2", "impact": -15, "frequency": 12},
{"param": "temperature", "impact": -8, "frequency": 5}
],
"alarms": {
"total": 165,
"by_priority": {"high": 15, "medium": 82, "low": 68}
}
} Что возвращает LLM:
{
"summary": "Состояние системы требует внимания. Основная проблема — повышенный уровень CO2, который негативно влияет на общий индекс здоровья.",
"main_issue": "Рост CO2 в Зоне 2 (12 превышений за сутки)",
"recommendations": [
"Проверьте работу приточной вентиляции в Зоне 2",
"Откалибруйте датчики CO2 (3 датчика показывают аномалии)"
]
} Почему это работает:
- LLM не решает математических задач (плохо умеет)
- LLM интерпретирует уже готовые цифры (хорошо умеет)
- Если LLM упадёт — система всё равно покажет виджеты с правильными числами
- Narrative — это «вишенка на торте», а не фундамент
Часть 4: Рендеринг — три канала информации
После того как данные собраны, посчитаны и описаны, мы рендерим их в три канала:
# modules/health/renderers.py
async def render_all(report: HealthReport) -- dict:
"""
Рендерит отчёт в три канала:
1. narrative — текст для чата
2. voice — текст для озвучивания
3. visual — виджеты для UI
"""
narrative = await _render_narrative(report)
voice = _render_voice(report)
visual = _render_visual(report)
return {
"narrative": narrative,
"voice": voice,
"visual": visual
} Visual — это набор виджетов:
def _render_visual(report: HealthReport) -- dict:
widgets = []
# 1. Health Score (круговая диаграмма)
widgets.append({
"type": "health_score",
"data": {
"score": report.score,
"status": report.status,
"status_ru": translate_status(report.status),
"sub_scores": report.sub_scores,
},
"size": "medium"
})
# 2. Life Support (параметры среды)
widgets.append({
"type": "life_support_card",
"data": report.life_support,
"size": "medium"
})
# 3. Environmental Panel
widgets.append({
"type": "environmental_panel",
"data": report.environmental,
"size": "wide"
})
# 4. Alarms Panel
widgets.append({
"type": "alarms_panel",
"data": report.alarms,
"size": "wide"
})
# 5. Energy Cost Card (из v3.1.0)
widgets.append({
"type": "energy_cost_card",
"data": report.energy_costs,
"size": "medium"
})
return {"widgets": widgets} Frontend рендерит виджеты через WidgetRouter:
<!-- frontend/src/components/WidgetRouter.svelte ---
<script-
import HealthScoreCard from './health/HealthScoreCard.svelte'
import LifeSupportCard from './health/LifeSupportCard.svelte'
import EnvironmentalPanel from './health/EnvironmentalPanel.svelte'
import AlarmsPanel from './health/AlarmsPanel.svelte'
import EnergyCostCard from './health/EnergyCostCard.svelte'
const componentMap = {
'health_score': HealthScoreCard,
'life_support_card': LifeSupportCard,
'environmental_panel': EnvironmentalPanel,
'alarms_panel': AlarmsPanel,
'energy_cost_card': EnergyCostCard,
}
export let widgets = []
</script-
{#each widgets as widget}
{@const Component = componentMap[widget.type]}
<Component data={widget.data} /-
{/each} Результат: Оператор видит красивые виджеты с правильными числами + narrative от LLM + голосовое озвучивание.
Часть 5: Таблица сравнения подходов
Давайте сравним три подхода к архитектуре AI-продукта:
| Критерий | «Всё через LLM» | «Гибрид» | «Чисто детерминированно» |
|---|---|---|---|
| Скорость | 5-10 сек | 3 сек (50 мс + LLM) | 50 мс |
| Стоимость | $1800/мес | $200/мес (только narrative) | $0 |
| Стабильность | Нестабильно | Стабильно | 100% |
| Качество текста | Отличное | Отличное | Шаблоны |
| Тестируемость | Сложно | Легко (кроме narrative) | Всё легко |
| Гибкость | Зависит от промпта | Можно менять слои | Жёстко |
| Время на разработку | Быстро (сначала) | Среднее | Долго |
| Время на поддержку | Ад (отладка LLM) | Нормально | Легко |
| UX при падении LLM | Всё сломалось | Виджеты работают | Всё работает |
Вывод: Гибрид — это золотая середина. Вы получаете лучшее от обоих миров: скорость и стабильность детерминированных вычислений, качество и гибкость LLM для narrative.
Часть 6: Правило 80/20 — что считать, а что оставить LLM
После года разработки мы вывели простое правило.
Считайте детерминированно (80% работы):
1. Математические формулы
- Средние значения, min/max
- Индексы, скоринг, рейтинги
- Корреляции, регрессии
- Статистические метрики
2. Бизнес-логика
- Расчёт стоимости (тарифы × потребление)
- Валидация данных (границы, пороги)
- Приоритизация проблем
- Агрегация по зонам/периодам
3. Статусы и классификация
- OK / WARNING / CRITICAL
- HIGH / MEDIUM / LOW приоритеты
- Битый / Офлайн / Рабочий датчик
4. Фильтрация и поиск
- Аварии за период
- Теги по зоне
- Логи по уровню
Оставьте LLM (20% работы):
1. Narrative (текст для пользователя)
- «Состояние системы требует внимания…»
- «Главная проблема — рост CO2…»
- «Рекомендуем проверить вентиляцию…»
2. Интерпретация сложных паттернов
- «Эти три аномалии могут быть связаны…»
- «Тренд похож на инцидент в марте 2025…»
3. Ответы на неструктурированные вопросы
- «Что мне делать прямо сейчас?»
- «Почему вчера был скачок температуры?»
- «Сравни эту неделю с прошлой»
4. Анализ неструктурированных данных
- Текстовые логи
- Комментарии операторов
- Отчёты о ремонтах
«Сделай чтобы работало, сделай правильно, сделай быстро — в таком порядке» — Kent Beck
В AI-контексте это звучит как:
- Сделай чтобы работало (детерминированно)
- Сделай красиво (добавь LLM для narrative)
- Сделай быстро (оптимизируй SQL и кэш)
Часть 7: Performance метрики
Давайте посмотрим на реальные цифры из production SCADA.AI.
Запрос «покажи здоровье здания»
| Этап | Время | % от общего |
|---|---|---|
| SQL queries к SCADA БД | 1.2 сек | 35% |
| Детерминированные расчёты | 50 мс | 1.5% |
| LLM narrative | 2.1 сек | 62% |
| Рендеринг виджетов | 10 мс | 0.3% |
| Итого | 3.4 сек | 100% |
Если бы всё было через LLM:
- LLM посчитал бы индексы: +5 сек
- LLM сгенерировал бы виджеты: +3 сек
- Итого: 11-13 секунд
Разница: 3.4 сек vs 12 сек = 3.5× быстрее
Стоимость за месяц (100 операторов × 20 запросов/день)
| Подход | Запросов к LLM | Стоимость |
|---|---|---|
| «Всё через LLM» | 60 000 | $1 800 |
| Гибрид (наш) | 10 000 (только narrative) | $300 |
| Чисто детерминированно | 0 | $0 (но нет narrative) |
Экономия: $1 500 в месяц = $18 000 в год
Часть 8: Fallback-логика — когда LLM падает
Одно из главных преимуществ гибридной архитектуры — graceful degradation.
# api/routes/chat.py
async def handle_health_query(message: str) -- dict:
# 1. Собираем данные (детерминированно)
data = await collect_all_health_data(period_hours=24)
# 2. Считаем отчёт (детерминированно)
report = compute_health_report(data)
# 3. Пытаемся получить narrative от LLM
try:
llm_response = await llm.generate(
HEALTH_SYSTEM_PROMPT.format(**report.to_dict()),
timeout=10
)
narrative = llm_response.get("summary", "")
recommendations = llm_response.get("recommendations", [])
except LLMTimeoutError:
# Fallback: используем шаблонный narrative
narrative = f"Здоровье системы: {report.score}/100 ({translate_status(report.status)})"
recommendations = []
log.warning("LLM timeout, using fallback narrative")
except LLMError as e:
# Fallback: минимальный narrative
narrative = "Не удалось получить AI-анализ, но виджеты работают."
recommendations = []
log.error(f"LLM error: {e}")
# 4. Рендерим виджеты (детерминированно — всегда работает!)
visual = render_visual(report)
# 5. Рендерим voice (детерминированно)
voice = render_voice(report, narrative)
return {
"response": narrative,
"voice": voice,
"visual": visual, # ВСЕГДА есть, даже если LLM упал
"recommendations": recommendations
} Что видит пользователь:
Когда LLM работает:
- «Состояние системы требует внимания (55/100). Главная проблема — повышенный CO2 в Зоне 2. Рекомендуем проверить вентиляцию.»
Когда LLM упал:
- «Здоровье системы: 55/100 (Внимание)»
В обоих случаях виджеты работают идеально — оператор видит все цифры, графики, статусы. Narrative — это дополнение, а не основа.
Часть 9: Когда LLM действительно НЕОБХОДИМ
Чтобы не сложилось впечатление, что я против LLM — вот случаи, где она незаменима.
1. Tool Calling для анализа логов
# modules/logs/tools.py
async def analyze_logs(level: str = None, limit: int = 100) -- dict:
"""
Tool для LLM: читает логи и возвращает для анализа.
"""
logs = await system_logger.get_logs(level=level, limit=limit)
return {
"logs": logs,
"count": len(logs),
"level_filter": level
} Flow:
User: "проанализируй логи за последний час"
↓
LLM: "Я вызову analyze_logs"
↓
Tool: возвращает 100 строк логов
↓
LLM: "В логах обнаружено 3 ошибки подключения к БД,
2 предупреждения о медленных запросах.
Основная проблема — нестабильное соединение с PostgreSQL
в 14:23-14:27. Рекомендую проверить сетевую инфраструктуру." Здесь LLM делает то, что невозможно детерминированно — интерпретирует неструктурированный текст и находит паттерны.
2. Ответы на неструктурированные вопросы
User: "Почему вчера был скачок температуры?"
↓
LLM: (анализирует контекст)
"Судя по логам, вчера в 15:42 был отключен приточный агрегат №3
для планового ТО. Это привело к росту температуры в Зоне 2
с 22°C до 28°C. Агрегат включили обратно в 17:15, после чего
температура вернулась в норму в течение 30 минут." Здесь LLM соединяет данные из разных источников (логи, аварии, параметры среды) и даёт связный ответ.
3. Генерация рекомендаций
LLM получает:
{
"top_issues": [
{"param": "co2", "impact": -15, "frequency": 12},
{"param": "alarms", "impact": -10, "frequency": 8}
],
"trends": {"co2": "rising", "temperature": "stable"}
} LLM генерирует:
“Рекомендую в первую очередь заняться системой вентиляции:
- Замените фильтры в приточной вентиляции (эффект: +10 баллов)
- Откалибруйте датчики CO2 в Зоне 2 (эффект: +5 баллов)
- Увеличьте кратность воздухообмена ночью с 0.5 до 1.0”
LLM приоритизирует проблемы и даёт конкретные действия — это то, что сложно формализовать в формулах.
Часть 10: Архитектурные принципы
Подытожим правила, которые мы вывели за год разработки.
Принцип 1: «LLM — это UI, а не Backend»
LLM должна быть на границе системы — там, где данные превращаются в человеко-читаемый текст. Внутри системы — только детерминированный код.
Правильно: [SQL] → [Python формулы] → [LLM narrative] → [UI]
Неправильно: [SQL] → [LLM считает score] → [UI]
Принцип 2: «Один запрос = один сбор данных»
Если LLM нужно 5 разных данных — соберите их один раз в Python и передайте в промпт. Не делайте 5 tool calls, если можно сделать 1.
Принцип 3: «Детерминированное ядро + LLM-обёртка»
Ядро системы (бизнес-логика, формулы, валидация) должно работать без LLM. LLM — это надстройка, которую можно отключить без потери функциональности.
Принцип 4: «Тестируй формулы, а не промпты»
Unit tests для Python-функций — обязательны. «Тесты» для промптов — это просто ручная проверка на 10 примерах. Не тратьте время на автоматизацию того, что по своей природе нестабильно.
Принцип 5: «Кэшируй то, что можно кэшировать»
- SQL queries → Redis на 5 минут
- Детерминированные расчёты → in-memory на время запроса
- LLM narrative → не кэшировать (каждый запрос уникален)
Часть 11: Эволюция нашего подхода
v1.0 — «Всё через LLM»
Мы начинали как все:
User: "покажи здоровье"
↓
LLM: (получает сырые данные из БД)
↓
LLM: (считает индексы, генерирует текст, выбирает виджеты)
↓
UI: (рендерит что вернул LLM) Проблемы:
- 15 секунд на ответ
- $3000/месяц на API
- Постоянные «галлюцинации» в числах
- Невозможно отдебажить
v2.0 — «Детерминированный слой»
Добавили analysis.py:
User: "покажи здоровье"
↓
Python: (считает индексы)
↓
LLM: (генерирует narrative на основе посчитанных данных)
↓
UI: (рендерит виджеты из Python + narrative от LLM) Улучшения:
- 5 секунд на ответ
- $500/месяц
- Стабильные числа в виджетах
- Можно писать unit tests
v3.0 — «Гибридная архитектура»
Текущая версия:
User: "покажи здоровье"
↓
SQL: (собирает данные)
↓
Python: (считает индексы, под-индексы, статусы)
↓
LLM: (только narrative + recommendations)
↓
Renderers: (narrative + voice + visual)
↓
UI: (виджеты + текст + голос) Результат:
- 3 секунды на ответ
- $300/месяц
- 100% стабильность виджетов
- Graceful degradation при падении LLM
v3.1.0 — «Модуль Бабло»
Добавили расчёт стоимости ресурсов (электричество, вода, тепло):
- 100% детерминированно (интервальные тарифы, SQL к тегам ЛЭРС)
- LLM только объясняет: «В мае вы потратили 114 005 ₽ на электричество»
Часть 12: Антипаттерны, которые мы видели
За год работы с клиентами и другими командами мы собрали коллекцию антипаттернов.
Антипаттерн 1: «LLM как калькулятор»
Prompt: "Посчитай 2 + 2 × 3"
LLM: "Ответ: 8" # Правильно
LLM: "Ответ: 7" # Иногда Почему плохо: LLM — это языковая модель, а не математическая. Используйте Python для вычислений.
Антипаттерн 2: «LLM как валидатор»
Prompt: "Проверь, что email 'user@example.com' валидный"
LLM: "Да, валидный"
LLM: "Нет, невалидный" # Иногда Почему плохо: Используйте regex или библиотеки валидации.
Антипаттерн 3: «LLM как классификатор»
Prompt: "Определи приоритет аварии: 'Протечка в серверной'"
LLM: "HIGH" # Обычно
LLM: "MEDIUM" # Иногда Почему плохо: Используйте правила и keywords в Python.
Антипаттерн 4: «LLM как база данных»
Prompt: "Какая была температура вчера в 14:00?"
LLM: "22.5°C" # Выдумало Почему плохо: LLM не имеет доступа к вашим данным. Используйте SQL + tool calling.
Антипаттерн 5: «Один промпт на всё»
Prompt:
Ты — инженер SCADA.
Собери данные из БД
Посчитай индексы
Найди аномалии
Сгенерируй отчёт
Предложи действия
Отформатируй в JSON
Переведи на русский Почему плохо: LLM делает всё плохо. Разделите на слои.
Часть 13: Практические советы
Если вы только начинаете AI-проект, вот мой чек-лист.
День 1-7: Прототип
- Используйте LLM для всего (быстрый MVP)
- Соберите обратную связь от пользователей
- Определите, что реально нужно
День 8-30: Разделение
- Выделите бизнес-логику в Python
- Определите, где LLM действительно нужна
- Напишите первые unit tests
День 31-90: Оптимизация
- Добавьте кэширование
- Оптимизируйте SQL queries
- Внедрите fallback-логику
День 91+: Полировка
- A/B тесты промптов
- Мониторинг стоимости и скорости
- Добавление новых модулей
Часть 14: Метрики для мониторинга
В production обязательно отслеживайте.
Скорость
p50_response_time— медианное время ответаp95_response_time— 95-й перцентильllm_latency— время LLM-запроса отдельноdb_latency— время SQL-запросов
Стоимость
daily_llm_cost— дневная стоимость LLMcost_per_request— средняя стоимость запросаmonthly_projection— прогноз на месяц
Стабильность
llm_error_rate— % ошибок LLMfallback_usage— % запросов с fallbackdeterministic_accuracy— точность формул
Качество
user_satisfaction— оценка пользователейnarrative_quality_score— качество narrativefalse_positive_rate— % ложных срабатываний
Заключение: LLM — это не волшебная палочка
«Распределённая система — это система, в которой поломка компьютера, о существовании которого вы даже не подозревали, может сделать ваш собственный компьютер непригодным к использованию» Leslie Lamport
Перефразируя для AI-систем: «AI-система — это система, в которой галлюцинация LLM, о которой вы даже не подозревали, может сделать ваши виджеты непригодными к использованию».
Гибридная архитектура — это не про экономию денег (хотя это приятный бонус). Это про надёжность, скорость и предсказуемость. LLM — это мощный инструмент, но только тогда, когда вы используете её для того, что она умеет лучше всего: понимать и генерировать естественный язык.
Всё остальное — формулы, валидация, агрегация, классификация — делайте в Python. Ваш продакшен скажет вам спасибо.