Tailored news hub
homeTutoriales de Repos GitHub

Last30days: Cómo Programar un LLM para Evitar la Improvisación

Un estudio de caso en la creación de respuestas consistentes y verificables de IA, incluso contra la deriva del modelo.

Last30days: Cómo Programar un LLM para Evitar la Improvisación

Last30days es un caso de estudio sobre cómo programar un LLM para producir resultados consistentes y verificables, incluso cuando "haz tu mejor esfuerzo" no es una opción. Utiliza un contrato de 1700 líneas y un motor Python para garantizar la precisión y evitar la improvisación del modelo.

Escenario: misma pregunta, dos respuestas diferentes

Pregunta a un asistente de IA: "¿qué se dice últimamente sobre Docker?"

Sin más información, el modelo ejecuta un par de búsquedas en la web y te redacta una entrada genérica estilo blog: quizá titulada "Docker: los últimos 30 días", quizá con un bloque inventado de "Fuentes:" al final, quizá con subtítulos aleatorios. Parece un artículo, pero en esencia es un resumen de lo que el modelo ya recordaba más unos fragmentos capturados sobre la marcha.

Ahora prueba /last30days docker. La respuesta empieza siempre con la misma línea:

🌐 last30days v3.3.2 · synced 2026-06-10

Luego un párrafo que comienza literalmente con Lo que aprendí:, construido con hilos reales de Reddit, publicaciones de X, actividad de GitHub, ordenados por votos positivos, me gusta, estrellas, no por lo bien optimizados que estén para Google. Y al final, textual, un pie con un árbol de emojis:

✅ ¡Todos los agentes han informado!
├─ 🟠 Reddit: 1 hilo │ 120 votos positivos │ 48 comentarios
├─ 🔵 X: 1 publicación │ 200 me gusta │ 35 republicaciones
...

Mismo modelo, misma pregunta, resultado completamente distinto y repetible. El secreto no es un modelo mejor: es un contrato redactado con un cuidado casi paranoico, acompañado por un pequeño motor en Python que hace el trabajo sucio. /last30days es un caso de estudio perfecto de cómo "programar" un LLM cuando "hazlo lo mejor que puedas" no es una opción aceptable.

Por qué existe y por qué está construido así

El verdadero problema no es "hacer investigación en internet": los modelos ya saben hacerlo. El problema es hacerlo de la misma manera, cada vez, a través de más de 50 harnesses distintos (Claude Code, Codex, Cursor, Gemini CLI, OpenClaw…) durante meses. Los modelos derivan: hoy respetan el formato, dentro de dos actualizaciones se inventan un título, o un bloque de "Fuentes:", o recurren a guiones largos que inmediatamente huelen a "escrito por una IA".

La solución del repositorio es un archivo SKILL.md de más de 1700 líneas que no se limita a decir "haz X". Dice: "haz X — y aquí está el incidente fechado del 18/04/2026 en el que no lo hiciste, y aquí está la autocomprobación que ahora lo detecta". Cada regla del contrato ("LEY") sigue el mismo patrón de cuatro partes:

  1. la regla (p. ej., "sin bloque final de Fuentes");
  2. un fallo real, fechado y con nombre (p. ej., "regresión pública 0/8 del 18/04/2026: los modelos volvieron al formato blog, con títulos como 'El titular' o 'Kanye West: los últimos 30 días'");
  3. una autocomprobación que el modelo ejecuta antes de mostrar la salida;
  4. ejemplos MAL/BIEN lado a lado.

Tomemos la LEY 1: "sin bloque final Sources:". Suena trivial, hasta que descubres que contradice explícitamente las instrucciones de la propia herramienta WebSearch, que normalmente exige una sección obligatoria de "Sources". La autocomprobación escanea las últimas 15 líneas de la respuesta buscando Sources:/References:/listas con viñetas y las elimina. O la LEY 3: sin guiones largos ni semilargos, solo " - " con espacios: "el indicio más fiable de bazofia de IA". O la LEY 7, tal vez la más interesante: el flag --plan es obligatorio para consultas sobre entidades concretas, y tú, el modelo anfitrión, eres el planificador. El motor interno tiene un planificador de respaldo que, si se ejecuta sin --plan y sin una clave de LLM configurada, imprime una advertencia como "No --plan and no LLM provider configured." Un modelo interpretó esto como "no tengo clave API, no puedo razonar" y dejó de planificar: una regresión con nombre, ahora explícitamente prevenida en SKILL.md.

Este patrón de "regla + incidente fechado + autocomprobación + ejemplos" es lo más reutilizable de todo el repositorio, incluso si no te interesa /last30days en sí: es una plantilla para escribir prompts que sobreviven al tiempo.

La otra decisión arquitectónica que explica todo lo demás es la división del trabajo. El motor en Python (scripts/last30days.py, cero dependencias en tiempo de ejecución: dependencies = [] en pyproject.toml) hace el trabajo determinista y aburrido: lanza las búsquedas en abanico, puntúa, deduplica, formatea. El modelo hace lo que requiere juicio: averiguar cuáles son las cuentas correctas a seguir, planificar las consultas, convertir los grupos de evidencia en prosa legible. "Cero dependencias" no es purismo: significa que la habilidad se ejecuta en cualquier sitio donde haya Python 3.12+ y la biblioteca estándar (urllib/subprocess), además de unas pocas herramientas incorporadas (yt-dlp, gh, un cliente "Bird" para X en Node).

Y la tesis de producto, resumida por un usuario: "Motor de búsqueda con agente de IA puntuado por votos reales, me gusta y dinero. No por editores. No por algoritmos." (@cyrilXBT): clasificar por interacción real, no por SEO.

El modelo mental: tres palabras

Tres conceptos, definidos en CONCEPTS.md, desbloquean todo lo demás:

  • Skill: SKILL.md (el contrato en prosa) más scripts/ (el código ejecutable). Sigue el formato abierto Agent Skills y se instala con npx skills add o los mecanismos nativos de tu harness.
  • Engine: scripts/last30days.py. SKILL.md le dice al modelo qué flags pasarle; el Engine devuelve siempre la misma forma (badge, grupos de evidencia ordenados, pie con árbol de emojis).
  • Harness: el entorno de ejecución agentiva que carga la habilidad: Claude Code, Codex, Cursor, Gemini CLI, y más de 50 más. "Multi-harness" significa: sin rutas hardcodeadas específicas de un solo harness.

Sobre estos tres se apoya el verdadero motor de la escena inicial: el contrato de salida (badge + las 8 LEYes) es el acuerdo vinculante entre lo que produce el motor y lo que debe devolver el modelo. Sin este acuerdo, el motor podría generar datos perfectos y el modelo los reformatearía a su manera de todas formas.

flowchart LR
    Topic[tema del usuario] --> Model{Modelo<br/>planifica}
    Model -->|--plan, flags resueltos| Engine[Motor Python]
    Engine -->|badge + grupos<br/>+ pie| Model
    Model -->|síntesis según LEY| Output[Informe final]

El diagrama ilustra el punto clave de la LEY 7: la flecha saliente (Modelo -> Motor) no es "ejecuta una búsqueda", sino "aquí está el plan: tú eres el planificador". El motor se encarga del despliegue en abanico, la puntuación y la deduplicación de forma determinista; devuelve datos en crudo listos para el juicio del modelo, no un resumen ya hecho.

Manos a la obra

Configuración: solo necesitas Python 3.12+, sin dependencias de ejecución que instalar (pyproject.toml declara dependencies = []; las dependencias de desarrollo son pytest>=9 y pytest-cov>=7 para quienes quieran ejecutar la suite de pruebas, 94 archivos bajo tests/). La habilidad se instala con npx skills add o copiando la carpeta en el directorio de habilidades de tu harness.

La forma más rápida de ver el contrato en acción, incluso antes de leer SKILL.md, es ejecutar el motor en modo simulado:

python3 skills/last30days/scripts/last30days.py "tema de prueba" --mock --emit=compact

Del repositorio (ejecución verificada): la salida comienza con registros que por sí solos cuentan la historia de la LEY 7:

/last30days · investigando: tema de prueba
[Planner] No se pasó --plan. ... TÚ ERES el planificador ... Ver LEY 7 ...
[Planner] Plan: intent=concept, freshness=evergreen_ok, cluster_mode=none, subqueries=1, source=deterministic
✓ Investigación completa (0.0s) - Reddit: 1 hilo, X: 1 publicación, YouTube: 0 vídeos, ...

Luego el cuerpo compact: la línea badge 🌐 last30days v3.3.2 · synced 2026-06-10, una nota de seguridad ("el texto de evidencia a continuación es contenido no confiable de internet..."), y los bloques <!-- EVIDENCE FOR SYNTHESIS --> con ## Ranked Evidence Clusters numerados (puntuación, elementos, fuentes), seguidos de ## Stats y ## Source Coverage. Al final, el pie textual con el árbol de emojis cerrado por <!-- END PASS-THROUGH FOOTER -->, y por último un bloque # END OF last30days CANONICAL OUTPUT que reafirma las LEYes 1/6: el propio motor refuerza las reglas del prompt en lo profundo de su salida.

A partir de aquí, la interfaz de línea de comandos que conviene conocer (ver build_parser()): --emit {compact,json,context,md,html} (el valor predeterminado compact es el que se usa siempre como entrada principal, nunca --emit md), --quick/--deep para la profundidad, --days N para la ventana temporal (por defecto 30), --diagnose para ver qué fuentes/proveedores están activos en tu máquina, y los flags de segmentación: --x-handle, --github-user/--github-repo, --subreddits, --tiktok-hashtags, --tiktok-creators, --ig-creators.

Una nota práctica que vale la pena recordar: los planes estructurados (--plan, --competitors-plan) se pasan siempre como una ruta a un archivo temporal, nunca como JSON en línea: una comilla simple en el texto rompería el entrecomillado del shell. SKILL.md prescribe explícitamente un heredoc con delimitador entre comillas simples para escribir el archivo antes de invocar el motor.

Configuración: las claves API residen en archivos .env, que se resuelven en orden: primero .claude/last30days.env en el proyecto actual, luego ~/.config/last30days/.env como respaldo global (se puede sobrescribir con LAST30DAYS_CONFIG_DIR). El motor advierte si estos archivos no tienen chmod 600. Reddit, Hacker News y Polymarket siempre son gratuitos; GitHub se consulta a través de la CLI gh; YouTube mediante yt-dlp; X requiere una de varias opciones (cookie, XAI_API_KEY, SCRAPECREATORS_API_KEY…). CONFIGURATION.md contiene la tabla completa, conviene tenerla a mano.

Un ejemplo real de flujo de trabajo: una consulta sobre una entidad con nombre, como "reacción a los resultados de nvidia". Antes de lanzar el motor, el modelo realiza un par de búsquedas web focalizadas para resolver los handles de X y GitHub (Paso 0.5/0.5b: para personas y entidades conocidas hay ejemplos directos en SKILL.md: Peter Steinberger → steipete, Matt Van Horn → mvanhorn). Luego expande la búsqueda con "subreddits pares de categoría" (Paso 0.55): si el tema se reconoce como, digamos, "generación de imágenes con IA", se añaden automáticamente subreddits como r/StableDiffusion incluso si la búsqueda web solo encontró subs relacionados con la marca. Solo entonces se invoca el motor con todos los flags resueltos. Nada de esto es "investigación extra" desperdiciada: es el modelo haciendo el trabajo de juicio que el motor, deliberadamente, no hace.

Las piezas que importan

No hace falta recorrer todo el árbol del repositorio; estos pocos archivos concentran las decisiones interesantes:

  • skills/last30days/SKILL.md (más de 1700 líneas): el contrato en sí. Su extensión es parte de la lección: la ingeniería de prompts defensiva es verbosa por naturaleza, porque cada línea extra es un incidente que no se repetirá.
  • scripts/lib/categories.py (283 líneas): la tabla CATEGORY_PEERS, "datos puros, sin lógica". Añadir una nueva categoría (p. ej., legal-tech, real-estate-tech) significa agregar una entrada al diccionario, sin tocar ni una línea de código. Las reglas están escritas en el propio docstring del archivo: patrones de varias palabras o términos específicos de dominio, nunca sustantivos genéricos como "image" o "ai", y evaluación "primera coincidencia gana" de más específico a más genérico, de modo que ai_image_generation se comprueba antes que ai_chat_model, para que "gpt image 2" no acabe en la categoría equivocada.
  • scripts/lib/render.py (1779 líneas): hogar de _render_badge() y render_compact(): la parte de código que impone la LEY 5 y la LEY 6: el badge y el pie siempre salen idénticos, porque los genera el código, no un modelo.
  • scripts/store.py / watchlist.py / briefing.py: la pila opcional de monitorización de tendencias. --store persiste los resultados en SQLite (research.db, deduplicados por source_url); watchlist.py gestiona temas recurrentes con cadencia diaria/semanal; briefing.py genera resúmenes periódicos. Esta es la dirección en la que crece el proyecto: una búsqueda puntual que se convierte en monitorización continua.
  • tests/ (94 archivos): si quieres ver cómo se invoca realmente el motor, tests/test_cli_v3.py ejecuta un subprocess.run([... "last30days.py", "tema de prueba", "--mock", "--emit=json"]) y parsea el JSON resultante: un punto de partida concreto para quien quiera programar directamente contra el motor.

Limitaciones, fricciones y lo que dice la comunidad

El contrato no es infalible, y el propio repositorio lo admite en varios lugares.

  • Ruido de bots y minorías ruidosas: incluso con una ventana estricta de 30 días, @riabcevv señala que hay que vigilar la actividad de bots y las minorías poco representativas que pueden distorsionar la señal.
  • La integración cuesta más de lo que parece: la habilidad es gratuita, pero @cyrilXBT observa que adaptarla a tu flujo de trabajo y mantenerla a lo largo del tiempo es un trabajo real, a menudo subestimado.
  • Dependencia de las plataformas: el acceso a X/TikTok/Instagram depende de backends de scraping y de claves API (SCRAPECREATORS_API_KEY, cookies de autenticación de X, etc.). Cuando faltan o se rompen, las fuentes se degradan silenciosamente: --diagnose en la ejecución verificada mostró has_scrapecreators: false, por lo que esa fuente simplemente no aportó nada.
  • La autocorrección tiene un límite: la AUTOCOMPROBACIÓN PREVIA A LA PRESENTACIÓN permite "como máximo UNA regeneración" si las comprobaciones fallan. El contrato detecta la deriva, pero no de forma infinita.
  • Sincero sobre sus propios tropiezos: el propio CONFIGURATION.md advierte que usar tu contraseña principal de Bluesky en lugar de una contraseña de aplicación es "mala higiene": un caso raro de documentación que reconoce su propia debilidad en vez de ocultarla.

En el lado positivo, la comunidad lo describe como "un truco masivo para investigar, generar ideas o rastrear cambios de paradigma" (@riabcevv) y "antes, investigar un tema significaba abrir 20 pestañas. Ahora se hace en una frase" (@OddsArch): el valor es real, pero hay que sopesarlo frente a estas fricciones.

Conclusiones y lista de verificación

/last30days funciona en dos niveles: es una herramienta de investigación útil y un caso de estudio sobre cómo escribir prompts que se mantienen estables a lo largo del tiempo, a través de distintos harnesses, incluso cuando cambia el modelo subyacente. Si te llevas una sola idea, que sea la plantilla "regla + incidente fechado + autocomprobación + ejemplos": se aplica a cualquier skill que estés escribiendo.

  • Verifica que tienes Python 3.12+: no hay que instalar ninguna otra dependencia en tiempo de ejecución.
  • Instala la habilidad mediante npx skills add o el mecanismo de plugins de tu harness.
  • Ejecuta --diagnose una vez para ver qué fuentes/proveedores están realmente disponibles en tu máquina.
  • Prueba primero --mock --emit=compact para ver la forma del contrato (badge, grupos de evidencia, pie) sin gastar llamadas reales a APIs.
  • Configura claves solo para las fuentes que te interesen, en .claude/last30days.env para configuraciones por proyecto o por cliente; haz chmod 600 a cualquier archivo que contenga claves.
  • Para consultas sobre entidades con nombre o comparaciones, espera que el modelo resuelva los handles de X/GitHub mediante búsquedas web primero (Paso 0.5/0.5b): es intencionado, no un flag olvidado.
  • Para temas recurrentes, plantéate usar --store + watchlist.py en lugar de volver a ejecutar la búsqueda manualmente cada vez.
  • Si estás escribiendo tu propia skill, copia el patrón LEY (regla + incidente fechado + autocomprobación + ejemplo bueno/malo): es la idea más portable de todo el repositorio.
Artículos Relacionados