Почему корпоративный RAG - это не «загрузить документы в векторную базу»
В любой большой компании есть люди, которым регулярно пишут:
«А как у нас оформляется постоплата?»
«Кто согласует риски в сделке?»
«За сколько дней нужно оформить командировку?»
Они знают ответы не потому, что помнят наизусть сотни страниц внутренних документов. Просто они уже двадцать раз открывали нужный регламент и примерно представляют, где искать. Проблема в том, что таких людей мало, нормативных документов много, а вопрос вполне может прийти вечером в пятницу. В крупной компании более трёхсот внутренних нормативных документов: тендеры, пресейл, риски, договорная работа, командировки, информационная безопасность и множество других процессов.
Так появилась идея «Помощника для сотрудника»: сервиса, которому можно задать вопрос обычным человеческим языком и получить короткий ответ со ссылкой на конкретный документ и его пункт.
На бумаге задача выглядит почти стандартной:
загрузить документы;
разбить их на chunks;
построить embeddings;
найти top-k наиболее похожих фрагментов;
передать их LLM;
попросить ответить строго по источникам.
То есть обычный RAG.Первые недели разработки мы тоже думали, что примерно этим всё и закончится.
Не закончилось.
Попытка №1: RAG «по учебнику»
Первый прототип я собрал достаточно быстро:
n8n - оркестрация;
Qdrant - векторная база;
embeddings;
гибридный retrieval;
LLM для генерации ответа.
Через несколько дней всё работало. Документы индексировались. Векторы строились. По запросу находились похожие фрагменты. Модель формировала вполне убедительные ответы. Пока мы не начали задавать настоящие вопросы. Помощник путал заявку на тендер с заявкой на расчёт, называл не ту роль, иногда терял условия вроде «при сумме более…».
И довольно быстро выяснилось:
убедительный ответ LLM и правильный ответ на основании регламента - совершенно разные вещи.
Мы начали смотреть не на красивый конечный текст, а на то, какие chunks вообще получает модель.
И увидели основную проблему.
Регламент - не статья из интернета
Корпоративные нормативные документы выглядят примерно так:
пункты
5.3.1.2;вложенные подпункты;
таблицы на множество колонок;
объединённые ячейки;
матрицы ответственности;
примечания;
ссылки на другие разделы;
десятки корпоративных сокращений;
канцелярские конструкции;
условия и исключения.
Причём смысл часто существует только вместе со структурой документа. Например:
строка таблицы + название колонки + заголовок раздела = правило.
Уберите заголовок колонки - и строка превращается просто в несколько чисел. А стандартный чанкер этой семантики не знает. Если ему сказано нарезать по 1000 символов, он честно режет по 1000 символов. В результате:
Условие → chunk 17 Действие → chunk 18 Исключение → chunk 19
Каждый фрагмент может быть семантически похож на вопрос пользователя. Но ни в одном нет полного правила.
Как четыре дня превратились в неправильный ответ
Один случай хорошо запомнился. На вопрос о сроках подготовки коммерческого предложения система уверенно назвала четыре рабочих дня. Число действительно присутствовало в регламенте. Но относилось оно к другому этапу процесса. Шапка таблицы попала в один chunk, строки со сроками - в другой. LLM получила число, но потеряла связь этого числа с колонкой. Формально модель практически ничего не выдумала. Фактически ответ оказался неправильным. Именно тогда стало понятно:
проблема начинается раньше retrieval. Документ сначала надо правильно прочитать.
Увеличивать top-k, менять temperature или бесконечно крутить параметры vector search здесь почти бесполезно.
Железо и модели: одной «волшебной LLM» не получилось
Параллельно решался вопрос инфраструктуры. Для корпоративного сервиса хотелось одновременно получить:
достаточно сильную LLM;
большое контекстное окно;
нормальную производительность;
контролируемую инфраструктуру;
возможность обслуживать не одного экспериментатора, а множество пользователей.
В тот момент наш партнёр уже имел практический опыт работы с gpt-oss-120b.Поэтому основной генеративный слой решили строить на ней. Следующая задача значительно менее абстрактная: куда поместить 120B-модель? Проекту помог Selectel (не реклама! мы искренне благодарны ребятам из Selectel ) - удалось протестировать и получить на хороших условиях dedicated server с NVIDIA RTX PRO 6000 Blackwell, 96 GB VRAM. Это позволило поднять собственный OpenAI-compatible inference endpoint и уже измерять не только «качество модели вообще», но и вполне инженерные характеристики:
latency;
загрузку GPU;
concurrency;
время до первого токена;
скорость генерации;
поведение при параллельных запросах.
Но довольно быстро выяснилось, что основной LLM недостаточно. Для semantic retrieval после тестов на русскоязычных корпоративных документах выбрали GigaEmbeddings-instruct от Cloud.ru (не реклама, сам тестировал несколько вариантов!). Для reranking - bge-reranker-v2-m3. А для обработки схем, изображений и сложного визуального контента после тестов выбрали по соотношению скорость/качество/цена мультимодальную Qwen3.6-35B-A3B.
То есть вместо архитектуры
Vector DB → LLM
получилась система специализированных моделей. И это оказалось значительно ближе к реальному production RAG.
Главный архитектурный поворот: сначала правильно подготовить знания
Существенный поворот произошёл во время обсуждений с архитектором проекта, и руководителем Центра компетенций ИИ. У него уже был проверенный на других задачах набор приёмов для обработки сложных документов. Его основная мысль была простой:
Не надо компенсировать плохую подготовку документов всё более мощными LLM и всё более сложными prompts.
Задача была поставлена конкретно: перестроить pipeline и добиться требуемого качества retrieval и ответов. Именно здесь набор отдельных экспериментов начал превращаться в системную архитектуру.
Как сейчас выглядит pipeline
В упрощённом виде система состоит из двух больших контуров.
Индексация
DOCX / PDF / Confluence / вложения │ ▼ Извлечение содержимого │ ┌───────┴────────┐ │ │ Text Images / Schemes / PDF │ │ │ Qwen3.6-35B-A3B │ │ └───────┬────────┘ ▼ Markdown │ ▼ Нормализация структуры ├─ заголовки ├─ списки ├─ таблицы └─ объединённые ячейки │ ▼ Structure-aware chunking ├─ ~1000–2000 символов ├─ без разрыва абзацев ├─ без overlap └─ document/section breadcrumb │ ┌───────┴───────────┐ ▼ ▼ Natasha / Razdel GigaEmbeddings normalization dense vectors │ │ ▼ ▼ FTS5/BM25 vector index
Обработка запроса
Вопрос пользователя │ ▼ Нормализация запроса ├─ glossary ├─ раскрытие аббревиатур ├─ stop lemmas └─ проверка необходимости уточнения │ ├───────────────────┐ ▼ ▼ BM25 Dense retrieval │ │ └─────────┬─────────┘ ▼ RRF Reciprocal Rank Fusion │ ▼ bge-reranker-v2-m3 │ ▼ TOP-5 chunks │ ▼ gpt-oss-120b │ ▼ Ответ + документ + пункт + цитата │ ▼ Arize Phoenix trace / metrics / evaluators
Это намеренно упрощённая схема - например, в production-процессе ещё есть контроль сессий и прикладная логика. Но она показывает принципиальную вещь:
генерация ответа - это последний этап довольно длинной цепочки.
Markdown как внутреннее представление документа
Все источники по возможности приводятся к единому markdown-представлению.Не потому, что markdown обладает какими-то магическими свойствами для нейросетей. Он просто позволяет сохранить важную структуру:
# Регламент работы с подрядчиками ## 5. Выбор подрядчика ### 5.3 Согласование | Стоимость | Кто согласует | |-----------|---------------| | до ... | ... | | свыше ... | ... |
Вместо практически плоского текста:
Регламент работы с подрядчиками 5 Выбор подрядчика 5.3 Согласование Стоимость Кто согласует до ...
Для LLM и retrieval это две очень разные структуры.
Почему мы отказались от фиксированного chunk size
Обычно хочется подобрать красивое число:
chunk_size = 1000 overlap = 200
И забыть об этом. На нормативных документах такой подход работает плохо. Размер chunk у нас - не жёсткая граница, а ориентир. Если абзац или таблица продолжаются, лучше получить chunk в 1400 символов, чем два идеальных фрагмента по 700, каждый из которых потерял часть смысла. При этом chunk наследует контекст:
document └── section └── subsection └── chunk
То есть индексируется не просто:
Согласование производится в течение двух рабочих дней.
а логически скорее:
Документ: Регламент X Раздел: Работа с подрядчиками Подраздел: Согласование Согласование производится в течение двух рабочих дней.
Для semantic search разница весьма существенная.
Почему overlap оказался вреден
В классических туториалах RAG часто встречается:
chunk_size = 1000 chunk_overlap = 200
Идея разумная: если мысль пересекает границу chunks, мы её не потеряем. Но у регламентов другой характер текста. Предположим:
5.3. Для сделок категории A требуется согласование директора. 5.4. Для сделок категории B согласование осуществляется руководителем отдела.
При overlap конец 5.3 легко попадает в chunk с 5.4. Для обычной статьи это полезный соседний context. Для нормативного документа - потенциальное объединение двух разных правил. Кроме того, overlap создаёт почти дубликаты:
chunk 21 ████████████████████ chunk 22 ████████████████████ chunk 23 ████████████████████
Если retrieval возвращает три таких результата, фактически они занимают три места в top-k, хотя новых фактов там почти нет. Поэтому мы выбрали:
структурные границы + отсутствие overlap.
Почему одного vector search недостаточно
Semantic search очень полезен, когда пользователь спрашивает:
«Как оформить поездку?»
а в регламенте написано:
«Порядок направления работника в служебную командировку».
Embedding-модель способна увидеть смысловую близость. Но корпоративный документ содержит другую категорию информации:
5.3.2 15:00 500 000 рублей ПДЗ КИ РП ОЭБ
Здесь значение должно совпадать точно, а не семантически. Поэтому retrieval стал гибридным.
Лексическая ветка
Используем:
SQLite FTS5 + BM25
Перед этим текст нормализуется и лемматизируется с помощью Natasha / Razdel.
Так разные формы:
согласовать согласование согласовывается согласованный
становятся значительно ближе для lexical retrieval. Дополнительно отбрасывается часть стоп-лексики. Слово вроде «осуществляется» может встретиться в половине нормативной базы. Для определения нужного регламента его информационная ценность невелика.
Dense-ветка
Параллельно выполняется semantic search на GigaEmbeddings-instruct.
В результате получаем два ranking:
BM25: 1. chunk_17 2. chunk_51 3. chunk_33 Dense: 1. chunk_33 2. chunk_91 3. chunk_17
Как объединить два разных ranking: RRF
Для fusion используется Reciprocal Rank Fusion. В упрощённом виде:
score(d) = Σ 1 / (k + rank(d))
То есть документ получает баллы за высокую позицию в каждом независимом ranking. Практическая ценность RRF в том, что не нужно пытаться напрямую сравнивать:
BM25 score = 8.74 и cosine similarity = 0.817
У них разные шкалы. RRF работает прежде всего с порядком результатов, а не с абсолютными scores. После fusion кандидаты отправляются в reranker.
Реранкер: дорогое уточнение после дешёвого поиска
Embedding retrieval должен быстро сократить пространство поиска. Reranker решает немного другую задачу. Он получает:
query + candidate chunk
и уже непосредственно оценивает релевантность пары. Это существенно дороже, поэтому reranker запускается не на всех chunks корпуса, а только на кандидатах retrieval. Финально в LLM передаются пять наиболее релевантных chunks. Именно влияние этого шага мы затем проверили экспериментально.
Корпоративный язык пришлось объяснять отдельно
Пользователь редко формулирует вопрос нормативным языком.
Он не пишет:
«Каковы действия при наличии просроченной дебиторской задолженности?»
Он пишет:
«Что делать с ПДЗ?»
Поэтому в системе появился отдельный glossary. Например:
ПДЗ → просроченная дебиторская задолженность
Расшифровка используется дважды:
Для retrieval
Запрос расширяется полным термином.
Для LLM
В prompt передаётся релевантная подсказка:
ПДЗ — просроченная дебиторская задолженность.
При этом весь корпоративный справочник в context не отправляется - только определения, связанные с текущим запросом.
Дополнительно используются:
словарь шумовых лемм;
правила уточнения слишком общих вопросов.
Если пользователь спрашивает:
«Какой срок?»
правильным поведением системы иногда является не retrieval, а вопрос:
«Срок чего именно вы хотите уточнить?»
Потом мы обнаружили проблему хуже плохого retrieval
Допустим, после очередного изменения ответы стали выглядеть убедительнее. Что это означает? Почти ничего. Субъективная оценка:
«Мне кажется, стало лучше»
очень быстро перестала нас устраивать.
Понадобился gold dataset. В нём для каждого контрольного вопроса фиксируются:
вопрос;
эталонный ответ;
ожидаемые факты;
документ;
раздел/пункт;
релевантные chunks;
должен ли сервис вообще отвечать.
Особенно важен последний класс. Если ответа в регламентах нет, правильное поведение - честный отказ, а не наиболее правдоподобное продолжение текста.
Как мы измеряем качество
Evaluation пришлось разделить минимум на три уровня.
Retrieval ↓ Context ↓ Answer
Плохой конечный ответ ещё не означает, что виновата LLM. Например:
Нужный chunk не найден ↓ Context Recall низкий ↓ LLM физически не знает нужный факт ↓ Ответ неполный
И наоборот:
Все факты найдены ↓ Context Recall высокий ↓ LLM пропустила часть контекста ↓ Проблема generation/prompt
Это позволяет не «улучшать RAG вообще», а понимать, какой конкретно компонент деградировал.
Эксперимент: reranker ON против reranker OFF
Для одного из тестов мы взяли 20 контрольных вопросов. Для 17 существовал эталонный ответ и размеченные подтверждающие chunks, для трёх правильным поведением был отказ.
Каждый вопрос запускался три раза.
Итого:
20 вопросов × 3 повтора = 60 прогонов
для каждой конфигурации.
Сравнивали две версии:
A: retrieval без reranker B: тот же pipeline + reranker
Получились такие результаты:
Метрика | Без reranker | С reranker | Изменение |
|---|---|---|---|
Recall@5 | 0,285 | 0,464 | +63% |
MRR@5 | 0,531 | 0,814 | +53% |
Precision@5 | 0,220 | 0,378 | +72% |
Context Precision@5 | 0,451 | 0,759 | +68% |
Context Recall | 0,501 | 0,768 | +53% |
Faithfulness | 0,973 | 0,984 | +1% |
Correctness | 0,377 | 0,586 | +55% |
Completeness | 0,542 | 0,696 | +28% |
No-answer Correctness | 0,983 | 0,950 | −3% |
Важно: эти значения в первую очередь предназначены для сравнения двух конфигураций на одном наборе тестов. Интерпретировать Correctness = 0,586 как абсолютное утверждение «Помощник верен на 58,6%» пока некорректно: разметка фактов продолжает проходить валидацию владельцами регламентов. Здесь интересны не столько отдельные числа, сколько связи между ними.
Что означают эти цифры
Recall@5: 0,29 → 0,46
Recall@5 показывает, какая доля всех эталонно-релевантных фрагментов попала в итоговые пять chunks, переданных модели. После включения reranker:
Recall@5 0,285 → 0,464
То есть без reranker в top-5 попадало меньше трети нужных фрагментов, а с reranker - уже почти половина. Здесь важно правильно интерпретировать значение 0,464. Это не означает, что поиск «нашёл меньше половины того, что должен». Для многих контрольных вопросов эксперты отметили больше пяти релевантных chunks, тогда как в LLM мы намеренно передаём только top-5. Поэтому Recall@5 конструктивно не всегда может достичь единицы.
В нашем эксперименте рост Recall@5 означает, что после reranking в ограниченный контекст модели стало попадать существенно больше действительно полезных фрагментов. И это хорошо согласуется с последующим ростом:
Context Recall: 0,501 → 0,768 Correctness: 0,377 → 0,586
То есть reranker не просто переставил chunks местами — он помог собрать в финальном top-5 больше информации, необходимой для правильного ответа.
MRR@5: 0,53 → 0,81
MRR@5 показывает, насколько высоко появляется первый нужный chunk.
Если первый релевантный результат всегда стоит на первом месте:
MRR = 1
После reranking показатель вырос с 0,531 до 0,814.
То есть reranker сделал именно то, ради чего его добавляли:
важные фрагменты стали систематически подниматься вверх.
Context Precision: 0,45 → 0,76
Это уже вопрос качества final context.
До reranker полезные и бесполезные chunks были довольно сильно перемешаны.
После:
0,451 → 0,759
Нужные фрагменты заметно чаще оказываются выше шумовых.
Для LLM это существенно: начало context имеет особое значение, а каждый шумовой chunk занимает место, куда мог попасть полезный факт.
Context Recall: 0,50 → 0,77
Это одна из наиболее важных для диагностики метрик.
Она отвечает на вопрос:
Какая доля фактов, необходимых для эталонного ответа, вообще присутствует в переданном LLM контексте?
Результат:
0,501 → 0,768
То есть до reranker LLM видела примерно половину необходимых фактов, после - около трёх четвертей.
Фактически это потолок качества generation:
LLM не может корректно использовать факт, которого retrieval ей вообще не передал.
Correctness: 0,38 → 0,59
Для нас это одна из главных прикладных метрик.
Она учитывает наличие обязательных фактов, но при серьёзном противоречии регламенту результат обнуляется.
Это намеренно жёсткое правило.
В нормативном документе:
не более 10 дней
и
не менее 10 дней
лексически почти идентичны. С бизнес-точки зрения - противоположны.
После включения reranker:
Correctness 0,377 → 0,586
То есть изменение одного retrieval-компонента заметно повлияло уже на конечную правильность ответов.
Faithfulness почти не изменилась - и это тоже хороший результат
Получилось:
0,973 → 0,984
То есть почти все утверждения, которые генерирует Помощник, уже подтверждаются переданным context.
Это интересный диагностический сигнал.
Проблема системы была не столько:
«LLM всё придумывает»,
сколько:
«LLM не всегда получает все факты, необходимые для полного ответа».
То есть bottleneck сместился в retrieval и completeness.
А одна метрика даже ухудшилась
No-answer Correctness:
0,983 → 0,950
При reranker появилось три ложных отказа вместо одного.
При этом:
ложных ответов: 0 → 0
Во всех девяти прогонах вопросов, для которых ответа в базе не существовало, Помощник корректно отказался отвечать.
А снижение no-answer metric оказалось локальной проблемой retrieval по одному вопросу: reranker отбрасывал чужие фрагменты, после чего модель вместо неправильного ответа предпочитала отказаться. Причина была найдена и исправлена на стороне поиска.
На мой взгляд, это хороший пример того, зачем вообще нужны несколько метрик.
По одной итоговой цифре такой эффект легко принять за деградацию всей системы.
Один AI отвечает, другой проверяет
Для автоматической оценки generation используется LLM-as-a-judge. Сначала на эту роль рассматривалась Qwen3-235B-A22B. Но очень быстро выяснилось: размер модели ещё не делает её хорошим судьёй. Judge необходимо отдельно калибровать на ответах с известным результатом, причём особенно важны tricky cases:
противоречие источнику;
пропущенное условие;
лишний факт;
неверное число;
корректный no-answer.
В итоге в текущей конфигурации используется DeepSeek-V4-Pro. Для production-трафика judge может работать и без эталонного ответа.
Он получает:
question + retrieved context + assistant answer
и ищет три категории проблем.
Critical
Ответ противоречит источникам или содержит неподтверждённое утверждение.
Missing
В context есть важный для ответа факт, но LLM его не использовала.
Extra
В ответ попала информация, не требуемая вопросом.
Так можно оценивать не только gold dataset, но и живой пользовательский traffic.
Phoenix: как перестать отлаживать RAG по конечному ответу
Для observability и experiments в итоге выбрали Arize Phoenix.
Теперь отдельный запрос выглядит не как:
QUESTION ↓ ANSWER
а как trace:
request │ ├─ query normalization │ ├─ BM25 retrieval │ ├─ dense retrieval │ ├─ RRF │ ├─ reranking │ ├─ final context │ ├─ LLM generation │ └─ evaluation
И это очень сильно меняет отладку.
Если ответ неправильный, можно определить:
был ли нужный документ в candidate set;
попал ли релевантный chunk в top-5;
где он находился в ranking;
прошёл ли через reranker;
присутствовал ли нужный факт в final context;
использовала ли LLM этот факт;
добавила ли LLM что-то от себя.
То есть вместо:
«RAG опять плохо ответил»
получаем конкретный defect:
Context Recallнизкий из-за retrieval.
или:
context полный, но
Completenessответа низкая - проблема generation.
Как мы теперь тестируем изменения
При изменении одного компонента можно поднять две конфигурации:
baseline experiment ──────── ────────── chunking v3 chunking v3 GigaEmbeddings GigaEmbeddings BM25 BM25 RRF RRF reranker OFF reranker ON gpt-oss-120b gpt-oss-120b
Через обе версии прогоняется один и тот же dataset. В идеале меняется одна переменная. Это обычный A/B-подход, только применённый к RAG pipeline. Именно так был получен приведённый выше результат с reranker. После этого решение:
«reranker надо оставить»
принимается не потому, что несколько ответов субъективно понравились разработчикам.
А потому что он улучшил 8 из 9 измеряемых показателей, в том числе MRR, Context Precision, Context Recall, Correctness и Completeness.
Метрики неожиданно помогли найти баг в самих данных
Один вопрос стабильно получал отказ, хотя нужная информация в базе точно существовала. Из trace стало видно, что vector search постоянно попадал в большой кластер почти одинаковых fragments одного из вложений - реестра рисков. Для embedding-модели они находились очень близко друг к другу. В результате top-k выглядел примерно так:
risk_registry row 37 risk_registry row 81 risk_registry row 14 risk_registry row 102 risk_registry row 53 ...
То есть retrieval «проваливался» в один semantic cluster. Нужный регламент существовал в индексе, но не попадал достаточно высоко. Это важное наблюдение. Ошибки RAG могут находиться не только в:
LLM;
embeddings;
prompt;
chunk size.
Иногда проблема - в составе самого корпуса. Слишком большой однотипный документ способен изменить geometry vector space и качество top-k. Поэтому следующий этап работы - в том числе управление составом индексируемых источников и дальнейшее использование иерархической структуры документов.
Пять вещей, которые мы бы сказали себе в начале
1. Сначала подготовить, потом искать
Регламент - не последовательность символов. Это структура:
document → section → subsection → table → row → condition → action → exception
Уничтожив её при ingestion, затем приходится дорого восстанавливать потерянный смысл с помощью LLM.
2. Не начинать оптимизацию с самой дорогой модели
Для нашего кейса качественная подготовка документов, chunking и словари дали эффект не меньше, чем эксперименты с заменой generation LLM. Сначала стоит исправлять дешёвые архитектурные проблемы.
3. Hybrid search нужен не ради модного слова
Dense и lexical retrieval решают разные задачи. Для корпоративных регламентов нужны оба.
4. LLM-as-a-judge тоже нужно тестировать
Если judge не замечает противоречия источнику, красивые автоматические dashboards только создают ложное чувство контроля.
5. Нельзя улучшить то, что не измеряется
После каждого изменения должно быть возможно сказать не:
«вроде стало лучше»,
а:
MRR: +0.283 Context Recall: +0.267 Correctness: +0.209
И уже после этого принимать архитектурное решение.
Что получилось
Сегодня «Помощник для сотрудника» - это уже совсем не:
Vector DB → LLM
А скорее:
DOCUMENT PIPELINE │ ▼ parse → markdown → structure → chunks │ ┌──────┴──────┐ ▼ ▼ BM25 embeddings │ │ └──────┬──────┘ ▼ RRF │ ▼ reranker │ ▼ top context │ ▼ gpt-oss-120b │ ▼ answer │ ┌──────┴──────┐ ▼ ▼ user Phoenix traces/evals
Каждый компонент теперь можно менять независимо и сравнивать с baseline.
Можно протестировать:
другую embedding-модель;
новый reranker;
иной chunking;
другую стратегию fusion;
новый prompt;
другую LLM.
И увидеть, что именно произошло с retrieval, context и конечным ответом.
Вместо заключения
В начале проекта нам казалось, что основная задача - взять n8n, выбрать достаточно хорошую LLM и дать ей доступ к корпоративным документам. В процессе стало понятно, что для реального RAG модель - только последний участник достаточно длинного производственного процесса. Качество ответа закладывается гораздо раньше:
когда мы разбираем документ, сохраняем структуру таблицы, выбираем границу chunk, нормализуем терминологию, объединяем lexical и semantic search и формируем final context.
И здесь для проекта оказалась очень полезной помощь опытного архитектора из центра компетенций ИИ. Конечно, можно было прочитать кучу материалов на хабре, в сети и спросить у Chat GPT или Claude. Сначала так и сделали. Именно Claude собрал первый пайплайн в n8n, который показал достаточно средние результаты. Как оказалось, несколько дельных советов за 5 минут сделали из гипотезы, результаты которой радовали только разработчика, достаточно качественный сервис с реальными шансами превратиться из MVP в полноценный продукт. Самыми полезными стали накопленные инженерные практики: как обрабатывать сложные корпоративные источники, как строить retrieval и как превращать качество AI из субъективного впечатления в воспроизводимый эксперимент.
Первую версию Помощника можно было собрать за несколько дней. Научить его действительно читать регламенты оказалось намного сложнее. Но, пожалуй, главное изменение произошло даже не в pipeline. Оно произошло тогда, когда вместо фразы:
«Кажется, теперь стало лучше»
мы впервые смогли сказать:
«Вот baseline. Вот experiment. Вот девять метрик. Давайте сравним».
И именно в этот момент RAG-прототип начал превращаться в инженерный продукт.

