Перейти к основному содержимому

0015. RAG-минимум: контекст из базы знаний на pgvector

Копия ADR из репозитория: backend/docs/adr/0015-rag-pgvector-knowledge-context.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-06-24
  • Связанные задачи: OP-49 (F7 · базовый слой контекста/RAG)

Контекст и проблема​

Модули ИИ-помощника (5.9) и юр-блока (5.12) подставляют в промпт релевантный контекст из базы знаний (5.15). Нужен базовый механизм (RAG-минимум): проиндексировать материал (эмбеддинги), по запросу вернуть top-k фрагментов и собрать их в контекст для промпта.

Ограничения и развилки:

  • Тикет фиксирует подход — «pgvector в PostgreSQL». Векторное хранилище выбирать не нужно.
  • Эмбеддинги — через общий LLM-слой F7 (OP-47, LLM_PROVIDER.embeddings). Живого LLM-конфига пока нет; тесты — на моках (как OP-47/48).
  • Размерность вектора колонки pgvector фиксируется в DDL (vector(N)) и зависит от модели эмбеддингов. На момент OP-49 модель не выбрана (нет LLM_BASE_URL/LLM_EMBEDDINGS_MODEL).
  • TypeORM не знает тип vector: объявление такой колонки в entity ломает построение метаданных на старте (падают AppModule/docs:openapi/e2e).
  • Источник материалов (5.15) — вне scope; реальных данных пока нет.

Рассмотренные варианты​

Векторное хранилище​

  1. pgvector в PostgreSQL (выбран — задан тикетом). Один управляемый Postgres, без нового инфра-компонента; точный k-NN/ANN через <=>. Минус — расширение должно быть установлено в образе БД (CI/managed).
  2. Внешний векторный движок (Qdrant/Weaviate). Отвергнут: новый сервис в инфраструктуре заказчика ради «минимума» избыточен и противоречит тикету.

Моделирование вектор-колонки​

  1. Raw-SQL репозиторий поверх DataSource (выбран). Вектор пишем/читаем литералом '[..]'::vector; entity для таблицы не создаём. Плюсы: не ломает старт, полностью мокается в unit-тестах, нет лишней зависимости. Минус: отступление от entity-конвенции (оправдано типом).
  2. Entity с колонкой type: 'text' + трансформер. Отвергнут: вводит в заблуждение (тип в коде ≠ тип в БД), .save() всё равно требует ::vector-каста — выгоды нет.
  3. Зависимость pgvector (npm) + кастомный тип TypeORM. Отвергнут: лишняя зависимость, а <=>-поиск всё равно идёт через query-builder/raw — для «минимума» избыточно.

Размерность эмбеддинга​

  1. Константа KNOWLEDGE_EMBEDDING_DIMENSION = 1536 (выбран) — размер OpenAI text-embedding-3-small / ada-002. Колонка vector(1536); сервис валидирует длину каждого эмбеддинга и бросает RagEmbeddingDimensionError при несовпадении.
  2. Env RAG_EMBEDDING_DIMENSION. Отвергнут: DDL-размерность всё равно фиксирована в миграции — смена значения требует новой миграции, env лишь создаёт риск рассинхрона со схемой.

Решение​

Модуль src/modules/rag/:

  • KnowledgeChunkRepository (DI-провайдер над @InjectDataSource()): insertMany (вставка с эмбеддингом через $n::vector), searchTopK (ORDER BY embedding <=> $1::vector LIMIT k, score = 1 - distance). Параметризованный SQL — без склейки строк.
  • RagService: index(input|input[]) (эмбеддит контент → валидирует размерность → сохраняет), search(query, opts?) (эмбеддит запрос → top-k), buildContext(query, opts?) (search + чистый formatContext → { text, chunks } для подстановки в промпт). Зависит от LLM_PROVIDER и репозитория.
  • Миграция CreateKnowledgeChunks: CREATE EXTENSION IF NOT EXISTS "vector" + таблица knowledge_chunks (embedding vector(1536) NOT NULL) + HNSW-индекс vector_cosine_ops. down() дропает таблицу; расширение оставляет (идемпотентно, как и IF NOT EXISTS в up).
  • embedding не моделируется TypeORM — таблицей владеет миграция.

Последствия​

  • Положительные: появился переиспользуемый RAG-механизм за чистым API (RagService); полностью покрыт unit-тестами на моках (БД/LLM не нужны для Gate 1); вектор-операции изолированы в одном репозитории; нет новых runtime-зависимостей.
  • Инфраструктура: образ Postgres должен нести pgvector. CI и docker-compose переведены на pgvector/pgvector:pg16. Managed Postgres в Coolify (prod/stage) должен иметь доступное расширение vector — предусловие деплоя (DevOps, OP-51); вне кода OP-49.
  • Размерность 1536 — допущение. Если выбранная модель эмбеддингов даёт другую размерность, нужна follow-up миграция (ALTER/новая колонка) и правка константы; до тех пор сервис падает явной RagEmbeddingDimensionError. Реальных данных пока нет (5.15 вне scope), риск миграции данных отсутствует.
  • Отступление от entity-конвенции для таблицы knowledge_chunks — осознанное, обусловлено отсутствием поддержки типа vector в TypeORM; задокументировано в TSDoc репозитория.
  • Без эндпоинтов — механизм потребляют будущие фиче-модули через DI; OpenAPI-контракт не меняется.