Engenharia de dados & IA · Human in the loop

Dados que sustentam.
IA com critério humano.

Construo pipelines de dados e sistemas de IA sem tirar as decisões das mãos de quem entende o problema.

Gyovana Santos do PradoSão Paulo, BrasilEngenheira de Dados & IA — Human in the Loop
01 / Trabalho selecionado

Do problema à evidência.

Projetos, escolhas de arquitetura e resultados. O contexto importa tanto quanto a entrega.

Portfólio Vivo

Um portfólio que se atualiza sozinho: um site estático em Astro cujo conteúdo é curado automaticamente a partir de sessões reais de trabalho, através de um pipeline de publicação via MCP.

astro / typescript / python / mcp / claude-code / github-actions · 2026
O problema
Um portfólio desatualiza quando manter o conteúdo vira mais um trabalho.
A decisão
Transformar sessões reais em rascunhos bilíngues, sanitizados e versionados no Git. Sem CMS, banco ou backend.
A evidência
Este site é produzido por esse pipeline: a automação escreve, e um humano autoriza a publicação.
Estudo de caso

O problema: sites pessoais envelhecem porque mantê-los atualizados é um trabalho que ninguém sustenta. A ideia: um hook de fim de sessão avalia cada sessão do Claude Code contra um rubric de relevância; quando o trabalho vale para o portfólio, um LLM o escreve em duas camadas (narrativa + técnica) e dois idiomas (EN/PT), sanitiza detalhes confidenciais de clientes por categoria e commita numa branch drafts. O dono publica com um comando. O site é um build estático — a 'vida' vem do pipeline de conteúdo, não de um servidor rodando. O git é o banco de dados: cada mudança é um commit, com histórico completo e custo de infra zero.

Otimização de Pipeline Crítico (88% Mais Rápido)

Uma tabela materializada estava estourando timeout. Modularização e boas práticas de modelagem cortaram seu tempo de execução em até 88%.

dbt / bigquery / sql / prompt-engineering · 2023
O problema
Uma tabela materializada cresceu até estourar o tempo limite.
A decisão
Quebrar a transformação monolítica em etapas modulares, com boas práticas de modelagem em dbt.
A evidência
Até 88% menos tempo de execução e fim dos timeouts.
Estudo de caso

Uma tabela materializada crítica havia crescido a ponto de estourar timeout. Quebrei a transformação monolítica em passos modulares e apliquei boas práticas de dbt, reduzindo o tempo de execução em até 88% e eliminando os timeouts. Também usei técnicas assistidas por IA (prompt engineering e in-context learning) para acelerar a investigação e validar as refatorações candidatas.

Rotas-SP — rotas de transporte que pensam além do óbvio

Um planejador de rotas de transporte público para São Paulo que gera alternativas estruturalmente diferentes (ir a pé até um terminal, pegar a estação mais distante), classifica quão segura é cada baldeação e usa um LLM para ranquear e explicar as opções conforme a prioridade que o usuário escolhe na hora da consulta).

python / opentripplanner / gtfs / osm / docker / typer / pydantic / esperanto / claude-code
Estudo de caso

O difícil não é traçar uma rota — APIs prontas fazem isso. É revelar a boa alternativa que um app comum nunca mostra. O motor roda um OpenTripPlanner próprio sobre os dados GTFS/OSM de São Paulo e faz um sweep de parâmetros: várias consultas de roteamento com funções de custo diferentes (anti-caminhada, pró-caminhada-menos-baldeação, só-trilhos) cuja união produz candidatas genuinamente diversas. Um índice pré-computado classifica cada baldeação como terminal / estação / rua, e um scorer determinístico por perfil as ordena antes de um LLM reordenar as melhores opções e escrever justificativas em linguagem natural — com a regra estrita de só citar números presentes nos dados. Spec-first: o sistema inteiro foi documentado antes de uma linha de código, então a implementação expôs só divergências pequenas e isoladas (um bug de regex, um orçamento de latência que não sobreviveu ao grafo real — cada uma medida e corrigida). A fase 1 é um CLI com 74 testes; a fase 2 é uma API + PWA reaproveitando o mesmo motor.

Modelagem de Jornada Multi-Desfecho em dbt

Redesenhei um modelo de dados cuja forma original não conseguia representar múltiplos desfechos por jornada de usuário, usando conceitos de modelagem de data warehouse para torná-lo conciso e escalável.

dbt / bigquery / sql
Estudo de caso

O modelo existente assumia um único desfecho por jornada, o que quebrava à medida que o negócio precisava rastrear vários. Identifiquei a limitação estrutural e então propus e implementei um modelo dbt reestruturado, alinhado às regras de negócio — trocando uma forma rígida e difícil de estender por uma que permanece concisa conforme novos desfechos são adicionados.

02 / Diário de engenharia

O raciocínio fica registrado.

Logs reais de trabalho: o que estava em jogo, por que escolhi um caminho e como validei.

21 de 21 registros

  1. Problema resolvido

    Nem toda queda no placar é uma regressão.

    O placar de uma release mostrou nove métricas piores, o que parecia uma regressão. Em vez de reverter, separei os resultados: todas as métricas que caíram eram da rodada do agente ponta a ponta, enquanto busca, carga e checks estavam idênticos. Um rejulgamento às cegas das respostas das duas versões, misturadas e com o mesmo juiz, descartou desvio do juiz, e o agente de referência caiu ainda mais que o novo. A release estava boa; o ruído vinha da execução do agente.

    Resultado: 9 regressões aparentes atribuídas a ruído da rodada do agente, com zero mudança nas métricas de busca; vantagem sobre a referência subiu de +5 para +8 de 20.

    Detalhe técnico

    • Placar v-ant → v-nova: 9 melhoraram, 13 iguais, 9 pioraram. As 9 regressões estão todas no bloco agente-vs-juiz (20 perguntas); métricas de busca (R@10, nDCG, MRR, nDCG híbrida e com re-rank) e checks de dados ficaram idênticos.
    • Controle: o agente de referência, estilo produção, rodou com o mesmo código e banco nas duas rodadas e caiu mais (15→9 aceitas de 20) que o sistema novo (20→17). Se a busca tivesse mudado, as métricas de busca teriam se mexido.
    • Hipótese do juiz descartada: respostas das duas versões foram rejulgadas juntas, às cegas, nos mesmos lotes e com o mesmo juiz; a queda continuou.
    • Variáveis restantes: variância entre execuções de um modelo pequeno e a data do briefing mudando entre as rodadas. As 3 perguntas perdidas foram erros de raciocínio sobre fontes abertas corretamente (data inferida em vez de lida).
    • Piso de ruído: com 20 perguntas, cada uma vale 5 pontos, e a rodada anterior estava no teto de 20/20, só dava para descer.
    • Leitura que vale: comparar dentro da mesma rodada. A vantagem sobre a referência aumentou (+5 → +8 perguntas).
    • Próxima rodada: rodar os agentes das duas versões no mesmo dia e fixar a data do briefing como parâmetro registrado.
    evals · llm-as-judge · regression-analysis · retrieval · release-engineering
  2. Problema resolvido

    Um prompt melhor precisa de evidência.

    Ao revisar um app open source com um assistente de IA no chat, eu queria evidência de que um prompt reescrito era mesmo melhor, e não só uma impressão. Montei uma bancada local de testes: 96 respostas, cada uma gerada de forma isolada para que os casos não se contaminassem, e depois avaliadas às cegas por juízes independentes. A reescrita ajudou claramente o modelo mais forte, sobretudo ao impedir que ele inventasse fatos, e não fez diferença confiável no modelo menor.

    Resultado: O prompt reescrito subiu as respostas sem afirmação inventada de 75% para 92% no Sonnet (teste de sinais p≈0,04), sem ganho confiável no Haiku.

    Detalhe técnico

    Desenho: 2 modelos (Haiku, Sonnet) × 2 variantes de prompt (A base, B reescrita) × 8 casos × 3 repetições = 96 respostas. As fixtures foram geradas chamando o próprio código construtor de contexto do app sobre dados de seed, então o modelo recebeu exatamente o contexto que produção enviaria. Um subagente por resposta, lendo só o prompt e um único arquivo de caso, para eliminar vazamento entre casos. Quatro juízes Opus cegos deram nota de 1 a 5 e marcaram afirmações fora do contexto fornecido.

    Resultados (nota média / ≥4 / sem afirmação inventada): Haiku A 3,75 / 54% / 58%; Haiku B 4,08 / 71% / 62%; Sonnet A 4,54 / 96% / 75%; Sonnet B 4,92 / 100% / 92%. A dispersão entre repetições foi de 0,07 a 0,14, pequena o bastante para separar efeito do prompt de acaso.

    Estatística: pareado por caso e repetição, teste de sinais. Sonnet: B ganhou 10, perdeu 2, empatou 12 (p≈0,04). Haiku: 9/5/10 (p≈0,4), sem suporte. O modelo pesou mais que o prompt: o pior Sonnet superou o melhor Haiku.

    Achados: um erro de contagem se repetiu em 6/6 respostas do Haiku com qualquer prompt, então a correção é mandar o número pronto do servidor em vez de insistir no prompt. Os exemplos da variante B causaram um efeito colateral (4/24 respostas passaram do limite de negritos). Os juízes também expuseram uma inconsistência na fixture da demo.

    Trade-offs/ressalvas: os subagentes recebem o prompt como arquivo, não como system prompt real, e o deploy-alvo roda modelo de outro fornecedor, então o resultado vale para Claude; o próximo passo é uma rodada cross-vendor. ~4,5M de tokens de subagente, quase tudo overhead do harness; uma rodada direta via API com o system prompt verdadeiro seria bem mais barata e fiel.

    prompt-eval · llm-as-judge · subagents · experiment-design · rag
  3. Decisão

    Conversar por voz com um agente de código.

    A ideia era conversar por voz com um agente de código sem usar as mãos, como o modo de voz do ChatGPT: aperta falar, fala, e o agente responde em voz alta. A primeira versão mantinha o agente 'esperando' a próxima frase, o que quebrava quando a sessão ficava parada e ainda exigia um Enter. Em vez de remendar a espera, troquei o mecanismo de entrega: um plugin pequeno dentro de cada sessão envia a fala transcrita como um prompt normal, então nada fica bloqueado e não precisa de Enter.

    Resultado: A fala agora chega a uma sessão parada sem Enter e sem a janela de espera de 240s.

    Detalhe técnico

    Daemon local: VAD fecha a frase no silêncio, Parakeet transcreve no dispositivo, say do macOS fala a resposta (microfone pausado enquanto fala, pra não se ouvir). A v1 entregava a fala por um Stop hook bloqueante (decision: block com a transcrição como reason, espera de até 240s) mais PostToolUse para interrupções durante o trabalho. Falha: com a espera expirada ou a sessão parada, o botão aparecia como 'ouvindo' mas a fala nunca chegava ao agente. v2: um plugin de módulo de hooks do Claude Code (carregado via CLAUDE_CODE_PLUGIN_DIRS) consulta o /pending do daemon a cada 400ms e chama $.prompt.submit, o mesmo caminho de um prompt digitado, válido com a sessão parada. Espera do Stop hook vai a 0. Sessão alvo = a última que respondeu; o widget mostra quantas sessões estão conectadas. Trade-off: polling em toda sessão e dependência de uma API de plugin, contra a abordagem só com hooks, que é sem estado mas limitada por tempo.

    claude-code · plugins · voice · hooks · local-first
  4. Problema resolvido

    A incerteza estava subestimada.

    Um eval estima com que frequência um sistema de IA acerta combinando um conjunto pequeno de casos rotulados por humanos com um conjunto grande pontuado automaticamente. Um revisor de código montou um caso em que a incerteza reportada estava muito errada. A causa: os casos rotulados eram um subconjunto dos pontuados, então as duas médias andavam juntas, e a fórmula as tratava como independentes. Incluir o termo de correlação que faltava corrigiu a estimativa, e agora há um teste que protege o caso.

    Resultado: A variância no caso adversarial do revisor passou de 5/12 (errado) para exatamente 1/12; suíte completa com 662 passando e 1 xfail.

    Detalhe técnico

    Estimador de inferência potencializada por predição (PPI): a variância era calculada como var(f)/N + var(r)/n, o que assume amostra rotulada e amostra pontuada independentes. Neste desenho os casos rotulados são subconjunto dos pontuados, então as duas médias compartilham os mesmos valores de f e covariam. Variância corrigida: var(f)/N + var(r)/n + 2·cov(f_rotulado, r)/N. O caso adversarial do revisor (todos rotulados, y = 1 − f) agora dá exatamente 1/12 em vez de 5/12; três casos-limite ficaram fixados em teste. Os testes antigos continuaram válidos porque neles os f rotulados eram constantes (covariância zero). Na mesma rodada de review, eventos de timeout/interrupção/falha de sink passaram a contar como perda ambígua (cancelar a task não desfaz um write já confirmado), limitando o número de chamadas a um intervalo e devolvendo resposta PARTIAL; só build-failed e write-refused provam perda. Trade-off: intervalos mais largos e honestos em vez de mais estreitos e excessivamente confiantes.

    statistics · evals · prediction-powered-inference · code-review · python
  5. Decisão

    A medição não pode quebrar o que mede.

    Num framework que registra quais conteúdos os agentes de IA consultam, um registro podia carregar um link para uma unidade de conteúdo que parecia válida, mas apontava para o tipo errado de coisa. Em outra parte do mesmo sistema a correção era recusar a requisição. Aqui, o código de registro roda dentro do caminho de quem chama, então recusar deixaria a ferramenta de medição quebrar aquilo que ela mede. A decisão foi manter a linha e descartar só o link inseguro, unificando duas verificações espalhadas numa única regra.

    Resultado: Dois pontos divergentes de escrita de ponteiro unificados numa regra; 552 testes passando, sem novos erros de tipo.

    Detalhe técnico

    Uma função pura unit_pointer(type, key, ontology) ao lado da guarda de formato de record. O ponteiro unit só é gravado quando a chave tem formato de record E a tabela nomeada é igual à tabela declarada na ontologia para aquele tipo de unidade; qualquer outro caso grava NONE e o unit_key continua fiel. Os dois escritores (builder de eventos para ref rows e links de evidência de uso) agora chamam a função; o regex de record-id saiu do registry. Assimetria deliberada com o caminho de leitura: trace/neighbors falham cedo no mismatch antes da query, enquanto o instrumento do lado da escrita degrada com elegância (linha mantida, join retido). Testes: casos da regra pura sem servidor; integração provando que uma chave look-alike em outra tabela gera NONE nas duas tabelas, uma chave real gera ponteiro e um nome de tabela customizado funciona ao vivo. Suíte completa 552 passed, 1 xfailed; contagem de erros do mypy inalterada.

    data-integrity · api-design · surrealdb · observability · python
  6. Decisão

    Limitar o lote por bytes, não por registros.

    Uma rotina de ingestão em massa enviava registros ao banco em lotes de tamanho fixo, e a pergunta em aberto era qual deveria ser o tamanho do lote. Em vez de chutar, rodei um experimento controlado contra um servidor de banco real e descobri que o limite verdadeiro vem dos bytes do payload, não da contagem de registros. Um tamanho de lote que parece seguro só é seguro por acidente do tamanho de cada registro, então a recomendação foi limitar os lotes por bytes.

    Resultado: Lotes fixos de 100+ falharam 5/5 contra o servidor real; o lote limitado por bytes rodou a 601 stmts/s sem nenhum 413.

    Detalhe técnico

    Setup: SurrealDB 3.1.5 real via POST /sql, statements gerados pelo construtor de upsert de produção, corpus de 417 unidades (passagens reais mais sintéticas curtas/médias/longas) com embeddings de 384 dims pré-calculados. Os tetos em bytes foram decididos e registrados antes das rodadas. Cada estratégia rodou com aquecimento mais 5 repetições em ordem rotacionada, banco novo por run e fases separadas (carga inicial, reingestão, rebuild). Um proxy local com limite de 256 KiB simulou um 413 limpo.

    Achados:

    • O limite de corpo é exatamente 1 MiB, encontrado por busca binária com payloads de padding. Com statements de ~10 KB dominados pelo embedding, o teto cai entre 50 e 100 statements por lote. Lotes fixos de 100 e 200 falharam 5/5, e o default atual de 200 estoura com folga.
    • Acima do limite, a rejeição degrada de um 413 legível (1 MiB+1B: 6/6 limpo) para conexão fechada no meio do envio (3 MiB: 6/6 BrokenPipeError). Por isso a estratégia adaptativa 'subdivide só com 413' falhou 5/5 contra o servidor direto. Atrás de um proxy que devolve 413 limpo ela funciona, mas reenvia 1.275 statements e transfere 4x o payload (16,7 MB contra 4,1 MB).
    • O estado parcial é real: um 413 após 20 lotes bem-sucedidos deixou as 400 linhas anteriores persistidas, sem rollback.
    • A idempotência valeu em tudo: hash de conteúdo idêntico entre carga inicial, reingestão e rebuild, em todas as estratégias.

    Decisão: lote limitado por bytes com margem (768 KiB, 75% do limite) foi o mais rápido (0,70s, 601 stmts/s, 6 requests), não teve 413 por construção e foi o único que falha de forma limpa quando um statement sozinho excede o teto (recusa antes de enviar). Lote fixo de 20-50 é uma opção simples aceitável, mas sua segurança é coincidência do tamanho do statement. Limitações declaradas no relatório: corpus pequeno, localhost, concorrência 1, limite default do servidor, então os números não extrapolam, só a forma.

    surrealdb · batching · experiment-design · reliability · http
  7. Decisão

    Duas métricas de citação, lado a lado.

    Antes de implementar uma métrica de analytics que diz o quanto um agente de IA aproveita a evidência que recebe, perguntei se a definição óbvia era a certa. Dois candidatos pareciam equivalentes: a fração de chamadas que geraram uma citação ou a fração da evidência entregue que foi citada. Três subagentes em paralelo examinaram o contrato, simularam 13 cenários e avaliaram como as pessoas leriam cada versão. Seguir qualquer uma das métricas isoladamente levava à ação errada em vários casos, então a recomendação foi mostrar as duas lado a lado, com a divergência entre elas como sinal.

    Resultado: 13 cenários sintéticos calculados; 3 mostraram uma métrica isolada levando à ação errada, o que levou à recomendação de apresentar as duas.

    Detalhe técnico

    Montagem: investigação somente leitura (sem alterar código nem repositório), dividida em três subagentes: (1) contrato e validade: numeradores, denominadores, elegibilidade, deduplicação e limites de observação; (2) matriz de cenários sintéticos com taxas calculadas; (3) avaliação heurística de usabilidade mais protocolo de teste com usuários.

    Candidatos: A = chamadas com ≥1 evidência citada e diretamente atribuída / chamadas elegíveis. B = evidências entregues e citadas / evidências entregues, em três agregações: proporção por chamada, média das proporções por chamada (macro) e proporção agregada de todas as evidências (micro). Elas pesam as chamadas de formas diferentes e não são uma única métrica.

    Achados:

    • O schema atual já sustenta A e todas as variantes de B; falta só o código da métrica (o ramo existente é um stub incondicional).
    • "Entregue" deve ser o vínculo de entrega afirmado pelo consumidor, nunca os refs registrados pelo decorator.
    • Em 3 de 13 cenários, uma métrica isolada leva à ação errada: A esconde 90% da evidência não aproveitada (10 entregues, 1 citada); a variante macro inverte rankings e mascara chamadas mortas; a variante agregada pune ferramentas top-k por aritmética (teto ≈ 1/k), o que poderia sugerir desligar a busca que alimenta tudo.
    • B depende muito mais da ergonomia de atribuição do que A, então A degrada melhor quando a cobertura de atribuição é parcial.

    Regras mantidas: citação ≠ qualidade ou causalidade; atribuição ausente vira intervalo, nunca zero silencioso; sem crédito causal automático da busca para uma abertura posterior; sem fallback de "último evento global".

    Decisão: mostrar as duas, com nomes que deixam o denominador explícito, por ferramenta e etapa; a divergência (A alta, B baixa) diagnostica excesso de resultados. Confiança: média. A avaliação heurística está rotulada como tal, e foi entregue um protocolo de quadrado latino com 15 participantes (8 tarefas com gabarito, 3 armadilhas, critérios pré-registrados). Nenhuma validação com usuários foi alegada.

    metrics · analytics · multi-agent · llm-observability · evaluation-design
  8. Decisão

    Nem todo erro é observável.

    Um usuário de um framework Python open source percebeu que uma função decorada, chamada fora de uma sessão, rodava normalmente mas não deixava rastro, e a dúvida era se a biblioteca deveria avisar. Em vez de adicionar um aviso, rodei três subagentes independentes (diagnóstico opt-in, alternativas de doctor/teste e um crítico adversarial) sobre os mesmos nove cenários, com critérios de consistência definidos antes de qualquer teste. A conclusão: 'esqueceu a sessão' não é um estado observável. Ele é idêntico a uma chamada legítima sem sessão. Logo, nenhum diagnóstico pode afirmar isso, e a menor solução confiável foi documentação mais um teste contra o entrypoint de produção, sem tocar na biblioteca.

    Resultado: Nove cenários × cinco alternativas comparados; a única mudança de biblioteca considerada ficou reduzida a um contador passivo de 8 linhas, ou a nenhuma.

    Detalhe técnico

    Método: investigação somente leitura; protótipos descartáveis em scratchpad; matriz de cenários comum (legítimo sem sessão, esqueceu a sessão, registry/sink incompleto, nunca chamada, persistiu, falha de instrumentação, retorno, exceção, repetição/concorrência). Determinismo verificado com 5 repetições × 3 processos.

    Alternativas: A) tutorial + teste de persistência; B) doctor/check; C1) contador passivo no ramo sem sessão do wrapper (+8 linhas); C2) warn-once; C3) raise em modo dev.

    Achados:

    • C1 preserva identidade de retorno/exceção e é determinístico, mas só separa 'sessão aberta, nada chamado' de 'chamado sem sessão'. C2 não acrescenta informação e fica invisível em pytest verde ou logging em ERROR. C3 impede a tool de rodar e mascara a exceção dela, então foi descartado.
    • A saída do doctor foi idêntica byte a byte para os casos legítimo, esquecido e nunca chamado.
    • O teste de persistência 'natural' (abrir sessão em volta da chamada) passa com a produção cega; só o teste que chama o entrypoint de produção pega o problema.
    • Achados colaterais: 20 chamadas concorrentes com sessão persistiram 5 e descartaram 15 (circuit breaker, por desenho); o contador de drops, com flush a cada 100 escritas, escondia drops iniciais; a doc dizia 'sem sessão ⇒ raise' enquanto o decorator faz pass-through.

    Decisão: manter o wrapper intocado, entregar docs + teste no entrypoint, e deixar C1 como única mudança admissível de biblioteca quando houver um leitor. O relatório separa fatos observados, inferências e propostas, e declara o que os microbenchmarks não provam (escala, onboarding real).

    decision-making · observability · python · testing · adversarial-review
  9. Decisão

    Um debate resolvido por experimento, não por gosto.

    Um debate sobre como as tools devem referenciar conteúdo (objeto estruturado ou string simples "tipo:chave") foi resolvido com um experimento controlado, não com gosto. Dois subagentes construíram o mesmo consumidor isoladamente, um por formato, sobre um contrato comum e fixtures adversariais, e um terceiro auditou a equivalência. Os dois formatos se mostraram igualmente corretos, e o experimento expôs bugs reais na biblioteca de base, que acabou sendo o resultado mais útil.

    Resultado: Os dois formatos passaram todos os casos determinísticos (29/29 e 30/30 + 1 xfail estrito) com 0 refs malformadas em 14 runs de agente; 3 defeitos da biblioteca encontrados e reproduzidos.

    Detalhe técnico

    Desenho: o coordenador fixou primeiro fixtures, contrato funcional e critérios comuns; os implementadores A (objeto JSON) e B (string) trabalharam em diretórios separados sem ler o código um do outro; um auditor rodou casos adversariais. Ambos fixados na mesma revisão da biblioteca (não o checkout local, não uma PR aberta). As fixtures tornaram chave estável, referência tipada e record ID do grafo deliberadamente diferentes, com chaves hostis (:/::, Unicode, espaços, aparência de record ID, chave igual ao nome do tipo, mesma chave em dois tipos, chave composta).

    Camada determinística: 29 e 30 testes passaram, cobrindo round-trip busca→abertura→navegação→citação, refs malformadas/desconhecidas/incompatíveis, fonte removida após a busca, mudança de conteúdo com identidade estável e um vocabulário fechado de erros, para que nenhum vire "não há informação".

    Camada com agente: 7 tarefas × 2 cenários, sessões novas de claude -p, mesmo modelo, ordem alternada, transcripts stream-json. Rodada 1: 6/7 vs 7/7 round-trips, zero refs malformadas ou alteradas em 14 runs, 100% de citações válidas. A única falha foi desistir após buscas vazias (BM25 com AND vs. perguntas em português), não o formato. As replicações com ordem invertida e modelo mais fraco ainda estavam pendentes ao fim da sessão, e o relatório as marca como tal.

    Achados da biblioteca, não do formato: o unit do link de evidência era deduzido da chave por regex (chave com cara de record ID gerava ponteiro para o registro errado); cite e validate_claims divergiam sobre candidato ser citável; formas de ref aceitas eram inconsistentes entre três pontos de entrada.

    Recomendação: sem base experimental para preferir um; decidir pelo que o experimento não mede (a biblioteca já fala tipo:chave; o objeto permite schema fechado) e corrigir a inferência de qualquer forma.

    llm-agents · experiment-design · api-design · mcp · evals
  10. Problema resolvido

    Corrigir o login sem abrir brecha de takeover.

    Depois que usuários de um cliente enterprise começaram a falhar no login por single sign-on, a causa raiz apareceu: contas antigas, criadas antes do SSO, faziam o novo login tentar criar um registro duplicado e esbarrar numa restrição de e-mail único. A correção óbvia, casar a conta antiga pelo e-mail, deixaria quem controla aquele e-mail no provedor de identidade assumir uma conta existente, inclusive de administrador. A solução foi desenhada em torno desse risco: contas só são adotadas quando é seguro, contas privilegiadas exigem prova de posse, e o papel de admin vem do tipo da conta salvo no banco, não do e-mail.

    Resultado: Login SSO corrigido para todas as contas legadas afetadas, com o account takeover por reuso de e-mail fechado; entregue em 4 PRs revisados.

    Detalhe técnico

    Causa raiz: o upsert buscava usuários por external_id; usuários legados não tinham external_identity, então caía no CREATE e colidia com um índice único global de e-mail. O caminho de recuperação só re-selecionava por external_id e relançava o erro. Cerca de 70% dos usuários do tenant estavam nesse estado, então todos falhariam no primeiro login SSO.

    Fix 1 (adoção por e-mail), com guardas do review: só usuários sem identity são adotáveis (e-mail reutilizado no provedor falha fechado), o e-mail é normalizado na busca e na escrita, a busca é escopada por tenant e entrou um log de diagnóstico cross-tenant. Um censo mostrou zero duplicados entre tenants.

    Follow-up ao descobrir que o provedor permite trocar e-mail livremente (sem verificação): e-mail sozinho não prova posse. Contas de staff nunca são adotadas automaticamente; só vinculam se o par (id do provedor, e-mail) estiver numa allowlist guardada em secret. is_admin agora é estampado na sessão assinada a partir do kind da conta no banco, e a promoção por lista de e-mails foi removida. A env var era só um atalho de release.

    Trade-off: a allowlist exige um passo manual por membro da equipe (coletar os ids do provedor com o cliente) em troca de fechar o account takeover. A preocupação de um revisor de que engolir o erro de colisão desarmaria uma proteção acidental contra roubo de sessão fez o tratamento do erro sair em um PR separado, só depois da mudança de admin.

    Diagnóstico: logs do Cloud Run via gcloud logging.

    sso · auth · security · incident-response · multi-tenant
  11. Decisão

    Somente leitura garantido pelo banco, não por filtro.

    Um harness de avaliação deixa agentes de IA rodarem suas próprias consultas num banco de teste compartilhado. A revisão de código achou que uma checagem simples de 'essa consulta é só leitura?' podia ser enganada escondendo um delete dentro de um select inofensivo, o que corrompia em silêncio os dados contra os quais todas as rodadas seguintes eram pontuadas. Em vez de deixar o filtro mais esperto e torcer, a correção levou a garantia para o próprio banco: as consultas dos agentes agora rodam como um usuário que só pode ler. O filtro de texto continuou, mas só para dar mensagens de erro amigáveis.

    Resultado: DELETE como viewer sobre dados semeados verificado como no-op; a fixture de avaliação compartilhada não pode mais ser mutada por consultas dos agentes.

    Detalhe técnico

    Bug: o gateway validava só a primeira keyword, então SELECT * FROM (DELETE ...) passava e mutava a fixture compartilhada; as rodadas seguintes eram pontuadas contra golds que os dados já não refletiam. O check de introspecção do scorer tinha a mesma falha (ancorado na posição 0, então SELECT 1; INFO FOR DB; era creditado como correto).

    Decisão: fazer o role ser a garantia, não o parser. As consultas dos agentes rodam com um usuário de role VIEWER criado uma vez com root; um scan por palavra em cada statement (literais de string removidos) fica só como erro amigável antecipado. Trade-off: um passo extra de bootstrap e uma segunda credencial, contra uma denylist que só cresce e que uma consulta criativa sempre contorna. Verificado que um DELETE como viewer sobre dados semeados é no-op.

    Endurecimentos relacionados da mesma revisão: HTTPError capturado antes de URLError para não perder o corpo 400 com o erro real de parse; geradores recusam sobrescrever uma rodada que já tem manifest sem --force; a regeneração do relatório reescreve só a região entre marcadores gerados, preservando a análise escrita à mão.

    evals · security · surrealdb · defense-in-depth · llm-agents
  12. Entregue

    Insights de LLM ancorados em fatos.

    Entregou o primeiro marco de uma API de geração de insights com LLM para um cliente enterprise. O sistema recebe dados de pesquisa estruturados, atomiza-os em fatos fundamentados e produz briefings em camadas — com um guardrail que exige que cada afirmação rastreie sua fonte. Validado com dados reais: 29 insights gerados com 100% de correção de grounding e zero falhas de guardrail.

    Resultado: 29 insights gerados em dados reais de pesquisa; 100% de correção de grounding; 0 falhas de guardrail.

    Detalhe técnico

    Construído em FastAPI com Pydantic AI para parsing. Pipeline central: extração de fatos → atomização multi-pass → geração de documentos (padrão H3). Guardrail de grounding rejeita qualquer afirmação sem proveniência direta de registro. Arquitetura multi-provider (OpenAI e Anthropic validados de forma intercambiável via model-as-config). Sequenciamento de build ciente de DAG: cards em paralelo onde dependências permitiam, sequencial onde schemas precisavam estabilizar primeiro. Fechou empiricamente uma questão arquitetural — cálculo de fatos sob demanda foi descartado após zero ocorrências em runs com dados reais, evitando abstração prematura. Seis PRs empilhados estruturados para sequência de merge limpa.

    fastapi · llm-pipeline · pydantic-ai · grounding · insights-generation
  13. Decisão

    Escolher a estratégia antes do modelo.

    Ao extrair insights estruturados de milhares de respostas abertas sobre um app mobile de consumo, a primeira pergunta não era qual modelo usar — mas qual *estratégia* de extração usar. Três abordagens foram comparadas ponta a ponta: um pipeline de claims atômicos em dois estágios, uma abordagem de documento direto e um híbrido com etapa de rascunho preliminar. A estratégia vencedora não foi a mais sofisticada.

    Resultado: H3 selecionado como estratégia padrão de extração: menor custo ($0,0216/doc), zero violações de altitude, 100% de grounding em todos os documentos de teste.

    Detalhe técnico

    Spike comparando três fluxos de extração LLM para dados qualitativos (respostas abertas de pesquisa de satisfação) usando Pydantic AI e prompts Jinja via ai-prompter:

    • H3 (dois estágios: fatos → claims atômicos → síntese): $0,0216/doc, 0 violações de altitude, 100% de grounding
    • H1+doc_separado: $0,0296/doc, 1 violação, 71% de cobertura de famílias de fatos
    • H3+predraft (H3 + rascunho prévio): $0,0348/doc, 3 violações, 60% mais claims mas com mais ruído

    Trade-off central: H3+predraft gerou 60% mais claims que H3, mas introduziu 3 'violações de altitude' (generalizações sem suporte nos dados). H1 teve maior cobertura de fatos (71% vs 57% de famílias) mas custo maior e uma violação. H3 venceu em todos os critérios ponderados.

    O pipeline inclui um guard determinístico de PII (emails, telefones, CPF, handles) rodando antes de qualquer chamada LLM — 20% das respostas reais continham PII, tornando esse passo obrigatório. Um validador quote_grounding confirma que cada claim gerado rastreia até uma citação verbatim, tornando o pipeline auditável. O fact index cobre 67 fatos em 7 famílias (boolean, categorical, cooccurrence, cross_tab, matrix, multi_select, persona).

    llm · pydantic-ai · prompt-engineering · qualitative-analysis · data-pipeline
  14. Problema resolvido

    Uma migration que parecia ter funcionado.

    Durante a validação de uma migração de schema de banco de dados, foi descoberto que o SurrealDB v3 ignora silenciosamente instruções `DEFINE FIELD` em campos já existentes — a operação parece ter sucesso, mas o schema permanece inalterado. Um runner de migração customizado agravava o problema ao engolir o erro subjacente e marcar a migração como aplicada. O bug foi comprovado empiricamente consultando os metadados da tabela em um namespace isolado e cruzando os tipos de campos antes e depois da execução da migração.

    Detalhe técnico

    Comportamento do SurrealDB v3: DEFINE FIELD (sem IF NOT EXISTS) em um campo já existente gera uma falha silenciosa — The field already exists — mas sem lançar uma exceção que interromperia a execução. O runner de migração customizado (sbl-migrate, construído sobre surreal_basics) captura isso no nível da transação e registra a migração como [APPLIED], fazendo com que todas as redefinições de campo na migração M0→M1 fossem no-ops. Descoberto consultando INFO FOR TABLE em um namespace isolado (test_sup513b) e comparando os metadados de tipo de campo. Correção: substituir cada DEFINE FIELD direto em um campo existente por uma sequência explícita de REMOVE FIELD IF EXISTS + DEFINE FIELD. A migração DOWN (usando REMOVE TABLE IF EXISTS) estava corretamente implementada e não foi afetada.

    surrealdb · schema-migration · debugging · database · python
  15. Entregue

    Quando a ausência de um campo também é informação.

    Construí um segundo parser LLM-powered para uma plataforma de análise de pesquisas — esse converte descrições em texto livre de personas de audiência em uma spec estruturada e validada. A restrição interessante: a fábrica de clustering downstream verifica configuração de features usando `'key' in dict` (presença de chave), não `dict.get('key')` (verificação de valor). Essa distinção forçou uma escolha de design de modelo não-óbvia.

    Resultado: 16 testes passando (2 skippados aguardando API key), ruff limpo — parser, modelos, rota e suíte de testes entregues como uma unidade coesa.

    Detalhe técnico

    O parser usa Pydantic AI com output_type=PersonaSpec, e PersonaSpec contém objetos PersonaVariable com campos opcionais order e scale. A escolha de design: ambos os campos usam Optional[list] = None em vez de list[str] = []. Razão: a fábrica downstream chama model_dump(exclude_none=True) e depois verifica 'order' in var — uma lista vazia [] seria falsy em checagem booleana mas truthy para in, quebrando silenciosamente o tratamento de variáveis ordinais. Com defaults None, exclude_none=True remove as chaves completamente, e 'order' in var retorna corretamente False para variáveis categóricas. A suíte de testes usa TestModel(custom_output_args={...}) do Pydantic AI para substituir o LLM por output estruturado determinístico — sem chamadas de API no CI. Um teste (test_parse_persona_spec_model_dump_exclui_nulos) documenta explicitamente o invariante de ausência de chave. Stack: Pydantic AI, FastAPI, Pydantic v2, pytest.

    pydantic-ai · llm-parsers · python · fastapi · api-design
  16. Problema resolvido

    Um namespace compartilhado corrompia as migrations.

    Escrevi uma migration reversível para um novo milestone de modelo de dados. No processo, descobri que compartilhar um namespace SurrealDB entre dois projetos locais causava corrupção silenciosa do tracker de migrations — a tabela de histórico acumulava entradas fantasma de outro projeto, quebrando o rollback. Isolei a causa raiz, limpei o estado manualmente e documentei o padrão de isolamento.

    Resultado: Migration aplicada e revertida sem erros: ruff limpo, 10/10 testes passando, tipos verificados via INFO FOR TABLE.

    Detalhe técnico

    SurrealDB v3, Python, uv. A migration cria novas tabelas SCHEMAFULL e re-tipa campos existentes. O ponto crítico: re-tipar um campo em uma tabela SCHEMAFULL pré-existente exige DEFINE FIELD OVERWRITE — um DEFINE FIELD simples pode não fazer nada ou lançar erro dependendo da versão do SurrealDB. O down migration reverte na ordem inversa de dependências e remove explicitamente sub-campos de array para evitar campos fantasma residuais.

    Bug de ambiente: dois projetos compartilhando o mesmo namespace/database no SurrealDB faziam suas tabelas de tracking de migrations colidirem. O runner do outro projeto havia inserido linhas na tabela compartilhada; ao tentar rollback, o runner não encontrava os arquivos de migration correspondentes no diretório do projeto atual e abortava. Correção imediata: deleção manual dos registros contaminantes. Solução definitiva: cada projeto usa seu próprio par namespace/database em dev local.

    surrealdb · schema-migration · database · dev-environment
  17. Decisão

    Medir antes de debater arquitetura.

    Projetar uma API que transforma dados de pesquisa survey em insights de negócio trouxe uma questão central: como tornar achados gerados por IA confiáveis o suficiente para agir? Em vez de debater no abstrato, medimos primeiro — e só então decidimos. A resposta combinou duas estratégias de verificação independentes que mantêm a IA accountable aos dados originais em cada etapa, sem adicionar infraestrutura de embedding nem descartar as runs que falham.

    Detalhe técnico

    Sessão de design arquitetural para um serviço FastAPI async (Python / Pydantic AI / SurrealDB) que processa dados de pesquisa survey em insights estruturados e auditáveis — substituindo um pipeline legado baseado em claude -p no shell.

    Medir antes de decidir: Antes de comprometer com um pipeline de retrieval/seleção, escrevemos um script para consultar o SurrealDB local e medir o fact space persistido. Resultado: 12–14 k tokens (6% da janela de contexto). Nenhum pipeline se justificava até esse sintoma aparecer em produção — um número concreto fechou um debate que seria puramente especulativo.

    Regime híbrido determinístico/LLM: O núcleo de insights foi dividido em dois regimes com invariantes distintos:

    • Questões fechadas (Likert, NPS, múltipla escolha): Python puro, completamente determinístico, testável com golden files — LLM nunca toca esse caminho.
    • Questões abertas/qualitativas: LLM identifica temas e padrões de frequência; um guardrail de quote-grounding (substring match verbatim contra o CSV fonte) valida cada afirmação antes da persistência. Alucinação capturada sem embeddings — custo zero, 100% auditável. O invariante: todo fato qualitativo precisa citar um fragmento rastreável da fonte.

    Falha de guardrail como sinal de avaliação: O output validator do Pydantic AI faz 1 retry em falha; falhas persistentes são gravadas com flag guardrail_failed em vez de descartadas. Justificativa: nas primeiras runs em produção, a taxa de falha é o sinal de qualidade mais valioso — descartar silenciosamente os outputs que falham esconde exatamente o que se precisa aprender.

    Fact space recomputado por run: Cada run reconstrói seu próprio fact space a partir dos specs da fonte e o persiste de forma independente. Trade-off: computação redundante vs. reprodutibilidade total — qualquer run pode ser reauditada em isolamento, independente de mudanças futuras no schema ou nos dados.

    architecture · pydantic-ai · llm-guardrails · survey-analysis · api-design
  18. Entregue

    Do trabalho real ao portfólio publicado.

    Entreguei o motor por trás deste exato site: um portfólio bilíngue cujo conteúdo é curado automaticamente a partir de sessões reais de trabalho. Um hook de fim de sessão avalia cada sessão do Claude Code contra um rubric de relevância; quando algo passa na régua, é escrito em dois idiomas, sanitizado e commitado como rascunho que publica com um comando.

    Resultado: O site que você está lendo foi produzido por esse pipeline.

    Detalhe técnico

    Três componentes: um site estático em Astro (i18n EN/PT, feed/projetos/CV renderizados de JSON no build), um servidor MCP em Python expondo cinco tools (log_activity, review_and_publish, add_project, update_now, get_portfolio_state) que escrevem pela API de Contents do GitHub, e um hook de Stop que embute o rubric verbatim — mudar critérios não exige mudar código. A validação tem três camadas: JSON Schema, completude bilíngue (tradução faltando quebra o build, de propósito) e uma denylist local como retaguarda determinística atrás da sanitização por categoria do LLM.

    astro · mcp · python · claude-code · i18nParte de Portfólio Vivo
  19. Aprendizado

    Uma linha do tempo que se desenha sem JavaScript.

    Primeiro uso em produção de scroll-driven animations do CSS: a linha do tempo deste feed se desenha conforme você rola, com zero JavaScript. O conceito de movimento do site — pulso, traço, respiração — precisava comunicar 'vivo' sem nenhuma biblioteca de animação.

    Resultado: A linha do feed se desenha no scroll em ~20 linhas de CSS; zero JS, zero bibliotecas.

    Detalhe técnico

    animation-timeline: view() mapeia o progresso de uma animação à posição do elemento no viewport — a borda da linha do tempo escala de 0 à altura total conforme cada entrada a cruza. Embrulhado em @supports para que navegadores sem suporte vejam só a borda estática, e desligado sob prefers-reduced-motion. O resto do sistema de movimento é um IntersectionObserver inline de ~30 linhas (reveal escalonado) atrás de uma classe html.js, então uma falha de JavaScript nunca deixa conteúdo escondido.

    css · scroll-driven-animations · progressive-enhancementParte de Portfólio Vivo
  20. Problema resolvido

    Um LLM de custo zero, sem chave de API.

    Precisava de reranqueamento de rotas com IA para um projeto pessoal de planejamento de transporte, mas sem chave de API — apenas uma assinatura Claude Code. Construí uma integração LLM de custo zero chamando o CLI do Claude em modo headless via subprocess. Deparei com um bug não óbvio: os hooks globais de sessão eram herdados pelo subprocess, injetando texto de avaliação na saída JSON e corrompendo silenciosamente cada resposta. Rastreei a corrupção, encontrei a causa raiz e corrigi com uma única flag.

    Resultado: Milestone M4 concluído: reranqueamento com IA funcionando sem chave de API; 57 testes verdes, 6/6 saídas de ranking válidas.

    Detalhe técnico

    • Stack: Python + subprocess, Claude Code CLI (claude -p --output-format json)
    • Adicionado provider ROTAS_LLM_PROVIDER=claude-code ao lado do caminho baseado em API existente, para coexistência
    • Causa raiz: hooks globais de Stop (usados para avaliação de sessão do portfólio) eram herdados pelo processo filho do CLI headless, acrescentando a saída de avaliação ao campo result e quebrando o parsing JSON
    • Correção: --settings '{"disableAllHooks": true}' passado na chamada do subprocess
    • Parser JSON tornado tolerante a prosa markdown ao redor, como medida defensiva contra deriva de prompt
    • Trade-off aceito: ~95s por consulta vs 5-10s com chave de API real — custo zero supera a latência para um protótipo pessoal; UX exibe aviso de 'IA lenta'
    • Critérios de aceite cumpridos: 6/6 IDs de itinerário corretos, justificativas ≤280 chars, reranqueamento contextual inverteu corretamente a ordem de prioridade e sinalizou uma baldeação noturna como eliminatória
    llm-integration · subprocess · transit-routing · bug-fix · zero-costParte de Rotas-SP — rotas de transporte que pensam além do óbvio
  21. Decisão

    Git como banco. Revisão como limite.

    Meu portfólio precisava se atualizar sozinho sem virar um servidor pra manter. A decisão: sem CMS, sem banco de dados, sem backend — o conteúdo vive como JSON num repositório git, cada atualização é um commit, e o site é um build estático disparado por push. Uma branch drafts funciona como buffer obrigatório: a automação pode escrever, mas nada vai a público sem um merge humano.

    Resultado: Zero infraestrutura pra operar; toda mudança de conteúdo é auditável no histórico do git.

    Detalhe técnico

    Um par competente poderia razoavelmente ter escolhido um CMS headless ou uma API pequena. Git-como-banco-de-dados venceu em três frentes: histórico completo e rollback de graça, custo de infraestrutura zero, e um pipeline de deploy (push → rebuild) que hosts estáticos oferecem de fábrica. A branch drafts existe por confidencialidade, não relevância — trabalho de cliente nunca pode auto-publicar sem um olhar humano. Trade-off aceito: sem atualizações em tempo real, que um portfólio não precisa.

    architecture · static-site · git · astroParte de Portfólio Vivo
03 / Em movimento

No que estou trabalhando.

Retrato do Agora · 28 out 2026

04 / Sobre

Entre os dados e as decisões.

Engenheira de Dados & IA com 9 anos em tecnologia, focada em construir pipelines de dados escaláveis e desenhar sistemas de IA onde o julgamento humano permanece no circuito. Especialista em dbt, BigQuery e GCP, com experiência prática construindo soluções baseadas em Claude — skills customizadas, servidores MCP e integrações de API — para clientes enterprise. À vontade tanto com stakeholders técnicos quanto de negócio, e reconhecida por liderar treinamentos internos e democratização de dados.

SQL · Python · Jinja · dbt Cloud · BigQuery · GCP · Cloud Functions · GitHub Actions · Claude API · Claude Code
Supernova LabsEngenheira de Dados & IA (Human in the Loop)
fev 2026 – atual
UOL EdTechEngenheira de Dados (Pleno)
out 2022 – jan 2026
TenbuEngenheira de Dados (Pleno)
dez 2021 – jul 2022
IBM BrasilDesenvolvedora de Aplicações Associada & Estagiária BTP
ago 2019 – mai 2021
Trajetória completa

Experiência

Engenheira de Dados & IA (Human in the Loop) · Supernova Labs

fev 2026 – atual

Remoto

Desenho e opero pipelines de dados e IA no GCP, construindo ferramentas baseadas em Claude que permitem aos modelos operar com o conhecimento, as regras e o contexto de cada cliente, mantendo o julgamento humano no circuito.

  • Desenho e opero pipelines de dados/IA no GCP (Cloud Functions, BigQuery), integrando APIs externas para alimentar contexto estruturado aos LLMs e processar as saídas antes do consumo final.
  • Construo skills reutilizáveis do Claude e servidores MCP (Model Context Protocol) customizados para Claude Code, Claude API e Claude Enterprise.
  • Entreguei um pipeline de analytics com IA para um grande cliente de fitness enterprise que lê respostas de pesquisa em escala, com validação e refinamento humanos mantidos no circuito.
  • Curo prompts e contexto para garantir saídas de LLM consistentes e prontas para produção, e traduzo necessidades de negócio em arquiteturas onde a IA acelera a entrega sem substituir o julgamento humano.

Engenheira de Dados (Pleno) · UOL EdTech

out 2022 – jan 2026

São Paulo, Brasil

Construí pipelines escaláveis com dbt Cloud, BigQuery e Looker, e liderei trabalhos de modelagem de dados e performance junto a iniciativas de democratização de dados.

  • Reestruturei modelos de dados para suportar múltiplos desfechos por jornada de usuário.
  • Otimizei uma tabela crítica, reduzindo o tempo de execução em 88%.
  • Contribuí em RFCs, revisões de solução e entrevistas com stakeholders.
  • Fiz o onboarding de contratações júnior e pleno e criei um canal interno de compartilhamento de conhecimento para democratização de dados.

Engenheira de Dados (Pleno) · Tenbu

dez 2021 – jul 2022

São Paulo, Brasil

Desenvolvi pipelines de ETL e soluções analíticas, apoiando squads do levantamento de requisitos à entrega.

  • Desenvolvi pipelines de ETL com IBM DataStage e desenhei workflows no Control-M.
  • Construí dashboards em Power BI e apoiei squads com levantamento de requisitos e entrega.

Desenvolvedora de Aplicações Associada & Estagiária BTP · IBM Brasil

ago 2019 – mai 2021

São Paulo, Brasil

Desenvolvi e mantive jobs de ETL e apoiei relatórios de BI em ambiente enterprise.

  • Desenvolvi e mantive jobs de ETL com IBM DataStage.
  • Produzi documentação técnica, gerenciei cubos OLAP e apoiei relatórios com IBM Cognos.

Habilidades

Linguagens & Frameworks
SQLPythonJinja
Dados & Cloud
dbt CloudBigQueryGCPCloud FunctionsGitHub Actions
Engenharia de IA & LLM
Claude APIClaude CodeClaude EnterpriseMCPClaude SkillsPrompt Engineering
BI & Visualização
LookerPower BIIBM Cognos
ETL & Orquestração
dbtIBM DataStageControl-MPrefect

Formação

Pós-graduação em Ciência de Dados e Inteligência Artificial · PUC-RSnov 2023 – atual
Tecnólogo em Análise e Desenvolvimento de Sistemas · UNASPabr 2018 – jun 2020
Ensino Médio com Técnico em Informática · ETEC Euro Albino de Souzafev 2015 – dez 2018

Certificações

dbt Learning Path · dbt Labsmai 2025
dbt Fundamentals · dbt Labsmai 2025
Refactoring SQL to Modularity · dbt Labsmai 2025
Big Data Foundations · IBMnov 2019
Python for Data Science · IBMout 2019

Idiomas

PortuguêsNativo
InglêsAvançado

Prêmios

1º lugar — Hackathon ABStartups (desafio universitário com foco em soluções inovadoras e orientadas a mercado) · ABStartups2019
Vamos conversar

Um problema de dados ou IA que merece ser bem pensado?

Me escreva →