RAG в проде тормозит из-за зоопарка индексов: как закрыть код, логи, PDF и CSV одним локальным MCP-поиском
Я — Denis Shokhirev, Agentic AI Systems Architect из Freiburg im Breisgau, Germany; в DennisCraft AI Studio я собираю production AI-системы на Claude, Supabase, n8n, Doppler и self-hosted Postgres. Самая частая причина, почему RAG в проде отвечает медленно и нестабильно, у меня не в модели и не в embeddings, а в том, что данные распиханы по 4–6 отдельным индексам: репозиторий кода ищется одним способом, логи другим, PDF третьим, CSV вообще через временный ETL. Пока команда лечит latency подборо
Я — Denis Shokhirev, Agentic AI Systems Architect из Freiburg im Breisgau, Germany; в DennisCraft AI Studio я собираю production AI-системы на Claude, Supabase, n8n, Doppler и self-hosted Postgres. Самая частая причина, почему RAG в проде отвечает медленно и нестабильно, у меня не в модели и не в embeddings, а в том, что данные распиханы по 4–6 отдельным индексам: репозиторий кода ищется одним способом, логи другим, PDF третьим, CSV вообще через временный ETL.
Пока команда лечит latency подбором модели, реальная проблема остается: агенту приходится угадывать, куда идти за фактом. Это увеличивает число запросов, плодит дубликаты контекста и ломает трассировку. В regulated среде это еще и аудитная проблема: трудно объяснить, почему один и тот же вопрос вчера ходил в vector store, сегодня в SQL, а завтра в ad hoc parser на воркере.
Почему index sprawl ломает production RAG
В демо отдельные индексы выглядят логично. У кода одна схема chunking, у PDF другая, у CSV третья. Но в проде вопрос пользователя редко уважает эти границы. Он спрашивает: «Почему агент отправил не тот invoice?» Ответ почти всегда живет сразу в 3 местах: код бизнес-правила, лог выполнения и табличные данные.
Что я вижу на реальных внедрениях
Обычно появляются 4 симптома. Первый — растет latency, потому что оркестратор дергает несколько retrieval-путей последовательно. Второй — качество ответа скачет: разные индексы ранжируют документы по разным правилам. Третий — растет стоимость поддержки: у каждого коннектора свой ingestion, свой retry и свои схемы обновления. Четвертый — наблюдаемость почти исчезает, потому что retrieval-путь становится ветвящимся.
Anthropic в документации по tool use рекомендует давать модели четкие инструменты с понятными контрактами, а не размытые возможности поиска по разным источникам: docs.anthropic.com, 2024. Это хорошо совпадает с моим опытом: если агент видит один локальный search tool с единым интерфейсом, поведение стабильнее, чем при выборе между 5 почти одинаковыми retriever-инструментами.
| Подход | Что происходит в демо | Что ломается в проде | Что я делаю вместо этого |
|---|---|---|---|
| Отдельный vector index на каждый тип данных | Быстрый старт | Много роутинга и рассинхрон обновлений | Единый MCP search tool поверх локальных адаптеров |
| SQL только для таблиц | Хорошо для CSV | Код и PDF выпадают из одного ответа | Нормализую метаданные и общий контракт поиска |
| Парсер PDF как отдельный сервис | Работает на тестах | Сложно аудировать путь ответа | Локальный вызов через один сервер инструментов |
Один локальный MCP search layer вместо зоопарка
Под MCP-поиском я имею в виду не новый бренд и не еще один индекс, а паттерн: один локальный сервер инструментов, который отдает модели единый search API, а внутри сам решает, как искать по коду, логам, PDF и CSV. Модель не знает о внутреннем зоопарке. Для нее есть один инструмент: search(query, source_types, limit).
Почему локально? Потому что в DACH B2B я часто работаю с данными, которые нельзя гонять через лишние внешние прослойки. Локальный MCP-сервер рядом с Postgres, файловым storage и логами дает меньше сетевой болтовни и предсказуемее аудит.
Минимальный контракт инструмента
from typing import List, Optional
from pydantic import BaseModel
class SearchRequest(BaseModel):
query: str
source_types: Optional[List[str]] = None
limit: int = 8
class SearchHit(BaseModel):
source_type: str
uri: str
title: str
snippet: str
score: float
def search_local(req: SearchRequest) -> List[SearchHit]:
filters = req.source_types or ["code", "logs", "pdf", "csv"]
hits = []
hits.extend(search_code(req.query, req.limit) if "code" in filters else [])
hits.extend(search_logs(req.query, req.limit) if "logs" in filters else [])
hits.extend(search_pdf(req.query, req.limit) if "pdf" in filters else [])
hits.extend(search_csv(req.query, req.limit) if "csv" in filters else [])
return sorted(hits, key=lambda x: x.score, reverse=True)[:req.limit]Ключевое здесь не Python, а единый выходной формат. Если лог, кусок кода и строка CSV приходят в одном типе ответа с метаданными, следующий шаг агента проще: он сравнивает факты, а не пытается привести 4 разных схемы к общему виду на лету.
Как закрыть 4 источника без лишней магии
Код
Для кода мне не нужен отдельный «AI-индекс» на старте. Достаточно пройтись по репозиторию, вытащить функции, классы, README и конфиги, сохранить чанки и метаданные в Postgres. Перед подачей в RAG я почти всегда прогоняю semgrep, bandit и gitleaks, чтобы не утащить в контекст секреты и опасные паттерны. OWASP в LLM Top 10 2025 отдельно указывает prompt injection и sensitive information disclosure как реальные риски для retrieval-цепочек: genai.owasp.org.
Логи
Логи часто дают самый точный ответ, но только если их нормально нормализовать. Я храню timestamp, service, correlation_id, severity и message как обязательные поля. Тогда агент может искать не просто по тексту, а по комбинации текста и исполнения конкретного workflow. Для n8n это особенно полезно, когда надо связать шаг агента, SQL-вставку и ошибку внешнего API.
PDF не надо романтизировать. В проде это обычно договор, спецификация, счет или policy. Я извлекаю текст, сохраняю page number, section heading и checksum файла. Если документ обновился, старые чанки инвалидируются по checksum, а не по имени файла. Это убирает типичную ошибку, когда агент цитирует прошлую версию SLA.
CSV
CSV лучше воспринимать как табличный факт-слой, а не как plain text. Я храню имя файла, строку, колонки и легкий текстовый рендер строки. Тогда один и тот же поиск может вернуть как точную строку таблицы, так и документ, который ее объясняет.
python ingest_code.py --repo /srv/app --out postgres
python ingest_logs.py --input /var/log/app.jsonl --out postgres
python ingest_pdf.py --input /srv/docs --out postgres
python ingest_csv.py --input /srv/data --out postgres
psql "$DATABASE_URL" -c "create index if not exists idx_docs_type on docs(source_type);"
psql "$DATABASE_URL" -c "create index if not exists idx_docs_uri on docs(uri);"
psql "$DATABASE_URL" -c "analyze docs;"Где здесь реальный выигрыш
Первый выигрыш — latency. Вместо того чтобы агент сам выбирал между 4 инструментами и делал несколько раундов tool use, он вызывает один поиск и получает уже объединенный top-k. Второй — предсказуемость. Если поиск централизован, вы можете логировать запрос, фильтры, hits и дальнейший ответ модели в одной трассе. Третий — управление доступом. Проще один раз ограничить локальный search tool по путям, таблицам и типам документов, чем держать одинаковые правила в нескольких retrieval-сервисах.
OpenAI Cookbook в материалах по retrieval и tool usage в 2024 последовательно показывает ценность простых интерфейсов между моделью и инструментами: cookbook.openai.com. В production это ощущается еще сильнее: чем меньше у модели вариантов «куда сходить», тем меньше лишних токенов и ложных маневров.
tools:
- name: local_search
description: Search code, logs, pdf and csv from local sources
input_schema:
type: object
properties:
query: { type: string }
source_types:
type: array
items: { type: string }
limit: { type: integer, default: 8 }
required: [query]Что не делать
Не начинайте с 4 vector database подряд, если у вас еще нет стабильного контракта поиска. Не заставляйте модель выбирать между code_search, pdf_search, csv_search и logs_search без крайней причины. Не храните метаданные в одном месте, а сырой контент в другом без общего идентификатора. И не тащите секреты из репозитория в retrieval: gitleaks на ingestion стоит дешевле, чем разбор инцидента.
FAQ
MCP обязателен, чтобы сделать такой слой?
Нет. Сначала важен единый контракт инструмента и локальная реализация. MCP удобен как стандартный способ дать этот инструмент агенту, но сам паттерн работает и без него.
Нужен ли vector search для всех типов данных?
Нет. Для логов и CSV часто хватает keyword + metadata filtering. Я добавляю embeddings там, где это реально поднимает recall, а не потому что это модно.
Можно ли все хранить в Postgres?
Да, на многих B2B-нагрузках этого достаточно. Если schema и индексы сделаны аккуратно, Postgres закрывает ingestion, метаданные, аудит и часть retrieval без лишних сервисов.
Как это связано с безопасностью?
Единый слой упрощает контроль доступа, логирование и санитизацию контекста. Вам проще доказать, какие источники видел агент и почему он их использовал.
Когда отдельные индексы все-таки оправданы?
Когда источники очень разные по нагрузке, retention и правам доступа. Но даже тогда я стараюсь оставить один внешний search contract для модели и спрятать сложность внутри.
Где у вас сейчас теряется больше времени в production RAG: на раздельном retrieval по источникам, на нормализации метаданных или на объяснимости ответа для аудита? Я правда хочу это понять на реальных стеках, а не на презентациях.
I run a free 30-min stack audit for DACH founders building AI in regulated markets. DM me on LinkedIn or write to @ger_dennis_ai.
Превратить процесс в систему, которая работает
Продакшн-качество, а не демо.