12k
All articles

Как проверить файл llms.txt

Проверьте ссылки, названия и версии пакетов, а также структуру Markdown в llms.txt с помощью Node.js-скрипта без зависимостей и настройте проверки в CI.

OpenReplay Team
OpenReplay Team
Как проверить файл llms.txt

Чтобы проверить файл llms.txt, нужно убедиться в четырёх вещах: он начинается с заголовка H1, каждая ссылка ведёт на страницу, которую описывает её текст, каждое имя пакета и каждая версия существуют в реестре, и ничто в файле не противоречит текущей документации.

Скорее всего, ваш llms.txt сгенерировал какой-нибудь инструмент для документации во время подготовки к релизу, и с тех пор его никто не открывал. За это время страницы переименовали, пакеты объявили устаревшими, а ИИ-агенты для разработки продолжают читать этот файл.

Если нужно освежить в памяти сам формат, начните с нашего обзора llms.txt. В этой статье мы пропустим основы. Мы разберём проверки, приведём Node-скрипт без зависимостей и покажем, как запускать его в CI, чтобы ловить проблемы до того, как агент начнёт действовать на их основе.

Основные выводы

  • Исходное предложение llms.txt требует только один элемент — заголовок H1 с названием проекта или сайта. Краткое описание в blockquote, подробные абзацы и разделы ссылок под H2 необязательны.
  • Официального валидатора для llms.txt нет, а некоторые онлайн-чекеры считают ошибкой отсутствие blockquote или раздела H2, хотя предложение называет оба элемента необязательными.
  • Имя пакета из llms.txt, которого больше нет в реестре, может зарегистрировать кто угодно. Агент с правом выполнять команды установки затем скачает всё, что там опубликуют.
  • Запускайте проверку при каждом деплое, чтобы ловить собственные ошибки, и еженедельно по расписанию, чтобы ловить ссылки и пакеты, сломавшиеся на стороне третьих лиц без каких-либо изменений с вашей стороны.

Почему устаревший llms.txt — это угроза цепочке поставок?

Устаревший llms.txt — это угроза цепочке поставок, потому что агенты считают llms.txt доверенным справочным материалом и часто действуют на его основе без участия человека. В результате устаревшая ссылка или неверное имя пакета превращаются в инструкцию. Человек, наткнувшись на мёртвую ссылку, просто сдастся. Агент с доступом к оболочке может выполнить npm install для имени, которое теперь принадлежит кому-то другому.

Этот риск уже продемонстрирован на практике. TechRadar рассказал об исследовании Алона Герца (Alon Hertz), который просканировал 6214 действующих доменов и нашёл 8265 файлов llms.txt и llms-full.txt. По его подсчётам, файлы на 120 разных сайтах ссылались на пакеты или домены, которые никто не зарегистрировал. Затем он занял часть этих свободных имён и опубликовал пакеты, которые сообщали ему о каждой установке. По словам исследователя, системы одной из компаний из списка Fortune 500 начали выходить на связь уже в течение часа. В отчёте среди задействованных агентов названы Claude, Codex и Hermes.

Важно сохранять трезвый взгляд: это был контролируемый эксперимент, а не зафиксированный случай распространения вредоносного ПО. Тем не менее это реальная уязвимость цепочки поставок, и закрыть её с помощью CI-задачи несложно.

Управление доступом — отдельный вопрос. Блокировка ИИ-краулеров через robots.txt указывает краулерам, какие части сайта им можно читать. Проверка llms.txt гарантирует, что прочитанное ими по-прежнему корректно.

Что проверять при валидации llms.txt

Корректный llms.txt содержит H1 и правильно оформленные ссылки, а каждая ссылка и упоминание пакета указывают на то, что существует и соответствует описанию. Официального валидатора для llms.txt нет. Приведённые ниже проверки охватывают то, что тестируют онлайн-чекеры, плюс проверку по реестру для пакетов, упомянутых в файле.

Ссылки открываются

Каждый URL в llms.txt должен возвращать статус 2xx после следования редиректам. Считайте ошибками ответы 4xx, 5xx и сбои DNS. Домен, который больше не резолвится, — это то же самое, что незарегистрированный пакет, только на уровне доменов: его может купить кто угодно. Редиректы тоже стоит логировать. Редирект на корень сайта обычно означает, что страницу удалили. Сначала отправляйте HEAD, а в случае неудачи — GET, потому что некоторые серверы отклоняют HEAD или обрабатывают его некорректно.

Ссылки ведут туда, куда обещают

Ответ 200 доказывает лишь то, что URL отвечает. Он не доказывает, что страница по-прежнему содержит то, что обещает текст ссылки. Убедитесь, что content-type — ожидаемый HTML или Markdown. Сравните <title> или первый заголовок страницы с текстом ссылки и отслеживайте soft 404 — страницы ошибок, которые отдаются со статусом 200. Эта проверка нечёткая, поэтому выполняйте её как периодический аудит, а не как условие прохождения сборки.

Пакеты и версии существуют в реестре

Проверяйте наличие в реестре каждого пакета, который llms.txt предлагает агенту установить. Вручную это можно сделать командой npm view <name>: она показывает, что хранится в реестре для пакета, и завершается ошибкой, если такого пакета нет. В скрипте запрашивайте документ пакета с registry.npmjs.org. Имя, неизвестное реестру, может занять кто-то другой. Для зафиксированных версий убедитесь, что точная версия присутствует в списке опубликованных версий пакета. Для PyPI, crates.io и других экосистем есть аналогичные способы поиска.

Структура Markdown корректна

Исходное предложение llms.txt делает обязательным только один раздел — заголовок H1 с названием проекта или сайта. Если не считать необязательного BOM, файл должен начинаться с # Name. Текущая спецификация llms.txt гласит, что каждый раздел H2 содержит Markdown-список, и каждый его элемент обязан начинаться со ссылки вида [name](url). Примечания после двоеточия необязательны. Сборка должна падать при отсутствии H1 или нарушенном синтаксисе ссылок. Но не роняйте её из-за отсутствия blockquote или H2. Некоторые онлайн-чекеры всё равно выдают предупреждения в обоих случаях — не считайте их нарушениями спецификации.

Файл не противоречит текущей документации

Сравните набор URL в llms.txt с вашим sitemap.xml или манифестом маршрутов, который генерирует сборка документации. Любой URL, который есть в llms.txt, но отсутствует в sitemap, должен вызывать подозрение. То же относится к пакетам: каждое имя в файле должно совпадать с полем name в одном из публикуемых вами package.json или с зависимостью, которую вы намеренно рекомендуете.

Node-скрипт без зависимостей для проверки llms.txt

Этот скрипт принимает URL или локальный путь. Он проверяет H1 и все абсолютные ссылки, а затем ищет в реестре npm каждый пакет, упомянутый в командах установки. Скрипт использует глобальный fetch Node, поэтому работает без каких-либо зависимостей на актуальной LTS-версии, например Node 24.

// scripts/check-llms.mjs — usage: node scripts/check-llms.mjs <url-or-path>
import { readFile } from 'node:fs/promises';

const src = process.argv[2];
if (!src) { console.error('usage: node check-llms.mjs <url-or-path>'); process.exit(2); }
const failures = [];

async function load(s) {
  if (!/^https?:\/\//.test(s)) return readFile(s, 'utf8');
  const res = await fetch(s);
  if (!res.ok) { console.error(`✗ ${s} returned ${res.status}`); process.exit(1); }
  return res.text();
}

async function checkLink(url) {
  let res;
  try {
    res = await fetch(url, { method: 'HEAD', redirect: 'follow' });
  } catch {
    // some servers reset the connection on HEAD; fall through to GET
  }
  try {
    if (!res?.ok) res = await fetch(url, { redirect: 'follow' }); // some servers refuse HEAD
    if (res.status >= 400) failures.push(`link ${res.status}: ${url}`);
    else if (res.redirected) console.warn(`↪ ${url} -> ${res.url}`);
  } catch (err) {
    failures.push(`link unreachable (${err.cause?.code ?? err.message}): ${url}`);
  }
}

async function checkPackage(spec) {
  const [, name, version] = spec.match(/^(@?[^@\s]+)(?:@(\S+))?$/) ?? [];
  if (!name) return;
  const res = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2F')}`);
  if (res.status === 404) return failures.push(`npm: "${name}" is not registered`);
  if (!res.ok) return failures.push(`npm: registry returned ${res.status} for "${name}"`);
  const doc = await res.json();
  if (/^\d+\.\d+\.\d+/.test(version ?? '') && !doc.versions?.[version])
    failures.push(`npm: ${name}@${version} does not exist`);
}

const text = await load(src);
if (!/^# \S/.test(text.trimStart())) failures.push('structure: file does not start with an H1');

const links = new Set([...text.matchAll(/\]\((https?:\/\/[^)\s]+)\)/g)].map((m) => m[1]));
const specs = new Set([...text.matchAll(/\b(npx|npm (?:install|i|add)|pnpm add|yarn add)\s+([^\n`]+)/g)]
  .flatMap(([, cmd, args]) => {
    const tokens = args.trim().split(/\s+/).filter((t) => !t.startsWith('-'));
    return cmd === 'npx' ? tokens.slice(0, 1) : tokens;
  }));

for (const url of links) await checkLink(url);
for (const spec of specs) await checkPackage(spec);

failures.forEach((f) => console.error(`✗ ${f}`));
console.log(`${links.size} links, ${specs.size} package refs, ${failures.length} failures`);
process.exit(failures.length ? 1 : 0);

Как это работает:

  • load читает файл с диска или по HTTP. Если не удаётся загрузить сам llms.txt по URL, скрипт сразу завершается.
  • checkLink сначала пробует HEAD и повторяет запрос через GET, если HEAD завершился ошибкой или вернул неуспешный статус. Редиректы выводятся как предупреждения, а сетевые сбои, например нерезолвящийся домен, фиксируются как ошибки.
  • checkPackage ищет каждое имя в реестре npm и помечает несуществующие имена, а также точные версии, которые никогда не публиковались.
  • Запросы намеренно выполняются по одному, чтобы снизить нагрузку на ваши серверы и реестр.
  • При любой ошибке скрипт завершается с кодом 1.

У скрипта есть известные ограничения. Относительные ссылки пропускаются. Текст, идущий после команды установки в той же строке, может давать ложные срабатывания. Диапазоны версий не проверяются — только точные версии.

Неудачный запуск выглядит примерно так (иллюстративный вывод с вымышленными именами):

↪ https://docs.example.com/guide -> https://docs.example.com/
✗ link unreachable (ENOTFOUND): https://docs.old.example.com/api
✗ npm: "example-sdk-legacy" is not registered
42 links, 3 package refs, 2 failures

Запуск проверки в CI при каждом деплое и раз в неделю

Запускайте валидатор llms.txt в двух режимах: при каждом деплое — для только что собранного файла, и по расписанию — для файла на продакшене. Запуск при деплое ловит ошибки, которые вносите вы. Запуск по расписанию ловит «протухшие» ссылки и пакеты, снятые с публикации, пока ваш репозиторий остаётся без изменений.

name: Validate llms.txt
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'   # Mondays 06:00: catches link rot and unpublished packages
  workflow_dispatch:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: node scripts/check-llms.mjs https://docs.example.com/llms.txt

Триггер schedule принимает стандартную cron-строку из пяти полей, и GitHub интерпретирует её в UTC, если вы не добавите ключ timezone с названием часового пояса IANA. workflow_dispatch позволяет запускать проверку вручную. Если деплой документации находится в том же workflow, добавьте перед шагом публикации шаг, указывающий на результат сборки, например node scripts/check-llms.mjs dist/llms.txt, чтобы проваленная проверка блокировала релиз. Задачу по расписанию оставьте нацеленной на продакшен-URL.

Синхронизация llms.txt через генерацию

Если генерировать llms.txt из того же источника, что и документацию, в файле не окажется страницы или пакета, которых в документации уже нет. Собирайте его на том же шаге, на котором рендерится документация, из того же списка страниц и frontmatter:

// scripts/build-llms.mjs
import { writeFile } from 'node:fs/promises';
import pages from '../dist/pages.json' with { type: 'json' };

const lines = ['# Example SDK', '', '## Docs', ''];
for (const p of pages.filter((p) => !p.draft)) {
  lines.push(`- [${p.title}](https://docs.example.com${p.path}): ${p.description}`);
}
await writeFile('dist/llms.txt', lines.join('\n') + '\n');

Никогда не редактируйте сгенерированный файл вручную. Либо коммитьте только генератор, либо коммитьте и генератор, и его результат, а в CI проверяйте, что повторная генерация не даёт диффа, и роняйте сборку, если он есть. Когда генерация налажена, валидатор становится страховкой для генератора, а не задачей по уборке, о которой кто-то должен помнить.

Заключение

Файл llms.txt полезен, только если всё перечисленное в нём по-прежнему существует и соответствует описанию, потому что агенты действуют на его основе без проверки. Добавьте scripts/check-llms.mjs в репозиторий, один раз запустите его для файла на продакшене, исправьте всё, о чём он сообщит, а затем закоммитьте workflow — тогда следующая битая ссылка или «осиротевшее» имя пакета уронит сборку раньше, чем на них наткнётся агент.

Часто задаваемые вопросы

Нужно ли проверять llms-full.txt так же, как llms.txt?

Да — в части ссылок и пакетов, причём пакеты стоит проверять даже строже. llms-full.txt встраивает полное содержимое документации, поэтому обычно содержит гораздо больше команд установки и примеров кода, чем llms.txt, и каждый из них — это имя пакета, которое агент может запустить. Требования к структуре можно ослабить, поскольку файл состоит в основном из содержимого страниц, а не из короткого списка ссылок. Натравите один и тот же скрипт на оба файла.

Почему проверка ссылок возвращает ошибки 403 для страниц, которые нормально открываются в браузере?

Скорее всего, автоматический запрос блокирует защита от ботов или WAF. Некоторые CDN и средства защиты отклоняют запросы без заголовков, характерных для браузера, или запросы с диапазонов IP-адресов CI-раннеров. Добавьте ваш CI-раннер или User-Agent чекера в белый список для собственного домена документации либо отправляйте в fetch информативный заголовок User-Agent. Не игнорируйте ошибки 403 просто так: агенты могут блокироваться точно так же.

Как проверить имена Python-пакетов из llms.txt по PyPI?

Обращайтесь к JSON API PyPI по адресу pypi.org/pypi/NAME/json. Для существующего проекта он возвращает метаданные и объект releases, ключами которого служат строки версий, поэтому можно проверить и имя, и любую зафиксированную версию. Если запрос вернул 404, считайте имя незарегистрированным. Извлекайте имена из команд pip install и uv add так же, как скрипт обрабатывает npm install.

Означает ли успешная проверка по реестру, что пакет из llms.txt безопасно устанавливать?

Нет. Проверка подтверждает, что имя существует, но не то, что им владеют нужные люди. Кто-то мог уже занять имя после того, как исходный пакет был заброшен, а опечатка в llms.txt может указывать на существующий пакет с похожим именем. Для каждого рекомендуемого пакета убедитесь, что издатель и ссылка на репозиторий соответствуют вашей организации, и отдавайте предпочтение scoped-именам, которые вы контролируете.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.