BookinglyTech News
Infraestructura

Servir markdown a agentes desde el gateway, sin tocar el origen

Un shim de 200 líneas y una ruta en el gateway bastan para que los agentes reciban markdown donde el origen solo sirve HTML, pero la táctica falla en silencio en dos escenarios habituales.

4 min de lecturaDev.to0 vistas

Los agentes de IA parsean HTML mal y caro. La solución habitual es que el sitio emita markdown por su cuenta: un index.md por página, un llms.txt, cambiar el build. Funciona cuando el sitio es tuyo. No funciona con la documentación heredada, la base de conocimiento de un proveedor ni la wiki interna que nadie va a redesplegar este trimestre.

La alternativa que plantea el autor de un repositorio de ejemplo es mover la decisión al gateway. Un origen que sirve solo HTML, un shim de unas 200 líneas y una ruta de agentgateway que elige entre uno y otro según la cabecera Accept. El origen no se modifica ni se entera de nada. El código está en agent-content-negotiation.

El gateway compara una regex contra la cabecera Accept de la petición. Si menciona text/markdown, la petición va al shim, que hace fetch al origen y convierte. Todo lo demás va directo al origen. Se añade Vary: Accept para que las cachés no sirvan una variante a la audiencia equivocada.

La misma página, mismo origen: 894 bytes en HTML y 420 en markdown. Menos de la mitad, y lo que queda no incluye nav, etiquetas script ni footer que el agente tenga que leer y descartar.

Las dos formas de fallar en silencio

La primera es la que muerde de verdad. Los agentes piden */*, que es el default de curl y de bastantes clientes HTTP de agentes. */* no menciona markdown, así que la ruta no coincide y el agente recibe HTML. Sin error, sin 406, sin nada raro en los logs: la petición está bien formada y el 200 es válido. Uno concluye que funciona porque al probar escribe Accept: text/markdown a mano, pero sus agentes no lo hacen nunca. Si se despliega esto, lo primero no es celebrar la ruta markdown, sino mirar la distribución real de cabeceras Accept que llegan y ver cuánto del tráfico de agentes son wildcards.

La segunda: una regex no es negociación de contenido. Un cliente puede mandar Accept: text/html, text/markdown;q=0.1. Está diciendo que prefiere HTML y que markdown es el último recurso. La regex ve presencia, no preferencia, así que sirve markdown igual. Es una limitación real de hacerlo en la capa de ruta en vez de en una aplicación que parsee bien la cabecera, y conviene saberlo antes de decir que el gateway negocia contenido: hace routing que se le parece.

El bug que se colgó su propio autor

El shim elimina el chrome del sitio abriendo una región de skip al encontrar una etiqueta de chrome y cerrándola en su etiqueta de cierre. <link> es un elemento vacío: no tiene cierre. La región se abría y nunca se cerraba, el conversor se comía el resto del documento y devolvía un documento vacío con un 200, Content-Type: text/markdown y un salto de línea de cuerpo. Todas las comprobaciones de estado pasaban. El agente recibe una página que existe y no dice nada, y lo más probable es que concluya que la documentación está vacía en vez de que el gateway está roto.

La lección general: separar etiquetas contenedoras de elementos vacíos al recorrer HTML — <link>, <meta>, <img>, <br>, <hr> no cierran nunca — y comprobar el cuerpo, no el estado. Un 200 con el content type correcto no es prueba de que se haya servido algo.

Hay un detalle menor que cuesta diez minutos: agentgateway pone en minúsculas los nombres de cabecera que se inyectan desde la config. X-Served-Variant vuelve como x-served-variant. Los nombres de cabecera son insensibles a mayúsculas, así que nada está mal, pero un test que busque la forma capitalizada falla contra una respuesta completamente correcta.

Probarlo

Todo va con Docker y Compose v2, sin nada más. El shim usa solo la librería estándar de Python. El autor lo validó contra agentgateway v1.5.0 y python:3.12-slim sobre macOS arm64.

git clone https://github.com/themsquared/agent-content-negotiation
cd agent-content-negotiation
docker compose up -d --build
./scripts/demo.sh

El resultado esperado son 15 aserciones pasadas y 0 fallidas. El autor reejecutó la suite al escribir la nota, no copió del README.

Merece la pena aunque solo sea por los dos avisos: el reparto real de Accept en el tráfico de agentes y la diferencia entre enrutar por cabecera y negociar de verdad. Cualquiera que tenga delante una wiki heredada o un portal de documentación de proveedor puede montarlo sin tocar el backend, siempre que asuma que la ruta no distingue preferencias y que la mitad de sus agentes quizá nunca la pidan.