Por que o banco de dados do seu bot secretamente controla tudo
O primeiro bot “bem-sucedido” que lancei em 2018 falhou não porque o modelo era ruim, ou o código era lento, mas porque o banco de dados estava uma bagunça.
Os usuários recebiam respostas destinadas a outra pessoa. As sessões “esqueciam” aleatoriamente quem você era. As análises eram inúteis.
Tudo porque tratei o DB como uma reflexão tardia.
Se você está criando bots para usuários reais, o banco de dados é a espinha dorsal. Não o modelo. Não o gateway. O modelo de dados.
Em 2026, temos LLMs mais legais e frameworks mais brilhantes, mas a mesma dor de antes:
- “Onde armazeno o estado da conversa?”
- “Como mantenho as sessões separadas?”
- “O que eu registro sem estourar o armazenamento?”
Deixe-me explicar como eu projeto bancos de dados para bots que realmente funcionam em produção, e não apenas em demonstrações.
Entidades principais: usuários, sessões, mensagens
Todo bot que eu lanço começa com três tabelas. Os nomes mudam, as ideias não:
- users — quem está falando com você
- sessions — um período de conversa relacionada
- messages — o que foi dito, em ordem
users
Eu trato os usuários como “quem esta plataforma acha que você é”, não “o conceito filosófico de uma pessoa”.
users - id (pk) - external_id (único, por exemplo, slack_user_id, telefone) - platform (slack | web | whatsapp | etc.) - created_at - deleted_at (nulo)
Um usuário do Slack e um usuário do WhatsApp podem ser a mesma pessoa, mas eu não complico “identidade global” até realmente precisar.
Se você tentar resolver a identidade perfeitamente no dia um, vai ficar paralisado.
sessions
Uma sessão é “um fio de conversa coerente.” Para um bot de suporte, pode começar quando alguém faz uma pergunta e terminar após 30 minutos de silêncio ou quando um agente fecha o ticket.
sessions - id (pk) - user_id (fk users.id) - platform (novamente, slack/web/etc.) - status (ativo | fechado) - started_at - ended_at (nulo) - metadata (jsonb) -- por exemplo, tópico, channel_id, ticket_id
Eu sempre armazeno metadata como JSON para informações específicas da plataforma que não quero que vazem para o esquema central.
Exemplo: channel_id do Slack, browser_session_id da web, o que for.
messages
Mensagens são a única fonte de verdade para “o que realmente aconteceu.”
messages - id (pk) - session_id (fk sessions.id) - sender_type (user | bot | system) - sender_id (nulo, fk users.id para usuário, talvez bot_id mais tarde) - external_message_id (nulo, por exemplo, slack_ts) - content (texto) - content_type (texto | json | rico) - created_at - raw_payload (jsonb, nulo)
Duas razões pelas quais esta tabela é importante:
- Depuração: quando alguém diz “o bot ficou louco”, você precisa de uma transcrição exata.
- Treinamento: você vai querer conversas reais para ajustes finos ou alterações de prompt.
Para um bot que lida com ~50 mil mensagens/dia (um que rodamos no Postgres 15 na AWS RDS),
essa estrutura se manteve boa com indexação adequada em session_id e created_at.
Consultar as últimas 50 mensagens de uma sessão ainda é barato.
Onde colocar o “estado” do bot para não te prejudicar
O maior erro de design que vejo: entulhar o estado em todo lugar. Redis. JWTs. Colunas JSON aleatórias. Oculto em strings de prompt.
Então, ninguém sabe qual estado é o verdadeiro.
Estado de curto prazo: armazene em cache
Coisas que você precisa apenas durante a interação atual, e que pode reconstruir do zero, vão para um armazenamento rápido:
- Redis,
KeyDB, ou armazenamento em memória para chaves efêmeras - Expiração em minutos, não dias
Exemplos de chaves que realmente uso:
session_state:{session_id} -> json, ttl=30m
rate_limit:{user_id} -> contadores, ttl=1h
otp:{phone} -> código, ttl=5m
Se o Redis falhar, seu bot pode esquecer onde estava em um formulário. Irritante, mas não catastrófico.
Esse é o tipo certo de coisa para colocar ali.
Estado persistente: use banco de dados
Qualquer coisa que você se arrepender de perder vai para um banco de dados real:
- Progresso de integração
- Flags de recursos por usuário
- Fluxos de trabalho de longo prazo (“aplicação de empréstimo #1234 etapa 3/7”)
Eu geralmente mantenho isso em uma tabela separada em vez de inchar sessions:
conversation_state - id (pk) - session_id (fk sessions.id, único) - state_name (texto) -- por exemplo, "verificar_email", "coletar_endereço" - data (jsonb) -- estado estruturado arbitrário - updated_at
Sim, JSON. Não, não para tudo. Mas para o estado do fluxo do bot, JSON é bom. Ele muda com frequência, e você é o único consumidor.
Não esconda o estado em JWTs
“`html
Armazenar a lógica da sessão em declarações JWT é tentador. Também é assim que você obtém bugs onde vários clientes discordam sobre a realidade.
Eu mantenho JWTs para autenticação, e só isso. O estado vive no banco de dados ou no cache.
Registro, análise e não se afogar em dados
Bots geram uma quantidade absurda de dados. Se você registrar cada token para cada requisição e mantê-lo para sempre, sua conta de infraestrutura vai te lembrar mensalmente.
Separe “logs de runtime” e “dados de análise”
Eu trato-os como duas preocupações diferentes:
- Logs de runtime: para depuração. Retenção curta (7–30 dias).
Envie-os para algo como Loki, Elasticsearch ou CloudWatch. - Dados de análise: estruturados, consultáveis, a longo prazo. Vivem em SQL ou em um armazém.
Para análise, gosto de um esquema ou banco de dados separado:
conversation_metrics - id (pk) - session_id - user_id - messages_user_count - messages_bot_count - started_at - ended_at - first_response_ms - resolved (bool)
Você computa isso através de um trabalho que escaneia messages todas as noites ou via streaming.
Em um bot de cliente, ativamos a agregação noturna em março de 2025 e cortamos pela metade as perguntas de suporte “o que está acontecendo?” porque conseguimos ver realmente os pontos de desistência.
Logs de prompt e modelo
Se o seu bot usa LLMs, mantenha um registro do que você enviou e do que recebeu de volta, com um apontador para a mensagem/sessão.
llm_calls - id (pk) - session_id - message_id (fk messages.id, nullable) - provider (openai | anthropic | local) - model - prompt_tokens - completion_tokens - cost_usd - request_payload (jsonb) -- redacted - response_payload (jsonb) -- redacted - created_at
Duas notas importantes:
- Redija segredos e PII antes de persistir logs.
- Defina uma política de retenção. Você não precisa de 3 anos de prompts brutos para 99% dos bots.
SQL vs NoSQL vs armazéns vetoriais (e como eu os misturo)
Versão curta: Eu padrão para Postgres. Vou adicionar outros armazéns quando a dor for real, não hipotética.
Relacional (Postgres, MySQL)
Perfeito para:
- Usuários, sessões, mensagens, estado
- Relatórios, joins, migrações
- Consistência forte para “quem disse o quê e quando”
Postgres com jsonb lhe dá flexibilidade suficiente para evitar criar cinco bancos de dados diferentes muito cedo.
Cache (Redis)
Use isso para:
- Limites de taxa
- Estado de sessão efêmero
- Tabelas de pesquisa pequenas e quentes (flags de configuração, toggles de recurso)
Armazém vetorial
Se o seu bot faz geração aumentada por recuperação (RAG), você vai querer embeddings e busca por similaridade.
Na prática, vejo três padrões:
- Postgres + pgvector
- Serviços dedicados como Pinecone, Weaviate, Qdrant
- Opções específicas de nuvem como Aurora + pgvector, AlloyDB, etc.
Eu me apoio no pgvector até:
- Embeddings > ~5–10 milhões de linhas ou
- Requisitos de latência de consulta se tornarem apertados (< 50ms) com tráfego intenso
Esboço do esquema:
documents - id (pk) - source (kb | ticket | faq) - source_id - content - metadata (jsonb) - created_at document_embeddings - id (pk) - document_id (fk documents.id) - embedding vector(1536) - created_at
Mantenha o “o que” (documentos) separado do “como buscamos isso” (embeddings).
Você vai mudar de modelo; não quer que isso esteja misturado com seu conteúdo central.
FAQ: perguntas sobre esquema que continuo recebendo
P: Devo armazenar mensagens completas no DB ou apenas resumos?
Armazene mensagens completas, pelo menos por 30–90 dias. Resumos são ótimos para busca e análise, mas quando algo quebra, você quer o texto bruto.
Se você está preocupado com armazenamento, defina uma política de retenção e archive mensagens antigas para um armazenamento mais barato.
P: Como eu lido com bots multi-tenant (muitos clientes)?
Adicione um tenant_id em todos os lugares que importam: users, sessions, messages, conversation_state.
Indexe. Cada consulta que toca em dados de usuário deve filtrar por tenant_id.
Se você precisar de isolamento mais rigoroso depois, pode dividir inquilinos entre esquemas ou bancos de dados, mas comece com uma coluna de inquilino sólida.
P: Onde devo armazenar arquivos (imagens, PDFs) que os usuários enviam?
Não no banco de dados. Armazene-os no S3/GCS/Blob storage e mantenha referências:
attachments - id (pk) - message_id (fk messages.id) - file_url - file_type - size_bytes - created_at
Se você precisar executar OCR ou embedding neles, armazene essa saída em tabelas separadas vinculadas de volta via attachment_id.
“`
🕒 Published: