Задайте вопрос о Redis, и задача покажется простой: найти нужную документацию и объяснить ее. Но вопросы по документации бывают самыми разными.
«Что делает XACK?» — это точный вопрос о команде. «Как относиться к политикам вытеснения?» — более широкий вопрос о концепции. «Почему мой потребитель потока отстает?» может охватывать несколько страниц, команд и идей.
Мы решили создать агентский интерфейс, который мог бы отвечать на вопросы о Redis с помощью точных, подкрепленных цитатами доказательств из redis.io/docs. В этой статье рассказывается, как мы прошли путь от одной широкой сущности документации к нескольким сущностям, связанным явными отношениями, которые помогают агенту переходить от поиска к доказательствам. Наши оценки показали улучшение качества поиска в целом, но ценой большего количества вызовов инструментов и увеличения хвоста задержки.
Начинаем с простого
Context Retriever позволяет разработчикам моделировать свои данные как ORM. «Сущность» определяет схему для данных; Context Retriever предоставляет инструменты поиска, которые агент может вызывать. Приложение по-прежнему решает, какие инструменты предоставить агенту и как использовать извлеченные данные в окончательном ответе.
Для первой версии (V1) мы начали с одной сущности, представляющей страницу документации, раздел или фрагмент текста: RedisioDoc. Она содержала стабильный идентификатор, название, тему, раздел, содержимое, векторное встраивание (эмбеддинг) и канонический URL.
Ниже представлена модель данных для записей, полученных из документации:
Приведенная выше схема создала шесть инструментов, но наше приложение открыло агенту только четыре из них:
Это была полезная отправная точка. Небольшой набор инструментов облегчил проверку системы, пока мы улучшали охват документации, стабильные идентификаторы, цитирование, обработку запросов и размер результатов.
Но простая модель также свела несколько поисковых задач к одному формату. Независимо от того, требовался ли вопросу обзор страницы, точная справка по команде или конкретный фрагмент, агент искал по одной и той же сущности и был вынужден самостоятельно просеивать результаты. Наши оценки в конечном итоге показали, где этот подход начал давать сбой.
Когда хороший ответ скрывал слабый поиск
Сначала мы оценивали систему так, как это сделал бы пользователь.
Звучал ли ответ правильно? Ссылался ли он на релевантные страницы? Сколько времени это заняло?
Эти вопросы важны, но они упускают из виду одну проблему. Мощная базовая модель может превратить неполные данные в убедительный текст, используя общие знания для заполнения пробелов, поэтому гладкий ответ может опираться на слабый поиск.
Это важно даже тогда, когда конечный ответ выглядит нормально. Если поиск слаб, модели приходится читать больше нерелевантного текста, делать больше вызовов и больше полагаться на то, что она уже знает. Это стоит задержек и токенов, а также подталкивает к использованию более крупной модели в качестве компенсации. Хороший поиск позволяет меньшей по размеру и более быстрой модели выдавать тот же ответ на основе данных, которые она может процитировать.
Поэтому мы начали оценивать сами данные с помощью жестких метрик до того, как модель написала ответ. Это дало нам отдельное представление о качестве поиска: какие страницы нашел агент? Какое место они заняли? Какие инструменты привели его туда?
Оценка на эталонных данных
Чтобы понять, улучшается ли поиск, нам нужно было измерить данные до того, как модель превратит их в ответ. Мы создали автономный набор для оценки, который мог тестировать различных поисковых агентов, MCP-серверы, API или сохраненные трассировки на одном и том же корпусе документации.
[Image placeholder] Evaluation kit workflow diagram (01 Set up a new retrieval target → 02 Run the shared evaluation). Upload from the source draft. Suggested alt text: “Two-phase evaluation workflow: an agent skill writes a target-specific adapter, then a reusable harness scores the adapter’s RetrievalTrace against a golden dataset.”
На рисунке выше показан двухфазный рабочий процесс.
Сначала мы подключаем поисковую цель к набору. Навык агента управляет этой настройкой, исследуя репрезентативные сырые выводы и создавая небольшой адаптер для конкретной цели. Адаптер знает, как вызывать эту систему, анализировать ее ответ, сохранять исходный порядок результатов и сопоставлять свои идентификаторы источников со стабильными идентификаторами страниц документации. Он содержит логику интеграции для конкретной системы, но не логику оценки.
После проверки адаптера можно запустить совместную оценку. Для каждой строки эталонный набор данных предоставляет:
- Вопрос.
- Основные источники документации, необходимые для ответа на него.
Вопросы были разделены на четыре группы, представляющие различные шаблоны поиска:
- Простые запросы требовали одной четкой концепции или команды. Например, для вопроса «Что делает BITCOUNT?» в качестве основного источника требовалась страница команды BITCOUNT.
- Многошаговые вопросы требовали данных из нескольких источников. «Как создать фильтр Блума, добавить элементы и проверить членство?» — ожидались страницы команд BF.RESERVE, BF.ADD и BF.EXISTS.
- Сложные случаи включали вопросы по устранению неполадок, двусмысленную терминологию и вопросы, охватывающие несколько частей документации. Для запроса «XACK не уменьшил память потока. Что мне обрезать или удалить?» ожидаемые источники включали страницы команд XACK, XTRIM и XDEL, а также руководство по потокам (Streams).
- Вопросы в стиле чата отражали более короткие промпты, похожие на производственные. «Как отслеживать задержку Redis?» — ожидалось руководство по задержке Redis.
Повторно используемая обвязка (harness) отправляет вопрос через адаптер, который вызывает цель поиска. Адаптер преобразует необработанный ответ в общую структуру RetrievalTrace: упорядоченный список извлеченных идентификаторов страниц вместе с вызовами инструментов, ошибками и задержкой, записанными во время поиска.
Затем обвязка удаляет дубликаты страниц, не меняя их исходный порядок, и сравнивает ранжированные результаты с ожидаемыми источниками. Это позволяет нам измерить как то, появились ли нужные данные, так и то, насколько рано они появились. Каждый запуск создает подробный файл trace.jsonl для проверки отдельных вопросов и отчет report.md, содержащий такие метрики, как nDCG, полнота (recall) и коэффициент успешных попаданий (hit rate).
Разделение адаптера и обвязки сделало набор универсальным. Чтобы оценить другого агента или MCP на том же корпусе, мы пишем новый адаптер, оставляя эталонный набор данных и процесс оценки неизменными. Это также разделяет два вопроса: нашел ли агент нужные данные и использовал ли модель эти данные для получения хорошего ответа?
Проблема с одним общим «документом»
Трассировка указала на проблему с формой нашей модели контекста.
Одна трассировка прояснила проблему. На вопрос «Что делает BF.RESERVE?» V1 четыре раза выполнила поиск по всей коллекции документации. В ее первые результаты попали страницы Kubernetes и REST API, в которых случайно оказались такие слова, как «reserve» или «reservation». Руководство по фильтрам Блума появилось только в самом низу первых десяти результатов, в то время как страница команды BF.RESERVE так и не появилась. Проблема заключалась не в недостатке документации; у поисковика не было прямого пути к команде как к сущности.
V1 представляла страницы, разделы и справочники по командам через одну общую сущность: RedisioDoc. Агент мог искать ее по тексту или смыслу, фильтровать по широкой теме или получать документ, если он уже знал идентификатор. Это работало для многих вопросов, но рассматривало различные поисковые задачи как одинаковые. Вопрос мог потребовать:
- Точной справки по команде.
- Обзор страницы для поиска правильной темы.
- Сфокусированный фрагмент для подтверждения ответа.
- Дополнительные свидетельства со страницы, на которой был найден полезный фрагмент.
Для этих задач требуются разные типы информации. Короткий фрагмент является полезным подтверждением, но в нем может не хватать контекста. Сводка страницы помогает в поиске, но может оказаться слишком общей для цитирования. Команда имеет четкое название и структуру, поэтому ее поиск в виде обычного текста может привести к появлению посторонних совпадений.
Именно это произошло с BF.RESERVE. Агент знал название команды, но V1 не мог распознать ее как команду и сразу перейти к ее записи. Вместо этого он выполнил поиск по более широкому набору документации, где слово «reserve» также встречалось на страницах Kubernetes и REST API.
Три сущности, связанные путями поиска
Трассировка BF.RESERVE подсказала более конкретное решение: перестать требовать от одной сущности обработки всех видов поиска. В нашей следующей архитектуре мы разделили документацию на три сущности:
- RedisioChunk содержит сфокусированные фрагменты, используемые в качестве доказательств для ответа.
- RedisioPage содержит сводки страниц, используемые для поиска.
- RedisioCommand содержит структурированные описания команд для точного поиска.
Это предоставило агенту лучший выбор, но сущности все равно должны были работать вместе. Мы также объявили явную связь между страницами и фрагментами, а также между страницами и командами. Эти связи использовали стабильный идентификатор page_id, общий для записей.
[Заглушка изображения] Диаграмма связей сущностей (RedisioPage ↔ RedisioChunk ↔ RedisioCommand, обе связи объединяются по page_id). Загрузка из исходного черновика. Предлагаемый альтернативный текст: «RedisioPage для общего поиска ссылается на RedisioChunk для получения доказательств ответа и RedisioCommand для точного поиска команд, объединенных по page_id».
Добавленные связи сделали соединения частью поисковой модели, и теперь агент мог следовать более четким путям. Например:
- Для прямого вопроса выполните поиск по фрагментам и верните наиболее веские отрывки.
- Для широкого вопроса найдите страницу по ее сводке, возьмите ее page_id и извлеките фрагменты с этой страницы.
- Для вопроса о команде получите запись команды напрямую, а затем извлеките связанный текст, когда потребуется больше пояснений.
Теперь агент мог выбирать маршрут на основе вопроса, а не рассматривать каждый запрос как одинаковый тип поиска документов. Переход заключался не просто в переходе от одного индекса к трем. Мы дали агенту небольшую карту документации: четкие места для поиска и стабильные пути для перемещения между ними.
Что показали результаты
Мы сравнили V1 и V2 на том же наборе из 62 вопросов для поиска. Мы использовали gpt-5.4-mini в качестве основы для поискового агента, и у обоих был одинаковый лимит рекурсии, равный 16 (то есть агент может сделать максимум 16 шагов для одного вопроса, прежде чем сработает защитное ограничение и он остановится).
Основной метрикой ранжирования для нас был nDCG@5. Полнота (recall) лишь показывает, появилась ли ожидаемая страница где-то в результатах. nDCG также вознаграждает за ее размещение ближе к началу и отдает предпочтение первичным источникам перед связанными, что соответствует разметке нашего эталонного датасета. Ранг здесь имеет значение, поскольку агент читает результаты по порядку, и правильная страница, зарытая на девятой позиции, с гораздо меньшей вероятностью будет использована или процитирована, чем страница на первой позиции. Мы также приводим показатель recall@5, чтобы показать, появились ли вообще нужные доказательства.
Проще говоря, nDCG измеряет, появилась ли ожидаемая документация ближе к началу первых пяти результатов. Чем выше, тем лучше. V2 в целом оказалась сильнее:
Последний запуск V2 улучшил поиск по каждой группе вопросов. Наибольший прирост дали простые поисковые запросы, где точные пути поиска команд и конкретных сущностей помогли ожидаемым источникам выйти на первые результаты. Многошаговые вопросы также значительно улучшились, что говорит о том, что агент стал лучше собирать доказательства из нескольких частей документации.
Улучшение также сохранилось за пределами первых пяти результатов. Общий показатель nDCG@10 увеличился с 0,493 до 0,658, в то время как recall@5 увеличился с 0,610 до 0,743.
Более богатая поисковая среда потребовала от агента больше работы:
V2 нашел и ранжировал лучшие доказательства, но он делал больше вызовов инструментов и работал дольше. Эта дополнительная задержка имеет наибольшее значение для самых медленных запросов. Общедоступный путь запроса имеет ограничение по времени в 25 секунд. На p95 V1 использовал около 40% этого бюджета, в то время как V2 — около 72%. Это оставляло меньше возможностей для задержек в сети, вариативности провайдера или еще одного шага поиска. Поэтому следующий шаг заключается не в том, чтобы отменить более сложную модель, а в том, чтобы сделать навигацию более эффективной; например, за счет сокращения ненужных вызовов инструментов и завершения поиска, как только будет найдено достаточно доказательств.
Чему мы научились к настоящему моменту
Связи превращают поиск в навигацию
Разделение корпуса на страницы, фрагменты и команды предоставило агенту лучшие места для поиска. Их соединение сделало каждый результат полезным в качестве следующего шага.
Поиск по странице не обязательно должен заканчиваться сводкой страницы. Ее page_id можно передать в фильтр фрагментов для извлечения доказательств с этой страницы. Запись команды может использовать ту же идентичность страницы для связи точных деталей команды с поддерживающим текстом.
Без этих путей агент вынужден выводить связь самостоятельно, конструировать правильный идентификатор и запоминать соединение из своего промпта. Явные связи переносят эти знания в модель поиска. Проектирование решения не выбирает и не выполняет маршрут за агента, но оно делает доступный маршрут понятным и надежным.
Это стало важным изменением в том, как мы оценивали качество поиска. Хороший результат не просто релевантен; он также должен облегчать следующее полезное поисковое действие.
Моделирование поиска с учетом реальных намерений пользователя
Само по себе разбиение на фрагменты не создает стратегию поиска. Вопрос о команде, вопрос для общего поиска и запрос на подтверждающие доказательства требуют разной формы результатов. Моделирование этих форм в виде отдельных сущностей дало агенту возможность выбирать то, что ему нужно, до начала поиска, вместо того чтобы выяснять смысл широких результатов после.
Стабильные идентификаторы — это ключевая инфраструктура
Связи работают только тогда, когда обе стороны согласны в отношении идентичности. Стабильные идентификаторы страниц, разделов, фрагментов, команд, якорей и URL-адресов позволили результатам одного инструмента становиться входными данными для другого. Они также сделали цитирование, оценку и отладку более надежными. В следующий раз мы спроектируем эту систему идентификаторов раньше.
Качество поиска и качество ответов требуют раздельного тестирования
Пользователи видят конечный ответ, поэтому качество ответа по-прежнему имеет значение. Но тесты на уровне доказательств показывают нам, действительно ли изменение схемы, индекса или маршрутизации улучшило поиск. Использование обоих подходов не позволяет красноречивой модели скрывать слабый поиск.
Лучший поиск может потребовать большего объема поиска
V2 улучшил ранжирование в целом, но он также увеличил использование инструментов и задержку в хвосте распределения. Следующий шаг — использовать его инструменты более избирательно: направлять вопросы о командах напрямую, использовать обнаружение страниц только при необходимости и установить детерминированные критерии для LLM, чтобы она останавливалась, как только доказательства станут достаточно весомыми.
Лучшая карта документации
Мы начали с вопроса о том, как найти правильный документ Redis.
В итоге мы пришли к лучшому вопросу: какая карта документации помогает агенту сделать правильный следующий шаг?
Иногда этот шаг заключается в поиске отрывка. Иногда — в обнаружении страницы. Иногда — в поиске команды. Иногда — в переходе по ссылке от общего результата к материалу, готовому для цитирования.
Context Retriever был механизмом, который мы использовали для моделирования сущностей и связей, но более общий урок заключается в том, чтобы не добавлять больше сущностей или инструментов по умолчанию. Он заключается в том, чтобы смоделировать те немногие поисковые действия, которые соответствуют реальной структуре корпуса, связать их стабильными идентификаторами и измерить, действительно ли агент хорошо их использует.
Переход от одного типа документов к связанной документационной поверхности не устранил сложность. Он сделал эту сложность более заметной, измеримой и доступной для улучшения.
Для агентного поиска это может оказаться самым полезным результатом из всех.











