← Voltar às Notas
· 6 min de leitura / IA, Sistemas, Engenharia, Arquitetura

A Fricção Estava no Que Eu Não Disse

Construí um motor de notícias curadas com um agente. Todo bug era o sistema fazendo exatamente o que eu mandei, colidindo com algo que nunca falei em voz alta. Um relato de campo sobre a distância entre o que você constrói e o que você assume.

Sistemas são previsíveis. As suposições que você esquece de declarar não são.

Eu queria uma coisa pequena: um lugar dentro de um dos meus próprios sistemas para ler as notícias uma vez por dia, curadas por interesse. Não uma mangueira de incêndio. Não mais um crawler de RSS pelo qual eu perderia o interesse. Não mais uma aba para me sentir culpado. Uma lista que algo já tivesse afinado para mim, com base no que ele sabe sobre mim e no que eu de fato me importo.

O que eu consegui, no fim, foi exatamente isso. Mas a parte interessante não era a funcionalidade. Era que cada bug era o sistema fazendo exatamente o que eu mandei, colidindo com algo que nunca falei em voz alta.

Este é um relato de campo sobre essa distância.

Um engenheiro solitário diante de uma máquina viva de canos luminosos, um fio se partindo na sombra

Construa sobre as junções, não sobre o terreno

A primeira decisão foi a mais barata e a mais importante: não invente arquitetura. O sistema que eu já rodo tinha cada junção que essa funcionalidade precisava. Eu só precisava encontrá-las.

  • Armazenamento? Já havia um padrão, um banco SQLite operado por uma pequena CLI em Python, chamada a partir das server functions do app. Clonei isso em um news-db.py.
  • Um job diário? Um cron via launchd já rodava um punhado de tarefas toda manhã. Copiei o formato dele.
  • "Curadas por interesse"? Um turno do engine já triava minha caixa de entrada. Pontuar notícias é o mesmo movimento com um prompt diferente.

Eu estava adicionando um cômodo ligando-o no encanamento existente, não cavando novas valas. A funcionalidade inteira é uma junção reutilizada quatro vezes: CLI em Python → server function → data provider → query hook. Nada novo. É esse o ponto. Novidade é um custo que você paga em bugs, e eu queria gastar meu orçamento de novidade em outro lugar.

As duas decisões que de fato importaram

Duas escolhas importaram, e ambas eram sobre robustez em vez de alcance.

RSS primeiro, não scraping. Feeds são um contrato; HTML é um boato. Eu faço o parse de RSS/Atom com a biblioteca padrão do Python, urllib mais xml.etree, zero dependências. Fazer scraping de sites arbitrários teria dobrado a superfície e cortado pela metade a confiabilidade. A porta fica aberta para HTML por fonte depois. Eu só não atravessei ela no dia um.

Um agente que tem permissão para falhar. Um LLM local pontua cada artigo por interesse e escreve uma justificativa de uma linha. Mas se o gateway estiver fora, o pipeline não para. Ele recorre à recência e te mostra os artigos mesmo assim. A leitura nunca é bloqueada pela curadoria. A curadoria é um luxo colocado em cima de um sistema que já funciona sem ela.

Essa é a arquitetura. Levou um tempo para decidir e mais ainda para projetar. Então o sistema começou a me ensinar o que eu tinha deixado de dizer.

A fricção vive no que não foi dito

Aqui está a parte honesta. Os quatro momentos em que um sistema perfeitamente correto fez algo que eu não queria, porque eu nunca tinha dito a ele para não fazer.

1. O interruptor ligado a nada. Construí um toggle: "curadoria diária automática." Ele salvava. Ele virava. E o cron o ignorava por completo, porque eu tinha adicionado a flag na UI e na config e nunca liguei o runner para lê-la.

Um interruptor conectado a nada é pior do que nenhum interruptor. É uma mentira com uma animação bonita. E era óbvio, para mim. Mas quando você está trabalhando com uma IA (ou, na minha experiência, com outro humano), suas suposições são óbvias só para você. O óbvio precisa ser dito.

2. O schema que comeu minhas configurações. Aí o toggle começou a reverter sozinho. Liga, dá refresh, está desligado de novo. A causa era linda na sua correção: o save da config era validado contra um schema que ainda não sabia que o campo news existia. O validador fez seu trabalho perfeitamente. Ele removia o campo que eu não tinha declarado, todas as vezes.

Uma whitelist vai comer silenciosamente qualquer coisa que você esqueça de adicionar a ela. O sistema não estava quebrado. Meu modelo mental estava. Eu estava editando um valor que a camada de persistência tinha discretamente concordado em nunca guardar.

3. Dois limites, colidindo. Primeira execução de verdade: "385 novos · 80 curados." Parece um bug. Não é. São dois limites corretos se encontrando.

O ingest não tinha piso de idade, então puxou todo o backlog de cada feed. E eu só tinha ligado um punhado de fontes para testar, senão ainda estaria rodando agora. O curador tinha um teto de 80 por execução para manter o custo sob controle. Ambos se comportaram exatamente como escrito, e juntos produziram um absurdo.

A correção não era um teto maior. Era uma entrada menor: limite a entrada, não o processamento. Uma janela de frescor de sete dias no ingest, e dedup para que reexecuções adicionem só o que é genuinamente novo. Limite o rio, não o balde.

4. O heartbeat de 15 segundos. Abri a aba de rede e a mesma requisição estava disparando a cada quinze segundos, para sempre, cada uma criando um processo Python. O dashboard tinha um default global de "live mode": faça polling de tudo, refetch ao focar. Minhas queries de notícias herdaram isso sem perguntar.

Mas notícia não é dado ao vivo. Muda uma vez por dia. Um default é uma opinião que o resto do sistema tem sobre os seus dados, e ela estava errada sobre os meus. Tornei as queries de notícias estáticas e deixei as mutations as invalidarem. O heartbeat parou.

O que nem estava no código. Um feed, o da Anthropic, dava 404 sem parar. Não era meu bug, só uma URL morta. Mas apareceu porque eu tinha construído saúde por fonte: último erro, falhas consecutivas, auto-desativação depois de cinco. O sistema não escondeu a podridão. Observabilidade não é uma funcionalidade que você parafusa no fim; é o que te diz a diferença entre "quebrado" e "alimentado com um endereço errado." Falhe rápido, conserte rápido, mas só quando o sistema é honesto o bastante para te dizer qual é qual.

O formato do polimento

Depois que a fundação parou de mentir para mim, o resto foi subtração, que, se você já me leu antes, é basicamente o único tipo de engenharia em que eu confio.

  • Paginação em vez de scroll infinito. Mostre vinte, não trezentos.
  • Curadoria em pequenos lotes em vez de um único turno gigante. Mais rápido, e honesto sobre o que ainda está pendente.
  • Ordenação selecionável. Relevância por padrão, recência quando eu só quero o que é novo. O sistema tinha uma opinião sobre a ordem; eu a devolvi ao leitor.
  • Filtros como dropdowns, não uma parede de chips.

Nada disso é esperto. Tudo isso é remoção.

O que eu levo comigo

A lição não é "teste mais." É mais afiada que isso.

Um sistema previsível torna suas suposições estruturais. Cada peça se comportou exatamente como especificado. O toggle salvou exatamente o que o schema permitia. O ingest puxou exatamente aquilo para onde foi apontado. O dashboard fez polling exatamente como configurado. A fricção nunca esteve na máquina. Ela vivia no espaço silencioso entre o que eu construí e o que eu assumi. E em como eu estava falando com meus modelos.

Você não depura esse espaço lendo o código com mais afinco. Você o depura dizendo a suposição em voz alta e vendo o sistema discordar de você.

A funcionalidade foi rápida de construir. Entender o que eu não tinha dito levou dias.

O sistema fez tudo certo. É exatamente por isso que foi tão difícil enxergar o que eu tinha feito errado.