AI как помощник QA-инженера: локальный RAG для работы с тестовой документацией

2053 Слова 11 Минуты RAG QA LLM local AI N8N Qdrant Python

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

Локальный AI-агент, который живёт внутри корпоративного периметра и работает с той же базой знаний, что и я.

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


Документация в QA: объём как системная проблема

В QA документация — это не сопутствующий материал. Это рабочая среда: программы и методики испытаний, технические требования, спецификации, тест-кейсы, протоколы, отчёты.

На активном проекте объём таких материалов растёт постоянно. Появляются новые версии. Старые редактируются. Документы ссылаются друг на друга. В какой-то момент экосистема документации становится по-настоящему сложной — и держать её целиком в голове одного человека уже невозможно.

Проблема не в том, что работать лень. Проблема в том, что человек физически плохо справляется с большими объёмами слабо структурированного текста — особенно когда формулировки меняются от версии к версии, компонентов в проекте много, а требования и тест-кейсы разнесены по разным документам.

Я хотел получить не исполнителя, а именно двойника: агента, который уже знает контекст проекта, понимает иерархию документов и способен рассуждать на уровне смысла — «что здесь проверяется», «с чем это пересекается», «чего не хватает». И при этом — работает полностью локально, без единого байта, покидающего периметр компании.


Почему не облако

Технически самый простой путь — подключить облачную LLM и построить RAG поверх неё. Это можно сделать за несколько дней.

Но в моём случае такой вариант не рассматривался — по нескольким причинам сразу.

Конфиденциальность. Продукт, над которым работает команда, является проприетарной разработкой. Документация содержит коммерчески чувствительную информацию. Отправка технических спецификаций во внешний LLM-сервис создаёт юридические риски и прямо нарушает обязательства перед заказчиком.

Зависимость от внешней инфраструктуры. Доступ к облачным API может быть ограничен — и не только по техническим причинам. Геополитическая ситуация оказывает реальное влияние на доступность сервисов: без выделенного VPN получить доступ к большим моделям попросту не получится. Добавьте сюда риски сбоев в облачной инфраструктуре и невозможность официальной оплаты — и картина становится полной.

Стоимость и предсказуемость. Локальная система работает независимо от состояния внешней инфраструктуры и не генерирует переменные операционные расходы.

Поэтому я поставил жёсткое ограничение:

Ни один байт внутренней документации не должен покидать рабочую машину.

Это усложнило задачу, но одновременно сделало её интересной.


Что такое RAG и почему без него нельзя

LLM сама по себе — плохой источник истины для внутренней документации. Она не знает о конкретном продукте. Особенно если вашей документации нет в online. Если задать вопрос напрямую, она либо откажется отвечать, либо начнёт «додумывать» правдоподобный, но неверный ответ. В профессиональном сообществе это явление называют галлюцинацией.

В QA галлюцинации недопустимы.

RAG (Retrieval-Augmented Generation) решает эту проблему: перед генерацией ответа система находит релевантные фрагменты из базы знаний и передаёт их в контекст модели. Модель отвечает не «из головы», а опираясь на реальные данные, которые вы ей явно предоставили.

Вопрос пользователя
Поиск релевантных фрагментов в базе знаний
Передача фрагментов + вопроса в LLM как контекст
Ответ на основе реальных данных

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


Эволюция RAG: как я выбирал уровень сложности

RAG — это не один фиксированный подход. За несколько лет сформировалась явная эволюция уровней сложности: от наивного вброса всего документа в промпт до полноценных агентных систем с графами знаний.

УровеньНазваниеСутьМетод поискаВ проекте
0Naive RAGВесь документ → в промпт целикомНет поиска
1Basic RAGЧанки фиксированного размера + cosine similarityDense only
2Hybrid RAGDense + Sparse + RerankerDense + BM25 + RerankТекущий
3Structured RAGМетаданные, фильтры, иерархия документаFiltered searchТекущий
4Agentic RAGLLM планирует цепочку запросов, multi-hopИтеративныйСледующий шаг
5Graph RAGKnowledge 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 / EmbedLM StudioЛокальный инференс моделей
Embeddingbge-m3Мультиязычные векторы, 1024 dim
Vector DBQdrantХранение и поиск векторов
RerankerFlashRankПереранжирование, CPU-only, < 50 мс
API слойFastAPIHTTP-интерфейс между N8N и Python
БД N8NPostgreSQLХранение состояния 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:

| Метод испытаний              | Критерии оценки              |
| ---------------------------- | ---------------------------- |
| 1. Перезагрузить компьютер   | 1. Компьютер загрузился      |
| 2. Открыть Клиент            | 2. Клиент открылся           |

Такой формат LLM воспринимает значительно лучше, чем сырой XML или разрозненные строки. Текстовые блоки режутся с перекрытием 100 символов, что сохраняет контекст на границах фрагментов.

Структурированный payload в Qdrant

Каждый вектор хранит не только контент, но и богатые структурные метаданные:

payload = {
    "node_type":          "test_case",
    "section_hierarchy":  "2.1.1.1",
    "tested_function":    "загрузка Клиента",
    "chunk_type":         "table",
    "parent_test":        "Проверка загрузки Клиента...",
    "source_document":    "TestSpec_v2.3.docx",
}

Это превращает базу знаний в управляемую структуру, а не в набор текстовых фрагментов. Запрос «покажи только тест-кейсы из раздела 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:

client = OpenAI(
    base_url="http://host.docker.internal:1234/v1",
    api_key="lm-studio",  # значение не важно, требуется только формат
)

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. Усложнение оправдано только тогда, когда простое решение перестаёт справляться с реальными задачами.

Если вы строите что-то похожее или уже прошли этот путь — буду рад обсудить в комментариях, особенно в части оценки качества и перехода к агентным архитектурам.