Собираем RAG-пайплайн на .NET: практический разбор
Пошаговая сборка production-ready RAG на .NET 10 — от извлечения текста до evaluation-пайплайна. С реальными решениями, а не абстракциями.
За последний год мы собрали несколько RAG-пайплайнов на .NET для корпоративных систем. Ниже — схема, которая работает в проде, и грабли, на которые мы наступали.
Порядок сборки
Порядок важен: если начать с промпта, потом прикрутить retrieval и только затем задуматься про данные — потеряете недели на переделку.
- Извлечение и структурирование
PDF, DOCX, HTML → единый внутренний формат с сохранением иерархии заголовков, таблиц и подписей. Без этого шага все следующие работают вслепую.
- Индексация
Эмбеддинги в векторное хранилище плюс полнотекстовый индекс. Два индекса, один retrieval.
- Retrieval
Гибридный поиск с RRF поверх двух выдач. Без ранжирования качество упрётся в потолок эмбеддингов.
- Генерация с цитированием
Ответ только из переданного контекста, каждое утверждение привязано к источнику.
- Evaluation
Набор реальных вопросов, метрики Recall@k и MRR, регрессия на каждом релизе.
Почему чанкинг решает больше, чем модель
За время этих проектов мы убедились: качество retrieval определяет ~80% итогового качества ответа. Выбор модели на оставшиеся 20% влияет в разы меньше, чем правильная нарезка документов.
Что делаем: режем по структуре. Для договоров — стороны, предмет, сроки, ответственность, приложения. Для технической документации — разделы и подразделы, блоки кода сохраняются целиком вместе с соседним пояснением.
public sealed record Section(
string Title,
int Level,
string Text,
string? ParentId);
public static IEnumerable<Section> Split(Document doc)
{
// Режем по заголовкам, а не по длине. Длина — только
// верхняя граница: слишком крупный раздел режем дальше,
// но уже с сохранением соседнего контекста.
var buffer = new List<string>();
foreach (var block in doc.Blocks)
{
if (block.IsHeading && buffer.Count > 0)
{
yield return Flush(buffer);
buffer.Clear();
}
buffer.Add(block.Text);
}
if (buffer.Count > 0) yield return Flush(buffer);
}
Гибридный поиск: вектор плюс FTS
Векторный поиск отлично ловит смысл и проваливает точные идентификаторы. «Контракт 44-А/2024» не найдётся, потому что эмбеддинг буквенно-цифрового кода почти не несёт смысла.
const double K = 60.0; // сглаживающий коэффициент RRF
public static IReadOnlyList<Chunk> Fuse(
IReadOnlyList<Chunk> vectorHits,
IReadOnlyList<Chunk> keywordHits,
int limit)
{
var scores = new Dictionary<string, double>();
foreach (var (chunk, rank) in vectorHits.Select((c, i) => (c, i)))
scores[chunk.Id] = scores.GetValueOrDefault(chunk.Id) + 1.0 / (K + rank + 1);
foreach (var (chunk, rank) in keywordHits.Select((c, i) => (c, i)))
scores[chunk.Id] = scores.GetValueOrDefault(chunk.Id) + 1.0 / (K + rank + 1);
return scores
.OrderByDescending(kv => kv.Value)
.Take(limit)
.Select(kv => store.Get(kv.Key))
.OfType<Chunk>()
.ToList();
}
Evaluation — этап, который все пропускают
Минимальный жизнеспособный вариант — 50–100 вопросов из реальной поддержки, покрывающих типовые сценарии:
| Метрика | Что показывает | Порог для продакшна |
|---|---|---|
| Recall@5 | Нашёл ли нужный документ в выдаче | > 0.85 |
| MRR | Насколько высок ранжирован верный ответ | > 0.75 |
| Faithfulness | Не выдумала ли LLM то, чего нет в контексте | > 0.90 |
| Answer relevancy | Отвечает ли на вопрос, а не на смежную тему | > 0.85 |
Faithfulness — метрика, которую чаще всего игнорируют, и именно она определяет, пойдёт ли система в прод. Модель, уверенно выдумывающая несуществующий пункт договора, хуже, чем честно сказавшая «не нашла».
Стоимость: два рычага
Кеш эмбеддингов. Документы меняются редко — пересчитываем только дельту. Это сразу снимает 70–80% повторных затрат на эмбеддинг.
Маршрутизация по сложности. Простые вопросы («где реквизиты») дешёвой моделью, сложные с рассуждением — дорогой. На типовом проекте это даёт снижение счёта в 3–4 раза без потери качества.
public sealed class Router(ModelCatalog catalog) : IModelRouter
{
public IModel Select(Question q) => q switch
{
{ RequiresReasoning: true } => catalog.Advanced,
{ EntityLookup: true } => catalog.Fast, // точные идентификаторы
_ => catalog.Fast,
};
}
Что обязательно до продакшна
- Набор из 50+ eval-вопросов из реальных обращений
- Recall@5 выше 0.85 на этом наборе
- Чанкинг по структуре, а не по фиксированному окну
- Гибридный поиск, а не только векторный
- Faithfulness замеряется, а не предполагается
- Кеширование эмбеддингов настроено
- Трейсинг каждого шага доступен в проде
Пункт 7 отсутствует чаще всего — и когда в проде что-то ломается, диагностировать нечем.
Если хотите разобрать свой пайплайн на конкретике — напишите нам.
RAG на .NET: архитектура, которая переживает продакшн
Разбираем, почему наивный RAG ломается на реальных данных, и строим пайплайн на .NET: чанкинг, гибридный поиск, evaluation и cost-control.