AI как помощник QA-инженера: локальный RAG для работы с тестовой документацией
Идея пришла не из желания «автоматизировать рутину» и не из следования трендам. Я хотел другого: чтобы рядом был кто-то, кто так же хорошо знает документацию, как я сам. Кто держит в голове всю структуру — требования, методики, тест-кейсы, их связи и противоречия. Кто не забывает, что было в версии 2.1, и помнит, как это соотносится с разделом 4.3.
Локальный AI-агент, который живёт внутри корпоративного периметра и работает с той же базой знаний, что и я.
Не исполнитель, которому нужно объяснять контекст с нуля. Не поисковик по ключевым словам. Он должен понимать структуру документа, улавливает смысл запроса и оперирует теми же понятиями, которыми оперирует команда.
Документация в QA: объём как системная проблема
В QA документация — это не сопутствующий материал. Это рабочая среда: программы и методики испытаний, технические требования, спецификации, тест-кейсы, протоколы, отчёты.
На активном проекте объём таких материалов растёт постоянно. Появляются новые версии. Старые редактируются. Документы ссылаются друг на друга. В какой-то момент экосистема документации становится по-настоящему сложной — и держать её целиком в голове одного человека уже невозможно.
Проблема не в том, что работать лень. Проблема в том, что человек физически плохо справляется с большими объёмами слабо структурированного текста — особенно когда формулировки меняются от версии к версии, компонентов в проекте много, а требования и тест-кейсы разнесены по разным документам.
Я хотел получить не исполнителя, а именно двойника: агента, который уже знает контекст проекта, понимает иерархию документов и способен рассуждать на уровне смысла — «что здесь проверяется», «с чем это пересекается», «чего не хватает». И при этом — работает полностью локально, без единого байта, покидающего периметр компании.
Почему не облако
Технически самый простой путь — подключить облачную LLM и построить RAG поверх неё. Это можно сделать за несколько дней.
Но в моём случае такой вариант не рассматривался — по нескольким причинам сразу.
Конфиденциальность. Продукт, над которым работает команда, является проприетарной разработкой. Документация содержит коммерчески чувствительную информацию. Отправка технических спецификаций во внешний LLM-сервис создаёт юридические риски и прямо нарушает обязательства перед заказчиком.
Зависимость от внешней инфраструктуры. Доступ к облачным API может быть ограничен — и не только по техническим причинам. Геополитическая ситуация оказывает реальное влияние на доступность сервисов: без выделенного VPN получить доступ к большим моделям попросту не получится. Добавьте сюда риски сбоев в облачной инфраструктуре и невозможность официальной оплаты — и картина становится полной.
Стоимость и предсказуемость. Локальная система работает независимо от состояния внешней инфраструктуры и не генерирует переменные операционные расходы.
Поэтому я поставил жёсткое ограничение:
Ни один байт внутренней документации не должен покидать рабочую машину.
Это усложнило задачу, но одновременно сделало её интересной.
Что такое RAG и почему без него нельзя
LLM сама по себе — плохой источник истины для внутренней документации. Она не знает о конкретном продукте. Особенно если вашей документации нет в online. Если задать вопрос напрямую, она либо откажется отвечать, либо начнёт «додумывать» правдоподобный, но неверный ответ. В профессиональном сообществе это явление называют галлюцинацией.
В QA галлюцинации недопустимы.
RAG (Retrieval-Augmented Generation) решает эту проблему: перед генерацией ответа система находит релевантные фрагменты из базы знаний и передаёт их в контекст модели. Модель отвечает не «из головы», а опираясь на реальные данные, которые вы ей явно предоставили.
Вопрос пользователя
│
▼
Поиск релевантных фрагментов в базе знаний
│
▼
Передача фрагментов + вопроса в LLM как контекст
│
▼
Ответ на основе реальных данных
Модель становится не источником знания, а инструментом интерпретации уже существующей информации. Ключевое преимущество: при изменении документации не нужно переобучать модель — достаточно переиндексировать изменившиеся документы.
Эволюция RAG: как я выбирал уровень сложности
RAG — это не один фиксированный подход. За несколько лет сформировалась явная эволюция уровней сложности: от наивного вброса всего документа в промпт до полноценных агентных систем с графами знаний.
| Уровень | Название | Суть | Метод поиска | В проекте |
|---|---|---|---|---|
| 0 | Naive RAG | Весь документ → в промпт целиком | Нет поиска | ✗ |
| 1 | Basic RAG | Чанки фиксированного размера + cosine similarity | Dense only | ✗ |
| 2 | Hybrid RAG | Dense + Sparse + Reranker | Dense + BM25 + Rerank | ✅ Текущий |
| 3 | Structured RAG | Метаданные, фильтры, иерархия документа | Filtered search | ✅ Текущий |
| 4 | Agentic RAG | LLM планирует цепочку запросов, multi-hop | Итеративный | Следующий шаг |
| 5 | Graph RAG | Knowledge Graph поверх векторов | Graph + Dense | Перспектива |
Уровень 0 — Naive. Весь документ помещается в промпт целиком. Работает для небольших файлов, но неприменим для большой базы знаний: контекстное окно любой модели ограничено, а стоимость токенов (даже при локальном инференсе — во времени) делает подход нежизнеспособным.
Уровень 1 — Basic. Документы разбиваются на чанки фиксированного размера, каждый чанк векторизуется, запрос сопоставляется с чанками по косинусному расстоянию. Работает, однако плохо справляется с точным поиском технических терминов, аббревиатур и числовых значений — именно тех вещей, которые критичны в QA-документации. Кроме того, такая система не развивается: данные представляют собой набор изолированных чанков без структурных связей, и добавить фильтрацию по разделу или типу содержимого туда практически невозможно без переосмысления всей архитектуры.
Уровень 2 — Hybrid. К плотному (dense) поиску добавляется разреженный (sparse) — BM25 или SPLADE — для точного поиска по ключевым словам. Результаты из обоих источников объединяются через алгоритм Reciprocal Rank Fusion (RRF), после чего переранжируются cross-encoder reranker'ом. Это существенно улучшает точность на технических терминах.
Уровень 3 — Structured. Чанки обогащаются структурными метаданными: номер раздела, тип блока (тест-кейс, секция, таблица), название тестируемой функции. Поиск можно ограничить конкретным разделом или типом содержимого ещё до векторного сопоставления — что снижает шум и многократно улучшает релевантность результатов.
Уровень 4 — Agentic. LLM сам определяет, что и когда искать, при необходимости делает несколько запросов подряд и объединяет информацию из разных источников. Необходим для сложных вопросов вида «сравни методику испытаний модуля А с модулем Б» — многошаговая задача, которую линейный пайплайн решить не в состоянии.
Уровень 5 — Graph RAG. Граф связей между сущностями поверх векторного поиска. Позволяет отвечать на вопросы о зависимостях, которые не выражены явно ни в одном отдельном документе, но вытекают из структуры всей базы знаний.
Я остановился на комбинации уровней 2 и 3. Это оказалось оптимальным компромиссом: гибридный поиск с реранкером обеспечивает качество поиска, структурированные метаданные — точность фильтрации. Реализуются за разумное время, дают измеримый результат и оставляют чёткий путь для дальнейшего роста.
Уровни 4 и 5 — интересная перспектива, но преждевременная инвестиция. Агентные циклы и графы знаний оправданы тогда, когда базовый пайплайн уже исчерпан. Начинать с Graph RAG — значит решать проблемы, с которыми я ещё не столкнулся.
Архитектура решения
Технологический стек
| Компонент | Технология | Роль |
|---|---|---|
| Оркестрация | N8N (Docker) | Workflow-автоматизация, триггеры |
| LLM / Embed | LM Studio | Локальный инференс моделей |
| Embedding | bge-m3 | Мультиязычные векторы, 1024 dim |
| Vector DB | Qdrant | Хранение и поиск векторов |
| Reranker | FlashRank | Переранжирование, CPU-only, < 50 мс |
| API слой | FastAPI | HTTP-интерфейс между N8N и Python |
| БД N8N | PostgreSQL | Хранение состояния workflow |
| Парсер | DOCXConverter (custom) | Извлечение структуры .docx |
Все компоненты живут в Docker на одной машине. LM Studio запущен на хосте Windows и доступен контейнерам через host.docker.internal. Это несколько нестандартная топология, но она позволяет LM Studio использовать GPU хоста без дополнительной виртуализации.
На первый взгляд архитектура выглядит сложной. На практике она достаточно прозрачна: индексация и запрос — два независимых пайплайна.
Общая схема системы
┌──────────────────────────── INDEXING PIPELINE ──────────────────────────────────┐
📄 DOCX ──► DOCXConverter ──► TableSafeChunker ──► Embedder (bge-m3)
(иерархия) (smart chunking) (LM Studio)
│
▼
Qdrant
(векторы + payload)
└─────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────── QUERY PIPELINE ─────────────────────────────────────┐
💬 N8N ──► QueryRouter ──► HybridRetriever ──► AnswerGenerator
(фильтры) (dense + rerank) (LM Studio LLM)
│ │
▼ ▼
Qdrant Ответ
└─────────────────────────────────────────────────────────────────────────────────┘
Индексация: самая важная часть
Самое сложное в RAG — не поиск. Самое сложное — подготовка данных.
Восстановление иерархии DOCX
Стандартные библиотеки — например, python-docx — видят документ как плоский список параграфов. Для QA это неприемлемо. Я написал собственный DOCXConverter, который опирается исключительно на стили заголовков Word (Heading 1, Heading 2 и т.д.), игнорируя ручную нумерацию и текстовые числа в теле документа.
В результате каждый блок получает section_hierarchy — строку вида "2.1.1.2", точно указывающую его положение в дереве документа. Каждый тест-кейс знает своё место — например, 2.1.1.3. Это открывает возможность для точной фильтрации по разделам.
Table-Safe Chunking
Таблицы в тестовой документации содержат критически важные структурные связи: шаги теста в одной колонке, ожидаемые результаты — в другой. Разрезание таблицы на чанки уничтожает эту связь и делает извлечённые фрагменты бессмысленными.
TableSafeChunker никогда не разрезает таблицы. Они всегда индексируются как единый блок, сконвертированный в Markdown:
Такой формат LLM воспринимает значительно лучше, чем сырой XML или разрозненные строки. Текстовые блоки режутся с перекрытием 100 символов, что сохраняет контекст на границах фрагментов.
Структурированный payload в Qdrant
Каждый вектор хранит не только контент, но и богатые структурные метаданные:
=
Это превращает базу знаний в управляемую структуру, а не в набор текстовых фрагментов. Запрос «покажи только тест-кейсы из раздела 2.1» выполняется через фильтрацию по метаданным ещё до векторного поиска — качество результатов при этом значительно выше, чем при глобальном поиске по всей базе.
Батчевая векторизация
Embedder отправляет все чанки одним батч-запросом в LM Studio (bge-m3). Не N последовательных запросов — один. При индексации сотен чанков это принципиально важно с точки зрения времени.
Пайплайн запроса
Когда я задаю вопрос, происходит следующее:
1. QueryRouter анализирует вопрос и определяет стратегию поиска: фильтровать только по типу test_case? Ограничить разделом 2.1? Или искать по всей базе без фильтров?
2. HybridRetriever векторизует запрос, выполняет поиск в Qdrant с применением фильтров и намеренно извлекает в 3× больше кандидатов, чем нужно финально — over-retrieval, необходимый для качественного переранжирования.
3. FlashRank переранжирует результаты с помощью cross-encoder модели. В отличие от косинусного расстояния, cross-encoder оценивает пару (запрос, документ) совместно — это даёт значительно более точный скоринг релевантности. Работает на CPU менее чем за 50 мс.
4. AnswerGenerator формирует промпт с контекстом, где таблицы представлены в Markdown, и отправляет его в LLM. Температура 0.1 — осознанный выбор: в QA нужна воспроизводимость, а не креативность.
Локальный инференс через LM Studio
LM Studio предоставляет OpenAI-совместимый API поверх любой GGUF-модели. Тот же Python-код, что работает с OpenAI API, работает локально — достаточно поменять base_url:
=
bge-m3 выбрана за нативную поддержку русского языка без дополнительной настройки — что принципиально важно для документации, написанной по-русски.
Что это даёт на практике
Архитектура позволяет решать несколько классов задач, которые в ручном режиме занимали значительное время.
Анализ покрытия требований. Запрос «Какие функциональные требования из раздела 3.2 не покрыты тест-кейсами?» — AI-агент находит все требования, находит все тест-кейсы, сравнивает их и формирует gap-анализ. Задача, которая вручную занимает несколько часов.
Генерация тест-кейсов. На вход — описание новой функциональности и существующие тест-кейсы аналогичного модуля. Агент генерирует черновик в том же формате и стиле. Результат требует ревью, но существенно сокращает время на первичную разработку.
Поиск противоречий. «Есть ли расхождения между требованиями раздела 4 и критериями оценки раздела 6?» Агент анализирует оба раздела одновременно и указывает на несоответствия — задачу, которую человек при большом объёме документов легко пропускает.
Поиск по смыслу, а не по ключевым словам. «Найди все тест-кейсы, связанные с авторизацией» — при том что в документах используются термины «аутентификация», «вход в систему» и «логин». Векторный поиск находит семантически близкие фрагменты независимо от конкретных слов.
Это не замена QA-инженера. Это ускоритель работы с текстом.
Ограничения, куда ж без них
Ресурсные ограничения. Локальный запуск означает только квантизированные модели — неизбежный компромисс между конфиденциальностью и качеством. Приходится экспериментировать с вариантами квантизации (Q4, Q5, Q8), сравнивая соотношение качество/скорость для конкретных задач.
Зацикливание агента. Периодически возникают ситуации, когда агент уходит в бесконечный цикл переспрашивания. Это известная проблема агентных систем на малых моделях. Я рассматриваю переход на llama.cpp как бэкенд — он даёт более предсказуемое поведение на GGUF-моделях.
Время ответа. Локальный инференс медленнее облачного. Использование GPU хоста позволило выйти на сотни токенов в секунду вместо исходных единиц — для задач типа «подготовить summary перед ревью» задержка в 15–20 секунд вполне приемлема. Но это не та скорость, к которой привыкли пользователи облачных систем.
Отсутствие версионирования. При переиндексации документа история предыдущих версий не сохраняется. Если нужно сравнить текущую методику с прошлой — это пока невозможно без ручного вмешательства.
Что планирую улучшить
Sparse vectors в Qdrant. bge-m3 умеет генерировать разреженные векторы — Qdrant поддерживает их нативно. Интеграция даст расчётный прирост точности +15–25% на технических терминах и аббревиатурах.
Parent Document Retrieval. Искать по маленьким, точным чанкам, но в промпт подавать целый родительский блок. LLM получает больший контекст — ответ становится связнее.
Agentic loop. Переход на уровень 4: LLM сам решает, сколько раз и что искать. Необходимо для multi-hop запросов: «найди все модули, в тест-кейсах которых упоминается процесс client.exe» — задача, требующая нескольких последовательных поисков.
Версионирование документов. Хранение истории версий через метаданные позволит отвечать на вопросы вида «что изменилось в методике испытаний между версиями 2.1 и 2.3?»
Evaluation pipeline. Это, пожалуй, самое важное. Пока нет автоматической оценки качества ответов — непонятно, стало ли лучше после изменений или только кажется. Внедрение RAGAS или аналога даст измеримые метрики: контекстуальную точность, полноту, релевантность. Без этого итерации происходят вслепую.
Итог
Локальный AI дополненный RAG уже точно не игрушка, а рабочим инструмент.
Он не идеален. Он не такой быстрый, как облако. Он требует инженерных усилий. Но он полностью автономен, защищает данные, масштабируется вместе с документацией — и реально экономит время.
LM Studio (локальные модели)
+ bge-m3 (мультиязычный embedding)
+ Qdrant (векторная база)
+ FlashRank (reranker)
+ FastAPI (API-слой)
+ N8N (оркестрация)
= Полностью локальный RAG-агент для QA-документации
Никаких облачных зависимостей.
Никаких утечек данных.
Работает на рабочей станции разработчика.
Главный вывод: начинать стоит с разумного уровня сложности. Не с графов знаний и не с агентных архитектур. А с хорошо реализованного hybrid + structured RAG. Усложнение оправдано только тогда, когда простое решение перестаёт справляться с реальными задачами.
Если вы строите что-то похожее или уже прошли этот путь — буду рад обсудить в комментариях, особенно в части оценки качества и перехода к агентным архитектурам.