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 médio dos últimos 90 dias, número de compras, canal preferido, estágio no funil) é calculada de um jeito no notebook do cientista de dados e de outro jeito no código que roda em produção. Resultado: o modelo aprende com um número e recebe outro na inferência, a personalização erra e ninguém entende por quê. Esse descompasso tem nome, training-serving skew, e a ferramenta que existe justamente para eliminá-lo é o feature store. Este artigo mostra o que é um feature store, os quatro problemas que ele resolve, a diferença entre features batch e online, como garantir consistência treino/inferência com um exemplo real e como levar isso para produção sem montar uma plataforma gigante.

2026-07-04 / IA Aplicada / 12 min

01

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

Um feature store é 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 inferência. Em vez de cada serviço recalcular "quantas compras esse cliente fez" com sua própria query, todos consomem o mesmo valor, da mesma fonte, com a mesma definição. Em personalização de atendimento isso é crítico: a decisão de qual mensagem enviar, se oferece desconto, se prioriza na fila ou se já transfere para humano depende de features do cliente que precisam estar certas e disponíveis em milissegundos.

A dor aparece quando o mesmo conceito vive em três lugares: na query SQL do relatório, no notebook de treino e no endpoint de produção. As três divergem com o tempo, e a personalização vira um jogo de adivinhação. O feature store existe para que "ticket médio 90 dias" tenha UMA definição, calculada uma vez, servida para todos os consumidores.

02

Os quatro problemas que ele resolve

Feature store não é 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
Consistência treino/inferênciaA feature é calculada num script no treino e reescrita no código do endpoint; as duas divergemA mesma transformação gera o dado de treino e o de inferência; skew eliminado por construção
Latência na conversaO bot faz JOINs pesados em tempo real e estoura o SLA da respostaA feature já está pré-calculada e servida do online store em poucos milissegundos
Reuso entre modelosCada projeto reimplementa "ticket médio" do zero, com pequenas diferençasA feature é 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 informação do futuroA materialização respeita o timestamp do evento e usa só o que era conhecido naquele instante

O quarto ponto é o mais silencioso e o mais perigoso: point-in-time correctness. Se você treina um modelo de próxima melhor ação usando o ticket médio ATUAL do cliente para rotular conversas de três meses atrás, o modelo aprende com informação que não existia naquele momento. Ele parece ótimo no backtest e falha em produção. Um feature store sério guarda o histórico 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) lê 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 último valor de cada feature por chave (o cliente) com latência baixíssima, para a inferência 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 "Transformação" é a MESMA para os dois caminhos. Se o código 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 histórico completo com timestamp (para treino e point-in-time joins); o online store guarda só o valor mais recente por chave (para servir rápido).

04

Definindo features com uma transformação única

O antídoto para o training-serving skew é nunca escrever a lógica da feature duas vezes. Você define a transformação uma vez, como função pura sobre eventos, e chama a mesma função para materializar o offline e para atualizar o online. Abaixo, um exemplo enxuto: features de atendimento derivadas do histórico 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 parâmetro asOf é o que garante point-in-time correctness. Para servir online, você chama computeFeatures(events) e o asOf padrão é agora. Para gerar dado de treino, você chama computeFeatures(events, timestampDoRotulo), e a função filtra apenas os eventos que existiam naquele instante. Mesmo código, dois usos, zero skew.

05

Servindo online: o online store de baixa latência

Na conversa, o bot não pode fazer scan de eventos: ele lê o valor já pronto. Um job de materialização 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 inferência, uma única 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 código de decisão lê exatamente as mesmas features que o modelo de treino viu, com os mesmos nomes e a mesma semântica. Se amanhã você trocar a regra por um modelo, ele consome getOnlineFeatures sem reimplementar nada. E se o cliente não tiver valor materializado (cold start), o código trata o null com um fallback explícito, em vez de estourar.

06

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

Montar o dataset de treino é onde a maioria dos times vaza informação do futuro. Você tem uma lista de rótulos (exemplo: "essa conversa acabou em venda?") com seus timestamps, e precisa anexar as features como elas eram naquele instante, não como são hoje. Fazer isso errado, pegando o valor atual, infla a métrica no backtest e derruba o modelo em produção.

  1. Parta dos rótulos: cada linha tem customer_id e o timestamp do evento que você quer prever (o instante da conversa, não o de hoje).
  2. Para cada rótulo, chame computeFeatures(events, timestampDoRotulo): a janela e o filtro asOf garantem que só eventos anteriores ao rótulo entram.
  3. Junte features e rótulo numa única linha do dataset: agora cada exemplo carrega o estado do cliente como era antes da decisão.
  4. Materialize o offline store em Parquet particionado por data, para reprodutibilidade e para reprocessar quando a definição de uma feature mudar.
  5. Treine com esse dataset: o modelo aprende com a mesma computeFeatures que servirá online, só que com asOf no passado. Consistência de ponta a ponta.

Esse cuidado é o que separa um número bonito de backtest de um modelo que funciona. Se o offline e o online usam a mesma transformação e o treino respeita o point-in-time, o valor que o modelo viu no treino e o valor que ele recebe na conversa são, por construção, a mesma coisa.

07

Levando para produção sem overengineering

Não é preciso adotar uma plataforma pesada no dia um. Um feature store pragmático para personalização de atendimento cabe em poucas peças, e você só cresce quando a dor justificar.

  • Registro de features versionado: um arquivo (como o features.js do exemplo) que é a fonte única de verdade das definições, revisado em pull request.
  • Online store: Redis ou outro KV rápido, com o último valor por chave e TTL para dado que expira. Latência 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 materialização: batch agendado para features de janela longa e atualização reativa (via evento) para as que precisam ser frescas na conversa.
  • Monitoramento de skew e frescor: alerta quando a distribuição online diverge do treino e quando a feature de um cliente ficou velha demais para ser confiável.

Comece pelo registro único e pela transformação compartilhada, que já matam o skew, que é a causa raiz da maioria das falhas de personalização. Redis e Parquet resolvem serving e treino. Framework dedicado (Feast e afins) só quando o número 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?

Não no começo. O que define um feature store não é a ferramenta, é a disciplina: uma definição única de cada feature, a mesma transformação no treino e na inferência, e point-in-time correctness no dataset. Dá para atender esses três pontos com um arquivo de definições versionado, Redis para o online e Parquet para o offline. Um framework dedicado como o Feast passa a compensar quando você tem muitas features, vários times e precisa de catálogo, controle de acesso e materialização gerenciada. Antes disso, ele adiciona mais complexidade do que valor.

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

Training-serving skew é a divergência entre o valor de uma feature no treino e o valor da mesma feature na inferência, geralmente porque foram calculados por códigos diferentes. O modelo aprende com um número e recebe outro em produção, então a qualidade cai sem erro aparente. O feature store elimina isso ao forçar que a MESMA função de transformação gere os dois valores: no exemplo do artigo, computeFeatures roda igual no batch de treino e no update online, mudando apenas o parâmetro asOf. Se a lógica vive em um só lugar, os dois lados não têm como divergir.

Como garanto que o treino não vaza informação do futuro?

Com point-in-time correctness. Ao montar o dataset, para cada rótulo você anexa as features como elas eram no timestamp daquele evento, não como são hoje. Na prática, isso é chamar a transformação com asOf igual ao instante do rótulo, para que a janela filtre apenas eventos anteriores. Se você usa o valor atual para rotular o passado, o backtest fica otimista demais e o modelo decepciona em produção. O offline store com histórico e timestamp é o que torna esse join temporal possível de forma reprodutível.

Uma definição, dois planos, zero skew

Personalizar atendimento sem feature store é apostar que três cópias da mesma feature vão concordar para sempre, e elas nunca concordam. Posso desenhar e implementar um feature store pragmático para o seu atendimento: registro único de features, serving online de baixa latência, treino com point-in-time correctness e monitoramento de skew, sem montar uma plataforma que você ainda não precisa.