Как мы создали Redis Docs MCP для агентов

Источник: Redis•

Как мы создали Redis Docs MCP для агентов

Спросите агента по программированию, как настроить вытеснение (eviction) в Redis 8, и вы обычно получите ответ. К сожалению, этот ответ не всегда основан на официальной документации. Агент либо вспоминает что-то из предварительного обучения, либо парсит отрендеренный HTML с сайта redis.io...

Если вы спросите ИИ-агента, как настроить вытеснение (eviction) в Redis 8, вы, скорее всего, получите ответ. Однако этот ответ не всегда основан на официальной документации, как нам хотелось бы. Агент либо вспоминает что-то из своих обучающих данных, либо парсит HTML-код с сайта redis.io (включая маркетинговые тексты и примечания к старым версиям) и перефразирует его. В Redis 8 изменилось несколько параметров по умолчанию, поэтому модель, обученная на более ранних версиях, с полной уверенностью выдаст вам ответ для Redis 7.

Существующие варианты решения этой проблемы не позволяют полностью закрыть разрыв. Веб-поиск возвращает страницы без стабильных идентификаторов, поэтому агент не может сослаться на прочитанный материал или вернуться к нему. Клиентская выборка (retrieval) работает, но заставляет каждый фреймворк заново реализовывать разбиение на фрагменты (chunking), создание эмбеддингов и проверку актуальности для корпуса данных, которым он не владеет.

Поэтому мы создали Redis Docs MCP — публичный MCP-сервер, доступный по адресу redis.io/mcp. Он предоставляет три инструмента для работы с документацией Redis (поиск, получение и запрос) без аутентификации для любого MCP-клиента. Четверо из нас работали над ним в течение одного квартала, с апреля по июль 2026 года.

В этой статье мы рассмотрим:

  • Поверхность инструментов MCP и причины, по которым поиск/получение и запрос идут разными путями к одному и тому же корпусу данных.
  • Почему индекс документации имеет одного автора и двух читателей, и во что обходится это ограничение.
  • Как мы проверяли дизайн до написания кода и что изменилось, когда большую часть работы стали выполнять ИИ-агенты.
  • Практический пример: как сделать так, чтобы инструмент «запрос» возвращал цитаты, которые модель не может выдумать.
  • Как мы измеряли качество поиска и на что указывали показатели, которые мы изначально не принимали во внимание.

Если вы создаете сервис для агентов в небольшой команде, средние разделы будут для вас наиболее полезными. Чтобы подключить клиента к документации, переходите сразу в конец.

Все началось с демо

Проект не задумывался как MCP-сервер. В конце апреля 2026 года у нас было три недели, чтобы создать помощника по документации для страницы продукта Context Retriever — инструмента Redis, на котором базируется весь этот проект. Вы объявляете схему сущностей, и он предоставляет управляемую поверхность поиска поверх базы данных Redis, а затем открывает сгенерированные инструменты поиска для агента через MCP. Этот помощник, чат-эндпоинт на маркетинговой странице, стал первым, что мы выпустили в рамках проекта.

За три недели мы создали FastAPI-сервис, передающий события от сервера (SSE), агента, который разбивает вопрос на вызовы поиска, и около сотни отобранных страниц документации, внедренных в поверхность Context Retriever. Мы вывели процесс поиска на экран: этап размышления, вызов инструмента, результат инструмента и последующий вывод ответа. Он заработал в начале июня 2026 года.

Эти три недели доказали, что конвейер работает, но ничего не сказали об интерфейсе. У демо есть один пользователь — человек, читающий вывод, и дата запуска, заменяющая спецификацию. У публичного эндпоинта ничего этого нет: пользователи — анонимные MCP-клиенты, никто не следит за потоком, и нет даты, подгоняющей дизайн. Мы зафиксировали этот риск в квартальном плане перед началом следующей фазы. Мы должны были избежать сценария, при котором «весь квартал уходит на полировку веб-демо без создания пригодного для повторного использования интерфейса для агентов».

Один контракт, много клиентов

MCP — это стандарт, через который ИИ-агенты ожидают получать доступ к внешним инструментам, поэтому вопрос о протоколе даже не стоял. Нужно было решить, какой будет поверхность инструментов: что именно она должна предоставлять и в каком объеме.

Один контракт MCP позволяет охватить Claude, Codex, Cursor и ChatGPT Deep Research без необходимости выпускать четыре интеграции или просить четырех вендоров делать одну. Протокол также передает описания инструментов во время рукопожатия, что оказалось главным рычагом управления поведением агента.

Другие провайдеры уже решили эту задачу по-своему. Microsoft Learn и Cloudflare используют свои MCP-серверы для документации. Context7 от Upstash доказал, что публичный MCP-сервер для документации без ключей — это вполне рабочая модель. Supabase аутентифицирует каждого вызывающего, что правильно для сервера, работающего от имени прав разработчика. Наш сервер отдает публичные страницы, поэтому мы этого не делали.

Отсутствие ключей было осознанным выбором, исходящим из того, кто, по нашему мнению, будет этим пользоваться. Документация Redis служит как нашему сообществу open-source, так и корпоративным клиентам, и требование регистрации перед поиском по документации отсекло бы большую часть первой группы.

Это решение накладывает ограничения на все последующие этапы. Любой клиент может обратиться к серверу, и мы не знаем, кто это, поэтому текст запроса конкретного пользователя никогда не попадает в логи, телеметрия носит только совокупный характер, а внутренние оценки ранжирования или недоступные для получения идентификаторы не передаются по сети. Вопросы корпоративного клиента для нас так же анонимны, как и любые другие, и именно этого мы и добивались. А поскольку все три инструмента приходят как один JSON-RPC POST /mcp, лимиты на количество запросов для каждого инструмента должны быть реализованы на уровне приложения: пограничный слой не может отличить поиск от запроса без парсинга тела запроса.

Что мы выпустили

Redis Docs MCP предоставляет три инструмента и ничего больше. Мы намеренно ограничили их количество: описание каждого инструмента включается в контекст клиента для всего соединения, поэтому четвертый инструмент «стоит» каждому клиенту токенов при каждом вызове, независимо от того, использует он его или нет.

Названия инструментов могут совпадать, поэтому уточним: это не официальный Redis MCP Server, который является самохостируемым и подключает агента к вашему собственному экземпляру Redis для выполнения команд над вашими данными. Redis Docs MCP хостится нами, работает только на чтение и не взаимодействует ни с какими экземплярами Redis, кроме нашего собственного индекса документации.

Прямой путь: поиск и получение

Инструменты поиска и получения соответствуют формату, установленному OpenAI для MCP-серверов документации, вплоть до именования фрагмента текста как «text» вместо «content». Коннекторы ChatGPT Deep Research требуют именно такого контракта, поэтому следование ему означает, что эти клиенты работают с Redis Docs MCP без каких-либо дополнительных интеграций с обеих сторон.

Оба инструмента запрашивают индекс Redis напрямую через RedisVL, минуя Context Retriever. Это было наиболее важным решением, так как владение запросом позволяет нам его настраивать. Прямое обращение означает, что поиск выполняет собственный FT.HYBRID к движку запросов Redis, поэтому текстовое поле, скорер, метод объединения и количество кандидатов — все это мы можем настраивать, и мы настроили все четыре параметра. Благодаря сгенерированным инструментам, запрос принадлежит шлюзу.

Путь через шлюз: запрос

Инструмент «запрос» (ask) выходит за рамки конвенции и является единственным, который взаимодействует с Context Retriever и языковой моделью. Он использует тот же конвейер поиска и синтеза, что и чат-эндпоинт маркетингового демо, но представлен через другой транспорт, как общая библиотека, а не как HTTP-вызов к сервису. Важно уточнить порядок действий: модель владеет сгенерированными инструментами Context Retriever и сама решает, какой из них вызвать, поэтому модель управляет поиском, а не просто суммирует результаты после. Исключение поиска и получения из этого пути обеспечивает доступность: два дешевых и высоконагруженных инструмента продолжают работать, даже если шлюз или модель недоступны. Цена этого — необходимость отлаживать два пути отдельно, а также то, что они могут выдавать разные результаты для одного и того же запроса.

Поиск и получение продолжают работать, даже когда модель и шлюз недоступны.

Один автор, два читателя

Две службы считывают индекс документации, и только один процесс записывает его, что и предотвращает их рассинхронизацию.

Конвейер приема данных считывает Hugo markdown из репозитория redis/docs, удаляет метаданные (front matter) и шорткоды, разбивает длинные страницы по заголовкам ##, векторизует каждый фрагмент и импортирует результат через Context Retriever. Ни демо-эндпоинт чата, ни Redis Docs MCP не владеют жизненным циклом индекса, схемой или генерацией векторов. MCP-сервер даже не хранит имя индекса в конфигурации в продакшене; он обнаруживает индекс при запуске.

Поскольку Context Retriever генерирует свои инструменты для агентов на основе объявленной схемы, схема является API для всего, что обращается к корпусу данных через шлюз. Объявление поля в качестве тега также создает инструмент filter_by_<field>, который могут вызывать агенты.

Это имеет свои нюансы. search возвращает id, который разрешает fetch, поэтому оба поля должны возвращаться из запроса, а в Redis-индексе в режиме JSON только объявленные атрибуты разрешаются по простому имени. Очевидным решением было объявить doc_id и url как теги. Это сработало и добавило filter_redisiodoc_by_doc_id и filter_redisiodoc_by_url в набор инструментов агента: два инструмента, которые никто не должен вызывать, но которые каждый раз конкурируют за внимание модели. Мы перешли на получение обоих полей по JSON-пути и их продвижение обратно в адаптере, а затем записали это решение, чтобы никто не выводил его заново.

Это ограничение легко сформулировать, но трудно поддерживать. EMBEDDING_MODEL и EMBEDDING_DIM — это экспортируемые константы, импортируемые как процессом приема, так и путем запроса, поэтому они не могут разойтись. Однако изменение схемы требует одновременного обновления процесса приема и обоих читателей, а также перенастройки поверхности, чтобы шлюз перегенерировал свои инструменты.

Мы проверяли спецификации тщательнее, чем код.

Проверка дизайна до появления какого-либо кода — это то, что сделало работу делегируемой, что оказалось важнее, чем мы ожидали.

Спецификационно-ориентированная разработка означает запись дизайна изменений в репозитории и предварительную проверку этого документа, а затем проверку реализации на соответствие ему. Наши спецификации живут в директории spec/ и насчитывают около тридцати документов, сгруппированных по предложениям, интерфейсным контрактам, обзорам и заметкам. Каждый из них содержит строку статуса, а не перемещается между папками по мере продвижения, поэтому его URL никогда не меняется.

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

Последняя часть — это главный результат, потому что агенты-кодеры написали здесь много кода: 135 из 374 коммитов содержат агента в качестве автора. Агент, которому передали согласованный контракт и набор проверяемых критериев приемки, выполняет ограниченную работу. Агент, которому передали цель, выполняет исследовательскую работу, которую кому-то придется переделывать.

Мы также добавили агентов на сторону проверки, в циклах. У этой практики теперь есть название: петлевое проектирование (loop engineering), проектирование цикла обратной связи вокруг агента, а не просто промпта, подаваемого в него. Что принесло результат, так это прогон одной реализации через несколько персон рецензентов последовательно, каждая из которых искала что-то свое. Один проверяет соответствие контракту, другой — что изменение позволяет «утечь» к вызывающему объекту (внутренний балл, недоступный id, тело запроса, попадающее в лог), третий пытается сломать изменение, четвертый проверяет, описывает ли документация код.

Эти проходы изменили дизайн, а не просто отполировали его. Пять из них прошлись по описаниям инструментов, а один уничтожил наше первое правило эскалации для ask, которое просило модель заранее судить, понадобится ли вообще вопрос. Два рецензента независимо высказали одно и то же возражение: это требует прогноза в тот момент, когда модель меньше всего знает о вопросе. Мы заменили это на нечто наблюдаемое, эскалируя только после того, как поиск возвращается пустым или нерелевантным.

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

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

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

Агенты также пишут слишком много. Повторяющийся класс коммитов здесь делает только одно: удаляет баннерные комментарии и избыточные пояснения из кода и спецификаций, которые создал агент.

Цитаты, которые модель не может создать сама

Модель напишет цитату, которая выглядит правильно, но никуда не ведет, поэтому мы вообще перестали позволять ей их писать.

Некоторое время ask цитировал документы так, как это делает языковая модель по умолчанию, записывая URL-адреса самостоятельно. Затем он процитировал страницу, которой не существует: develop/ai/context-engineering/langcache/api-examples, которая выдает 404, в то время как реальная страница — develop/ai/context-engine/langcache/api-examples#sec:w0. Один сегмент пути лишний, context-engineering вместо context-engine, и это именно та ошибка, которую читатель не заметит при просмотре. Суффикс #sec:w0 — наш: процесс приема разбивает длинную страницу на оконные секции и дает каждой свой id, поэтому цитата называет секцию, а не всю страницу целиком.

ask теперь возвращает массив источников вместе с ответом, полученный из того, что конвейер поиска фактически извлек во время выполнения запроса. Каждый id был получен в результате реального вызова инструмента, поэтому вымышленный появиться не может, и каждый id разрешается через fetch. Шлюз Context Retriever возвращает структурированную запись того, что он извлек, поэтому список цитат уже был готов к чтению, а не выведен из текста ответа. Мы этого не планировали.

Это оставило проблему упорядочивания. Каждый источник получает вес, рассчитанный на основе отличительных фактов, которые утверждает ответ (имена команд, директивы конфигурации, числа с единицами измерения), и того, насколько редок каждый из них во всем наборе извлеченных данных. Мы переупорядочиваем весь набор по весу и никогда не обрезаем его, потому что риск асимметричен: отбрасывание документа, который обосновывал утверждение, невозможно обнаружить по результату, в то время как ранжирование документа на три позиции ниже стоит читателю лишнего клика. Важно то, что находится в начале списка, так как вызывающий объект, который читает только первые несколько записей, должен получить документы, которые обосновали ответ.

Алгоритм Round-robin кажется подходящим способом защиты, но он дает сбой, который проявляется только во время выполнения. Длинные страницы добавляют по одной записи на каждый раздел, поэтому одна страница может занимать шесть из пятнадцати слотов, и чередование по страницам кажется решением. Однако Round-robin сдвигает k-е вхождение страницы на позицию (k−1) × P + 1, где P — количество найденных уникальных страниц, поэтому смещение отслеживает, сколько других страниц вернулось, а не то, насколько избыточна страница. При измерении на одной реальной полезной нагрузке, где страница внесла три хорошо обоснованных раздела с весами 8, 7 и 6 наряду с пятью слабыми документами с весом 0,2 каждый, чередование отправило второй и третий разделы на позиции 9 и 11, позади документов, имеющих тридцатую часть их веса. В итоге первая пятерка потеряла 42% общего веса. Это также не сработало в случае, который послужил мотивацией, оставив первую пятерку без изменений и сделав самую длинную последовательность одной и той же страницы еще длиннее, потому что, как только поверхностные страницы исчерпаны, остаток глубокой страницы все равно появляется последовательно.

Вместо этого сработал штраф за вес. n-е вхождение страницы должно превосходить лучшее вхождение другой страницы на n−1 единиц, где единица — это то, что токен зарабатывает при появлении ровно в одном из полученных документов (log(общее_количество_документов)). Таким образом, страница с тремя независимо сильными разделами сохраняет все три вблизи топа, в то время как страница, повторяющая саму себя, опускается после первого вхождения. Измеренное улучшение невелико: два повторения одной страницы покидают первую шестерку, и их места занимают две разные страницы.

Функция была выпущена за явным шлюзом, требующим, чтобы 100% возвращенных идентификаторов разрешались через fetch, прежде чем она была запущена.

Проверка работы

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

Это означало две обвязки, которые держались отдельно, потому что они измеряют разные уровни:

Обе работают с одним и тем же размеченным набором данных с оценкой релевантности, где документ, который напрямую отвечает на вопрос, ценится выше, чем тот, который предоставляет контекст, и обе используют одну и ту же функцию оценки. Разделение их изолировало вклад агента от вклада поисковика. При измерении по 11 простым вопросам поиска в этом наборе, переформулировка запроса агентом повысила recall@5 с 0,545 до 0,818, потому что он переписывает плохой запрос и ищет снова. По 8 вопросам с несколькими переходами он изменил точность чанков в топ-5 в обратную сторону, с 0,275 до 0,225, потому что он выполняет больше поисковых запросов и размывает окно.

Агенты построили большую часть окружающей обвязки: сценарии нагрузочного тестирования Locust, сборщики, которые захватывают необработанные кадры событий для последующего просмотра, и генераторы отчетов.

Нагрузочный тест измеряет все, до чего позволяет дотянуться ваша конфигурация. Наш первый серьезный запуск направил 50 одновременных пользователей на демо-эндпоинт чата и вернулся с 98,2% уровнем отказов, причем каждый отказ был HTTP 429. Мы измерили ограничитель скорости, а не сервис, и запуск ничего не сказал о том, где ломается сервис или успевает ли автоскейлинг, что было двумя вопросами, на которые мы собирались ответить.

Заставить изменение оправдать себя дешевле, чем спорить о нем. Прототип работал на чистом векторном поиске, и мы ожидали, что для любого промышленного уровня потребуется гибридный поиск, потому что точные токены — это то, где сходство эмбеддингов наиболее слабое: XADD и SINTERCARD почти не несут семантического сигнала как векторы и однозначны как строки. Вместо того чтобы утверждать это, pull request, реализующий его, был выпущен с приложенной оценкой, измеренной по 73 запросам относительно базовой линии только для векторов, которую он заменил, на чанковом корпусе с аппроксимированным векторным индексом.

Гибридный поиск работает как FT.HYBRID, где Redis выполняет текстовую оценку, нормализацию оценок и слияние рангов на стороне сервера. В коде приложения ничего не объединяется.

Hit@3 выигрывает 0,08, а recall@8 выигрывает 0,06, а задержка поиска остается на прежнем уровне: от 0,5 до 0,7 мс p50 и менее 1,3 мс p95 при измерении локально. Hit@1 теряет 0,04, потому что слияние рангов иногда меняет местами точный топовый результат: приемлемый компромисс для инструмента, вызывающий которого читает все восемь и может получить любой из них, и плохой для инструмента, который возвращает один ответ. Ранжирование по заголовку превзошло контент по всем метрикам, потому что совпадение заголовка срабатывает, когда запрос называет страницу, и редко иначе. Взаимное слияние рангов (Reciprocal rank fusion) победило взвешенную линейную комбинацию, которая была одновременно медленнее и хуже по всем метрикам ранжирования, которые мы измеряли.

Та же оценка рассказала нам то, что мы не искали. Большая часть выигрыша гибридного поиска происходит за счет подстраховки промахов аппроксимированного поиска ближайших соседей, а не за счет добавления лексического сопоставления: в индексе точного поиска те самые запросы по точным токенам, о которых мы беспокоились, уже занимали первое место только при векторном поиске. Поиск по целым страницам также превзошел поиск по чанкам на лексическом подмножестве этих запросов, 0,776 против 0,762 MRR. Относитесь к этой разнице как к направлению, а не как к измерению, потому что в двух запусках использовались разные векторные индексы, а на согласованном индексе чанковый корпус выигрывает в целом. Этого все равно было достаточно, чтобы мы посмотрели на корпус, а не на алгоритм: токены было несложно сопоставить, а наша нарезка на чанки разбивала страницы, которые их содержали.

Что бы мы изменили

Разработайте схему идентификаторов до появления корпуса. Поиск возвращает идентификатор, который должен разрешить fetch, и этот контракт проникает в ingest, нарезку на чанки, схему URL и оба ридера. Мы расширили валидаторы и переосмыслили, что делает идентификатор доступным для получения более одного раза, прежде чем все устоялось.

Измерьте корпус перед настройкой поисковика. Гибридный поиск был реальным улучшением, но не тем улучшением, которое имело наибольшее значение: наши цифры продолжали указывать на форму данных под ним. Мы объявили одну плоскую сущность документа, разбитую по заголовкам, без чего-либо, отличающего страницу справки по команде от концептуального руководства, и без способа выразить, что они связаны, поэтому у поисковика не было пути к команде как к сущности и не было ребра, по которому можно было бы перейти от концепции к командам, которые ее реализуют. Никакая настройка алгоритма поиска этого не исправит. Объявление сущностей и связей между ними превращает корпус в нечто, по чему агент может перемещаться, а не только выбирать образцы, и в этом разница между использованием Context Retriever как векторного индекса с фильтрами и использованием того, что он делает на самом деле.

Этот навигационный слой и онтология под ним — это большая часть работы, в которую превратился этот проект, и это тема сопутствующего поста Хиллари То.

Попробуйте

Redis Docs MCP является общедоступным и не требует ключа. Для клиента MCP, который читает конфигурацию JSON:

Начните с поиска, получите то, что вам нужно, и оставьте ask для вопросов, которые охватывают несколько областей документации.

Благодарности

Создано вместе с Робертом Шелтоном, Хиллари То и Джу Бин Лимом, каждый из которых работал над большей частью того, что описывает этот пост.

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от Redis