Перейти к содержанию

Мы начали писать документацию для ИИ-агентов

В 2024 году я писал документацию для разработчиков. В какой-то момент 2026-го заметил, что значительная часть читателей — не люди. Ассистенты, подключавшие мои библиотеки к сервисам, придумывали несуществующий класс, вызывали синхронную функцию с await и передавали session там, где нужен engine. Каждый раз — с полной уверенностью: прочитанная документация была рассчитана на читателя, который переходит между страницами. Расскажу, что я изменил: отдельная страница каждой библиотеки для модели, правила на ней, передача в окно чата одним нажатием и реальная стоимость такого подхода.

Два читателя, два вопроса

Человек в документации библиотеки ищет ответ: «Стоит ли её использовать и как она устроена?» Он начинает с обзора, просматривает концепции, открывает быстрый старт, возвращается к справочнику при неясной сигнатуре. Ему подходят объяснения и диаграммы. Подходит и руководство из шести последовательных страниц: он переносит контекст между ними и не требует повторять одно и то же.

Модель, которая сейчас будет писать код, решает более узкую задачу: «Какой именно здесь API и что сломается, если я ошибусь?» Она не листает сайт. Она разом получает вставленный в контекст материал и заполняет пробелы самым правдоподобным вариантом из других библиотек. Если написано «передайте координатор», но не указан импорт, модель импортирует его из корня пакета: обычно такие вещи лежат там. Если показан await run_alembic_upgrade(...), но не сказано, что get_all_revisions синхронна, модель поставит await перед обеими. Ошибки не случайны: это API вашей библиотеки, спроектированный как усреднение всех остальных.

С этого наблюдения всё началось. Модель правильно читала документацию, но та не сообщала нужного. Концепции были, инвариантов не было.

Одна страница на библиотеку

Теперь у каждой библиотеки организации есть agents.md: пункт «For AI agents» в навигации и адрес /agents/ на сайте. Это не краткий пересказ других страниц. Это вся библиотека на одной странице для читателя, который не увидит остальных, пока ему явно не предложат их загрузить.

У страниц общий каркас, и в нём основа подхода. Вот начало страницы deadline-budget — первое, что читает модель:

| Пакет       | deadline-budget на PyPI, корень импортов — deadline_budget                    |
| Требования  | Python 3.10+, без зависимостей времени выполнения                             |
| Установка   | pip install deadline-budget · extras: settings (модели Pydantic), dishka     |
| Точки входа | DeadlineBudget, BudgetContext — оба из deadline_budget                       |
| Асинхронность | Нет. Все методы синхронны и сразу возвращают результат: читают часы,        |
|               | не спят, ничего не ожидают и не отменяют                                    |

Имя пакета и корень импортов: они различаются, а модели их путают. Места импорта точек входа: именно здесь модель ошибается первой. Сведения о том, есть ли асинхронный API, ещё до первого примера: увидев один await, модель начинает ожидать всё.

Дальше — раздел Scope из двух абзацев: что библиотека делает и чего не делает. Второй полезнее. У deadline-budget прямо сказано: библиотека ничего не обеспечивает принудительно, не запускает таймеров и задач, ничего не отменяет, не оборачивает клиенты; у неё нет собственного транспорта — заголовков, контекстных переменных или thread-local. Каждая оговорка закрывает предположение, которое модель иначе приняла бы и заложила в код.

Затем Mental model — четыре–шесть сущностей и поток между ними; Wiring — минимальная полная программа. Потом API в таблицах: имя, сигнатура, возвращаемое значение, исключения. И наконец два раздела, которые делают основную работу.

ИДЕЯ В СХЕМЕПередайте агенту полный контракт
---
config:
  theme: default
  look: classic
  flowchart:
    useMaxWidth: false
    wrappingWidth: 150
    padding: 12
    nodeSpacing: 24
    rankSpacing: 32
---
flowchart LR
    accTitle: Передайте агенту полный контракт
    accDescr: Одна страница для агента объединяет импорты, сигнатуры, инварианты и примеры. Так меньше пробелов, которые модель иначе заполнила бы догадками.
 A["Импорты и сигнатуры"] --> D["agents.md"]
 R["Правила и границы"] --> D
 E["Ошибки и верные примеры"] --> D
 D --> M["ИИ-агент"] --> C["Код интеграции"]

Одна страница для агента объединяет импорты, сигнатуры, инварианты и примеры. Так меньше пробелов, которые модель иначе заполнила бы догадками.

Правила, от которых зависит корректность кода

Под таким заголовком на каждой странице есть нумерованный список. Это не советы. Каждый пункт — инвариант, который библиотека не проверит за вас и нарушение которого даёт внешне рабочий код. Несколько примеров из разных библиотек:

  • servicewright: «serve() возвращается, пока входящая работа ещё принимается. Когда установлен stop, верните управление; не закрывайте слушатель в этом месте. Host сначала переводит readiness в false, затем вызывает drain(grace)».
  • alembic-gauntlet: «Ваш env.py определяет, имеют ли эти проверки смысл. При наличии config.attributes['connection'] он обязан использовать это соединение. Если проигнорировать его, тесты пройдут, мигрируя public через соединение, которое никто не откатывает».
  • grpc-client-kit: «Таймаут — бюджет всего вызова, включая повторы. Длительность вызова не равна max_attempts × timeout».
  • pg-partsmith: «Передавайте Engine / AsyncEngine, никогда Session / AsyncSession. DDL выполняется через собственное соединение и сразу фиксируется; транзакция session для этого не подходит».
  • omni-box: «Библиотека никогда не открывает и не фиксирует транзакцию БД».
  • clientwright: «UNSET — не None. UNSET оставляет стандартное значение адаптера и отражает это в отчёте; None явно означает отсутствие ограничения».

Обратите внимание на форму. Правило называет правдоподобное действие модели, объясняет последствия и даёт короткую формулировку взамен. «Engine, никогда Session» модель может удержать на протяжении двухсот строк генерируемого кода. Абзац руководства о несовместимости DDL и session верен и полезен, но модель его так не удержит.

На библиотеку приходится от пятнадцати до двадцати правил. Их написание стало самым полезным упражнением с документацией за много лет по причине, не связанной с моделями: чтобы так сформулировать правило, нужно точно знать поведение. Руководство по концепциям не требует от автора такой точности.

Сначала WRONG, затем RIGHT

Второй раздел — Common mistakes, где каждый пример дан парой:

# WRONG — a mixin that does not exist in this package
from alembic_gauntlet.contrib.testcontainers import TestcontainersDatabaseMixin

class TestMigrations(TestcontainersDatabaseMixin, MigrationTestBase): ...

# RIGHT — contrib ships one fixture; import it into a conftest
# tests/conftest.py
from alembic_gauntlet.contrib.testcontainers import migration_db_url  # noqa: F401
# WRONG — the budget object sent to another service
await billing.charge(order_id, budget=pickle.dumps(ctx.budget))

# RIGHT — the number sent, a new budget built on arrival
await billing.charge(order_id, timeout=ctx.timeout_for_call("billing.charge"))
# ... in the callee:
budget = DeadlineBudget(total_seconds=timeout_from_request, safety_margin=0.2)

Половина WRONG не выдумана ради спора. Это правдоподобная форма: API, каким он был бы в усреднённой библиотеке. Именно к ней модель тянется, когда страница оставляет пробел. Упоминание «интеграции с testcontainers» без подробностей даст вам такой mixin. Неверный код рядом с верным работает лучше длинного объяснения: модель распознаёт форму, которую собиралась выдать, и выбирает соседнюю. Сейчас на четырнадцати страницах 83 такие пары.

Страница должна попасть к модели

Страница для модели бесполезна, если её приходится выделять целиком в браузере и надеяться, что форматирование сохранится. Поэтому каждая страница документации организации доступна и как исходный Markdown по собственному адресу: /guide/quickstart/ — ещё и /guide/quickstart.md, а /agents//agents.md. Никакой сложной генерации: после сборки сайта скрипт из тридцати строк копирует каждый docs/<path>.md в site/<path>.md, рядом с адресом HTML. Модель, способная загрузить URL, получает текст; агенту с инструкцией «прочитай /agents.md перед написанием кода» не нужно извлекать содержимое из HTML.

Над каждой страницей есть элемент управления для человека с открытым чатом. Основная кнопка Copy page копирует Markdown. В меню — View as Markdown, Open in ChatGPT, Open in Claude и Open in Perplexity: ассистент открывается с URL страницы в промпте и просьбой сначала её прочитать. Генерируемый справочник API отключает кнопку строкой метаданных copy_page: false: его Markdown содержит две строки инструкций рендереру docstring, а не сам API. Файл с вводящим в заблуждение содержимым хуже отсутствующего.

На самой странице agents всё это объясняет раздел «How to read this page»: независимо от способа получения модель знает, что ещё можно загрузить и как. Последний раздел — Documentation map, таблица других страниц и конкретных ситуаций, когда нужна каждая. «Прочитайте при выборе между DeadlineBudget и BudgetContext». Это указатель для модели: когда обращаться к материалу, а не просто перечень существующего.

Как сохранить достоверность

Такая страница — обещание о поведении API. Устаревшее обещание учит модель несуществующему API, что хуже отсутствующей страницы: модель доверится ей больше, чем коду. Поэтому страница считается частью публичного интерфейса. Это закреплено в руководстве для участников каждой библиотеки, в шаблоне pull request есть соответствующий пункт, а правило ревью механическое: если diff меняет публичный интерфейс, но не затрагивает docs/agents.md, работа над PR не завершена.

Каркас хранится в шаблоне библиотеки организации с маркерами TODO в специфичных для библиотеки местах. Инструкции создателю новой библиотеки — человеку или агенту — требуют заполнить его до первого коммита. Кнопка Copy page состоит из четырёх файлов, намеренно побайтово одинаковых во всех репозиториях: изменение переносится за один проход, без четырнадцати слегка разных исправлений. Образцом для остальных стала страница pg-partsmith.

Цена ощутима. Четырнадцать страниц — 7045 строк Markdown: от 374 у самой маленькой библиотеки до 657 у самой большой. Каждое публичное изменение затрагивает одну из них. Сделать это дешевле у меня не получилось, и я перестал искать способ из-за следующего результата.

Что это дало людям

Страница для агентов оказалась лучшей страницей каждого сайта и для опытного инженера, уже знакомого с предметной областью. Начальная таблица отвечает на пять его первых вопросов. Scope за два абзаца показывает, подходит ли библиотека. Правила описывают то, что иначе пришлось бы узнавать во время инцидента. Эту страницу я отправляю коллеге, спрашивающему о библиотеке и её подводных камнях, и открываю сам, возвращаясь к ней через месяц.

Думаю, общий вывод именно в этом. Читатель без терпения, контекста и возможности листать сайт заставил меня явно сформулировать инварианты, поставить импорт рядом с именем, показать неверный код рядом с верным и так же тщательно описать отсутствующее поведение, как реализованное. Всё это всегда стоило говорить. Модель просто стала первым читателем, который честно сломался без этих сведений.

У каждой библиотеки в каталоге такая страница есть в разделе «For AI agents». Чтобы увидеть структуру, посмотрите pg-partsmith, по которому делались остальные, и deadline-budget — самый короткий полный пример.