No meio dessas sessões de código, tive um pensamento mais simples: se eu, como desenvolvedor de software, conseguia dar sentido àquele mesmo fluxo — eventos tipados, com timestamp, nomes de jogadores, estatísticas anexadas — por que um LLM não conseguiria?

Foi a partir dessa premissa que comecei este projeto.


Dois sistemas, um banco de dados compartilhado

O pipeline de narração e o pipeline editorial não se chamam. A conexão entre eles é uma coleção no Firestore: um sistema escreve nela, o outro lê. Nenhum foi desenhado pensando no outro — a integração funciona porque os dados de narração acabaram sendo estruturados o suficiente para servir como material-fonte para jornalismo.

Esse banco de dados compartilhado é o contrato entre dois sistemas que não sabem da existência um do outro. O que é elegante, até algo mudar de um lado sem o outro saber. Dois sistemas compartilhando uma coleção sem contrato de schema explícito significa que qualquer mudança em como os eventos de narração são estruturados pode quebrar silenciosamente o pipeline editorial. Não batemos nessa parede durante a Copa — mas ela está lá.


O que a IA realmente recebe

Um evento de gol, como vem da API de dados, é um objeto plano: ID do jogador, ID do time, minuto, período, timestamp. Isso é o suficiente para mandar uma push notification. Está longe de ser suficiente para escrever um parágrafo.

Antes de qualquer coisa ir para a IA, o job editorial monta o contexto. Quando uma partida termina, ele lê todos os eventos de narração daquela partida no Firestore — em ordem cronológica, gols primeiro, depois cartões, substituições e mudanças de período — e junta com estatísticas agregadas da partida buscadas na API de dados: posse de bola, chutes a gol, faltas, escanteios.

O resultado não é um despejo bruto de dados. É mais parecido com um relatório de partida estruturado: o que aconteceu, em ordem, com os números que dão sentido a esses eventos. Um gol no minuto 78 do time que está perdendo se lê de um jeito muito diferente quando você também sabe que esse time teve 35% de posse de bola e três chutes no alvo em 90 minutos. Esse contexto é ao redor do que construímos o prompt — e estruturar a entrada como um jornalista estruturaria suas anotações acabou importando mais do que o próprio prompt. A primeira versão mandava ruído demais e sinal de menos.

Usamos LangChain para orquestrar a cadeia — montar a entrada, chamar o modelo, e fazer o parsing da resposta. O modelo retorna quatro campos: manchete, linha de apoio, deck e corpo. Os três primeiros são estruturais; o corpo é onde mora a configurabilidade. Diferente de templates Jinja2, que sempre produzem a mesma estrutura, o corpo muda com o prompt — tom, ênfase, voz — sem tocar no pipeline. Orquestrar essas etapas como uma cadeia manteve a lógica legível e as peças testáveis de forma independente. Os dados são sempre os mesmos. O que muda é como você conta a história — e essa foi a parte que conseguimos colocar nas mãos do jornalista.

Foi isso que saiu do outro lado. Para uma partida que terminou 2–1, decidida por um gol no minuto 89 depois de um cartão vermelho no minuto 64:

Manchete: Drama no fim garante vitória apertada com time reduzido a dez

Deck: Um cartão vermelho no segundo tempo transformou uma vantagem tranquila num final tenso — e as estatísticas confirmaram cada minuto de aflição.

Corpo: Por sessenta e três minutos, a partida parecia resolvida. Então o cartão vermelho mudou tudo. O que se seguiu foram vinte e seis minutos de pressão sustentada — 71% de posse de bola para o time que perdia no segundo tempo, oito chutes no alvo — que só se converteu uma vez, tarde demais para empatar. O gol que decidiu veio no minuto 89. Até lá, já parecia inevitável.

O modelo não teve acesso a palavras como “tenso” ou “inevitável” na entrada. Ele as inferiu a partir do formato dos números.


O que a IA fez com isso

Essa é a parte que exigiu um pouco de ajuste para confiar.

Modelos de linguagem têm priors surpreendentemente bons sobre futebol. Eles entendem que um cartão vermelho no primeiro tempo muda a forma de uma partida. Sabem que um gol contra a lógica do jogo merece ser nomeado como tal. Conseguem ler “67% de posse, 0 gols” e entender isso como uma narrativa de domínio sem recompensa. Nada disso está explícito nos dados — é inferido a partir do que os dados descrevem.

O que o modelo não consegue fazer é se recuperar de lacunas no registro de eventos. Se o pipeline de narração perdeu uma substituição — por causa de um gap no polling ou uma resposta atrasada da API — a IA escreve sobre a partida como se aquela substituição nunca tivesse acontecido. Ela não sabe sinalizar a ausência. Só conta a história que recebeu.

Essa dependência vale a pena notar: a qualidade da saída da IA é limitada pela completude do registro de eventos. Prompt engineering ajuda, mas só até o ponto em que os dados acabam.

Há uma segunda limitação, mais silenciosa: o modelo não tem memória entre partidas. Ele não consegue dizer que um jogador marcou em três dos últimos cinco jogos, ou que esse time nunca venceu uma fase eliminatória contra esse adversário. Esse tipo de inferência exige dados históricos — e agora não alimentamos nada disso. Cada artigo é escrito como se o torneio tivesse começado cinco minutos atrás.

A correção não é complicada em conceito: depois de cada partida, ingerir os dados estruturados de eventos e estatísticas num armazenamento persistente. Para queries agregadas — taxas de vitória, contagem de gols, histórico de confrontos — um banco de dados relacional funciona bem. Para algo mais próximo de recuperação semântica, onde o modelo poderia puxar narrativas históricas relevantes em vez de números brutos, um banco vetorial seria mais adequado. De qualquer forma, o pipeline não muda muito; o que muda é o quão rico pode ser o contexto montado antes da chamada ao modelo.


A interface editorial que não existe

Quando o job termina, o artigo vai direto para o CMS. Sem fila, sem tela de revisão, sem etapa de aprovação. O time editorial recebe um e-mail — manchete e tags — depois que a matéria já está no ar.

Essa é a parte mais contraintuitiva do produto, e vale nomear com clareza. Não estamos construindo ferramentas para editores. Estamos rodando um pipeline de publicação que notifica editores quando termina. A distinção importa mais do que parece — ela muda como é a falha, quem é responsável pela qualidade, e o que o monitoramento realmente precisa capturar.

A visibilidade do time editorial termina no e-mail. A visibilidade do time de engenharia é outra história. Com Prometheus instrumentado em todo o pipeline, temos dashboards que rastreiam o que o time editorial nunca vê: quantos artigos foram gerados por partida, quanto tempo cada etapa levou, com que frequência jobs falharam silenciosamente. Quando não há um humano no loop de aprovação, métricas se tornam a coisa mais próxima de supervisão que você tem.


Latência, e por que ela importa menos do que eu pensava

Um leitor que chega numa crônica pós-jogo quer algo para ler, não um placar que ele já sabe — então ser o primeiro por trinta segundos é irrelevante. A pressão de latência pertence ao produto de narração: notificações de gol, alertas de cartão, atualizações ao vivo. Para o editorial, correção e completude importam mais que velocidade. Alguns minutos depois do apito final está de bom tamanho.

A latência aqui é composta: até 60 segundos para o evento de fim de partida ser detectado pelo pipeline de narração, e depois mais o tempo até o próximo ciclo do job editorial. A janela prática costuma ser de dois a cinco minutos depois do apito final. Isso é rápido o suficiente — mas “costuma” está fazendo bastante trabalho nessa frase.

Saber o que realmente acontece em produção exige medição. Rastreamos a latência ponta a ponta como um histograma no Prometheus: do momento em que o evento de fim de partida é detectado até o momento em que o artigo chega ao CMS. Histogramas permitem olhar além das médias — o p90 mostra como é a experiência num dia ruim, não só num dia típico. Durante a Copa, o p90 ficou abaixo de quatro minutos. Esse número só tem significado porque medimos; sem o histograma, “geralmente dois a cinco minutos” é só um chute.

As funções rate() e increase() do Prometheus foram úteis de outra forma: rastrear com que frequência os jobs disparavam, quantos artigos eram gerados por janela de competição, e se o pipeline de polling estava chamando a API de dados na frequência esperada. Essa última métrica importou mais do que parecia — e se tornou central para o que quebrou na próxima fase do projeto.

O caso de borda mais arriscado é o job editorial disparar antes que todos os eventos tenham sido escritos no Firestore. Uma resposta atrasada da API, um ciclo de polling que pegou o apito final mas não o gol marcado logo antes dele. O job pega o que está lá e publica. Não há reconciliação, não há segunda passada — e nenhum alerta avisando que a crônica está sem um evento.


O que aprendemos

Estruture a entrada como um jornalista estruturaria suas anotações. O modelo performa melhor quando o contexto espelha como um humano se prepararia para escrever. Eventos cronológicos mais estatísticas de apoio, não uma lista plana de campos. Esse enquadramento levou iteração até chegar no formato certo — a primeira versão mandava ruído demais e sinal de menos.

O LangChain conquistou seu lugar na stack. Orquestrar a montagem, a chamada ao modelo, e o parsing da resposta como uma cadeia deixou a lógica legível e as peças testáveis de forma independente. Para um pipeline de uma passada com um schema de saída estruturado, foi a ferramenta certa.

O banco de dados compartilhado é conveniente até deixar de ser. Dois sistemas compartilhando uma coleção no Firestore sem contrato de schema explícito significa que qualquer mudança em como os eventos de narração são estruturados pode quebrar silenciosamente o pipeline editorial. Não batemos nessa parede durante a Copa, mas ela está lá.

“A IA vai resolver” só funciona quando os dados são bons. O modelo é confiante. Ele vai escrever uma crônica coerente mesmo com dados incompletos, e não vai te contar o que não sabia. O risco editorial não vem da IA escrever algo errado — vem da IA escrever algo plausível que está faltando um gol.


Tudo descrito neste post assume que os dados estão fluindo. O pipeline de polling está rodando, a API está respondendo, os eventos estão chegando no horário. Essa suposição se sustentou — até que, no meio de uma partida ao vivo, os dados simplesmente pararam de chegar. O pipeline estava bem. O problema não era nosso.