AgentGrade
EnglishEspañol日本語中文
← Base de Conocimientos

Negociación de Contenido para Agentes de IA

¿Qué es la negociación de contenido para agentes?

La negociación de contenido significa servir distintas respuestas desde la misma URL según lo que pida el cliente. Para los agentes de IA, esto se traduce en devolver markdown o texto plano —el formato que los LLM ya procesan bien— cuando la solicitud proviene de un agente, mientras se sigue sirviendo HTML a los navegadores. Misma URL, distinta representación, según RFC 9110 §12.5.

Por qué importa

Los agentes basados en LLM normalmente reciben solo el cuerpo de la respuesta, no los encabezados HTTP, códigos de estado ni cadenas de redirección. Servir HTML obliga al agente a parsear la estructura del DOM, descartar el layout y la navegación — desperdiciando tokens antes de que el modelo vea algo útil. Una respuesta de texto limpia le da al modelo el contenido directamente.

La trampa del orden de preferencia de Accept

El error más común en negociación de contenido es tratar el encabezado Accept como una simple búsqueda de subcadena. Por ejemplo, la herramienta WebFetch de Claude Code envía:

Accept: text/markdown, text/html, */*

Esto es el cliente diciendo, en orden de preferencia: "Prefiero markdown si lo tienes, si no HTML, si no cualquier cosa." Una verificación ingenua como if (accept.includes('text/html')) ve text/html en la cadena y sirve HTML — ignorando que text/markdown estaba listado primero.

Según RFC 9110 §12.5.1, cuando no se especifican valores q, el orden de los tipos de medios expresa preferencia. Una implementación correcta parsea la lista Accept, aplica los valores q y elige el tipo más a la izquierda que el servidor puede servir.

Qué verifica AgentGrade

El UA de agente recibe contenido no-HTML — Enviamos User-Agent: claude-code/1.0.0 con Accept: text/markdown, text/html, */* a tu homepage. La verificación pasa si sirves text/markdown, text/plain o application/json con cuerpo ≥20 bytes. Los sitios que hacen búsqueda de subcadena en Accept y sirven HTML fallan aquí.

Accept: JSON devuelve JSON — Enviamos Accept: application/json y verificamos JSON válido.

Accept: text devuelve texto — Enviamos Accept: text/plain y verificamos texto plano o markdown.

Accept: markdown devuelve markdown — Enviamos Accept: text/markdown y verificamos markdown o texto plano.

Vary: Accept establecido — Cuando negocias, la respuesta debe incluir Vary: Accept para que las cachés compartidas indexen las entradas correctamente.

Cómo implementarlo correctamente

Usa un negociador Accept apropiado en lugar de búsqueda de subcadena:

// Express — req.accepts usa el paquete negotiator internamente
app.get('/', async (req, res) => {
  res.vary('Accept');
  const best = req.accepts(['text/html', 'text/markdown', 'text/plain', 'application/json']);
  if (best === 'text/markdown' || best === 'text/plain') {
    return res.type(best).send(await buildLlmsTxt());
  }
  if (best === 'application/json') {
    return res.json({ name: 'Tu Servicio', api: '/openapi.json' });
  }
  res.sendFile('index.html');
});

Otros ecosistemas:

En línea vs redirección — elige en línea

Hay dos formas de servir contenido amigable para agentes. En línea es mejor:

En línea (recomendado): La misma URL sirve distintos cuerpos según Accept.

GET / → 200 OK
  Content-Type: text/html (navegador) | text/markdown (agente)

Redirección (legado): Envía a los agentes a /llms.txt.

GET / → 302 Found, Location: /llms.txt
GET /llms.txt → 200 OK

En línea gana porque: (1) una solicitud en lugar de dos — la mitad de la latencia; (2) la URL que el agente reporta al usuario es la URL sobre la que se le preguntó, no el destino de la redirección; (3) el caching es más limpio con Vary: Accept. La ruta /llms.txt sigue existiendo para herramientas que la solicitan directamente — ambas rutas llaman a la misma función de contenido.

Vary: Accept es crítico

Siempre que la misma URL devuelva distintos cuerpos según Accept, establece Vary: Accept. Esto le dice a las cachés compartidas (CDNs, proxies, navegadores) que la clave de caché debe incluir el valor del encabezado Accept.

Sin él, un CDN podría cachear la respuesta markdown de una solicitud de agente y servirla a una visita de navegador — o al revés. El encabezado Vary es lo único que evita que las entradas de caché sean intercambiables cuando los cuerpos no lo son.

User-Agents conocidos de agentes de IA

AgenteUser-AgentPropósito
ClaudeBotMozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)Crawler de entrenamiento de Anthropic
Claude-UserMozilla/5.0 ... (compatible; Claude-User/1.0; +Claude-User@anthropic.com)claude.ai web_fetch, lecturas de página de Claude API web_search
Claude-SearchBot(cadena no publicada)Crawler del índice de búsqueda de Anthropic
claude-codeclaude-code/<version>Herramienta WebFetch de Claude Code CLI
ChatGPT-UserMozilla/5.0 ... (compatible; ChatGPT-User/1.0; +https://openai.com/bot)Navegación ChatGPT iniciada por usuario
OAI-SearchBotMozilla/5.0 ... (compatible; OAI-SearchBot/1.3; +https://openai.com/searchbot)Índice de búsqueda de OpenAI
OAI-AdsBotMozilla/5.0 ... (compatible; OAI-AdsBot/1.0; +https://openai.com/adsbot)Crawler de anuncios de OpenAI
GPTBotMozilla/5.0 ... (compatible; GPTBot/1.3; +https://openai.com/gptbot)Crawler de entrenamiento de OpenAI
PerplexityBotMozilla/5.0 ... (compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)Crawler de resultados de búsqueda de Perplexity
Perplexity-UserMozilla/5.0 ... (compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)Solicitudes de Perplexity iniciadas por usuario
Google-Extended(usa UA de Googlebot)Entrenamiento de Google Gemini, controlado vía robots.txt

Web Bot Auth — la próxima señal

Una lista creciente de agentes (ChatGPT Agent confirmado hoy; Anthropic, Perplexity, Google esperados) firma criptográficamente sus solicitudes según RFC 9421 HTTP Message Signatures. La señal es el encabezado Signature-Agent:

Signature-Agent: "https://chatgpt.com"
Signature-Input: sig=("@authority" "signature-agent"); keyid="..."; tag="web-bot-auth"
Signature: sig=...

Si ves Signature-Agent en una solicitud entrante, trata al cliente como un agente conocido incluso si el UA parece un navegador. Para verificación completa, descarga el JWKS desde el host nombrado en Signature-Agent (/.well-known/http-message-signatures-directory) y verifica la firma con el paquete web-bot-auth de npm. Para fines de negociación de contenido, la presencia del encabezado por sí sola es una señal suave suficiente.

Más información

Tipo preferido vs no-HTML — el siguiente listón

"El UA de agente recibe contenido no-HTML" es una verificación básica: ¿el servidor sirvió algo distinto de HTML? "Devuelve el Content-Type preferido" es la versión estricta: ¿coincide el Content-Type de la respuesta con el tipo líder señalado por el cliente?

Por ejemplo, cuando el cliente envía Accept: text/markdown, text/html, */*:

El escáner ejecuta cuatro pruebas: markdown primero (el patrón de Claude Code / Cursor), HTML primero con markdown listado en segundo lugar (el patrón de navegador / ChatGPT Agent — detecta sitios que ignoran el orden del cliente y usan preferencia del lado del servidor en su lugar), valores q explícitos favoreciendo HTML sobre markdown (detecta sitios que ignoran los valores q por completo), y JSON primero (patrón de descubrimiento programático). Las cuatro deben pasar. Hoy la etiqueta Content-Type es mayormente decorativa para los agentes basados en LLM — analizan los bytes del cuerpo independientemente del tipo MIME. Pero las extensiones de IA basadas en navegador y las herramientas MCP emergentes se ramifican según Content-Type, y la brecha se ampliará a medida que madure el ecosistema.

La solución es un cambio de una línea en tu handler: establece el Content-Type de la respuesta desde el tipo negociado, no un valor codificado. Si tu código ya devuelve text/plain tanto para Accept: text/plain como para Accept: text/markdown, ramifica según el tipo negociado y etiqueta apropiadamente.

Esta verificación es requerida — fallarla cuesta puntos en el grupo de Negociación de contenido.

Diagnóstico de tu bug — valores q y los tres patrones

Cómo funcionan los valores q

Cuando un cliente envía múltiples tipos en Accept, puede adjuntar valores q (factores de calidad) entre 0.0 y 1.0 para expresar preferencia relativa:

Accept: text/markdown;q=1.0, text/html;q=0.5, */*;q=0.1

Significa: "Realmente quiero markdown. Tomaré HTML como respaldo. Cualquier otra cosa es último recurso."

Cuando no se da un valor q, predetermina a 1.0. Entonces Accept: text/markdown, text/html, */* significa que los tres son igualmente preferidos — y el orden en el encabezado rompe el empate. Un servidor correcto elige markdown.

Un negociador Accept adecuado (Express req.accepts(), el paquete negotiator de npm, Python werkzeug, Go goautoneg) maneja todo esto automáticamente: analiza valores q, respeta el orden en empates, elige la mejor coincidencia que el servidor puede servir.

Los tres patrones de bug que vemos en producción

Si tu sitio falla la verificación "El UA de agente recibe contenido no-HTML", la causa es casi siempre una de estas:

Patrón 1: Coincidencia de subcadenas. Código que verifica si el encabezado Accept contiene un tipo, en un orden if-else fijo. Ejemplo:

// INCORRECTO — el orden de las verificaciones, no el orden en Accept, gana
if (accept.includes('text/html')) return html;
else if (accept.includes('text/markdown')) return markdown;

El cliente envía Accept: text/markdown, text/html → el servidor devuelve HTML porque text/html está en la cadena. El orden de preferencia del cliente se ignora por completo.

Patrón 2: Valor predeterminado del framework que sirve HTML en */*. Algunos frameworks tratan */* en Accept como una licencia para recurrir a HTML, incluso cuando se enumeran tipos no-HTML explícitos primero. respond_to de Rails 8 es un ejemplo notable:

Accept: text/markdown, */*       → Rails devuelve HTML (markdown ignorado)
Accept: text/markdown, text/html → Rails devuelve markdown (sin */*, respeta orden)

Patrón 3: Orden de preferencia interno del servidor + valores q ignorados. El servidor tiene su propia lista de prioridad (a menudo codificada en algún lugar) y elige el tipo del encabezado Accept que sea más alto en la lista del servidor — no en la lista del cliente. Los valores q no se analizan en absoluto:

Accept: text/plain;q=0.9, text/html;q=0.5 → devuelve HTML
                                            (el servidor prefiere html a pesar de los
                                             valores q que favorecen explícitamente plain)

La prueba inequívoca del patrón 3 es que los valores q se ignoran. Si el mismo sitio devuelve HTML para la fila anterior y markdown para Accept: text/plain, text/markdown (markdown ganó a pesar de que plain está listado primero), es patrón 3.

Una prueba diagnóstica rápida

Ejecuta estos cinco comandos curl contra tu homepage. El patrón en las respuestas te dice qué bug tienes:

curl -sI -H "Accept: text/markdown" TU_SITIO/
curl -sI -H "Accept: text/markdown, text/html, */*" TU_SITIO/
curl -sI -H "Accept: text/plain, text/markdown" TU_SITIO/
curl -sI -H "Accept: text/markdown, */*" TU_SITIO/
curl -sI -H "Accept: text/plain;q=0.9, text/html;q=0.5" TU_SITIO/

La receta de solución es la misma en los tres casos: reemplaza la lógica de selección ad-hoc con un negociador Accept adecuado de la lista anterior.

Inline vs redirección 302 — qué hacer

Dos patrones para servir contenido amigable para agentes en tu homepage:

Inline — la misma URL devuelve diferentes cuerpos según el encabezado Accept.

GET /  + Accept: text/html      →  200 + HTML
GET /  + Accept: text/markdown  →  200 + markdown

Redirección — el servidor envía las solicitudes de agentes a una URL canónica separada.

GET /  + Accept: text/markdown  →  302, Location: /llms.txt
GET /llms.txt                   →  200 + markdown

Usa inline. Es la mejor práctica documentada en RFC 9110 §12.2, que enumera explícitamente las desventajas de la negociación basada en redirecciones (reactiva): "sufre de transmitir una lista de alternativas... y necesitar una segunda solicitud" y "no define un mecanismo para soportar selección automática."

Todos los principales sitios con negociación de contenido que probamos usan inline:

Por qué inline gana concretamente

  1. La mitad de la latencia. Una solicitud HTTP en lugar de dos. La multiplexación de HTTP/2 y HTTP/3 no elimina el costo de la redirección.
  2. Fidelidad de URL. La URL que el agente reporta al usuario es la URL sobre la que preguntó. Con 302, el agente termina en /llms.txt — una URL diferente.
  3. Caching más limpio. Inline con Vary: Accept permite que las cachés almacenen ambas representaciones bajo una clave de URL.
  4. Sin URL mágica solo-para-agentes. Inline mantiene el espacio de URLs unificado.

Qué verifica AgentGrade

La verificación Inline content negotiation envía una solicitud con forma de agente (claude-code/1.0.0 UA con Accept: text/markdown, text/html, */*) y verifica que la respuesta no termine en una URL diferente a la que terminaría una solicitud de navegador. Las redirecciones universales (HTTPS, normalización de barra) no se penalizan — solo las redirecciones específicas para agentes.

Cómo solucionarlo

Reemplaza tu lógica 302 con negociación inline. Ejemplo en Express:

app.get('/', async (req, res) => {
  res.vary('Accept');
  const best = req.accepts(['text/html', 'text/markdown', 'text/plain']);
  if (best === 'text/markdown') {
    return res.type('text/markdown').send(await buildLlmsTxt());
  }
  res.sendFile('index.html');
});

La ruta /llms.txt aún puede existir como URL separada — ambas rutas llaman a la misma función de contenido.

Esta verificación es emergente (opcional) hoy — todavía no penaliza a los sitios que usan 302. Graduará a requerida una vez que la adopción de inline en la industria sea lo suficientemente amplia.