Перейти к основному содержимому
БЛОГ

Markdown для ИИ-агентов: правильное content negotiation

Одна демо-страница Cloudflare сжалась с 12 345 HTML-токенов до 725 в Markdown. Отдавайте его ИИ-агентам через Accept: text/markdown, верный Content-Type и Vary.

Автор: IMozzОбновлено 2026-06-07
Serve Markdown to AI agents — aiSiteReady

Отдавать Markdown ИИ-агентам — это не новый протокол, а обычное content negotiation в HTTP. Агент шлёт Accept: text/markdown, ваш сервер отвечает Content-Type: text/markdown; charset=utf-8 и сообщает кэшам Vary: Accept. Вот и весь механизм. Медиатип text/markdown зарегистрирован ещё в RFC 7763, а правила — это обычный RFC 9110 (IETF). Никакого ИИ-специфичного стандарта не нужно.

Воспринимайте это как один слой в стеке готовности, а не отдельный трюк. Сначала бот должен дойти до страницы и прочитать её без запуска JavaScript. Markdown-представление делает это чтение чище. Оно отдаёт модели ваш текст вместо DOM, забитого навигацией, стилями и виджетами. Это естественный следующий шаг после того, как вы перечислите .md-файлы в llms.txt: там вы указываете на Markdown, здесь — реально его отдаёте.

Ключевые выводы

  • Markdown для агентов — это стандартное content negotiation поверх зарегистрированного медиатипа, а не новый ИИ-протокол (IETF RFC 7763).
  • На одной демо-странице Cloudflare HTML оценили в 12 345 токенов, а Markdown — в 725. Это пример для одной страницы, а не средняя по индустрии (Cloudflare).
  • Три способа отдачи: .md-варианты URL, согласование на том же URL в рантайме или экспорт на этапе сборки. Гибрид часто лучше любой одиночной стратегии (Vercel).
  • Vary: Accept сам по себе спасает не на каждом CDN — CloudFront и Cloudflare требуют явной настройки ключа кэша, иначе кэш «отравляется».
  • Поломки почти всегда одни и те же: HTML, помеченный как Markdown, отсутствие Vary, отсутствие charset и soft 404. aiSiteReady проверяет это и оценивает сайт от 0 до 100.

Почему ИИ-агенты предпочитают Markdown, а не HTML?

Это операционное предпочтение, а не правило. Ни один RFC не требует, чтобы агенты «предпочитали Markdown». Модели, пайплайны извлечения и инструменты браузинга в основном извлекают и обрабатывают текст, а не DOM-деревья. Поэтому и Cloudflare, и Vercel рекомендуют отдавать агентам Markdown, и часть ИИ-клиентов уже запрашивает его напрямую (Cloudflare, Vercel).

Самый явный выигрыш — плотность токенов. В собственном примере Cloudflare для одной документационной страницы HTML оценили в 12 345 токенов, а Markdown — в 725, примерно на 94% меньше (Cloudflare). Считайте это демонстрацией одной страницы, а не средней величиной. Анонсирующий пост Cloudflare приводит более скромную цифру: 16 180 HTML-токенов против 3 150 в Markdown, примерно на 80% меньше (Cloudflare).

Сравнение бок о бок: карточка HTML-страницы, где nav, сайдбар, cookie-баннер, скрипты и футер выделены как шум и помечены 12 345 токенами, рядом компактная карточка Markdown только с заголовками и текстом, помеченная 725 токенами, и значок сокращения на 94% между ними.

Почему это важнее, чем просто трафик? Каждое меню, футер, cookie-баннер и клиентский виджет, который читает модель, — это контекст, потраченный не на то. RFC 7763 проводит линию чётко: HTML — формат публикации, Markdown — формат письма (IETF). Отдать агенту уже чистый текст проще и предсказуемее, чем заставлять его каждый раз восстанавливать основной контент из разметки.

Как работает content negotiation для text/markdown?

Семантика полностью стандартная. Accept — это заголовок запроса, в котором клиент перечисляет приемлемые медиатипы. Сервер выбирает представление и объявляет его через Content-Type. Веса q идут от 0 до 1: q=1 — максимальный приоритет, q=0 означает «неприемлемо», а отсутствие q по умолчанию равно 1 (IETF RFC 9110).

Схема content negotiation. Запрос клиента с Accept: text/markdown приходит к развилке — нужен ли Markdown и есть ли он у ресурса? Если да, сервер возвращает 200 с Content-Type text/markdown; charset=utf-8 и Vary: Accept. Иначе по умолчанию 200 text/html с Vary: Accept. Если приемлемого представления нет и сервер не делает fallback, он возвращает 406 Not Acceptable с Vary: Accept.

Две детали сбивают с толку. Во-первых, charset обязателен для text/markdown. RFC 7763 говорит это прямо, так что всегда шлите ; charset=utf-8. Во-вторых, Vary: Accept выполняет двойную работу. Он сообщает кэшам, что ответ можно переиспользовать только для запросов с тем же Accept, что фактически расширяет ключ кэша. Он же сигнализирует, что тело выбрано через negotiation (IETF RFC 9110). Вот тот же URL, отвечающий на три разных запроса.

GET /guide HTTP/1.1
Accept: text/markdown, text/html;q=0.8

HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Cache-Control: public, s-maxage=600

# Guide
Page text...
GET /guide HTTP/1.1
Accept: application/json

HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
Vary: Accept

No acceptable representation for this resource.

Когда сервер не может удовлетворить Accept и не хочет давать fallback, корректный статус — 406 Not Acceptable. Но RFC 9110 также разрешает серверу проигнорировать предпочтение и отдать дефолтное представление вместо 406. Так что это решение политики, а не автоматическое поведение (IETF RFC 9110). RFC 7763 определяет необязательный параметр variant для flavor Markdown, а RFC 7764 регистрирует конкретные flavor. Это лишь идентификатор, а не настоящий механизм совместимости, поэтому большинство публичных деплоев его не используют.

Какие есть три способа отдавать Markdown?

Практических паттернов три, и правильный зависит от того, насколько вы контролируете свой CDN. Отдельные .md-файлы — самый безопасный и кэшируемый вариант. Согласование на том же URL — самый чистый, но самый чувствительный к кэшу. Экспорт на этапе сборки — самый предсказуемый для статических сайтов. Vercel рекомендует поддерживать и .md-эндпоинты, и negotiation, потому что многие агенты вообще не шлют Accept: text/markdown (Vercel).

ПодходСложностьКэшированиеЛучше дляКогда выбирать
.md-варианты URLНизкаяСамое простоеСтатический хостингНужен быстрый и безопасный запуск
Согласование на том же URLСредняя/высокаяСамое капризноеAPI-логикиВы контролируете origin и CDN
Экспорт на этапе сборкиСредняяОчень предсказуемоеDocs / SSGКонтент из единого источника

.md-варианты URL дают каждой HTML-странице соседа, например /docs/intro и /docs/intro.md. Диагностика тривиальна, статический хостинг работает «из коробки», и вы никогда не зависите от Vary. Цена — дублирование URL, поэтому держите HTML каноническим, а Markdown рекламируйте как альтернативу. В Next.js это делает rewrite плюс Route Handler. Обработчики App Router используют стандартный Web API Request/Response (Next.js).

// app/api/md/[...path]/route.ts
export async function GET(req: Request, { params }) {
  const { path } = await params;
  const doc = await loadDocAsMarkdown(path.join('/'));
  if (!doc) return new Response('Not found', { status: 404 });

  return new Response(doc.body, {
    status: 200,
    headers: {
      'Content-Type': 'text/markdown; charset=utf-8',
      'Link': `</docs/${path.join('/')}>; rel="canonical"`,
    },
  });
}

Затем объявите альтернативу на HTML-странице: <link rel="alternate" type="text/markdown" href="/docs/intro.md" />.

Согласование на том же URL позволяет человеку и агенту обращаться по одному адресу, пока сервер выбирает формат. Архитектурно это самый чистый вариант: один ресурс, без дублей в навигации. Загвоздка — кэширование. Ответ обязан нести Vary: Accept, иначе первый пришедший вариант отравляет кэш для всех остальных. Обработчик на Express передаёт логику.

app.get('/guide', async (req, res) => {
  res.vary('Accept');
  const page = await loadPage('guide'); // { html, markdown }
  if (!page) return res.status(404).type('text/plain').send('Not found');

  const best = req.accepts(['text/markdown', 'text/html']);
  if (best === 'text/markdown')
    return res.type('text/markdown; charset=utf-8').send(page.markdown);
  if (best === 'text/html')
    return res.type('text/html; charset=utf-8').send(page.html);
  return res.status(406).type('text/plain').send('No acceptable representation');
});

Экспорт на этапе сборки публикует и HTML, и Markdown из одного источника во время сборки. Нет рантайм-конвертера, кэш максимально предсказуем, а артефакты можно проверить до деплоя. Hugo считает это нативным паттерном через несколько output-форматов (Hugo).

[mediaTypes."text/markdown"]
  suffixes = ["md"]
[outputFormats.MARKDOWN]
  mediaType = "text/markdown"
  isPlainText = true
  permalinkable = true
[outputs]
  page = ["HTML", "MARKDOWN"]

Почему Vary: Accept важен на CDN?

Контент, зависящий от Accept, безопасен только тогда, когда кэш реально различает варианты, а реальное поведение CDN расходится со спецификацией. Стандартный ответ — Vary: Accept. Но Fastly трактует его как вторичный ключ кэша (Fastly), CloudFront по умолчанию не кэширует по заголовкам запроса (AWS), а Cloudflare вообще не учитывает общий Vary при кэш-решениях — поэтому ему нужен кастомный ключ кэша или Worker, чтобы различать Markdown и HTML (Cloudflare). Так что фраза «я выставил Vary: Accept, значит всё безопасно» верна лишь отчасти.

Вот failure mode, который Vercel описывает прямо. Агент первым запрашивает URL с Accept: text/markdown, origin честно возвращает Markdown, CDN кэширует этот объект как обычный /guide, и следующий браузер получает Markdown вместо страницы (Vercel). На CloudFront это поведение по умолчанию, потому что без cache policy он вообще не варьирует кэш по заголовкам.

Когда я строил проверку Markdown в aiSiteReady, именно на эту ловушку я натыкался чаще всего. На дефолтной политике CloudFront Markdown-ответ первого агента кэшировался как обычный /guide и затем отдавался каждому браузеру следом. Код origin был верным; багом был ключ кэша.

Последовательность отравления кэша в четыре шага: ИИ-агент запрашивает /guide с Accept text/markdown, origin корректно возвращает Markdown, CDN без Vary кэширует это как обычный объект /guide, и затем браузер получает сырой Markdown вместо HTML — отмечено как неверный результат.

Исправления зависят от CDN. На CloudFront добавьте Accept в ParametersInCacheKeyAndForwardedToOrigin вашей cache policy. Учтите, что response-headers policy не чинит ключ кэша: она лишь меняет заголовки, которые CloudFront возвращает клиентам, и применяется к любому ответу — из кэша он или из origin (AWS). На Fastly сначала нормализуйте Accept до двух бакетов, потому что перестановок Accept огромное количество и они могут просадить hit ratio (Fastly). На Cloudflare добавьте кастомный ключ кэша или используйте managed-функцию Markdown-for-Agents. Правило простое: если не уверены в своём CDN, отдавайте .md-соседей.

Какие ошибки ломают Markdown для агентов?

Почти любой сломанный деплой попадает в один и тот же короткий список. Каждая ошибка прямо вытекает из правил Content-Type / Accept / Vary или из того, как поисковики трактуют статус-коды.

ОшибкаКак обнаружитьКак исправить
HTML-тело, помеченное как text/markdowncurl -i показывает заголовок, но тело начинается с <!doctype html>Верните настоящий Markdown или исправьте Content-Type
Negotiation без Vary: AcceptСравните два значения Accept через CDNДобавьте Vary: Accept к обоим вариантам ответа
Отсутствует charsetПроверка заголовковОтдавайте Content-Type: text/markdown; charset=utf-8
Отсутствующий .md возвращает 200curl -i /page.md, Search ConsoleВерните 404 или 410, а не «красивую» 200-страницу
Неверный статус для неподдерживаемых типовПроверьте Accept: application/jsonВозвращайте 406, когда не делаете fallback
Canonical только на HTMLПроверьте заголовки на .mdОбъявите .md альтернативой; добавьте canonical через заголовок Link

Строка про soft 404 важнее всего для SEO. Google определяет soft 404 как страницу, контент которой говорит «не найдено», тогда как сервер всё равно отвечает статусом 2xx вроде 200 OK (Google). Если .md-ресурса больше нет, возвращайте 404 или 410. Если же сломался сам слой negotiation, это 5xx — никогда не 200 с заглушкой. А если ваши факты появляются только после запуска клиентского JavaScript, ни один статический экспорт Markdown их не захватит.

Как проверить свою настройку Markdown?

Проверяйте сразу две плоскости: заголовки negotiation и само тело. Несколько вызовов curl подтвердят статус, Content-Type, наличие Vary: Accept и то, что тело соответствует заявленному типу. Запускайте их по публичному edge-URL, а не только по localhost — большинство багов negotiation живёт в кэше, а не в коде origin.

# только заголовки
curl -sSI -H 'Accept: text/markdown' https://example.com/guide
# полное тело — должно быть Markdown, а не <html>
curl -sS  -H 'Accept: text/markdown' https://example.com/guide | head -20
# негативный тест: неподдерживаемый тип должен дать 406 (или fallback по политике)
curl -sSI -H 'Accept: application/json' https://example.com/guide

Встройте это в CI как smoke-тест, чтобы регрессия не уехала в прод. Проверка маленькая и ловит частые поломки до продакшена.

#!/usr/bin/env bash
set -euo pipefail
URL="${1:?usage: check-md.sh https://example.com/page}"
HEADERS="$(curl -sSI -H 'Accept: text/markdown' "$URL")"
BODY="$(curl -sS -H 'Accept: text/markdown' "$URL")"
grep -qi '^content-type: text/markdown;.*charset=utf-8' <<<"$HEADERS" || { echo 'FAIL: Content-Type'; exit 1; }
grep -qi '^vary: .*accept'                              <<<"$HEADERS" || { echo 'FAIL: missing Vary'; exit 1; }
grep -qi '<html\|<!doctype'                             <<<"$BODY"    && { echo 'FAIL: body is HTML'; exit 1; }
echo OK

Одна честная оговорка, прежде чем вкладываться по-крупному: поддержка Accept: text/markdown у агентов всё ещё неравномерна. Многие агенты не шлют этот заголовок, и именно поэтому гибрид (.md-файлы плюс negotiation) надёжнее ставки на один путь (Vercel). Держите агентский Markdown консервативным, потому что flavor-расширения стандартизованы не одинаково у разных потребителей.

Как проверить весь сайт сразу?

Проверить один маршрут вручную — нормально. Проверять каждый шаблон на каждом релизе, по Markdown-negotiation и по слоям под ним, — нет. Эту работу делает aiSiteReady. Он загружает ваш сайт так, как это сделал бы агент, и запускает тест Markdown-negotiation внутри блока доступности контента, возвращая приоритизированные исправления и воспроизводимое HTTP-доказательство — как часть оценки от 0 до 100.

Это стоит рядом с проверками обнаружимости и протоколов, поверх шлюза доступности контента, который решает, может ли агент вообще прочитать вашу страницу. Точные проверки и веса описаны на странице методологии. Этот гайд — отдающая половина к карте обнаружения, которую вы публикуете через llms.txt. Обе — спицы под материалом о том, что значит готовность к ИИ-агентам.

Запустите бесплатное сканирование, чтобы увидеть, могут ли ChatGPT, Claude, Perplexity и Google AI получить чистую Markdown-версию ваших страниц: честен ли ваш Content-Type, присутствует ли Vary: Accept и какие исправления сделать первыми — на английском, украинском или русском.

Если коротко: чтобы кормить агентов чистым текстом, новый стандарт не нужен. Нужны верный Content-Type, честный Vary и кэш, который различает HTML и Markdown.

У IMozz 20 лет в разработке ПО, а последний год он строит продукты с помощью LLM. Он развивает aiSiteReady, сканер только для чтения, который проверяет, может ли ИИ-агент прочитать сайт. Сканер серверно рендерит собственный контент как рабочий пример.

Частые вопросы

Нужен ли специальный протокол, чтобы отдавать Markdown ИИ-агентам?
Нет. Это обычное content negotiation в HTTP: агент шлёт Accept: text/markdown, а вы отвечаете Content-Type: text/markdown; charset=utf-8 плюс Vary: Accept. Сам медиатип зарегистрирован в RFC 7763, а правила согласования — это стандартный RFC 9110. Никакого нового ИИ-специфичного стандарта нет; вы просто используете HTTP так, как он задуман.
Что выбрать: .md-URL или согласование на том же URL?
Часто оба сразу. Vercel рекомендует поддерживать и .md-эндпоинты, и negotiation вместе, потому что многие агенты вообще не шлют Accept: text/markdown. Отдельные .md-файлы — самый безопасный и кэшируемый запуск, и они работают на статике «из коробки». Согласование на том же URL чище архитектурно, но только если ваш CDN реально учитывает Accept в ключе кэша.
Достаточно ли Vary: Accept, чтобы CDN был безопасен?
Сам по себе — нет. Fastly трактует Vary как вторичный ключ кэша, но CloudFront по умолчанию игнорирует заголовки запроса, пока вы не добавите Accept в cache policy, а Cloudflare не учитывает общий Vary без кастомного ключа кэша или Worker. Всегда проверяйте публичный edge-URL, а не только origin, прежде чем доверять конфигурации.
Не повредит ли отдача Markdown моему SEO?
Не повредит, если HTML остаётся канонической версией. Объявите Markdown-копию альтернативным форматом тегом link rel=alternate type=text/markdown, а на самом .md-файле отдайте rel=canonical через HTTP-заголовок Link — Google поддерживает это для не-HTML документов. Избегайте soft 404: отсутствующий .md должен возвращать 404 или 410, а не 200 с заглушкой.