Pipeline editorial MNCV · Engenharia de aplicações com IA · 2026

Quando a busca falha, o texto deve esperar

Recuperação de fontes, estados da fila e revisão humana no pipeline editorial do Meu Novo Carro Velho

  • n8n
  • Supabase/PostgreSQL
  • Edge Functions
  • SQLite
  • Python
  • JavaScript
  • APIs REST
  • Gemini File Search
  • Claude Sonnet
  • RAG gerenciado
  • fila de processamento
  • revisão humana
  • tratamento de falhas
Esquema do fluxo, com revisão antes da publicação
Minha contribuição
Curadoria das pautas, preparação da base, filtros e decisões sobre geração e revisão.
Entrega
Automação do blog com revisão humana; extensão social testada manualmente.
Operação
2026 · desativada em 04/07/26
Projeto preservado como portfólio

O Meu Novo Carro Velho (MNCV) era um produto pra ajudar na compra de carros usados. Reunia microaulas, checklist e comparação de carros. O blog tratava de dúvidas dessa compra, como avaliar sinais de batida, conferir documentos e evitar golpes.

Pra produzir os artigos, construí uma automação que partia de pautas escolhidas por mim, buscava material de apoio e gerava um texto para revisão. Eu podia aprovar, pedir ajustes ou rejeitar. A publicação dependia dessa decisão.

É esse caminho que chamo aqui de pipeline editorial: uma sequência de etapas que leva a pauta até o artigo publicado. O projeto juntava banco de dados, busca em documentos e modelos de IA. Entre essas partes, era preciso guardar o estado de cada pauta e decidir o que fazer quando alguma etapa falhasse.

A automação do blog rodou em 2026 e foi desligada em 04/07/26. Hoje, o MNCV serve como portfólio. Este relato mostra como o fluxo foi construído e o que mudou depois dos testes e das falhas em operação.

O que cada tecnologia fazia

O n8n coordenava a automação. Ele encadeia etapas, chamadas de nós, que podem buscar dados, executar código, chamar outro serviço ou esperar uma resposta humana. O fluxo rodava numa VPS, um servidor virtual, com Docker.

TecnologiaPapel no projeto
n8nDisparar o fluxo no horário agendado, passar os dados entre etapas e pausar para minha revisão
Supabase / PostgreSQLGuardar as pautas, seus estados, os artigos e a fila de tarefas para as redes sociais
**Edge Function admin-blog**Receber as chamadas da automação para reservar pautas, atualizar estados e gravar conteúdo no banco
Gemini File SearchBuscar trechos numa base de documentos de vistoria, filtrados pelo assunto da pauta
Claude SonnetUsar a pauta e os trechos recebidos para gerar a estrutura do artigo, o rascunho e os metadados de publicação
Gmail e formulário do n8nEntregar o texto para revisão e receber minha decisão de aprovar, refazer ou rejeitar
SQLite do n8nGuardar as execuções do fluxo, inclusive as que estavam pausadas
Python e JavaScriptPython gerava a definição dos fluxos e os scripts de operação; JavaScript tratava os dados dentro dos nós

Os serviços se comunicavam por APIs, interfaces que permitem a um programa pedir dados ou executar ações em outro. A função admin-blog fazia essa ponte com o banco do produto.

O banco do Supabase e o SQLite tinham funções diferentes. No primeiro, ficava a pauta e sua situação no processo editorial. No segundo, o registro da execução da automação. Essa separação ficou importante quando uma pauta apareceu como “em andamento”, mas o fluxo já tinha parado.

A base de vistoria também tinha outro papel. Ela reunia documentos de apoio à escrita, enquanto a fila guardava o que deveria virar artigo. O File Search organizava esses documentos para a busca e recuperava trechos. O Sonnet recebia os trechos para escrever. Essa combinação de busca em uma base com geração de texto é chamada de RAG.

O caminho da informação

Eu escolhia as pautas. Cada uma tinha tema, palavra-chave, categoria e um briefing com as orientações de escrita. Depois, entraram campos para termos de busca e perguntas frequentes. No horário agendado, o n8n pegava a pauta pendente mais antiga.

Pauta no Supabase
  → consulta à base de vistoria no Gemini File Search
  → extração dos trechos recuperados
  → estrutura do artigo → rascunho → título e metadados
  → e-mail + formulário de revisão
      aprovar → gravar e publicar → enfileirar variações sociais
      refazer → incorporar feedback e gerar outro rascunho
      rejeitar → encerrar a pauta sem publicar

Entre a busca e a escrita, um nó de código extraía os trechos encontrados na base. Eles vinham no campo groundingChunks[].retrievedContext.text da resposta da busca. O código tirava repetições exatas e montava o campo fontes.

Esses trechos entravam nos prompts, as instruções enviadas ao modelo para gerar a estrutura do artigo e o rascunho.

As fontes tinham um teto de 6.000 caracteres. O corte controlava o tamanho da entrada, mas podia parar no meio de um trecho. E o nome rag_ok prometia mais do que a variável media: ela só dizia se tinha vindo algum texto. Relevância, correção dos fatos e fidelidade às fontes continuavam por conferir.

Minha parte estava em preparar a base, definir os filtros e ligar a busca à escrita e à revisão. A indexação e a recuperação dos documentos ficavam com o serviço gerenciado do File Search.

A consulta estava pedindo a coisa errada

Uma pauta de segurança voltou sem fontes. Só que havia material sobre o assunto na base. A consulta misturava tema e briefing, então a busca recebia a pergunta técnica junto de instruções como “tom de amiga que já passou por isso”.

O modelo entrou no personagem. Respondeu naquele tom e não trouxe os trechos da base. A busca tinha recebido instruções de escrita.

A correção foi separar o que cada etapa precisava receber. O briefing ficou com o modelo que escrevia. Pra busca, foram palavra-chave, tema e termos relacionados ao assunto, guardados no campo termos_semanticos:

const query = [j.palavra_chave_alvo, j.tema, j.termos_semanticos]
  .map(x => (x || '').toString().trim())
  .filter(Boolean)
  .join(' | ');

A busca usava a categoria da pauta para filtrar os documentos pela classificação guardada nos metadados. No teste de 05/06/26, a mesma pauta voltou com zero trechos na consulta antiga e cinco na nova. O teste mostrou que a mudança resolveu aquele caso. A fila inteira não passou por essa comparação.

Já uma pauta sobre combustível voltava vazia por outro motivo. Na categoria consultada, havia documentos sobre preço e critérios de escolha, mas nenhum sobre aquele subtema. Podia tentar de novo: o material continuaria faltando.

Vazio e falha pedem decisões diferentes

Em 29/06/26, chegou pra eu revisar um texto sobre partida e sistema elétrico de carro. Tinha sido escrito sem os trechos da base de vistoria. A busca estourou o tempo de espera. O fluxo seguiu só com o briefing, e o e-mail avisou que o texto tinha saído sem a base.

Estava funcionando como previsto. Só que isso vinha se repetindo. Decidi que, se a busca estivesse fora do ar, o texto tinha que esperar.

A primeira versão continuava escrevendo nos dois casos, busca vazia ou erro. O e-mail avisava que o rascunho tinha saído sem a base, e eu decidia o que fazer com ele.

A fila andava, mas a falha da busca virava trabalho de revisão pra mim. Em junho, o serviço devolveu erros 503, que indicam indisponibilidade, e deixou chamadas sem resposta. O tempo máximo de espera caiu de 300 para 45 segundos. O nó HTTP, responsável pela chamada ao serviço, passou a fazer até cinco tentativas curtas. Se o serviço demorasse mais pra voltar, o texto ainda saía sem fontes.

Em 29/06, o fluxo ganhou uma decisão antes de extrair as fontes:

Retorno da buscaCaminho implementado
Com trechos recuperadosExtrair as fontes e seguir para a geração
Sem erro informado, mas sem trechosSeguir com o briefing e avisar no e-mail de revisão
Com erro informado pelo nó HTTPSuspender a geração e esperar três horas para tentar de novo
Erro persistente após três rodadasDevolver a pauta para pendente e avisar que o post foi adiado

As três rodadas espaçadas vinham além das tentativas curtas do nó HTTP. Pra repetir a busca, o fluxo precisava voltar ao nó que tinha carregado a pauta e pegar os dados de novo.

A cada chamada à API, o nó HTTP substituía os dados de entrada pela resposta recebida. Se o fluxo tentasse montar a próxima consulta a partir dessa resposta de erro, os campos da pauta já não estariam ali.

O código ainda faz uma distinção grosseira. Olha se existe $json.error, sem separar erro passageiro de erro que vai continuar na próxima tentativa. E voltar sem trechos pede investigação. Pode faltar material, mas pode ser a consulta malfeita, como aconteceu com o briefing.

O registro de 29/06 confirma que a mudança foi instalada no ambiente de operação. O teste completo das esperas de três horas ficou pendente. O código está lá, mas ainda não achei nos registros a prova de uma execução que tenha esperado e se recuperado por esse caminho.

Uma pauta em andamento pode estar esperando alguém

O estado da pauta ficava no Supabase. A execução, inclusive a pausa para revisão, ficava no n8n.

SituaçãoEstado da pauta
Disponível para a próxima execuçãopendente
Reservada pelo fluxo, inclusive durante a revisãoem_andamento
Texto gravado após aprovaçãoaprovado
Recusada na revisãorejeitado
Falha encaminhada para tratamento ou limite de pedidos para refazer atingidoerro

Pra pegar uma pauta, o fluxo primeiro lia a fila e depois tentava mudar a linha para em_andamento. A atualização só valia se ela ainda estivesse pendente. Assim, duas execuções não conseguiam pegar a mesma linha. Era uma fila pequena, sem distribuição de trabalho entre vários processos.

No botão Refazer, a pauta continuava em andamento e meus comentários voltavam pra geração do rascunho. O fluxo reaproveitava a estrutura e as fontes, então pedir outro ângulo para o artigo não cabia nesse botão. O teste percorreu uma volta: o texto incorporou os comentários e o contador subiu. O teste não chegou ao limite de pedidos para refazer previsto no código.

Em 10/06, a autorização de acesso ao Gmail falhou e o e-mail não saiu. Essa autorização usava OAuth. A execução parou antes da pausa de revisão, mas a pauta ficou em em_andamento. Olhando só o banco, dava pra achar que eu estava lendo o texto. O processamento tinha morrido antes de me avisar.

Um script fora do n8n varria as pautas presas. Antes de devolvê-las à fila, consultava as execuções no SQLite. Se encontrasse uma execução em waiting, ou seja, pausada, deixava os itens daquele fluxo quietos. Também respeitava uma folga de 15 minutos para execuções recém-iniciadas.

A proteção olhava o fluxo inteiro, sem ligar cada pauta à sua execução. Por isso, um texto esperando minha revisão podia segurar a recuperação de outra pauta que estivesse presa de verdade. Era o limite dessa solução na operação pequena.

Atualizar o fluxo podia quebrar uma revisão

O link do e-mail abria um formulário que retomava a execução pausada. Só que o script que gerava o fluxo criava novos identificadores de webhook toda vez que rodava. Esses identificadores faziam parte dos endereços de retomada. Atualizar o fluxo com uma revisão pendente podia matar o link que já estava na minha caixa de entrada.

O script de deploy, usado para instalar as atualizações, passou a consultar o SQLite. Se encontrasse alguma execução em waiting, recusava a atualização. A listagem consultada pela API REST do n8n não mostrava essas execuções naquele ambiente. Pela API, parecia livre. No banco, havia revisão esperando.

A checagem passou no teste ao vivo registrado. O custo era depender do banco interno do n8n e de acesso a ele. A consulta e a atualização continuam separadas: nada impede que uma execução entre em espera no intervalo entre as duas.

Ao processar em lote, a resposta perdeu o dono

Depois da aprovação, o blog criava uma tarefa na fila, chamada de job, para gerar versões de Instagram e Pinterest. Essa parte rodou em testes manuais e ficou desligada como rotina. A postagem automática nas redes ficou pra depois.

Com um item, o fluxo funcionava. Quando passou a receber um lote, perdeu a ligação entre a resposta e o job. De novo, a chamada HTTP tinha substituído os campos de entrada pela resposta do modelo.

A correção recuperava os identificadores da tarefa (job_id) e do artigo (blog_post_id) no nó anterior, pela posição do item. Essa associação funciona enquanto a chamada preserva a ordem e a quantidade dos itens.

Item inválido ganhou uma saída própria, pra um erro de leitura não derrubar o lote inteiro. No teste de 02/06/26, três jobs geraram seis variações gravadas e um único e-mail de resumo. Esse foi o alcance do teste; a rotina nas redes não chegou a operar.

O que foi verificado

Os testes de junho confirmaram que as fontes chegavam aos prompts e compararam textos gerados com e sem a busca. Também testaram os caminhos Aprovar, Refazer e Rejeitar. Busca vazia e erro provocado de propósito passaram pela regra antiga, que deixava escrever sem fontes. O bloqueio e as esperas vieram depois e precisam de outra validação.

Dos dez posts do lote de 05/06, nove receberam trechos da base. Isso conta quantos tiveram fontes à disposição. Pra dizer quantos estavam corretos ou se houve invenção, seria preciso conferir as afirmações dos artigos.

A partir de 05/06, clicar em Aprovar publicava o texto. Antes, os testes gravavam rascunhos. A conferência ficava comigo; o fluxo não checava automaticamente cada afirmação contra a fonte.

Onde olhar

Pra acompanhar essas decisões no código, o ponto de partida é build_workflow.py. Ele define o fluxo do blog, incluindo a consulta recortada acima. O desenho no início deste texto é um esquema simplificado.

As outras peças são:

  • deploy_blog.py: confere as execuções pausadas antes de atualizar o fluxo;
  • varredor_orfaos.py: recupera itens presos;
  • build_social_workflow.py: gera as variações para as redes sociais.

O projeto é pequeno. Mesmo assim, o campo em_andamento sozinho não dizia se um texto esperava revisão ou se o processamento tinha morrido. Era preciso consultar as execuções. E um texto bem escrito podia ter chegado sem a base que deveria sustentá-lo. Foi nesses pontos que o fluxo precisou mudar.

Feito com Claude Code: eu fiz a curadoria das pautas, preparei a base e decidi os filtros, a revisão e o bloqueio após falha. O código dos fluxos e o rascunho deste texto foram feitos com a ferramenta. Revisei o texto e assino.