AI/ML в веб-архитектуре
Когда в веб-системах появляются модели и рекомендации.
Проблема: Ваше веб-приложение работало отлично. Ровно до того момента, как бизнес сказал: «А давайте добавим рекомендации? Ну там, "пользователям, которые купили это, также понравится..."». Или: «А давайте детектить мошенничество в реальном времени?». Или, если вы особенно везучи, — «А давайте вставим LLM-чат в наш продукт?».
Теперь в вашей красивой REST API появляется нечто, что не подчиняется правилам. Модель не детерминирована. Она не отвечает «да/нет» с гарантией. Она даёт вероятности. И она требует инфраструктуры, которая живёт по своим законам.
ML в веб-архитектуре — это момент, когда в систему добавляется элемент, который работает на вероятностях, а не на логике.
До этого у вас были чёткие контракты: запрос → бизнес-логика → ответ. На каждый вход — предсказуемый выход. Если что-то сломалось, вы идёте в код, читаете условие if (x > 10) и говорите: «Ага, вот же баг».
С ML всё иначе. Модель может давать разные ответы на одинаковые входы (если данные изменились). Она может "забыть" то, что знала вчера (model drift). Она может быть быстрой, но ошибочной, или точной, но медленной. И вы не можете просто «починить» модель через git commit — её нужно переобучать, деплоить, мониторить, версионировать.
Задача этой главы — показать, как встраивать ML в веб-архитектуру, чтобы он не ломал то, что уже работает. Чтобы ML был частью системы, а не «чёрным ящиком, который иногда предсказывает».
#Когда ML приходит в архитектуру
Есть три сценария, когда ML появляется в веб-приложении:
Сценарий 1: Рекомендации и персонализация
Пользователю нужно показать «что-то релевантное». Товары, контент, статьи. Без ML вы делаете ORDER BY rating DESC или ручные правила. С ML вы строите embedding пользователя и ищете ближайшие объекты.
Сценарий 2: Классификация и предсказание Мошенничество, спам, риск-анализ, time-to-live. Система должна принять решение на основе исторических данных. Это notif-модели: если вероятность > порога — блокируем транзакцию.
Сценарий 3: Генерация контента LLM-чаты, суммаризация, генерация описаний, code review с AI. Это новый класс задач, который стал мейнстримом в 2024-2026 годах и кардинально меняет архитектуру веб-приложений.
В каждом сценарии ML — это не «просто библиотека». Это инфраструктура, которой нужно управлять.
#Serving ML Models — как модель отвечает пользователю
Самая базовая задача: вы обучили модель. Что дальше? Как ей пользоваться?
#Вариант 1: In-process модель (начальный этап)
Самый простой способ — загрузить модель прямо в веб-приложение.
# app.py — FastAPI приложение
import pickle
from fastapi import FastAPI
app = FastAPI()
model = pickle.load(open("model.pkl", "rb"))
@app.post("/predict")
async def predict(features: dict):
prediction = model.predict([features["values"]])
return {"prediction": prediction.tolist()}Плюсы: Просто, ничего нового не нужно, latency минимальная. Минусы: Модель жрёт память в том же процессе, что и веб-сервер. Перезагрузка модели требует рестарта приложения. Разные версии моделей не сосуществуют. И если ваша модель весит 2GB — у вас проблемы.
Это работает для MVP и для моделей, которые обновляются раз в полгода. Для всего остального — читайте дальше.
#Вариант 2: Model Server (промышленный)
Когда моделей становится больше одной, вы выносите их в отдельные сервисы. Специализированные model servers занимаются только одним: принимать запросы и возвращать предсказания.
TensorFlow Serving Зрелый продукт от Google для моделей в формате TensorFlow/SavedModel. Умеет:
- Версионирование моделей — можете держать v1, v2, v3 и переключать трафик
- Автоматическая загрузка новых версий — положили модель в models/ — сервер подхватил
- Баччинг — группировка запросов для GPU efficiency
- gRPC и REST интерфейсы
# Docker run TensorFlow Serving с моделью рекомендаций
docker run -t --rm \
-p 8501:8501 \
-v /models:/models \
-e MODEL_NAME=recommendation \
tensorflow/servingЗапрос к модели через REST:
POST /v1/models/recommendation:predict
{
"instances": [
{"user_id": 42, "context": {"page": "home", "device": "mobile"}}
]
}TorchServe Официальный model server от PyTorch. Позволяет писать кастомные handler'ы для препроцессинга и постпроцессинга. То есть не просто «предскажи», а «возьми JSON → преврати в тензор → предскажи → преврати обратно в JSON с вероятностями».
# TorchServe handler
class RecommendationHandler(BaseHandler):
def preprocess(self, data):
# Из сырого JSON в тензор
user_id = data[0]["body"]["user_id"]
return torch.tensor([user_id])
def postprocess(self, output):
# Из тензора в читаемый JSON
probs = torch.nn.functional.softmax(output, dim=1)
return [{"probabilities": probs.tolist()}]NVIDIA Triton Inference Server Когда на сцену выходят GPU. Triton умеет работать с моделями из разных фреймворков (TensorFlow, PyTorch, ONNX) одновременно. Поддерживает динамическое баччинг, ensemble моделей и. Добавьте сюда model pipelines — когда один запрос проходит через несколько моделей последовательно.
Архитектурное решение:
Когда вы выносите модели в отдельные model servers, вы получаете возможность:
- Масштабировать модели независимо от веб-приложения — recommendation может крутить 50 подов, а user-service — только 3
- Разные hardware requirements — CPU для лёгких моделей, GPU для тяжёлых
- Изоляция памяти — модель с 4GB не положит ваш API
- Zero-downtime обновление — выкатили новую версию, старый traffic постепенно переехал
Компромисс: Сетевой latency. Каждый вызов model server — это HTTP/gRPC запрос по сети. Если ваша модель лёгкая (feature → decision tree), эта добавочная latency может быть больше, чем само выполнение модели. В таких случаях — возвращаемся к in-process или используем sidecar pattern (модель в соседнем контейнере).
#Кейс: LLM Serving в Netflix (2025-2026)
Отдельного внимания заслуживает платформа, которую Netflix построил для serving LLM в production.
От TensorRT-LLM к vLLM
Изначально Netflix построил систему на TensorRT-LLM (NVIDIA). К лету 2025 они мигрировали на vLLM по четырём причинам:
- Custom architecture loading — vLLM загружает кастомные архитектуры моделей без multi-step compilation. TensorRT-LLM требовал сложного пайплайна компиляции для каждой архитектуры.
- Hooks для extensibility — vLLM предоставляет интеграционные точки, которых не было в TensorRT-LLM.
- Debugability — pure-Python код проще отлаживать и модифицировать.
- Practitioner familiarity — широкая пользовательская база vLLM облегчает найм и обучение.
vLLM V0 → V1: Урок по CPU Bottleneck
vLLM V0 имел GIL CPU bottleneck в Python-реализации constrained decoding. V1 переписал критический путь на C++ с batch-level design, что:
- Устранило GIL как узкое место
- Увеличило throughput для batched inference
- Стало примером: пайплайн инференса упирается в CPU, не в GPU
Архитектура Serving
Netflix использует унифицированную JVM-based систему:
Request → JVM Router
├── CPU models (in-process serving) — для маленьких моделей
└── GPU models → Model Scoring Service (Java, REST)
└── Triton Inference Server (GPU)
└── vLLMМаленькие модели (классификация, NER) запускаются in-process в JVM. Большие LLM требуют GPU → направляются в Model Scoring Service, который через Triton общается с vLLM на GPU-нодах.
Ключевые выводы из кейса:
- Выбор serving engine — не разовое решение. Будьте готовы мигрировать
- Pure-Python прототип может быть production-ready после оптимизации C++ ядра
- CPU-side performance важнее GPU utilization для latency-sensitive workloads
- Unified routing layer (JVM) упрощает A/B тестирование моделей
#Feature Store — когда данных для модели больше, чем модель весит
Вот штука, о которой новички в ML-инфраструктуре узнают слишком поздно.
Обучающая выборка собирается из сырых данных (user_action, item_metadata, time_of_day). Data scientist пишет ноутбук, где все эти фичи вычисляются пайплайном. Модель обучена. Теперь её нужно запустить в продакшене. И тут выясняется, что пайплайн расчёта фич написан на коленке в Jupyter, не воспроизводится, тянет данные непонятно откуда и съедает 30 минут на выполнение.
Feature store решает ровно эту проблему: единое место, где хранятся, вычисляются и подаются feature'ы для ML.
Feature — это числовое или категориальное представление какого-то аспекта данных, используемое моделью для предсказания. Грубо говоря, «средний чек пользователя за неделю» — это фича. «Количество лайков под постом за последний час» — тоже фича.
Feature store даёт две key capabilities:
#Online features (для serving)
Нужна фича прямо сейчас, с latency < 10ms. Хранится в Redis, DynamoDB, Cassandra — быстрый key-value доступ.
# Получить фичу пользователя в момент запроса
features = feature_store.get_online_features(
feature_refs=[
"user:avg_order_value_7d",
"user:total_orders_lifetime",
"item:popularity_score_1h"
],
entity_keys={"user_id": 42, "item_id": 123}
)#Offline features (для обучения)
Нужна историческая выборка фич за месяц. Хранится в Parquet в S3 / Data Lake. Можно делать SELECT * FROM features WHERE timestamp > '2026-01-01' через Spark или Trino.
# Получить датасет для ретренировки
training_df = feature_store.get_historical_features(
feature_refs=["user:avg_order_value_7d", "item:popularity_score_1h"],
entity_df="SELECT user_id, item_id, timestamp FROM orders WHERE date > '2026-06-01'"
)#Feast — Open Source Feature Store
Feast — самый популярный open-source feature store (сейчас часть Linux Foundation). Работает поверх вашей инфраструктуры.
# feature_store.yaml
project: book_recommendations
provider: gcp
online_store:
type: redis
connection_string: redis://redis-features:6379
offline_store:
type: bigqueryFeast ничего не хранит сам — он управляет метаданными о том, какие фичи есть, где они лежат и как их получить.
# Определение фичи в Feast
user_features = FeatureView(
name="user_features",
entities=["user"],
features=[
Feature(name="avg_order_value_7d", dtype=ValueType.FLOAT),
Feature(name="total_orders_lifetime", dtype=ValueType.INT64),
],
batch_source=BigQuerySource(table_ref="project.dataset.user_metrics"),
stream_source=KafkaSource(...), # real-time обновление
)#Tecton — Enterprise Feature Platform
Tecton — это Feast на стероидах. Он решает проблему point-in-time correctness: когда вы обучаете модель на фичах, вы должны брать те значения фич, которые были на момент события, а не текущие. Иначе утечка future data в обучение. Tecton делает это автоматически.
Когда вам нужен feature store:
- У вас больше 5 моделей, которые используют перекрывающиеся фичи
- Data scientist'ы тратят больше времени на подготовку данных, чем на модели
- Вы заметили, что фичи при обучении и в продакшене (training-serving skew) отличаются
- Вы хотите real-time фичи (например, «количество действий пользователя за последние 5 минут»)
Когда нет:
- У вас одна модель, 10 фич, все статические
- Ваш датасет — это SQL запрос из одной таблицы
#Feature Engineering — приготовление данных
Feature engineering — это процесс превращения сырых данных в то, что модель может съесть. Архитектурно это самая недооценённая часть.
#Batch feature engineering (offline)
Классический ETL/ELT пайплайн. Spark, dbt, BigQuery — вы считаете фичи раз в N часов/дней и записываете в feature store.
-- dbt модель: user_features.sql
SELECT
user_id,
AVG(order_value) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) AS avg_order_value_7d,
COUNT(*) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN 365 PRECEDING AND 1 PRECEDING) AS total_orders_lifetime,
MAX(created_at) OVER (PARTITION BY user_id) AS last_order_date
FROM orders#Real-time feature engineering (online)
Когда фича должна обновляться в реальном времени — например, «средняя скорость скролла пользователя» или «количество кликов за последнюю минуту». Тут в игру вступают stream processing фреймворки.
# Flink / Kafka Streams пайплайн для real-time фич
from pyflink.datastream import StreamExecutionEnvironment
env = StreamExecutionEnvironment.get_execution_environment()
stream = env.from_source(kafka_source, ...)
user_actions = stream.key_by(lambda e: e["user_id"])
.window(SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)))
.aggregate(AggregateCount())
# Пишем в online feature store
user_actions.sink_to(redis_sink(host="redis-features"))Ключевой архитектурный принцип: Фичи при обучении (offline) и при serving (online) должны вычисляться одинаково. Никакого «у data scientist'а одна формула avg, у инженера другая». Feature store с единым definition гарантирует это.
#Model Deployment — стратегии выкатки
Вы обучили модель, завернули в контейнер, model server ждёт. Как выкатывать?
Тут ML не уникален — те же паттерны, что и для обычного софта (глава 13, CI/CD). Но есть нюансы.
#Shadow Deployment
Новую модель запускаете, но её ответы никому не показываете. Модель получает тот же трафик, что и текущая production-модель. Сравниваете ответы: метрики, latency, распределение предсказаний.
Traffic → Current Model → User
↘ Shadow Model → Log & CompareКогда: Вы не уверены в новой модели. Хотите посмотреть, как она поведёт себя на реальном трафике, не рискуя.
Кейс: Вы заменили decision tree на нейросеть. Старая модель работала, новая — потенциально точнее. Shadow деплой показывает, что нейросеть даёт те же предсказания в 95% случаев, а в 5% — «уверенно неправильные». Вы вовремя заметили и не покатили в прод.
Важно: Shadow деплой увеличивает потребление ресурсов в 2x — каждую модель нужно обслуживать. Это нормально для тестового периода.
#Canary Deployment
Traffic постепенно переключается на новую модель: 1% → 5% → 25% → 100%. На каждом шаге мониторите метрики бизнеса.
# Istio VirtualService для canary model serving
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: recommendation-model
spec:
hosts:
- recommendation-model
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: recommendation-model-v2
weight: 100
- route:
- destination:
host: recommendation-model-v1
weight: 95
- destination:
host: recommendation-model-v2
weight: 5Нюанс ML: Обычный софт — если не упало, значит работает. ML — если ответы изменились, это может быть улучшением или ухудшением. Вам нужно не просто мониторить 5xx ошибки, а метрики качества: click-through rate, conversion rate, user engagement.
#Blue/Green Deployment
Две полные копии serving инфраструктуры: blue (текущая) и green (новая). Трафик мгновенно переключается.
┌→ Blue Model ─── Users (old)
Load Balancer ───┤
└→ Green Model ── Users (new)Когда: Модель меняется кардинально. Новая архитектура фич, другой фреймворк. Rollback — просто переключение обратно.
Для ML важен rollout gate: перед полным переключением вы держите green на isolation-трафике N часов/дней и сравниваете. Хороший признак: вы можете запустить оба варианта параллельно, направить на каждый случайный 1% пользователей, и через неделю посчитать, какой вариант лучше по бизнес-метрикам.
#A/B Testing ML — не всё, что точнее, лучше
Классический подход: обучили новую модель → accuracy выше на 2% → катим в прод. Но accuracy — это метрика качества модели, а не метрика бизнеса.
Новая модель может быть точнее на тестовой выборке, но давать рекомендации, которые никто не кликает. Или кликают, но ничего не покупают. Или покупают, но потом возвращают.
А/Б тест для ML — это эксперимент на пользователях.
Random Split ─┬─ Control (50%) → Model A → Measure CTR, Revenue
└─ Treatment (50%) → Model B → Measure CTR, RevenueКлючевое отличие от обычного A/B теста (глава 26): разделение пользователей должно быть стабильным во времени. Пользователь из treatment всегда должен получать модель B, иначе метрики смешиваются.
# Стабильное разбиение через hash
def get_experiment_group(user_id: int, experiment: str) -> str:
hash_val = hash(f"{experiment}:{user_id}") % 100
if hash_val < 50:
return "control" # Model A
return "treatment" # Model BЧто мерять:
- Online метрики: CTR, conversion, revenue, retention, engagement
- Offline метрики: accuracy, precision, recall, F1 — только как сигнал, что модель не сломалась
- System метрики: latency, throughput, 5xx — модель может быть точной, но тормозить так, что пользователь уходит
Ловушка: Interference между группами. Если модель А рекомендует товар X, и пользователь покупает X, это влияет на availability товара X для пользователя из группы B. Такое interference (interleaved experiments) — отдельная боль для рекомендательных систем.
#Embeddings и Vector Search — когда текст/изображения становятся числами
Одна из ключевых ML-технологий, которая в последние годы плотно вошла в веб-архитектуру, — это embeddings.
Embedding — это вектор чисел (например, 768 или 1536 float'ов), который представляет семантику объекта. Текст, изображение, пользователя, товар — всё можно превратить в числовое представление.
Важное свойство embedding'ов: семантически близкие объекты имеют близкие вектора. «Кот» и «кошка» — рядом. «Кот» и «автомобиль» — далеко.
# Пример: получить embedding текста
response = openai.embeddings.create(
model="text-embedding-3-small",
input="Книга про архитектуру веб-систем"
)
embedding = response.data[0].embedding # [0.012, -0.045, 0.078, ...] — 1536 чиселРаз вы можете сравнивать объекты по смыслу — вы можете искать.
#Vector Search: "Найди похожее"
Вместо WHERE title LIKE '%архитектура%' вы делаете: «найди 10 товаров, семантически ближайших к этому embedding'у». Это семантический поиск.
Инструментов для vector search много. Они различаются по скорости, точности, масштабу.
#pgvector — векторный поиск в PostgreSQL
Самый простой entry point. Вы ставите расширение pgvector в ваш существующий PostgreSQL и получаете векторный поиск без дополнительной инфраструктуры.
-- Создаём колонку для embedding'ов
CREATE TABLE products (
id SERIAL PRIMARY KEY,
title TEXT,
description TEXT,
embedding vector(1536) -- колонка с 1536-мерным вектором
);
-- Ищем 10 ближайших
SELECT id, title, embedding <=> '[0.012, -0.045, ...]' AS distance
FROM products
ORDER BY distance
LIMIT 10;
-- Создаём индекс для ускорения (IVFFlat — приближённый поиск)
CREATE INDEX ON products USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);Когда pgvector: У вас уже PostgreSQL, количество документов < 10M, вам нужен «просто векторный поиск внутри базы», точность не критична.
Когда нет: > 10M векторов, нужна latency < 5ms, high QPS. PostgreSQL с векторами — не специализированная система.
#Pinecone — managed vector database
Pinecone — это "Snowflake для векторов". Полностью managed, ничего не настраиваете. Просто отправляете вектора и ищете.
import pinecone
pinecone.init(api_key="...")
index = pinecone.Index("products")
# Загружаем вектора
index.upsert([
("prod_1", [0.012, -0.045, 0.078, ...], {"title": "Книга про архитектуру"}),
("prod_2", [0.089, 0.012, -0.034, ...], {"title": "Книга про ML"}),
])
# Ищем ближайшие
results = index.query(
vector=[0.015, -0.040, 0.080, ...],
top_k=10,
include_metadata=True
)Когда Pinecone: Вы не хотите заниматься ops векторной базы. Вам нужен SLA, auto-scaling, низкая latency, миллиарды векторов.
Цена: За managed приходится платить. Проект на 10M векторов может стоить $500-2000/мес.
#Milvus / Zilliz — Open Source векторная база
Milvus — open-source векторная база, которую можно запустить самостоятельно. Поддерживает несколько индексов (IVF, HNSW, DiskANN для огромных датасетов), hybrid search (вектора + метаданные), multi-tenancy.
# docker-compose для Milvus
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
minio:
image: minio/minio:RELEASE.2022-03-17T06-34-49Z
standalone:
image: milvusdb/milvus:v2.5.0
ports:
- "19530:19530"
depends_on:
- etcd
- minioКогда Milvus: У вас > 10M векторов, нужна производительность, но вы хотите control над инфраструктурой. Или compliance требует, чтобы данные не уходили к внешним провайдерам.
Decision matrix для vector search:
| Инструмент | Масштаб | Latency | Ops overhead | Цена |
|---|---|---|---|---|
| pgvector | до 10M | 20-100ms | Низкий | Бесплатно (как часть Postgres) |
| Pinecone | до 1B+ | < 10ms | Нулевой | $$$ |
| Milvus | до 10B+ | 5-50ms | Средний | Бесплатно (self-hosted) |
| Qdrant | до 100M | < 20ms | Низкий | Бесплатно / Cloud $$$ |
| Weaviate | до 100M | < 30ms | Средний | Open Source / Cloud |
#LLM в веб-приложениях — RAG и Agent архитектура
2024-2026 годы кардинально изменили ландшафт. LLM (Large Language Models) стали не просто игрушкой, а инструментом, который веб-приложения встраивают в свой продукт.
Но встраивать LLM в веб-архитектуру — это не «сделать fetch к OpenAI и показать ответ». Это архитектурная задача.
#RAG — Retrieval-Augmented Generation
RAG — это паттерн, который решает главную проблему LLM: модель «знает» только то, на чём её обучили. Она не знает ваши данные, ваших пользователей, ваш продукт. RAG добавляет к LLM поисковую систему.
User Query
│
├─→ Embedding Model → Vector Search → Relevant Documents
│ │
│ ▼
│ LLM Prompt:
│ "Based on these documents, answer..."
│ │
◄─────────────────────────────────────────┘
ResponseШаги RAG:
- Пользователь задаёт вопрос
- Текст вопроса превращается в embedding через embedding-модель
- Embedding'ом ищем похожие документы в векторной базе
- Найденные документы + вопрос отправляются LLM как контекст
- LLM генерирует ответ на основе предоставленного контекста
Архитектурные решения для RAG:
# Простой RAG pipeline
from openai import OpenAI
import pinecone
client = OpenAI()
def rag_answer(question: str) -> str:
# 1. Embedding вопроса
response = client.embeddings.create(
model="text-embedding-3-small",
input=question
)
query_vector = response.data[0].embedding
# 2. Поиск релевантных документов
results = pinecone_index.query(
vector=query_vector,
top_k=5,
include_metadata=True
)
# 3. Формирование контекста
context = "\n\n".join(
[r["metadata"]["text"] for r in results["matches"]]
)
# 4. Генерация ответа с контекстом
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"Answer based on this context:\n{context}"},
{"role": "user", "content": question}
]
)
return response.choices[0].message.contentКлючевые архитектурные компромиссы RAG:
- Chunking: Как резать документы на куски? С перекрытием (overlap)? По абзацам? По токенам? Размер чанка влияет на качество — слишком мелко → потеря контекста, слишком крупно → шум в поиске.
- Embedding модель: Открытая (BAAI/bge-small-en-v1.5, 50MB) или проприетарная (OpenAI text-embedding-3-large)? Первая — бесплатно, вторая — качественнее.
- Gemiddelde search: Hybrid search (вектора + BM25 текстовый поиск) даёт лучше recall, чем чистые вектора.
- Caching: LLM-запросы дорогие. Кэшируйте embedding'ы (они не меняются), кэшируйте ответы на частые вопросы, кэшируйте промежуточные результаты.
#Кэширование LLM: семантический кэш
Обычный кэш по exact match не работает для LLM — «Какая погода в Москве?» и «Что с погодой в Москве?» — разные запросы, но ответ одинаковый. Семантический кэш ищет похожие вопросы в embedding'ах и возвращает сохранённый ответ, если нашли близкий.
class SemanticCache:
def __init__(self, threshold: float = 0.95):
self.threshold = threshold
def get(self, query: str) -> str | None:
query_vec = embed(query)
results = cache_index.query(vector=query_vec, top_k=1)
if results and results["matches"][0]["score"] > self.threshold:
return results["matches"][0]["metadata"]["answer"]
return None
def set(self, query: str, answer: str):
query_vec = embed(query)
cache_index.upsert([(hash(query), query_vec, {"answer": answer})])Экономия: Semantic cache может отсекать 30-60% повторяющихся LLM-запросов, особенно для FAQ и customer support сценариев.
#Agent Architecture — когда LLM получает инструменты
RAG — это пассивный поиск информации. Agent — это когда LLM получает возможность действовать: вызывать API, менять состояние системы, писать в базу.
User: "Забронируй столик на сегодня в итальянском ресторане рядом"
│
▼
LLM (receives tools definitions)
│
├─→ tool: search_restaurants(cuisine="italian", proximity) → [list]
├─→ tool: check_availability(restaurant_id, time) → "available"
├─→ tool: book_table(restaurant_id, time, party_size) → "confirmed"
│
▼
"We have a table at La Piazza at 7pm. Confirmed."Архитектура AI Agent:
# Определение инструментов для Agent
tools = [
{
"type": "function",
"function": {
"name": "search_restaurants",
"parameters": {
"type": "object",
"properties": {
"cuisine": {"type": "string"},
"location": {"type": "string"},
"party_size": {"type": "integer"}
}
}
}
},
{
"type": "function",
"function": {
"name": "book_table",
"parameters": {
"type": "object",
"properties": {
"restaurant_id": {"type": "string"},
"time": {"type": "string"},
"party_size": {"type": "integer"}
}
}
}
}
]
# Agent loop
def agent_loop(user_message: str, max_steps: int = 5):
messages = [{"role": "user", "content": user_message}]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)
if response.choices[0].finish_reason == "stop":
return response.choices[0].message.content
# Есть tool call — выполняем
tool_call = response.choices[0].message.tool_calls[0]
result = execute_tool(tool_call.function.name, tool_call.function.arguments)
messages.append(response.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
return "Max steps reached"Ключевые архитектурные решения для Agent'ов:
- Как долго живёт сессия? Один запрос (stateless) или длинный диалог (stateful)? Если stateful — где хранить историю? Redis? У каждого сервиса свой контекст?
- Rate limiting на действия. Agent может случайно наделать дел. Нужен guard: max actions per session, approval на деструктивные операции (удаление, оплата), человеческий override.
- Observability для Agent'ов. Что решил, почему, какие инструменты вызвал, с каким результатом? Это не просто логи — это трассировка решений. Без неё debug Agent'a — гадание.
- Human-in-the-loop. Для чувствительных операций — approval от человека. Agent предлагает, человек подтверждает.
Когда Agent'ы — это оверхед:
- Простой FAQ — RAG справляется
- Один fixed workflow — обычный код без LLM, а Agent поверх — «модный костыль»
- Нет чётких границ допустимого — Agent уйдёт в отсебятину
#ML Observability — когда модель начинает врать
Вы задеплоили модель. Она работает. Метрики зелёные. Через месяц — пользователи пишут, что рекомендации стали странными. Модель не упала. Она просто начала хуже предсказывать.
#Data Drift — изменились входные данные
Модель обучалась на данных 2025 года. А в 2026 году поведение пользователей изменилось. Средний возраст, категории товаров, паттерны покупок — всё сместилось. Модель получает на вход данные, которых она «не видела» при обучении.
Как детектить:
# Сравнение распределения фич при обучении и сейчас
from scipy.stats import ks_2samp
def detect_drift(training_values, current_values, threshold=0.05):
statistic, p_value = ks_2samp(training_values, current_values)
if p_value < threshold:
return True # drift detected
return FalseДля каждой фичи вы считаете дрифт. Если > X% фич дрифтят — модель нужно переобучать.
Tools: Evidently AI, WhyLabs, whylogs, NannyML. Все они делают одно и то же: статистическое сравнение распределений.
#Model Drift — упало качество предсказаний
Даже если данные не изменились, модель может «портиться». Например, рекомендательная модель обучалась на исторических данных, но user-item matrix изменилась (новые товары, новые пользователи). Модель не знает про них — cold start.
Как детектить:
- Если есть ground truth (например, кликнул/не кликнул) — считаете accuracy/precision каждый день и смотрите тренд
- Если нет ground truth — смотрите proxy метрики: средний score предсказания, distribution logits, количество уникальных recommendations
# Мониторинг распределения предсказаний
class PredictionMonitor:
def __init__(self, window="7d"):
self.historical_scores = []
def log_prediction(self, score: float):
self.historical_scores.append(score)
def check_anomaly(self) -> bool:
mean_score = mean(self.historical_scores[-100:])
historical_mean = mean(self.historical_scores[:-100])
return abs(mean_score - historical_mean) > 0.1#Архитектура ML observability
ML Model → Predictions + Features → Log to Storage
│
▼
Drift Detection (scheduled job)
│
┌──────┴──────┐
▼ ▼
Data Drift? Model Drift?
│ │
└──────┬───────┘
▼
Alert + Retrain TriggerПрактический совет: Не пытайтесь мониторить всё. Мониторьте 5-10 ключевых фич и 2-3 метрики бизнеса. ML observability — это не data science, это раннее предупреждение. Вы не должны каждый день смотреть на p-value KS-теста. Вы должны получать алерт, когда модель «сломалась».
#Когда это применимо
Для стартапа:
- In-process модель или простой model server (Flask + pickle — норм)
- pgvector — ничего не нужно поднимать нового
- RAG с OpenAI API — без своего хостинга моделей
- ML observability → пока не надо, следите руками
Для средней компании (50-500 инженеров):
- TensorFlow Serving / TorchServe для моделей
- Feast как feature store (Open Source, бесплатно)
- Milvus или Qdrant для vector search
- Evidently для ML observability
- Canary/blue-green деплой через feature flags
Для enterprise:
- Triton Inference Server с GPU
- Tecton для feature platform (требует бюджета)
- Pinecone или собственный Milvus кластер
- Полный ML observability stack: Evidently + WhyLabs + custom dashboard
- Human-in-the-loop для Agent'ов
- Свой MLOps платформа (Kubeflow, MLflow, или внутренняя)
#Практический кейс: Netflix Recommendations
Netflix — пожалуй, самый известный пример ML в веб-архитектуре. Их рекомендательная система определяет, что пользователь видит на главной странице. И это не одна модель, а целая экосистема.
#Проблема
У Netflix больше 260 миллионов подписчиков. Каждый видит уникальную главную страницу. Нельзя просто показать «самые популярные фильмы» — у каждого пользователя свой вкус. Нужно предсказать, что именно этот пользователь захочет посмотреть прямо сейчас (учитывая время суток, устройство, настроение, историю).
#Архитектура (упрощённо)
Netflix использует трёхуровневую персонализацию:
Уровень 1: Candidate Generation Из каталога в 20,000+ тайтлов нужно отобрать 100-200 кандидатов. Это делается через несколько подходов параллельно:
- Collaborative filtering (матричная факторизация) — «похожие пользователи смотрели это»
- Content-based filtering — «на основе того, что вы смотрели»
- Trending — глобально популярное
- Continue Watching — то, что пользователь начал
Каждый подход — отдельная модель, работающая независимо.
Уровень 2: Ranking 100 кандидатов ранжируются по вероятности того, что пользователь их посмотрит. Используется глубокая нейросеть с сотнями фич:
- Контекст: время, устройство, сезон, день недели
- История: что смотрел, как долго, на чём остановился
- Социальное: что популярно среди похожих пользователей
- Фичи контента: жанр, актёры, режиссёр, длительность
Уровень 3: Presentation Отранжированные тайтлы нужно показать. Какие поставить в топ ряда? С какой картинкой? Какое описание показать? Netflix A/B тестирует даже обложки — один и тот же фильм может показываться с разными постерами разным пользователям.
Feature infrastructure: Netflix построила одну из первых feature store систем. Она называется Netflix Feature Platform (внутренняя). Каждая фича имеет:
- Определение (definition)
- Пайплайн расчёта (batch или real-time)
- Owner'а (команда, которая отвечает за фичу)
- Историю изменений
A/B testing на масштабе: Netflix проводит сотни A/B экспериментов одновременно. Каждый пользователь может участвовать в нескольких экспериментах (по разным слотам — рекомендации, UI, цены). Эксперименты скоординированы: если один эксперимент меняет рекомендации, другой — UI, они не должны интерферировать.
Model lifecycle:
- Модели обучаются на Hadoop/Spark кластере (offline)
- Фичи считаются batch-пайплайнами (daily) и streaming (real-time)
- Модели деплоятся через canary: 1% → 5% → 20% → 100%
- Каждая модель мониторится: latency, throughput, метрики качества
- Если метрики падают — automatic rollback (по alert'у вручную, auto при risk)
Источник: Netflix Tech Blog, "System Architectures for Personalization and Recommendation", Netflix Research
#Чек-лист для самопроверки
- Модели вынесены в отдельные model servers или обосновано решение о in-process
- Есть feature store (или хотя бы единый источник фич для обучения и serving)
- Фичи при обучении и serving вычисляются одинаково (training-serving skew исключён)
- Процесс деплоя модели автоматизирован (не ручной SCP model.pkl на сервер)
- Есть стратегия деплоя: shadow / canary / blue-green — не просто "заменил файл"
- A/B эксперимент спланирован: метрики, размер выборки, длительность
- Embedding'ы версионируются и кэшируются (не пересчитывать каждый раз)
- LLM-запросы ограничены по стоимости — есть caching, rate limiting, fallback
- ML observability настроена: data drift, model drift, метрики качества
- Есть runbook для "модель сломалась" — кто, когда, как переобучает
- Security: модели не принимают произвольный пользовательский input без валидации
- Agent'ы имеют guardrails: max steps, human-in-the-loop для опасных действий
#"Не делай так"
#"Jupyter → прод" — скопировал ноутбук в production
Я видел это своими глазами. Data scientist написал красивый ноутбук, export'нул модель как pickle, инженер положил pickle в репозиторий, приложение загружает pickle при старте.
Три месяца всё работало. Потом data scientist изменил формулу расчёта одной фичи в ноутбуке, но забыл сказать инженеру. Модель продолжала получать старые фичи. Качество рекомендаций упало. Никто не заметил, потому что ML observability не было.
В итоге: 3 недели расследования, падение retention на 5%.
#"LLM ответит на всё" — RAG без поиска, Agent без guardrails
Самая модная ошибка 2025 года: поставить LLM перед пользователем без RAG, без ограничений, без мониторинга.
Последствия: LLM выдаёт конфиденциальные данные (потому что они были в контексте), генерирует галлюцинации, Agent случайно удаляет данные пользователя (потому что ему дали DELETE tool и никто не поставил guard).
Лечение: RAG — это обязательная архитектура для LLM в продукте. Guardrails — не опция, а requirement. Human-in-the-loop для деструктивных операций.
#"Одна модель на всё" — монолитная ML-архитектура
«Давайте обучим одну большую модель, которая будет делать всё: рекомендации, поиск, ранжирование, классификацию».
Это ML-аналог big ball of mud. Огромная модель, которую невозможно отлаживать, её фичи переплетены, latency страдает, и замена одной функции требует переобучения всего.
Лучше: несколько специализированных моделей, каждая со своей ответственностью. Candidate generation отдельно, ranking отдельно, классификация отдельно. Как микросервисы в мире ML.
#"Модель обновили — метрики выросли" — только offline
Самообман: новая модель показала accuracy +3% на offline тесте. Катим в продакшен. Online метрики упали. Почему? Offline тест — это исторические данные. Online — реальные пользователи с реальным поведением, которое меняется в ответ на рекомендации модели (feedback loop).
Лечение: offline метрики — это sanity check, не более. Решение о rollout — по online A/B тесту.
#Ключевые выводы
-
ML в веб-архитектуре — это инфраструктурная задача, не data science. Самая сложная часть — не обучить модель, а встроить её в систему так, чтобы она работала стабильно, быстро и предсказуемо. Feature store, model serving, deployment pipeline, observability — вот настоящая работа.
-
Training-serving skew убивает модели незаметно. Если фичи при обучении и в продакшене различаются — модель будет давать неправильные предсказания, и вы узнаете об этом от пользователей, а не от мониторинга. Feature store с единым definition — ваш главный щит.
-
Embedding'и и vector search — это новый SQL. Semantic search стал базовым строительным блоком. Любое веб-приложение, которое работает с текстом или контентом, через год будет использовать векторный поиск. pgvector — идеальный entry point.
-
LLM в продукте — это RAG + guardrails, а не "чат на главной". RAG даёт актуальность и контроль над данными. Guardrails защищают от галлюцинаций и опасных действий. Agent'ы — мощный паттерн, но каждый tool — это риск, и human-in-the-loop обязателен для чувствительных операций.
-
ML observability — не опция, а обязательное условие. Data drift, model drift, quality metrics — всё это должно быть настроено до того, как модель попадёт под нагрузку. Единственная вещь хуже, чем сломанная модель — это сломанная модель, о которой никто не знает.