Методология AgentFit: как детерминированно измерить AI-готовность API-документации
От интуиции к метрике
В предыдущей статье я описал 26 критериев AI-готовности документации и готовый запрос (промпт), которым можно прогнать любой сайт через языковую модель с доступом в сеть. Это работало как опросный лист и как разговорный аргумент. Но как измерение у него были два изъяна, которые я не мог себе простить.
Во-первых, невоспроизводимость. Прогон одного и того же сайта через языковую модель дважды даёт два разных результата: модель то находит llms.txt, то ленится, то по-разному трактует «реалистичность примеров». Балл плавал на ±5–10 пунктов между прогонами. Метрика, которая зависит от настроения оценивающего, — это не метрика.
Во-вторых, отсутствие гарантий. Языковая модель может выдумать обоснование ровно так же, как выдумывает несуществующие конечные точки (эндпоинты). Запрос явно это запрещал («знание из обучающих данных — не доказательство»), но запрет в тексте запроса — не то же самое, что архитектурная невозможность.
Поэтому я переписал систему критериев в программный код. Получился AgentFit — детерминированный измеритель на языке Go, который выдаёт байт-в-байт одинаковый ответ в формате JSON для одного и того же входа, обосновывает каждый балл конкретным ответом по протоколу HTTP и не делает ни одного обращения к языковой модели во время аудита. Эта статья — про методологию измерения, которая за ним стоит: формальную постановку, функцию подсчёта, два места, где всё-таки нужно машинное обучение, регрессионный контроль на эталонах и проверку того, как шкала ведёт себя на корпусе из ~2000 сайтов.
Сразу разведу два понятия, которые легко спутать и которые я держу раздельно всю статью. Надёжность — это воспроизводимость: одинаковый вход даёт одинаковый выход. Её AgentFit обеспечивает полностью и по построению. Валидность — это вопрос «а измеряем ли мы вообще то, что хотим». Её обеспечить куда труднее, и честный ответ — лишь частично; об этом отдельный раздел в конце, без приукрашивания. (Оба термина и все прочие — в разделе «Термины» ниже.)
Это не «как написать ещё один анализатор кода». Это попытка ответить на вопрос: можно ли вообще объективно и воспроизводимо измерить такую размытую вещь, как «насколько документация готова к чтению программным агентом» — и какой ценой.
Формальная постановка
Назовём аудитом функцию
audit: BASE_URL → Reportгде BASE_URL — корневой адрес документации без косой черты в конце (например, https://docs.stripe.com), а Report — структурированный отчёт. Внутри audit раскладывается на две стадии — BASE_URL → Env → Report (предзагрузка окружения, затем веер критериев); детали — в разделе про архитектуру. Система критериев R — это упорядоченное множество из 30 критериев, сгруппированных в 6 категорий A…F:
R = { c₁, c₂, …, c₃₀ }Каждый критерий cᵢ — это тройка (IDᵢ, mᵢ, runᵢ):
IDᵢ— устойчивый идентификатор (A1,C1b,F4, …);mᵢ ∈ ℕ— максимум баллов критерия;runᵢ: Env → Resultᵢ— детерминированная процедура оценки.
Сумма максимумов нормирована на 100:
Σ mᵢ = 100Результат критерия — это запись Resultᵢ, из которой для подсчёта важны три поля:
statusᵢ ∈ { present, partial, absent, error, not_applicable }(присутствует / частично / отсутствует / ошибка / неприменимо);scoreᵢ ∈ {0, 1, …, mᵢ}— целое число баллов;evidenceᵢ = (url, http_status, snippet₅₀)— обоснование: адрес, код ответа и фрагмент тела.
Связь статуса и балла зафиксирована и проверяется на выходе:
| Статус | Балл | Смысл |
|---|---|---|
present | scoreᵢ = mᵢ | все требуемые сигналы найдены |
partial | 0 < scoreᵢ < mᵢ | часть сигналов |
absent | scoreᵢ = 0 | сигналов нет, но проверка отработала |
error | scoreᵢ = 0 | проверку не удалось выполнить (сбой загрузки, разбора или вывода модели) |
not_applicable | scoreᵢ = 0 | предусловие отсутствует (например, нечего оценивать — нет спецификации OpenAPI) |
Разделение error и not_applicable появилось в версии v2.2.0; до этого был единый статус unknown («неизвестно»). Объединение двух статусов снова даёт старый unknown, поэтому суммарные баллы остаются сопоставимыми со статьёй. Различие важно для диагностики: error означает «наша проверка сломалась или сайт нас заблокировал», not_applicable — «у сайта честно нет того, что мы проверяем». Смешивать их — значит путать дефект измерителя с фактом об измеряемом объекте.
Функция подсчёта
Итоговый балл — это просто сумма:
total = Σᵢ scoreᵢ (0 ≤ total ≤ 100)Балл категории k ∈ {A,…,F}:
score(k) = Σ_{cᵢ ∈ k} scoreᵢНикаких весовых коэффициентов «на лету», никакого нормирования относительно других сайтов, никакого округления «на глаз». Вес критерия уже заложен в mᵢ — то есть в саму структуру системы критериев, а не в момент подсчёта. Это сознательное решение: метрика должна быть абсолютной, чтобы балл сайта не менялся от того, какие ещё сайты сегодня в выборке. (Ниже, в разделе про валидность, я покажу, что попытка перейти к относительному, зависящему от корпуса подсчёту ничего не даёт — ранжирование не меняется.)
Отсекающий критерий
Один критерий стоит особняком — E1 (контент виден в обычном HTML без JavaScript, 6 баллов). Это отсекающий критерий. Если score(E1) = 0 (контент отрисовывается только средствами JavaScript, а curl отдаёт пустую оболочку <div id="root"></div>), к отчёту добавляется метка unreliable («ненадёжно»):
unreliable = (score(E1) = 0) ∨ uniform_shell_detectedtotal при этом всё равно считается — отсечение не обнуляет балл, оно его дисквалифицирует как надёжный. Логика та же, что у инструмента Lighthouse от Google: «значение есть, но доверять ему нельзя». Сайт может набрать неплохой балл за счёт текстовых .md-версий поверх одностраничного приложения — так делает Anthropic: основной HTML — это оболочка одностраничного приложения (E1 проваливается), но отдельный путь с расширением .md и 76-мегабайтный llms-full.txt отдают тот же контент чистым текстом, и реальная доступность для модели не страдает. В этом случае метка unreliable честно говорит: «основной HTML непригоден, но запасной канал спасает».
uniform_shell_detected — отдельный детектор, про который ниже; он ловит сайты, которые отдают код HTTP 200 и один и тот же HTML на любой адрес.
Требования к методологии измерения
Чтобы число было метрикой, а не мнением, методология обязана удовлетворять четырём требованиям. Это не «возможности реализации» — это обязательные условия измерения, и их нарушение делает балл бессмысленным.
- Детерминизм (воспроизводимость). Для одного и того же входа
auditобязана возвращать байт-в-байт одинаковыйReport. Любой источник недетерминизма (порядок обхода ассоциативного массива, неотсортированная выборка страниц, текущее время, состояние гонки между проверками) — это дефект измерения. - Обоснованность. Каждый
scoreᵢсопровождается обоснованиемevidenceᵢ— реальным адресом, кодом ответа HTTP и фрагментом тела ≤ 50 символов. Балл без доказательства невалиден. - Отсутствие языковой модели во время работы. Во время аудита — ноль обращений к языковым моделям. Вся семантика, которую нельзя выразить регулярным выражением, выражается двумя маленькими встроенными классификаторами (см. ниже). Причина — пункт 1: языковая модель недетерминирована по построению.
- Ограниченная стоимость. Аудит — это не обход сайта (краулинг). ≤ ~80 запросов HTTP на сайт, ограничение по числу одновременных соединений с одним узлом, ограничение на размер тела (10 МБ), полный аудит — менее ~30 секунд для типичного сайта.
Дальше — как эти требования выполняются.
Система критериев: категории, веса и их эволюция
Исходная система критериев (статья) — 26 критериев в пяти категориях. Текущая (AgentFit v2.3+) — 30 критериев в шести. Добавилась категория F. Agent Surface (агентская поверхность): слой поверх документации, обращённый к программному агенту (llms.txt как канал обнаружения, WebMCP, MCP-сервер, доступность DOM-дерева для агента). Чтобы сохранить нормировку Σ mᵢ = 100, категории A–E были ужаты ровно на 10 баллов (A −5, B −2, C −1, D −1, E −1), а освободившиеся 10 ушли в F.
| Категория | Вес (статья) | Вес (сейчас) | Что проверяет |
|---|---|---|---|
| A. Discovery (обнаружение) | 18 | 13 | llms.txt, llms-full.txt, robots.txt с правилами для ИИ-ботов, чистый sitemap.xml, теги обнаружения |
| B. Per-page artifacts (постраничные артефакты) | 22 | 20 | .md-версия страницы, разметка JSON-LD, абсолютный канонический адрес, актуальность, семантические <main>/<article> |
| C. API spec (спецификация API) | 25 | 24 | OpenAPI/Swagger/AsyncAPI по предсказуемому адресу и её валидность, Postman/SDK, структура страниц эндпоинтов |
| D. Content (содержимое) | 20 | 19 | примеры curl и SDK, реалистичность тел запросов, каталог ошибок, аутентификация и ограничения частоты, глоссарий, метки устаревания |
| E. Hygiene (гигиена) | 15 | 14 | контент без JavaScript (отсечение), устойчивые адреса, версия в адресе, рабочие ссылки, пользовательское соглашение и политика по ИИ |
| F. Agent Surface (агентская поверхность) | — | 10 | ширина поверхности обнаружения, WebMCP, MCP-сервер (RFC 9728/8414), доступность DOM-дерева агенту |
| Итого | 100 | 100 |
Веса не выведены из первых принципов — они подобраны так, чтобы оценка кодом ложилась рядом с ручной оценкой из исходной статьи на горстке эталонных сайтов (раздел ниже). Это нормально: система критериев — операционализация размытого конструкта «AI-готовность», и веса в ней играют роль, аналогичную весам пунктов в составной шкале. Но у этого есть приятное следствие, которое я отдельно проверил: итоговое ранжирование почти не зависит от конкретных весов (ранговая корреляция Спирмена ρ = 0.960 при перевзвешивании — что именно это значит и чего не значит, разбираю в разделе про устойчивость). Произвол в выборе весов есть, но на порядок сайтов он почти не влияет.
Внутри категории F есть сознательное пересечение с A: и A1 (качество llms.txt), и F1 (ширина поверхности обнаружения) смотрят на llms.txt. Это не дублирование по ошибке — это тот же приём «глубина против ширины» (depth vs breadth), что у Lighthouse: один критерий оценивает «насколько хорошо сделано», второй — «сколько разных поверхностей вообще есть».
Как это реализовано: архитектура измерителя
Аудит — это две фазы.
Фаза 1 — buildEnv (предзагрузка). Параллельно (через группу горутин errgroup) скачиваются ресурсы, нужные многим критериям сразу: главная страница, /robots.txt, /sitemap.xml, /llms.txt, и проба OpenAPI по каталогу из 17 стандартных путей (/openapi.json, /swagger.json, /api-docs, …). Каталог разбит на уровни: сначала 6 частых путей, остальные 11 — только если первые промахнулись; а для под-путей внутри BASE_URL каждый путь пробуется и у корня узла, и относительно BASE_URL. Результаты складываются в неизменяемую структуру Env («окружение»). Если какая-то проба упала — это фиксируется, но аудит не прерывается; зависимый критерий просто вернёт статус error.
Фаза 2 — веер критериев. 30 критериев запускаются параллельно, тоже через errgroup. Ключевой инвариант: run никогда не возвращает ошибку языка Go. Любой сбой внутри превращается в Result{status: error, score: 0}. Это держит конкурентный код чистым и гарантирует, что набор результатов всегда полон — все 30 ячеек заполнены.
Критерии не общаются между собой. Каждый — чистая функция от Env к Result, без разделяемого изменяемого состояния. (Один реальный дефект в истории проекта был ровно про это: оркестратор переиспользовался между обработчиками массового аудита, и они состязались за общее поле диагностики — пришлось создавать оркестратор на каждый вызов.)
Весь внешний обмен по HTTP идёт через единственный клиент fetch.Client, который обеспечивает: режимы представления клиента (поле User-Agent: curl, Googlebot, браузер, пустое), кэш в оперативной памяти на время одного аудита, ограничитель «не более N одновременных соединений с одним узлом» и ограничение на размер тела. Для llms-full.txt (Anthropic публикует 76 МБ) тело не скачивается целиком — берётся заголовок Range: bytes=0-… только для фрагмента-обоснования.
Механизмы детерминизма
Требование №1 (воспроизводимость) выполняется не «по договорённости», а конкретными приёмами:
- Фиксированный порядок критериев. Набор в реестре жёстко упорядочен
A1 → F4; это часть гарантий на выходе. - Детерминированная выборка страниц. Когда критерию нужно
Nстраниц изsitemap.xml, он берёт их не «как пришло», а сортирует адреса лексикографически и выбирает по равномерным индексам[0, n/N, 2n/N, …]. Один и тот же сайт → одни и те же страницы → один и тот же балл. - Внедряемое время. Дата аудита
audit_dateберётся из внедряемой функцииNow(); в тестах она зафиксирована. Время — единственный «живой» вход, и он изолирован. - Упорядоченный вывод ассоциативных массивов. В Go порядок обхода ассоциативного массива (map) не определён, поэтому категории сериализуются через промежуточную упорядоченную структуру.
- Проверка на выходе. Перед возвратом
Reportпроходит проверку инвариантов: ровно 30 критериев в каноническом порядке;Σ scoreᵢ = total; для каждой категории сумма её критериев равнаscore(k);MaxScore = 100;unreliable ⟺ ¬E1GatingPassed; каждый фрагментsnippet≤ 50 знаков (рун). В тестах нарушение — аварийный останов, в проде — ответHTTP 500. Метрика, которая не сходится сама с собой, наружу не выходит. - Тест на детерминизм. Один и тот же эталонный сайт-снимок аудитируется дважды; ответы JSON сравниваются побайтово.
Детектор ловушки одинаковой оболочки
Отдельного упоминания стоит ловушка одинаковой оболочки (uniform-shell trap). Некоторые сайты отдают код HTTP 200 и один и тот же ~5 КБ HTML-каркас на любой запрошенный адрес — хоть /llms.txt, хоть /sitemap.xml, хоть случайный путь. Код ответа врёт: он говорит «всё в порядке», а тело никакой информации не несёт.
Наивный измеритель примет такой ответ за валидный llms.txt или валидную страницу в формате Markdown. Защита: E1 считает контрольную сумму SHA-256 тела для каждого скачанного адреса. Если ≥ 3 различных адреса вернули одинаковую сумму — срабатывает uniform_shell_detected, E1 принудительно обнуляется, и весь отчёт получает метку unreliable. Этот же принцип — «код HTTP 200 это мнение сервера, а не гарантия содержимого» — повторяется в нескольких критериях: и A1, и A2, и B1 отбрасывают тело, которое начинается с <!doctype/<html>, даже если код ответа 200.
Два места, где всё-таки нужно машинное обучение
Большинство критериев — это пробы по HTTP, разбор (HTML через библиотеку goquery, XML, JSON, OpenAPI через libopenapi) и эвристики на регулярных выражениях. Но ровно два критерия требуют семантического суждения, на котором регулярные выражения и ключевые слова упираются в потолок и дальше не растут:
- D2 — «реалистичны ли примеры?» (
POST /usersсname: "Jane Doe"противfoo/bar/<your_api_key>); - C3 — «полна ли страница эндпоинта?» (метод, адрес, типы, признак обязательности, примеры запроса и ответа).
Для них — и только для них — в исполняемый файл встроены (директивой //go:embed) две маленькие модели в формате ONNX. Это не противоречит требованию «никаких языковых моделей во время работы»: модели крошечные, детерминированные, и их вычисление пренебрежимо дёшево на фоне самого разбора страницы.
Принципиальный момент: почему машинное обучение только здесь. Другие «трудные» критерии — C2 (наличие Postman/SDK) и D5 (глоссарий) — это задачи поиска ссылок и структурного разбора, а не семантики. Для D5 машинное обучение было рассмотрено и отвергнуто архитектурным разбором: вручную сконструированные признаки (число вариантов написания термина, доля доминирующего написания, доля несогласованных групп) — это и есть решающее правило с порогом; обученная модель просто воспроизвела бы их на выборке в 50 сайтов с тяжёлой круговой зависимостью «признак = метка». Порог проще, прозрачнее и читается прямо в обосновании. Хорошее напоминание: не тянуться к машинному обучению, когда признаки уже и есть правило.
D2 — детектор заглушек-заполнителей
| Вход | текст блока кода |
| Признаки | 20 чисел: 7 сигналов «заглушка» (p_bootstrap, p_angle_type, p_templating, p_your_token, p_xxxx, p_repeat_digits, p_bracket), 8 сигналов «реалистично» (r_stripe_key, r_uuid, r_bearer, r_api_url, r_iso_ts, r_typed_id, r_long_numeric_id, r_jwt), 5 поверхностных (length_log, digit_ratio, upper_ratio, punct_ratio, line_count_log) |
| Модель | StandardScaler + LogisticRegression → P(заглушка) ∈ [0,1] |
| Метки | realistic (реалистично), placeholder (заглушка) |
| Качество | точность (accuracy) 0.970, AUC 0.998 (обучение на 3837 блоках кода, проверка на 960; метрика оптимистична — см. оговорку в конце раздела) |
D2 собирает все блоки кода с 3 выборочных страниц, для каждого считает P(заглушка), помечает блок заглушкой при P > 0.6 и считает долю заглушек placeholder_ratio. Балл — ступенчатая функция от доли: < 0.20 → 4, < 0.40 → 3, < 0.60 → 2, < 0.80 → 1, ≥ 0.80 → 0.
C3 — полнота страницы эндпоинта
| Вход | вектор признаков HTML-страницы |
| Признаки | 15 чисел: число заголовков h2/h3, число блоков кода, плотность ключевых слов методов, наличие таблицы параметров, число required, наличие curl, наличие примера ответа, наличие шаблона пути, число блоков по языкам SDK, число кодов статуса, число аннотаций типов, наличие списка определений <dl>, число строчных вставок кода, доля «навигационных» заголовков (отсеивает страницы-оболочки), число заголовков с именем метода |
| Модель | StandardScaler + GradientBoostingClassifier (max_depth=4, n_estimators=100) → softmax по {complete, partial, absent} |
| Качество | macro-F1 0.870 (на отложенной выборке; метрика оптимистична — см. оговорку в конце раздела) |
C3 берёт до 3 страниц эндпоинтов, классифицирует каждую и считает балл по преобладающему классу: complete → 5, partial → 3, absent → 1, нет страниц → 0.
Дисциплина вокруг моделей
Две вещи, без которых машинное обучение в детерминированном измерителе превращается в источник тихих ошибок.
Совпадение извлечения признаков Go ↔ Python. Признаки извлекаются на двух языках: на Python — при обучении, на Go — во время работы. Малейшее расхождение в предобработке = другие предсказания. Поэтому порядок признаков зафиксирован в четырёх местах одновременно (константа в Go, сопроводительный файл модели в формате JSON, схема на Python, литерал в Go-вычислении), и тест совпадения гоняет канонические входы через обе реализации, сверяя результат.
Защита от перестановки признаков (у C3). Если кто-то случайно переставит два признака местами в Go-литерале, модель продолжит работать — просто молча неправильно. Чтобы это поймать, у C3 при обучении делается полный перебор пар признаков; пары, перестановка которых сдвигает вероятность класса на ≥ 0.5, записываются в сопроводительный файл вместе с эталонным вектором. Go-тест читает эти пары и проверяет, что перестановка действительно сдвигает вероятность, — то есть что порядок признаков в коде не «поехал». (D2 защищён только тестом совпадения; отдельной проверки на перестановку у него нет.)
Две честные оговорки, прописанные прямо в поле Note критериев. Первая: это эвристики, аппроксимирующие семантику, а не понимание; модели обучены на англоязычной документации, и неанглоязычные сайты могут недооцениваться. Вторая, менее приятная: бо́льшая часть обучающих меток для D2/C3 получена автоматическим разворачиванием правил по тем же сигналам, что потом стали признаками. Это та же круговая зависимость «признак = метка», из-за которой я отверг машинное обучение для D5 — просто здесь она частичная, а не полная. Практический вывод: заявленные метрики на отложенной выборке (0.970 / 0.870) оптимистичны — они меряют согласие модели с правилом-разметчиком, а не с независимым человеком. Я привожу их как индикатор «модель выучила правило и обобщает за его края» (ручная доразметка краевых случаев это подтверждает), но не как оценку «настоящей» точности.
Надёжность, калибровка и (немного) валидности
Дальше — три разные проверки, которые я сознательно не сваливаю в одну кучу «валидация». Первая — про надёжность (регрессионный контроль). Вторая — про устойчивость к выбору весов (ρ). Третья — про то, как шкала ведёт себя на невиданном корпусе. Ни одна из них, строго говоря, не доказывает валидность; что они доказывают и чего не доказывают — проговорено явно.
Регрессионный контроль на эталонах (повторный тест)
Есть 7 эталонных сайтов с зафиксированными ожидаемыми баллами. Команда cmd/calibrate аудитирует их и сверяет с ExpectedTotal ± Tolerance (ожидаемый балл ± допуск). Базовое правило (из плана проекта): отклонение > 15 пунктов — это дефект эвристики, а не свойство сайта. Допуск ужат для «хорошо себя ведущих» сайтов (Stripe, Anthropic), где большой скачок означает регрессию, и ещё уже для VK, у которого узкий диапазон балла (0–10).
| Сайт | Ожидаемый балл | Допуск | Отсечение E1 |
|---|---|---|---|
docs.emergingtravel.com | 69 | ±12 | прошёл |
developers.booking.com | 48 | ±12 | прошёл |
docs.stripe.com | 42 | ±10 | прошёл |
docs.anthropic.com | 30 | ±10 | провал — одностраничное приложение, баллы держат .md |
docs.github.com/en/rest | 25 | ±10 | прошёл (но антибот-защита бьёт по измерителю) |
developers.expediagroup.com | 23 | ±12 | частично |
dev.vk.com | 10 | ±4 | провал — ловушка одинаковой оболочки |
Помимо суммарного допуска есть CategoryTolerance — контроль смещения внутри категории (±4, у VK ±3). Он ловит ситуацию, когда сумма осталась в пределах нормы, но одна категория просела, а другая выросла — взаимная компенсация, которую общий допуск пропустил бы.
И вот здесь — главная оговорка, на которой легко обмануться (и на которой я сам себя поначалу обманул). Это НЕ конструктная валидность. Ожидаемые баллы в таблице — не независимые экспертные оценки. Они переснятые фактические значения одного живого прогона, заново зафиксированные как регрессионный «предохранитель». Проверять, воспроизводит ли измеритель собственный недавний вывод в пределах допуска, — это надёжность повторного теста (test-retest) в пределах одной версии, а не валидность (при намеренном изменении эвристики эталон осознанно пересоснимается — об этом ниже). Единственный внешний якорь здесь — ручные баллы из исходной статьи, и то лишь на этих же 7 сайтах (то есть выборка n = 7, и калибровка с проверкой идут по одним и тем же сайтам — это оценка «по той же выборке», in-sample). Так что таблица говорит ровно одно: «измеритель стабилен и не разъезжается с собственной прошлой версией». Это ценно как сигнал о регрессии — но это не доказательство, что 42 балла Stripe «правильные».
Сами эталонные числа — это фактические значения одного прогона плюс его шум (±0–3 из-за случайной выборки ссылок в E4, из-за блокировок). Перед тем как ужимать допуски, их надо переснять на нескольких прогонах. И отдельная ловушка: docs.github.com Cloudflare блокирует по сетевому отпечатку нашего клиента и усиливает блокировку с частотой аудитов — github осциллирует между 25 и 1. Поэтому провал на github — это почти всегда временная блокировка, а не смещение балла; надо переподтверждать одним «остывшим» прогоном. (Это, кстати, ограничение всей методологии активного измерения: измеряемый объект может сопротивляться измерению.)
Устойчивость к выбору весов (ρ = 0.960)
Естественная придирка: фиксированные веса mᵢ подобраны вручную и наверняка неоптимальны — может, перейти к зависящему от корпуса подсчёту (перевзвешивать критерии по эмпирической функции распределения, как делает логнормальный подсчёт в Lighthouse)?
Я проверил это явно (скрипт lognorm_gate.py). Взял корпус из 112 сайтов, переаудированных под версией v2.3.2 (первая версия со всеми 30 критериями, включая F4; 106 дошли до отчёта, заблокированные антибот-защитой и нечитаемые ответы исключены), посчитал два ранжирования — по текущей фиксированной шкале и по зависящему от корпуса перевзвешиванию тех же критериев — и сравнил их ранговой корреляцией Спирмена:
ρ(фиксированная шкала, относительная шкала) = 0.960 (порог принятия: 0.85)Порог 0.85 задан заранее, до прогона, как консервативная граница «сильной» ранговой корреляции: если бы переход к относительной шкале переставлял сайты существеннее, мы бы это увидели и приняли всерьёз. (До появления F4 тот же тест на 29 критериях давал ρ = 0.965 — то есть добавление агентского критерия ранжирование тоже не сломало.)
Что это значит и, важнее, чего НЕ значит. Оба ранжирования порождены самим инструментом — ни эксперта, ни внешней «истины» здесь нет. Поэтому ρ = 0.960 говорит: порядок сайтов почти не зависит от того, как именно расставлены веса. Это снимает один источник произвола (спорить о «8 против 7 баллов за C1a» бессмысленно) — но это самосравнение, и валидности оно не доказывает: плохая метрика пережила бы этот тест ровно так же. Известный результат (Wainer, 1976): аддитивная сумма многих скоррелированных пунктов вообще слабо чувствительна к весам. Так что вывод скромный и честный: усложнять шкалу незачем (отрицательный результат, который экономит работу), но «устойчива к весам» ≠ «измеряет правильную вещь».
Отдельно логнормальная модель здесь вообще неприменима: на уровне критериев распределение раздуто нулями — 14 из 30 критериев получают 0 у ≥ 50% корпуса, то есть имеют точечную массу в нуле, которую логнормаль не представляет в принципе.
Распределение на невиданном корпусе (очевидная валидность)
Это не валидность в строгом смысле — для тех сайтов нет никакой «истины» (человеческих оценок или, тем более, замеров реального успеха агента на задаче). Это проверка на вменяемость: ведёт ли шкала себя осмысленно там, где её не калибровали. Корпус:
- из каталога
public-apisизвлечено ~1182 уникальных сайта; после проверки живости ~776 пригодны для аудита (alive_html), остальные — мёртвые, перенаправления на другой домен, оболочки одностраничных приложений, эндпоинты не в формате HTML; - отдельная широкая выборка: поддомены
developer.*/docs.*из топ-25 000 доменов рейтинга Majestic Million → 2099 ранее не встречавшихся сайтов, прогнанные через массовый аудит (под v2.7.0, уже 30 критериев). Средний балл — 14.6, а распределение баллов сильно скошено вправо. Оговорка про инструмент: массовый прогон использовал эвристические заглушкиD2/C3(без ONNX), поэтому верхушку (≥ 40) я потом переаудитировал на проде с настоящими моделями — расхождение оказалось небольшим.
Низкое среднее — это ожидаемый и осмысленный результат, а не дефект. Прочёсывание верхушки списка доменов ловит в основном продуктовую документацию, а не справочники API; система критериев честно ставит ей мало. Сигнал живёт в хвосте: 108 сайтов набрали ≥ 40, из них 14 — ≥ 60 (настоящие справочники API: docs.z.ai 76, docs.kalshi.com 75, docs.hedera.com 70). Иными словами, шкала разделяет «продуктовую страницу» и «настоящий справочник API» в нужную сторону — это и есть очевидная валидность, не больше и не меньше.
Ограничения
Честная методология перечисляет, где она слепа.
- Эвристики аппроксимируют семантику, а не понимают её.
D2/C3— обученные классификаторы,D5— порог; ни один не «читает» документацию. Это сознательный размен ради детерминизма. - Одностраничные приложения недооцениваются по построению.
E1штрафует контент, спрятанный за JavaScript, — в этом его смысл. Опциональный безголовый рендеринг (через браузер Chrome) есть, но по умолчанию выключен: отрисовка недетерминирована (непредсказуемое время гидратации DOM-дерева), а это нарушило бы требование №1. То есть либо детерминизм, либо полнота измерения одностраничных приложений — третьего на одном прогоне нет. - Модели обучены на англоязычном корпусе. Неанглоязычная документация систематически недооценивается в
D2/C3. - Активное измерение возмущает объект. Антибот-защита (Cloudflare на github) превращает балл в шум; методология умеет отличать блокировку (
error) от честного отсутствия (absent/not_applicable), но не умеет её обойти — и не должна. - Калибровка на одном прогоне содержит шум. Эталонные числа — фактические значения одного запуска ±0–3; они хороши как регрессионный предохранитель, но не как аналитическая истина.
- Настоящей валидности у меня нет. Я показал надёжность (детерминизм), устойчивость к весам (ρ) и вменяемость распределения (очевидная валидность). Чего нет — это сверки балла с независимым внешним критерием: например, с реальным успехом агента, который пишет код по этой документации. Пока такого замера нет, балл — это балл по системе критериев, а не доказанная «AI-готовность».
- Веса откалиброваны, а не выведены. Их произвол ограничен (ранжирование к ним почти нечувствительно), но не устранён. Это шкала, и относиться к ней надо как к шкале: полезной, проверяемой, но не «объективной» в наивном смысле.
Что это даёт
Главный результат — не конкретные баллы Stripe или VK. Главный результат — что размытый конструкт «AI-готовность документации» удалось операционализировать так, что измерение стало воспроизводимым, обоснованным и с честно очерченными границами применимости — и при этом дешёвым (< 30 секунд, ноль обращений к языковой модели во время работы). Надёжность достигнута полностью; валидность — лишь частично (распределение вменяемо, ранжирование устойчиво к весам, на 7 сайтах сходится с ручной оценкой), и эта граница обозначена явно, а не замолчана.
Три вывода, которые я бы вынес за пределы этого конкретного проекта:
- Воспроизводимость — это требование, а не возможность. Как только метрику начинают использовать для сравнения и принятия решений, недетерминизм (включая «всего лишь» языковую модель в роли оценщика) делает её непригодной. Детерминизм пришлось закладывать в архитектуру: сортированная выборка, фиксированный порядок, внедряемое время, проверка инвариантов.
- Машинное обучение — это последнее средство, а не первое. Из 30 критериев машинное обучение понадобилось ровно для двух; для одного похожего (
D5) оно было рассмотрено и отвергнуто, потому что признаки уже были решающим правилом. Дисциплина «сначала эвристика, машинное обучение только там, где семантика неустранима» сэкономила и сложность, и источники недетерминизма. - Отрицательные результаты экономят работу. Тот самый тест на перевзвешивание — это «не усложняй», подтверждённое числом: относительная шкала не дала бы ничего, и я её не стал строить. Важно не перепутать, что это доказывает: устойчивость к весам — это снятый источник произвола, а не доказательство правильности. Метрику стоит и дальше проверять — но уже против внешнего критерия, а не против самой себя.
Исходная статья с 26 критериями, готовым запросом и навыком (skill) для Claude — здесь. Она по-прежнему лучший способ быстро прикинуть готовность сайта руками. Эта статья — про то, что происходит, когда «прикинуть» хочется заменить на «измерить».
Термины
Где это возможно, я держусь русских слов; но часть терминов — это имена форматов, протоколов и библиотек, которые не переводятся, либо устоявшиеся понятия из психометрики и статистики. Собрал их здесь.
Методология измерения
- AI-готовность — степень, в которой документация пригодна для чтения и использования программным агентом на основе языковой модели. Размытый конструкт, который вся статья пытается операционализировать.
- Операционализация — превращение размытого понятия в конкретную измеримую процедуру (здесь: в систему из 30 критериев с функцией подсчёта).
- Детерминизм — свойство «одинаковый вход → байт-в-байт одинаковый выход».
- Надёжность (reliability) — воспроизводимость измерения.
- Валидность (validity) — измеряет ли инструмент именно то, что заявлено.
- Конструктная валидность (construct validity) — соответствие измерения теоретическому понятию, которое оно якобы измеряет.
- Очевидная валидность (face validity) — самая слабая форма: измерение «на первый взгляд» ведёт себя осмысленно.
- Надёжность повторного теста (test-retest) — согласие повторных измерений одного и того же объекта.
- Система критериев — упорядоченный набор из 30 критериев с приписанными весами (то, что в англоязычных текстах об оценивании называют scoring rubric; по-русски «рубрика» — это раздел издания, а не схема оценки, поэтому слово я не использую).
- Критерий — тройка «идентификатор, максимум баллов, процедура оценки».
- Эвристика — приближённое решающее правило (здесь — на регулярных выражениях и ключевых словах).
- Обоснование (
evidence) — реальный адрес, код ответа HTTP и фрагмент тела, подтверждающие балл критерия. - Отсечение (gating) — приём, при котором значение метрики не обнуляется, но помечается как недостоверное (
unreliable).
Форматы, протоколы, технологии
- HTTP — протокол обмена в вебе; коды ответа (200, 500 и т. д.) и заголовки (
Range,User-Agent). - HTML / DOM-дерево — разметка страницы и её объектная модель в браузере.
- JavaScript / одностраничное приложение (SPA) — сайт, отрисовывающий контент в браузере уже после загрузки; «голый» HTML у него пустой.
- Безголовый рендеринг (headless) — отрисовка страницы движком браузера без графического интерфейса, чтобы увидеть результат работы JavaScript.
- OpenAPI / Swagger / AsyncAPI — форматы машиночитаемого описания API.
llms.txt/llms-full.txt— файлы-указатели для языковых моделей (спецификация).robots.txt/sitemap.xml— стандартные файлы для поисковых и ИИ-роботов: правила обхода и карта сайта.- Канонический адрес / JSON-LD — постраничные метаданные: основной адрес страницы и структурированная разметка.
- MCP (Model Context Protocol) / WebMCP — протокол и его веб-вариант для интерактивного доступа агента к инструментам поверх API.
- Эндпоинт (endpoint) — конечная точка API: конкретный адрес и метод, выполняющие одну операцию.
- SDK — набор средств разработки (клиентские библиотеки под конкретный язык).
curl— консольная утилита для запросов по HTTP; здесь — синоним «голого» запроса без браузера.- ONNX — формат переносимых моделей машинного обучения; модели встроены в исполняемый файл и считаются локально.
errgroup,goquery,libopenapi— Go-библиотеки: группа горутин (лёгких потоков выполнения в Go) с общей отменой по первой ошибке, разбор HTML и разбор OpenAPI соответственно.
Машинное обучение и статистика
- Признак — одно входное число модели (например, доля цифр в тексте).
- StandardScaler — стандартизация признаков (вычесть среднее, поделить на разброс).
- LogisticRegression — логистическая регрессия, линейный классификатор.
- GradientBoostingClassifier — классификатор на градиентном бустинге деревьев.
- softmax — функция, превращающая вектор чисел в распределение вероятностей по классам.
- Отложенная выборка (holdout) — часть данных, отложенная от обучения для честной проверки.
- accuracy / AUC / macro-F1 — метрики качества классификатора: доля верных ответов; площадь под ROC-кривой; усреднённая по классам F-мера.
- Ранговая корреляция Спирмена (ρ) — мера согласия двух ранжирований; 1 — полное совпадение порядка.
- Распределение, раздутое нулями (zero-inflated) — распределение с большой точечной массой в нуле; логнормальная модель его не описывает.
- Логнормальный подсчёт (log-normal scoring) — способ перевода значения в балл через логнормальную функцию распределения (применяется в Lighthouse).
Литература
Спецификации и стандарты
- llmstxt.org — спецификация формата
llms.txt. - OpenAPI Specification 3.1 — формат описания REST API.
- Model Context Protocol — слой поверх OpenAPI для интерактивного агентского доступа.
- RFC 9728 — OAuth 2.0 Protected Resource Metadata (проверяется в
F3). - RFC 8414 — OAuth 2.0 Authorization Server Metadata (тоже
F3). - Lighthouse scoring — взвешивание, отсечение и логнормальный подсчёт, с которыми я сравниваю свою шкалу.
Измерение, надёжность и валидность
- Cronbach, L. J., & Meehl, P. E. (1955). Construct Validity in Psychological Tests. Psychological Bulletin, 52(4), 281–302. — классическая постановка конструктной валидности.
- Wainer, H. (1976). Estimating Coefficients in Linear Models: It Don’t Make No Nevermind. Psychological Bulletin, 83(2), 213–217. — про слабую чувствительность аддитивных шкал к весам.
- Spearman, C. (1904). The Proof and Measurement of Association between Two Things. The American Journal of Psychology, 15(1), 72–101. — исходная работа по ранговой корреляции.
- Spearman’s rank correlation coefficient — современное изложение ρ.
- Zero-inflated model — про распределения, раздутые нулями.
Контекст
- API-документация в эпоху LLM — исходная статья: 26 критериев, готовый запрос и навык для Claude.