Cómo hice que mi bot de noticias rechace el 87% de lo que lee —y por qué el filtro NO usa IA
Tengo un medio deportivo que publica solo. Lee decenas de fuentes RSS, reescribe cada noticia con un modelo de lenguaje, le busca imagen y la publica. Funciona desde hace años y nunca dejó de producir.
Antes de clonarlo a un segundo sitio hice algo que había estado evitando: medir cuántos de esos artículos le sirven a alguien.
El resultado fue incómodo. El 86,8% de los artículos publicados no había recibido una sola visita. No pocas: cero. Casi nueve de cada diez notas existían únicamente para el sitemap.
Un pipeline de contenido que nunca se detiene no es productividad: es un costo fijo disfrazado de crecimiento.
Así que cuando monté el segundo medio —el mismo motor, ahora en inglés y para deportes de Estados Unidos— tomé una decisión de arranque: no nace publicando todo lo que llega. Nace con un portón.
El problema real: el modelo cobra por decir que no
La solución obvia es dejar que la IA decida. Le pasas el titular, le preguntas «¿esto es noticia relevante?» y actúas según la respuesta. Es elegante y se escribe en diez minutos.
También es la más cara, por una razón que se ve solo cuando corres números reales: pagas cada rechazo igual que cada aceptación.
Mis fuentes en inglés entregan del orden de cien titulares por barrido. Si el modelo juzga los cien, pago cien juicios para publicar unos pocos. El gasto no escala con lo que produzco: escala con lo que descarto. Y como el descarte es la mayoría, termino financiando principalmente mis propios «no».
Hay otro problema, menos obvio: un juicio de relevancia hecho por un modelo no es reproducible. El mismo titular puede pasar hoy y caer mañana. Un portón editorial que cambia de opinión solo no es un portón, es un sorteo.
La decisión: un portón determinista en Python, antes de la primera llamada
Escribí un módulo nuevo —relevancia.py— que no existe en el sitio original. Es Python puro: sin modelo, sin red, sin costo. Entra un titular con su resumen y su fuente, sale una decisión.
Lo enganché como filtro 6 del lector de RSS, es decir, en el último escalón antes de que la noticia se convierta en gasto. Los cinco filtros anteriores eran mecánicos (duplicados, antigüedad, fuente, formato). Este es el primero que opina de contenido.
La regla de diseño fue una sola:
Cada peldaño del pipeline debe ser más barato que el siguiente. Si el escalón caro va al final, el pipeline se paga solo.
Para calibrarlo no usé intuición. Guardé 893 titulares reales de mis propias fuentes y ensayé el filtro contra ese lote hasta que el corte quedó donde yo lo habría puesto a mano. Pasa el 13%.
Ese número no es un objetivo de vanidad: es la traducción operativa de una política editorial. Entre diez y veinte notas buenas al día, no cien mediocres. El tope quedó escrito en la configuración —máximo 20 al día, entre 20 y 60 minutos de separación, dos artículos por corrida, tres por fuente— y no en el código, porque una política editorial se ajusta sin desplegar.
El conocimiento que el modelo no tiene: anclajes de dominio
El portón resuelve cuánto pasa. Falta qué se escribe bien. Y ahí aparece un problema distinto: el modelo sabe mucho de deportes en general y muy poco de este calendario, esta semana.
Reescribí el prompt entero para el mercado gringo —445 líneas— y la parte que más rinde no son las instrucciones de estilo, sino los anclajes: afirmaciones del dominio que el modelo no debe deducir.
- En agosto, la NFL está en pretemporada, no en temporada regular.
- La NBA no es la WNBA. Son ligas distintas con calendarios distintos.
- La MLB no es ligas menores.
- Profesional no es universitario.
- Un anuncio oficial no es lo mismo que un reporte de un periodista.
Cada una de esas líneas nació de un error que vi con mis ojos en un borrador. Un modelo sin anclas escribe con seguridad que un partido de pretemporada define la clasificación —y suena perfectamente bien mientras lo hace.
El anclaje es más barato que la corrección. Poner cinco frases en el prompt cuesta una vez; corregir artículos publicados cuesta siempre.
La trampa que me costó más tiempo: el modelo rompiendo su propio JSON
El generador devuelve JSON: título, cuerpo, categoría, metadatos. Y fallaba de forma intermitente, sin patrón claro.
La causa resultó ser tonta y perfectamente evitable: el modelo escribía comillas rectas dentro del cuerpo del artículo —una cita textual de un entrenador, por ejemplo— y esas comillas cerraban el campo de texto antes de tiempo. El JSON quedaba inválido y la corrida entera se perdía.
La solución no fue reintentar ni parchear el parser. Fue un bloque nuevo en el prompt, que llamo JSON safety: dentro del contenido se usan comillas tipográficas (“…”), nunca rectas. El carácter que rompe la estructura simplemente deja de estar disponible.
La lección general, que aplica a cualquier salida estructurada de un LLM: si un carácter puede romper tu formato, no lo dejes entrar al contenido. Es más confiable prohibirlo en la instrucción que limpiarlo después.
Por qué NO hice lo obvio
No usé la IA como filtro. Ya está explicado arriba, pero vale el resumen: el filtro barato va primero y el caro va después. Un portón determinista que rechaza el 87% convierte el costo del modelo en una función de lo que publico, no de lo que leo. La corrida completa del primer artículo —lectura de fuentes, filtrado, reescritura, imagen, publicación— costó menos de un centavo de dólar.
No arranqué publicando volumen para «alimentar al índice». La creencia de fondo del pipeline original era que más artículos equivalen a más tráfico. Mi propio sitio, medido, dice lo contrario: 86,8% con cero visitas. Y el mercado en inglés es notablemente más severo con la publicación masiva generada por IA que el mercado hispano donde crecí. Nacer filtrando es más fácil que aprender a filtrar después de haber publicado miles de notas irrelevantes.
No traduje el panel de administración. Traduje la superficie pública completa —encabezados, tarjetas, artículos, rutas, metadatos, zona horaria, formatos de fecha— y dejé el panel interno en español a propósito. Lo uso yo. Traducir lo que nadie más lee es trabajo que no le llega a ningún usuario.
No reusé la identidad del sitio hermano. Marca propia, propiedad de analítica propia, llave de indexación propia. Esto último por una razón práctica: la llave del sitio original estaba incrustada en el código del script de indexación, así que el clon la heredó y todos los envíos eran rechazados. Un valor que identifica al sitio no puede vivir escrito en una línea de código: se hereda en silencio y falla en silencio.
Un descubrimiento colateral: el cron que decía «listo» sin hacer nada
Mientras revisaba el servidor para el sitio nuevo encontré algo que llevaba cuatro meses roto en el sitio original y que nadie había reportado.
El proceso que actualiza los marcadores en vivo corría cada cinco minutos, terminaba sin error y escribía «completado» en el log. Pero guardaba cero partidos. El proveedor de datos había dejado de servir la temporada actual en el plan gratuito, el código caía a una temporada anterior y esa devolvía una lista vacía. Éxito del proceso, fracaso del resultado.
El rastro del daño: 806.432 líneas de error acumuladas en un archivo de log de 462 MB.
La lección quedó incorporada al sitio nuevo: un proceso automático no debe monitorearse por si terminó, sino por si produjo. «Corrió sin errores» y «hizo su trabajo» son dos afirmaciones distintas, y la primera se cumple perfectamente durante meses mientras la segunda es falsa.
Una trampa de verificación que casi me pasa
Al traducir el sitio barrí el HTML servido buscando restos de español. Salió limpio. No lo estaba.
El texto de los componentes que se ejecutan en el navegador no aparece en el HTML servido. El aviso de cookies, el formulario de suscripción, el pedido de notificaciones: todo eso vivía dentro de los paquetes de JavaScript, invisible para una revisión superficial de la página. Tuve que buscar directamente en el código fuente y en los paquetes compilados.
Si alguna vez verificas una traducción, una marca o un dato sensible en una aplicación moderna: lo que ves en la respuesta del servidor es una fracción de lo que ve el usuario.
El resultado
El segundo medio salió en vivo el 15 de agosto de 2026, con certificado propio, marca propia, dieciocho fuentes en inglés y el portón puesto desde el minuto cero.
- El primer artículo salió con 360 palabras, tres subtítulos específicos —no genéricos— e imagen optimizada en tres tamaños tomada de la fuente original.
- Costo de la corrida completa: US$0,0073.
- 13% de los titulares leídos llegan a ser evaluados por el modelo. El otro 87% se descarta gratis.
- Tope editorial: 20 artículos por día, deliberadamente por debajo de la capacidad técnica.
No es un sitio más grande que el primero. Es uno que decide qué no publicar antes de gastar en publicarlo.
La pregunta que se hace cualquier pipeline con IA no es «¿cuánto puedo producir?», sino «¿qué parte de lo que produzco merece existir?» —y esa pregunta se responde antes de la primera llamada al modelo, no después.
Preguntas frecuentes
¿Conviene usar un LLM para filtrar qué noticias procesar?
Casi nunca como primer filtro. Pagas cada rechazo igual que cada aceptación, y como los rechazos son la mayoría, terminas financiando descartes. Un filtro determinista —reglas, listas, heurísticas de dominio— cuesta cero y es reproducible. Deja el modelo para lo que solo él puede hacer: escribir. La regla es que cada peldaño del pipeline sea más barato que el siguiente.
¿Cómo calibro un filtro de relevancia sin adivinar?
Guarda una muestra grande de entradas reales de tus propias fuentes —yo usé 893 titulares— y ajusta el filtro contra ese lote hasta que las decisiones coincidan con las que tomarías a mano. Así el porcentaje que pasa deja de ser una corazonada y se vuelve un número medido. Después expresa el tope como configuración, no como código, para poder ajustarlo sin desplegar.
¿Publicar más artículos genera más tráfico?
No necesariamente, y conviene medirlo antes de asumirlo. En mi propio medio, el 86,8% de los artículos publicados no había recibido una sola visita. El volumen tiene un costo real —cómputo, almacenamiento de imágenes, mantenimiento— y un costo reputacional frente a los buscadores, especialmente con contenido generado por IA. Es más barato nacer filtrando que corregir el rumbo después de miles de publicaciones.
¿Cómo evito que un modelo devuelva JSON inválido?
Prohíbe en la instrucción los caracteres que pueden romper la estructura, en vez de limpiarlos después. En mi caso el modelo insertaba comillas rectas dentro del cuerpo del artículo y cerraba el campo antes de tiempo; la solución fue exigir comillas tipográficas en todo el contenido. Un carácter que no entra no necesita ser escapado.
¿Cómo evito que un modelo escriba con seguridad cosas falsas sobre un dominio?
Ánclalo con afirmaciones explícitas del dominio y del calendario, no de estilo: en qué fase está cada liga, qué ligas no deben confundirse, qué diferencia un anuncio oficial de un reporte. Cada anclaje suele nacer de un error concreto que ya viste. Escribir cinco líneas en el prompt cuesta una vez; corregir artículos publicados cuesta siempre.
¿Cómo sé si un proceso automático está realmente funcionando?
No basta con que termine sin errores. Encontré un proceso que llevaba cuatro meses corriendo cada cinco minutos, registrando «completado» y guardando cero resultados, porque el proveedor de datos había dejado de responder con datos útiles. Monitorea el resultado —¿hay registros nuevos?, ¿de cuándo es el último?— y no solo la ejecución. De paso, vigila el tamaño de los logs: ese fallo silencioso acumuló 806.432 líneas de error y 462 MB.
Soy Miller Millán. Construyo plataformas de software multi-cliente y opero los medios digitales que corren sobre ellas —incluido el que aparece en este caso. Trabajo en el cruce entre el periodismo y la ingeniería. ¿Estás armando algo parecido? Escríbeme.