ADR 0099 — Neo4j как граф знаний и шаблоны prompt, драйвер в api
- Статус: Принят
- Дата: 2026-08-19
- Контекст: фундамент графа знаний (RN-413/414/415), решение владельца продукта привнести Neo4j в продукт
Контекст
Два реальных пробела продукта мотивировали эту поставку. Первый: ни один
агент не потребляет RAG, который уже существует (pgvector, гибридный поиск вектор+лексика,
HybridSearchUseCase), — его использует лишь web-вкладка "Chat RAG". Второй:
каждый prompt агента — inline heredoc Elixir — идентичности
(Engine.Harness.Agents), kickoffs PO/Arquiteto/Dev Lead/UX/Infra,
prompt суммаризации ContextManager, — ничто из этого не живёт вне
кода, версионированно или переиспользуемо.
Владелец продукта попросил Neo4j по мотивам репозитория
ErickWendel/neo4j-ai-experiments,
который использует граф как память ИИ-агентов и хранит prompts в
файлах отдельно от логики приложения (каталог prompts/, с
именованными шаблонами для преобразования NL→Cypher, контекста и форматирования
ответа). Этот проект был конкретным вдохновением для принятого здесь
шаблона — стоит выразить явную благодарность: без него форма "prompt как
версионируемый файл, а не строка, запертая в коде" не имела бы столь
прямого прецедента для следования.
Решение
Neo4j входит как граф знаний — ПРОИЗВОДНАЯ память event log, никогда не источник истины — с двумя обязанностями:
- Версионируемые шаблоны prompt (
PromptTemplate/PromptVersion), постепенно заменяющие inline heredocs (сама миграция GenServers — будущая волна; эта поставка лишь строит фундамент). - Реляционная память: взаимодействия пользователя, гипотезы Psicólogo
со свидетельствами (
EVIDENCIA→Evento{sessionId,seq}), профили компетентности Anamnese, handoffs между агентами.
pgvector ПО-ПРЕЖНЕМУ является векторным индексом чанков — граф не хранит никаких embedding. Хранение одного вектора в двух базах разошлось бы при первой переиндексации, затронувшей лишь одну из них; решением было сохранить ОДИН векторный источник, а граф — как слой ОТНОШЕНИЙ поверх.
Драйвер: neo4j-driver (официальный пакет) в apps/api, а не в engine.
api уже владеет ВСЕМ хранением продукта (Postgres, pgvector,
permissions.json) и существующим RAG; engine уже потребляет всё через внутренний
HTTP с service token, никогда не открывал прямого подключения к базе
домена. Повторение этого шаблона — engine вызывает внутренний маршрут, api говорит с
базой — сохраняет ОДНУ границу учётных данных/пула вместо двух.
Минимальная схема с ограничениями уникальности (в Neo4j Community нет
составного NODE KEY, лишь IS UNIQUE по одиночному свойству):
PromptTemplate.name, Usuario.id, Projeto.id, Agente.slug,
Interacao.sessionId. Hipotese/Handoff используют MERGE по собственному
естественному ключу (id гипотезы; (sessionId, seq) handoff) без формального
ограничения — идемпотентность обеспечивает MERGE, а не дополнительное
структурное ограничение.
Деградация, а не crash
GraphStore.onModuleInit НИКОГДА не бросает исключение. Три переменные окружения
(NEO4J_URI/NEO4J_USER/NEO4J_PASSWORD) обязательны в
продакшене (ранний сбой при загрузке, в том же духе, что GIT_OAUTH_STATE_SECRET
из ADR 0059) и необязательны вне его — никому не нужно поднимать локальный Neo4j
только чтобы прогнать suite api. При отсутствии или недоступности драйвер остаётся
null, и каждая операция бросает GraphUnavailableError, преобразуемую в 503
(никогда не сырой 500) глобальным фильтром GraphErrorFilter, или в деградированный ответ
в use cases, у которых есть fallback (поиск RAG без обогащения
графом, например).
Последствия
- Граф восстанавливается replay event log/outbox (см. Волну 2, RN-416), — никогда не второй источник истины, который нужно синхронизировать ручной дисциплиной.
- Миграция inline kickoffs GenServers на потребление шаблонов графа — ПОСЛЕДУЮЩАЯ волна, объявленная вне этой поставки, — этот ADR лишь устанавливает, что механизм существует и его безопасно вводить постепенно.
- Принятое и новое для продукта последствие: это первая инфраструктурная
зависимость, не являющаяся ни Postgres, ни Ollama.
docker-compose.ymlполучает ещё один сервис для тех, кто запускаетpnpm devлокально, с ограниченной кучей (эталонная машина разработчика имеет 15 GB, уже разделённых между Postgres, Ollama, api, engine и web).