Inteligência artificial, sem ruído.
Tutoriais7 min

Como monitorar drift de embeddings em pipelines de LLM em produção

Duas técnicas práticas — classificador de domínio e distância de centroide — para detectar drift em embeddings.

Como monitorar drift de embeddings em pipelines de LLM em produção

Por que monitorar drift de embeddings virou obrigação em produção

Quando um modelo de linguagem (LLM) entra em produção, a história está longe de terminar. O comportamento dos usuários evolui, e os dados que o modelo consome também — tipicamente codificados em representações numéricas chamadas embeddings. Rastrear o chamado drift de embeddings é o que permite saber quando um modelo implantado precisa de atualização. O problema: métricas tradicionais de detecção de drift, feitas para dados tabulares, costumam falhar quando aplicadas a embeddings de alta dimensionalidade.

Neste tutorial você vai aprender duas técnicas práticas para detectar esse drift — detecção baseada em modelo (classificador de domínio) e distância de centroide — com scikit-learn sobre embeddings simulados e, em seguida, sobre embeddings reais gerados com SentenceTransformer via Scikit-LLM.

✅ O que você ganha

  • Detectar mudanças de tema nas consultas dos usuários antes que degradem a qualidade das respostas
  • Gatilho automático para retreinamento ou revisão do pipeline quando o drift passa de um limiar
  • Implementação com stack 100% open-source (numpy, scikit-learn, SentenceTransformer)
  • Duas técnicas com custos diferentes: uma precisa e cara, outra rápida e barata
  • Código pronto para adaptar a qualquer LLM em produção

⚠️ O que você NÃO ganha

  • Não substitui monitoramento de latência, custo ou alucinações — é só drift de distribuição
  • O limiar de alerta precisa ser calibrado para o seu modelo e a variância da sua baseline
  • A técnica de centroide perde nuance ao agregar vetores em um único ponto central

Três técnicas principais de detecção

Antes do código, as três abordagens mais usadas em produção:

  • Detecção baseada em modelo: treinar um classificador binário que aprende a distinguir dados de referência (baseline) de dados novos (produção). Se ele consegue separá-los com facilidade, há drift.
  • Distância de centroide: calcular a distância (geralmente cosseno) entre o centro de massa dos embeddings de referência e o dos embeddings novos. Mais barata, porém menos sensível a mudanças estruturais.
  • Redução de dimensionalidade + testes estatísticos: comprimir com UMAP ou PCA e aplicar testes como Kolmogorov-Smirnov.

Passo 1 — Simular embeddings de 384 dimensões

Primeiro, geramos dois conjuntos: um de referência (baseline) e outro de produção com a média deslocada para simular drift.

import numpy as np

n_samples = 500
n_features = 384

np.random.seed(42)
# Embeddings de referência (baseline)
X_reference = np.random.normal(loc=0.0, scale=1.0, size=(n_samples, n_features))
# Embeddings de produção com média deslocada (simula drift)
X_production = np.random.normal(loc=0.3, scale=1.0, size=(n_samples, n_features))

Passo 2 — Classificador de domínio (detecção baseada em modelo)

Treinamos um classificador leve (Random Forest) para separar baseline (rótulo 0) de produção (rótulo 1). Se o ROC-AUC ficar alto, há drift.

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

y_reference = np.zeros(n_samples)
y_production = np.ones(n_samples)

X_combined = np.vstack((X_reference, X_production))
y_combined = np.hstack((y_reference, y_production))

X_train, X_test, y_train, y_test = train_test_split(
    X_combined, y_combined, test_size=0.3, random_state=42)

drift_classifier = RandomForestClassifier(n_estimators=50, max_depth=5, random_state=42)
drift_classifier.fit(X_train, y_train)

y_pred_proba = drift_classifier.predict_proba(X_test)[:, 1]
roc_auc = roc_auc_score(y_test, y_pred_proba)
print(f"Domain Classifier ROC-AUC Score: {roc_auc:.3f}")

# ROC-AUC ~0.5 = sem drift; perto de 1.0 = drift alto
if roc_auc > 0.65:
    print("ALERT: Significant embedding drift detected! Trigger retraining.")
else:
    print("System stable: Distributions are sufficiently similar.")

No exemplo simulado, o resultado foi ROC-AUC de 0.970, disparando o alerta — esperado, já que deslocamos a média de propósito.

Passo 3 — Distância de centroide (método barato)

Calculamos o vetor médio de cada conjunto e medimos a distância de cosseno entre eles.

from sklearn.metrics.pairwise import cosine_distances

centroid_ref = np.mean(X_reference, axis=0).reshape(1, -1)
centroid_prod = np.mean(X_production, axis=0).reshape(1, -1)

distance = cosine_distances(centroid_ref, centroid_prod)[0][0]
print(f"Centroid Cosine Distance: {distance:.4f}")

threshold = 0.05  # calibre para o seu modelo e baseline
if distance > threshold:
    print("ALERT: Centroid distance exceeded threshold! System drifting.")
else:
    print("System stable: Centroids are aligned.")

Passo 4 — Drift em embeddings reais com Scikit-LLM

Para validar com dados reais, usamos um SentenceTransformer leve (all-MiniLM-L6-v2) para gerar embeddings de dois temas diferentes — suporte de TI versus criptomoedas — e repetimos a detecção.

from sentence_transformers import SentenceTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.metrics.pairwise import cosine_distances
import numpy as np

vectorizer = SentenceTransformer('all-MiniLM-L6-v2')

texts_reference = ["How do I reset my password?", "Where is the billing menu?"] * 100
texts_production = ["The new cryptocurrency system is failing", "How to mint an NFT?"] * 100

X_reference = vectorizer.encode(texts_reference)
X_production = vectorizer.encode(texts_production)

y_reference = np.zeros(len(X_reference))
y_production = np.ones(len(X_production))
X_combined = np.vstack((X_reference, X_production))
y_combined = np.hstack((y_reference, y_production))

X_train, X_test, y_train, y_test = train_test_split(
    X_combined, y_combined, test_size=0.3, random_state=42)
clf = RandomForestClassifier(n_estimators=50, max_depth=5).fit(X_train, y_train)
roc_auc = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])
print(f"ROC-AUC Score: {roc_auc:.3f}")

centroid_ref_st = np.mean(X_reference, axis=0).reshape(1, -1)
centroid_prod_st = np.mean(X_production, axis=0).reshape(1, -1)
distance_st = cosine_distances(centroid_ref_st, centroid_prod_st)[0][0]
print(f"Centroid Cosine Distance: {distance_st:.4f}")

Com dados reais, a distância de cosseno caiu para 0.8719 — ainda alta (os temas são bem diferentes), mas bem mais realista que o 0.9811 do caso puramente aleatório.

Qual técnica usar?

  • Classificador de domínio: mais preciso e capaz de capturar padrões complexos, porém mais caro (treina um modelo a cada verificação).
  • Distância de centroide: muito mais barata, ideal para monitoramento contínuo de alta frequência — mas perde nuance ao resumir tudo a um ponto.

Na prática, muitos times combinam as duas: o centroide roda continuamente como alerta rápido, e o classificador dispara apenas quando o centroide acusa suspeita.

🛠️ Troubleshooting

  • ❌ ROC-AUC sempre perto de 0.5 mesmo com mudança óbvia → o classificador pode estar fraco demais (poucas árvores, profundidade rasa). Aumente n_estimators e max_depth.
  • ❌ Distância de cosseno exageradamente alta → dados aleatórios ou mal normalizados geram valores irreais. Use embeddings reais e normalize os vetores.
  • ❌ Muitos falsos positivos no alerta → o limiar (0.65 ou 0.05) está mal calibrado. Ajuste com base na variância da sua baseline histórica.
  • ❌ Custo alto de recomputar centroides → calcule o centroide incrementalmente ou agende verificações em lote, não por requisição.
  • ❌ Modelo de embeddings mudou e tudo acusa drift → se você trocou o encoder, a baseline precisa ser regenerada; comparar embeddings de modelos diferentes não faz sentido.

FAQ

  • O que é embedding drift? É a mudança gradual na distribuição dos embeddings que o modelo recebe, refletindo que os dados reais se afastaram dos dados de treinamento.
  • Preciso monitorar isso em todo LLM? Sim, se o modelo atende usuários reais. Sem monitoramento, a degradação de qualidade passa despercebida até virar reclamação.
  • Qual a diferença entre drift de dados e drift de conceito? Drift de dados muda a entrada; drift de conceito muda a relação entre entrada e saída. As técnicas aqui focam no primeiro.
  • Posso usar outro modelo de embeddings? Sim. Qualquer SentenceTransformer serve; o importante é manter o mesmo encoder entre baseline e produção.
  • Isso substitui ferramentas como Evidently ou NannyML? Não exatamente — essas ferramentas fazem o mesmo de forma mais completa, mas o código aqui é a base conceitual e funciona sem dependências pesadas.

O panorama maior

À medida que os LLMs se tornam peças permanentes de infraestrutura, monitorar o comportamento dos embeddings em produção deixa de ser um luxo de times avançados e vira higiene básica de engenharia. Quem dominar essa disciplina cedo conseguirá detectar degradação antes do usuário final — uma vantagem competitiva silenciosa, mas decisiva.


Descubra mais sobre noticiAI

Assine para receber nossas notícias mais recentes por e-mail.

R
Sobre o autorRedação Noticiai

Equipe editorial dedicada a explicar inteligência artificial com clareza, independência e contexto.