Задача: сделать AI-аналитика из кучи PDF-отчетов
Госзаказчик. Ежегодно 500+ PDF-отчетов по строительным объектам. Каждый отчет - 50-200 страниц технических спецификаций, финансовых таблиц и заключений экспертов.
Пользователи (аналитики министерства) тратят недели на поиск информации. Нужно было создать систему, где можно спросить: "Покажи все объекты с превышением сметы в Московской области за 2023 год" и получить структурированный ответ с ссылками на документы.
Техническое ограничение №1: все должно работать в закрытом контуре. Никаких OpenAI API. Все локально. Бюджет - скромный. Сервера - существующие виртуалки с 32 ГБ RAM.
Архитектура: просто, как молоток
Выбрали классический RAG:
- Векторная БД: Qdrant (легковесная, на C++, отличная C# совместимость)
- LLM: Mistral 7B через Ollama (самая адекватная модель для наших ресурсов)
- Бэкенд: ASP.NET Core 8 Web API
- Эмбеддинги: all-MiniLM-L6-v2 (достаточно для русского технического текста)
Звучит разумно? На бумаге - да. В реальности - готовьтесь к сюрпризам.
1 Индексация документов: где мы ошиблись в самом начале
Первая ошибка - разбивать PDF на страницы. Кажется логичным: одна страница = один чанк. Но технические отчеты содержат таблицы, которые занимают несколько страниц. Разбивка постранично рвет таблицы пополам.
// КАК НЕ НАДО ДЕЛАТЬ
public async Task IndexDocument(string pdfPath)
{
var pages = PdfReader.ExtractPages(pdfPath); // Разбивка по страницам
foreach (var page in pages)
{
var embedding = await _embeddingService.GetEmbeddingAsync(page.Text);
await _qdrantClient.UpsertAsync(collectionName, embedding, page.Metadata);
}
}
Урок: разбивайте по семантическим границам. Таблицы - отдельно. Разделы документа - отдельно. Используйте заголовки как маркеры разбиения.
2 Интеграция Ollama с C#: проще, чем кажется (и сложнее)
Ollama предоставляет REST API. Казалось бы, просто HttpClient и готово. Но есть нюансы:
public class OllamaService
{
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
public async Task GenerateAsync(string prompt, List context)
{
// Контекст - это найденные релевантные чанки из Qdrant
var fullPrompt = BuildPromptWithContext(prompt, context);
var request = new
{
model = "mistral",
prompt = fullPrompt,
stream = false,
options = new { temperature = 0.1 } // Низкая температура для фактов
};
var response = await _httpClient.PostAsJsonAsync(
"http://localhost:11434/api/generate",
request);
if (!response.IsSuccessStatusCode)
{
// Тут начинается веселье
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("Ollama error: {Error}", errorContent);
// Ollama может вернуть 400, если модель не загружена
// Или 500, если не хватает памяти
// Или 503, если контейнер перезагружается
}
var result = await response.Content.ReadFromJsonAsync();
return result?.Response ?? "Ошибка получения ответа";
}
}
Проблема №1: таймауты. Mistral 7B на CPU думает 10-30 секунд на сложный запрос. Стандартный HttpClient.Timeout = 100 секунд. В продакшене с 10+ параллельными запросами это создает очередь.
Проблема №2: память. Ollama в контейнере по умолчанию жрет всю доступную RAM. Нужно явно ограничивать:
# Запуск Ollama с ограничением памяти
docker run -d \
--name ollama \
-p 11434:11434 \
--memory="8g" \
--memory-swap="8g" \
ollama/ollama
# Загрузка модели с указанием количества слоев для GPU/CPU
ollama pull mistral:7b-instruct-q4_K_M
3 Промпт-инженерия для бюрократических документов
Государственные отчеты пишут по шаблону. Используйте это. Наш промпт выглядел так:
Ты - аналитик государственной отчетности. Отвечай строго на основе предоставленных документов.
Контекст:
{context}
Инструкции:
1. Если информации нет в контексте - скажи "В предоставленных документах нет данных"
2. Не придумывай цифры
3. Для финансовых данных указывай единицы измерения (тыс. руб., млн руб.)
4. Ссылайся на номера документов в квадратных скобках, например [Док-12]
5. Форматируй списки с использованием маркеров
Вопрос: {question}
Ответ:
Кажется, все учтено? А вот и нет. Модель все равно галлюцинировала. Особенно с цифрами. Почему? Потому что в технических отчетах числа пишут по-разному: "1,5 млн", "1500000", "1 500 000". Модель путалась.
Что пошло не так: три главных провала
Провал 1: масштабирование индексации
500 PDF * 100 страниц = 50 000 документов для индексации. Эмбеддинг каждого чанка занимает ~300мс. Математика простая: 4 часа непрерывной работы.
Но в реальности:
- Сервис эмбеддингов падал при параллельной обработке
- Qdrant начинал тормозить после 20 000 векторов
- Обновление индекса (добавление новых отчетов) блокировало поиск
Пришлось переписать архитектуру индексации:
// Новая архитектура с очередями и батчингом
public class IndexingService
{
private readonly Channel _indexingChannel;
private readonly List _batchBuffer = new();
private const int BatchSize = 50;
public async Task StartIndexingAsync(CancellationToken ct)
{
await foreach (var chunk in _indexingChannel.Reader.ReadAllAsync(ct))
{
_batchBuffer.Add(chunk);
if (_batchBuffer.Count >= BatchSize)
{
await ProcessBatchAsync(_batchBuffer.ToArray());
_batchBuffer.Clear();
}
}
}
private async Task ProcessBatchAsync(DocumentChunk[] chunks)
{
// Параллельное создание эмбеддингов
var embeddingTasks = chunks
.Select(chunk => _embeddingService.GetEmbeddingAsync(chunk.Text))
.ToArray();
var embeddings = await Task.WhenAll(embeddingTasks);
// Батч-вставка в Qdrant
await _qdrantClient.UpsertBatchAsync(collectionName, embeddings, chunks);
}
}
Провал 2: качество поиска
Векторный поиск по техническим терминам работал плохо. "Сметная стоимость" и "расчетная стоимость" - синонимы в контексте отчетов, но эмбеддинги разные.
Решили гибридным поиском: векторный + лексический. Добавили Elasticsearch для keyword search и комбинировали результаты. Если вам интересна тема гибридного RAG, посмотрите статью про RAG 2026: roadmap, который работает - там подробно разбирают подобные кейсы.
Провал 3: мониторинг и диагностика
Пользователи жалуются: "Система дает неправильные ответы". Как это отладить?
Не хватало:
- Логов, какие чанки были найдены для запроса
- Скоринга релевантности каждого чанка
- Истории промптов и ответов для анализа ошибок
Добавили полноценную телеметрию:
public class RagServiceWithTelemetry
{
public async Task QueryAsync(string question)
{
var searchResult = await _vectorSearch.SearchAsync(question);
// Логируем контекст для отладки
_logger.LogInformation("Query: {Question}", question);
_logger.LogInformation("Found {Count} chunks", searchResult.Chunks.Count);
foreach (var chunk in searchResult.Chunks)
{
_logger.LogDebug("Chunk ID: {Id}, Score: {Score}, Source: {Source}",
chunk.Id, chunk.Score, chunk.SourceDocument);
}
var answer = await _ollama.GenerateAsync(question, searchResult.Chunks);
// Сохраняем в БД для анализа качества
await _analyticsRepository.SaveQueryAsync(new QueryAnalytics
{
Question = question,
Answer = answer,
RetrievedChunks = searchResult.Chunks,
Timestamp = DateTime.UtcNow,
UserId = userId
});
return new RagResponse { Answer = answer, Sources = searchResult.Chunks };
}
}
Финал: что работает сейчас
После 3 месяцев доработок система работает. Не идеально, но пользователи экономят 70% времени на поиск информации.
| Метрика | Было | Стало |
|---|---|---|
| Время поиска информации | 2-3 часа | 5-10 минут |
| Точность ответов | ~60% (первые версии) | ~85% (после доработок) |
| Стоимость инфраструктуры | План: 50к руб/мес | Факт: 120к руб/мес |
Чеклист для вашего RAG на C#
- Тестируйте чанкинг на реальных данных - PDF, Word, Excel рвутся по-разному
- Ограничивайте память Ollama с первого дня - иначе она сожрет все ресурсы
- Добавляйте таймауты и retry логику - LLM API нестабильны по определению
- Внедряйте телеметрию сразу - без логов вы слепы
- Планируйте гибридный поиск - чистый векторный поиск редко работает в продакшене
- Бюджетируйте в 2-3 раза больше - особенно на инфраструктуру и доработки
Самая большая ошибка - считать RAG простой технологией. Это не "подключил API - получил умный поиск". Это сложная система, где каждая часть может сломаться. Особенно в C# экосистеме, где тулзов меньше, чем в Python.
Если столкнетесь с похожими проблемами анализа структурированных данных, посмотрите как мы строили RAG для анализа таблиц - там много пересекающихся проблем и решений.
Прогноз на 2025: RAG станет стандартом для корпоративных систем, но перестанет быть отдельной технологией. Встроится в СУБД, в поисковые движки, в системы документооборота. Сейчас вы пишете интеграции - через год будете выбирать из готовых решений.
Главный урок этого проекта: RAG - это не про искусственный интеллект. Это про инженерию. Про пайплайны, про мониторинг, про обработку ошибок. AI - всего лишь один компонент в цепочке. И часто - не самый проблемный.