Blog

Feature store para personalização de atendimento

Todo time que tenta personalizar atendimento esbarra no mesmo muro: a feature que o bot precisa na hora da conversa (ticket medio dos ultimos 90 dias, numero de compras, canal preferido, estagio no funil) e calculada de um jeito no notebook do cientista de dados e de outro jeito no codigo que roda em producao. Resultado: o modelo aprende com um numero e recebe outro na inferencia, a personalizacao erra e ninguem entende por que. Esse descompasso tem nome, training-serving skew, e a ferramenta que existe justamente para elimina-lo e o feature store. Este artigo mostra o que e um feature store, os quatro problemas que ele resolve, a diferenca entre features batch e online, como garantir consistencia treino/inferencia com um exemplo real e como levar isso para producao sem montar uma plataforma gigante.

2026-07-04 / IA Aplicada / 12 min

01

O que e um feature store e por que atendimento precisa dele

Um feature store e uma camada central que calcula, versiona, armazena e serve as features (os sinais de entrada de um modelo) de forma consistente entre o treino e a inferencia. Em vez de cada servico recalcular "quantas compras esse cliente fez" com sua propria query, todos consomem o mesmo valor, da mesma fonte, com a mesma definicao. Em personalizacao de atendimento isso e critico: a decisao de qual mensagem enviar, se oferece desconto, se prioriza na fila ou se ja transfere para humano depende de features do cliente que precisam estar certas e disponiveis em milissegundos.

A dor aparece quando o mesmo conceito vive em tres lugares: na query SQL do relatorio, no notebook de treino e no endpoint de producao. As tres divergem com o tempo, e a personalizacao vira um jogo de adivinhacao. O feature store existe para que "ticket medio 90 dias" tenha UMA definicao, calculada uma vez, servida para todos os consumidores.

02

Os quatro problemas que ele resolve

Feature store nao e hype de MLOps: cada capacidade responde a um problema concreto que aparece em qualquer time que personaliza atendimento com dados.

ProblemaSem feature storeCom feature store
Consistencia treino/inferenciaA feature e calculada num script no treino e reescrita no codigo do endpoint; as duas divergemA mesma transformacao gera o dado de treino e o de inferencia; skew eliminado por construcao
Latencia na conversaO bot faz JOINs pesados em tempo real e estoura o SLA da respostaA feature ja esta pre-calculada e servida do online store em poucos milissegundos
Reuso entre modelosCada projeto reimplementa "ticket medio" do zero, com pequenas diferencasA feature e definida uma vez no registro e reusada por qualquer modelo ou regra
Point-in-time correctnessO treino usa o valor de hoje para prever o passado e vaza informacao do futuroA materializacao respeita o timestamp do evento e usa so o que era conhecido naquele instante

O quarto ponto e o mais silencioso e o mais perigoso: point-in-time correctness. Se voce treina um modelo de proxima melhor acao usando o ticket medio ATUAL do cliente para rotular conversas de tres meses atras, o modelo aprende com informacao que nao existia naquele momento. Ele parece otimo no backtest e falha em producao. Um feature store serio guarda o historico com timestamp e faz o join respeitando o tempo.

03

Features batch x online: os dois planos do mesmo dado

A arquitetura de um feature store tem dois caminhos para o mesmo dado. O plano offline (batch) le as fontes brutas, calcula as features em janelas e materializa o resultado, usado para treinar modelos e para popular o plano online. O plano online serve o ultimo valor de cada feature por chave (o cliente) com latencia baixissima, para a inferencia em tempo real na conversa.

Fontes brutas (eventos, pedidos, tickets)
        |
        v
  +-----------------------------+
  |  Transformacao (uma so vez) |
  +-----------------------------+
        |                     |
        v                     v
  Offline store          Online store
  (Parquet / DW)         (Redis / KV)
        |                     |
        v                     v
  Treino do modelo      Inferencia na conversa
  (historico + PIT)     (ultimo valor, < 10ms)

A regra de ouro: a caixa "Transformacao" e a MESMA para os dois caminhos. Se o codigo que gera a coluna de treino for diferente do que popula o Redis, o skew volta pela porta dos fundos. O offline store guarda o historico completo com timestamp (para treino e point-in-time joins); o online store guarda so o valor mais recente por chave (para servir rapido).

04

Definindo features com uma transformacao unica

O antidoto para o training-serving skew e nunca escrever a logica da feature duas vezes. Voce define a transformacao uma vez, como funcao pura sobre eventos, e chama a mesma funcao para materializar o offline e para atualizar o online. Abaixo, um exemplo enxuto: features de atendimento derivadas do historico de pedidos e conversas de um cliente.

// features.js - a UNICA definicao de cada feature.
// A mesma funcao roda no batch (treino/materializacao) e no online (update).

// Cada feature declara: nome, janela e como computar a partir dos eventos.
export const featureDefs = {
  ticket_medio_90d: {
    window_days: 90,
    compute: (events) => {
      const compras = events.filter((e) => e.type === 'order' && e.value > 0);
      if (compras.length === 0) return 0;
      const soma = compras.reduce((acc, e) => acc + e.value, 0);
      return Number((soma / compras.length).toFixed(2));
    },
  },
  num_compras_90d: {
    window_days: 90,
    compute: (events) => events.filter((e) => e.type === 'order').length,
  },
  canal_preferido: {
    window_days: 180,
    compute: (events) => {
      const msgs = events.filter((e) => e.type === 'message');
      const contagem = {};
      for (const m of msgs) contagem[m.channel] = (contagem[m.channel] || 0) + 1;
      const [canal] = Object.entries(contagem).sort((a, b) => b[1] - a[1])[0] || ['whatsapp'];
      return canal;
    },
  },
};

// Aplica TODAS as features a um cliente, respeitando a janela e o instante 'asOf'.
// asOf = agora  -> valor online (inferencia).
// asOf = timestamp do evento historico -> valor point-in-time (treino).
export function computeFeatures(events, asOf = Date.now()) {
  const out = { computed_at: asOf };
  for (const [name, def] of Object.entries(featureDefs)) {
    const inicio = asOf - def.window_days * 24 * 60 * 60 * 1000;
    const janela = events.filter((e) => e.ts <= asOf && e.ts >= inicio);
    out[name] = def.compute(janela);
  }
  return out;
}

O parametro asOf e o que garante point-in-time correctness. Para servir online, voce chama computeFeatures(events) e o asOf padrao e agora. Para gerar dado de treino, voce chama computeFeatures(events, timestampDoRotulo), e a funcao filtra apenas os eventos que existiam naquele instante. Mesmo codigo, dois usos, zero skew.

05

Servindo online: o online store de baixa latencia

Na conversa, o bot nao pode fazer scan de eventos: ele le o valor ja pronto. Um job de materializacao roda periodicamente (ou reage a eventos), chama computeFeatures com asOf igual a agora e escreve o resultado no online store, indexado pela chave do cliente. Na inferencia, uma unica leitura por chave devolve o vetor de features em poucos milissegundos.

import { createClient } from 'redis';
import { computeFeatures } from './features.js';

const redis = createClient();
await redis.connect();

const key = (customerId) => `features:${customerId}`;

// MATERIALIZACAO (batch/near-real-time): recalcula e grava o ultimo valor.
export async function materialize(customerId, events) {
  const features = computeFeatures(events); // asOf = agora
  await redis.set(key(customerId), JSON.stringify(features), { EX: 60 * 60 * 24 });
  return features;
}

// SERVING (inferencia na conversa): uma leitura por chave, < 10ms.
export async function getOnlineFeatures(customerId) {
  const raw = await redis.get(key(customerId));
  if (!raw) return null; // cold start: cliente sem historico materializado
  return JSON.parse(raw);
}

// Uso no fluxo do bot, antes de decidir a proxima acao.
const feats = await getOnlineFeatures('c_8123');
if (feats && feats.ticket_medio_90d > 500 && feats.num_compras_90d >= 3) {
  // cliente de alto valor e recorrente: prioriza fila e oferece atendimento VIP
}

Repare que o codigo de decisao le exatamente as mesmas features que o modelo de treino viu, com os mesmos nomes e a mesma semantica. Se amanha voce trocar a regra por um modelo, ele consome getOnlineFeatures sem reimplementar nada. E se o cliente nao tiver valor materializado (cold start), o codigo trata o null com um fallback explicito, em vez de estourar.

06

Evitando o vazamento: point-in-time joins no treino

Montar o dataset de treino e onde a maioria dos times vaza informacao do futuro. Voce tem uma lista de rotulos (exemplo: "essa conversa acabou em venda?") com seus timestamps, e precisa anexar as features como elas eram naquele instante, nao como sao hoje. Fazer isso errado, pegando o valor atual, infla a metrica no backtest e derruba o modelo em producao.

  1. Parta dos rotulos: cada linha tem customer_id e o timestamp do evento que voce quer prever (o instante da conversa, nao o de hoje).
  2. Para cada rotulo, chame computeFeatures(events, timestampDoRotulo): a janela e o filtro asOf garantem que so eventos anteriores ao rotulo entram.
  3. Junte features e rotulo numa unica linha do dataset: agora cada exemplo carrega o estado do cliente como era antes da decisao.
  4. Materialize o offline store em Parquet particionado por data, para reprodutibilidade e para reprocessar quando a definicao de uma feature mudar.
  5. Treine com esse dataset: o modelo aprende com a mesma computeFeatures que servira online, so que com asOf no passado. Consistencia de ponta a ponta.

Esse cuidado e o que separa um numero bonito de backtest de um modelo que funciona. Se o offline e o online usam a mesma transformacao e o treino respeita o point-in-time, o valor que o modelo viu no treino e o valor que ele recebe na conversa sao, por construcao, a mesma coisa.

07

Levando para producao sem overengineering

Nao e preciso adotar uma plataforma pesada no dia um. Um feature store pragmatico para personalizacao de atendimento cabe em poucas pecas, e voce so cresce quando a dor justificar.

  • Registro de features versionado: um arquivo (como o features.js do exemplo) que e a fonte unica de verdade das definicoes, revisado em pull request.
  • Online store: Redis ou outro KV rapido, com o ultimo valor por chave e TTL para dado que expira. Latencia de leitura em milissegundos.
  • Offline store: Parquet no object storage ou tabelas no data warehouse, com timestamp para point-in-time joins e reprocessamento.
  • Job de materializacao: batch agendado para features de janela longa e atualizacao reativa (via evento) para as que precisam ser frescas na conversa.
  • Monitoramento de skew e frescor: alerta quando a distribuicao online diverge do treino e quando a feature de um cliente ficou velha demais para ser confiavel.

Comece pelo registro unico e pela transformacao compartilhada, que ja matam o skew, que e a causa raiz da maioria das falhas de personalizacao. Redis e Parquet resolvem serving e treino. Framework dedicado (Feast e afins) so quando o numero de features, times e modelos crescer a ponto de o controle manual doer mais do que a plataforma.

FAQ

Perguntas frequentes

Preciso de um framework como o Feast para ter um feature store?

Nao no comeco. O que define um feature store nao e a ferramenta, e a disciplina: uma definicao unica de cada feature, a mesma transformacao no treino e na inferencia, e point-in-time correctness no dataset. Da para atender esses tres pontos com um arquivo de definicoes versionado, Redis para o online e Parquet para o offline. Um framework dedicado como o Feast passa a compensar quando voce tem muitas features, varios times e precisa de catalogo, controle de acesso e materializacao gerenciada. Antes disso, ele adiciona mais complexidade do que valor.

O que exatamente e training-serving skew e como o feature store elimina?

Training-serving skew e a divergencia entre o valor de uma feature no treino e o valor da mesma feature na inferencia, geralmente porque foram calculados por codigos diferentes. O modelo aprende com um numero e recebe outro em producao, entao a qualidade cai sem erro aparente. O feature store elimina isso ao forcar que a MESMA funcao de transformacao gere os dois valores: no exemplo do artigo, computeFeatures roda igual no batch de treino e no update online, mudando apenas o parametro asOf. Se a logica vive em um so lugar, os dois lados nao tem como divergir.

Como garanto que o treino nao vaza informacao do futuro?

Com point-in-time correctness. Ao montar o dataset, para cada rotulo voce anexa as features como elas eram no timestamp daquele evento, nao como sao hoje. Na pratica, isso e chamar a transformacao com asOf igual ao instante do rotulo, para que a janela filtre apenas eventos anteriores. Se voce usa o valor atual para rotular o passado, o backtest fica otimista demais e o modelo decepciona em producao. O offline store com historico e timestamp e o que torna esse join temporal possivel de forma reprodutivel.

Uma definicao, dois planos, zero skew

Personalizar atendimento sem feature store e apostar que tres copias da mesma feature vao concordar para sempre, e elas nunca concordam. Posso desenhar e implementar um feature store pragmatico para o seu atendimento: registro unico de features, serving online de baixa latencia, treino com point-in-time correctness e monitoramento de skew, sem montar uma plataforma que voce ainda nao precisa.