Вы когда-нибудь видели, как AI-кодинг-агент с радостью вставляет console.log прямо в продакшн-ветку? Или пытается переписать модуль, нарушая все архитектурные решения, на которые команда потратила три месяца? Это не баг, это фича — отсутствие enforcement layer. Простого промпта «пиши хороший код» недостаточно. Модель — это генератор вероятностей, а не послушный инженер. Она может выдать решение, которое синтаксически верно, но убивает всю архитектуру.
В этой статье мы строим слой принуждения (enforcement layer), который перехватывает каждое действие агента — будь то изменение файла, вызов API или создание новой функции — и проверяет его через локальный граф знаний + гибридный RAG. Всё работает на вашей машине, без облаков. Neo4j хранит структуру проекта и правила, ONNX runtime тянет лёгкую модель для быстрой оценки, а all-MiniLM-L6-v2 в паре с BM25 обеспечивает интеллектуальный контекстный поиск.
Если вы ещё не знакомы с основами RAG — сначала пройдите RAG за 15 минут на Python. А для понимания полного цикла агента читайте гайд по Agentic RAG.
Почему простой RAG не справляется
Возьмём типичного AI-агента: он получает задачу, лезет в кодовую базу, генерирует патчи. Без enforcement layer он сравнивает своё решение с векторами из Chroma или Faiss, и часто — выдаёт мусор. Почему? Потому что семантический поиск по эмбеддингам не понимает структуры связей. Он найдёт похожий код по смыслу, но не поймёт, что этот модуль несовместим с текущей архитектурой, потому что нет графа зависимостей. Мы уже разбирали эту проблему в статье про production-ready агента.
Enforcement layer решает тройную задачу:
- Запретить действия, нарушающие структурные правила (например, вызов приватного API извне пакета).
- Корректировать действия, которые неоптимальны (использовать готовую утилиту вместо копирования кода).
- Обогащать контекст агента информацией из графа, которую RAG просто не найдёт по эмбеддингам.
Архитектура enforcement layer
Представьте, что агент собирается вызвать функцию dangerous_delete() из модуля core.cleanup. Enforcement layer перехватывает это действие и прогоняет через три фильтра:
- Граф знаний (Neo4j) — проверка, что вызываемый метод не помечен как deprecated, что у него не более N вызовов в коде, что он не нарушает layer dependency (например, infra → domain).
- Гибридный RAG (BM25 + all-MiniLM-L6-v2) — поиск похожих решений в истории коммитов, issue tracker, документации, чтобы подтвердить или опровергнуть решение агента.
- ONNX runtime + малая модель — быстрый бинарный классификатор (куча примеров: «хороший патч» / «плохой патч» на нескольких сотнях реальных ревью).
Все три решения взвешиваются, и если итоговая оценка ниже порога — запрос отклоняется, агенту возвращается структурированное объяснение с альтернативой.
Компоненты детальнее
| Компонент | Роль | Почему именно это |
|---|---|---|
| Neo4j (граф знаний) | Хранит узлы: модули, функции, API, правила (edge), отношения зависимости, метаданные. | Реляционные БД плохо справляются с глубокими связями в коде; графовые запросы (Cypher) дают ответ за миллисекунды. |
| all-MiniLM-L6-v2 | Генерация эмбеддингов для семантического поиска. | Лёгкая (80 MB), работает на CPU, даёт 384-мерные векторы — достаточно для кодовых фрагментов. |
| BM25 (лексический поиск) | Поиск точных совпадений по ключевым словам (названия функций, типы ошибок). | Простые эмбеддинги не ловят «console.log» или «DELETE FROM», а BM25 находит дословно. |
| ONNX Runtime | Запуск предобученной модели классификации (например, LightGBM или tiny BERT). | В 5–10 раз быстрее PyTorch inference, можно запускать на дешёвом железе. |
Сравнение с альтернативами
Можно, конечно, попробовать запихнуть все правила в системный промпт агента. Но это ломается при первом же длинном ответе — модель забывает или игнорирует инструкции. Pydantic-схемы и output parsers тоже не панацея: они проверяют только формат вывода, а не семантику. Чистый RAG (векторный) — даёт контекст, но не связи. Если вам интересно, как работает Agentic RAG без enforcement layer — почитайте статью про проектирование AI-агента.
| Подход | Проверка структуры | Проверка контекста | Скорость | Локальность |
|---|---|---|---|---|
| Промпт + правила | Низкая (забывчивость) | Низкая | Высокая | Да |
| Векторный RAG | Низкая (нет связей) | Средняя | Средняя | Да |
| Граф знаний (только Neo4j) | Высокая | Низкая (нет текста) | Высокая | Да |
| Enforcement layer (предлагаемый) | Высокая | Высокая (гибрид) | Средняя (3 фильтра), но компенсируется локальным ONNX | Полностью |
Пример: настройка графа знаний в Neo4j
Допустим, у нас проект на Python. Сканируем код, вытаскиваем все функции, классы, модули, вызовы — и загружаем в Neo4j. Вот как выглядит Cypher-запрос для проверки, можно ли агенту импортировать модуль X из модуля Y:
MATCH (m1:Module {name: 'Y'})-[r:DEPENDS_ON]->(m2:Module {name: 'X'})
RETURN r.allowed AS allowed, r.risk AS risk
Если отношение DEPENDS_ON отсутствует или allowed = false — блокируем. Согласитесь, в промпт такое не засунешь.
А вот как обогатить контекст агента перед генерацией кода — найти все связанные issue:
MATCH (f:Function {name: 'prepare_database'})<-[:REFERENCES]-(i:Issue)
RETURN i.title, i.status, i.resolution
Это реально быстрее, чем семантический поиск по тексту issue — граф чётко знает, какие функции где упоминаются.
Гибридный RAG: BM25 + all-MiniLM
Граф — это скелет, а мясо — это текстовая информация: документация, комментарии, старые патчи. Для поиска мы используем гибрид: BM25 ловит точные совпадения по ключевым словам (например, @deprecated, typo), а all-MiniLM находит семантически похожие фрагменты. Финальный вес — конфигурируемый (0.5 / 0.5 по умолчанию).
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import numpy as np
# all-MiniLM-L6-v2 — самая легкая версия (актуально на 2026)
encoder = SentenceTransformer('all-MiniLM-L6-v2')
# Токенизированный корпус
corpus = ["You should never call external APIs in this module", "Deprecated function remove_item"]
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# Запрос агента
query = "Is it allowed to call remove_item?"
bm25_scores = bm25.get_scores(query.split())
norm_bm25 = bm25_scores / np.max(bm25_scores) if np.max(bm25_scores) > 0 else bm25_scores
embs = encoder.encode([query] + corpus)
cos_sim = np.dot(embs[0], embs[1:].T) # упрощённо
norm_cos = cos_sim / np.max(cos_sim) if np.max(cos_sim) > 0 else cos_sim
hybrid = 0.5 * norm_bm25 + 0.5 * norm_cos
best_idx = np.argmax(hybrid)
print(f"Best match: {corpus[best_idx]}, score: {hybrid[best_idx]:.2f}")
ONNX Runtime: быстрая классификация
Финальный фильтр — бинарный классификатор на ONNX. Модель обучена на парах: (предлагаемый патч, контекст) → 1 (принять) / 0 (отклонить). Используем сжатый LightGBM (экспорт через m2cgen или конвертацию в ONNX). Запуск:
import onnxruntime as ort
import numpy as np
# Представим, что features — это вектор длиной 10 (нормализованные метрики)
session = ort.InferenceSession("classifier.onnx")
input_name = session.get_inputs()[0].name
features = np.array([[0.7, 0.1, 0.9, 0.3, 0.5, 0.2, 0.8, 0.4, 0.6, 0.0]], dtype=np.float32)
pred = session.run(None, {input_name: features})[0][0][0]
if pred < 0.5:
print("❌ Действие отклонено enforcement layer")
else:
print("✅ Действие разрешено")
Модель можно дообучать на историях неудачных решений — это даёт быстрый фидбэк без перегенерации всего пайплайна.
Кому это реально нужно?
Enforcement layer — это не для маленьких пет-проектов. Если у вас один микросервис и два разработчика — промпта хватит. Но как только появляется:
- Более 10 модулей с перекрёстными зависимостями
- Архитектурные решения, которые нельзя нарушать (например, запрет на прямой вызов БД из UI)
- Команда из 5+ разработчиков + AI-агенты, которые правят код
...тогда такой слой окупается за неделю. Он превращает AI-агента из опасного джуниора в дисциплинированного инженера, который дважды подумает, прежде чем удалить строчку. Если вы строите Agentic AI Engineer — советую прочитать план обучения.
Главные грабли (и как не наступить)
1. Раздувание графа. Не храните каждую строчку кода — только модули, классы, функции, их сигнатуры и ключевые зависимости. Остальное — в гибридном RAG.
2. Слишком строгие фильтры. Начните с режима audit (только логируем нарушения, не блокируем), иначе агент будет просто игнорировать вашу систему.
3. ONNX без валидации. Модель может ошибаться. Всегда добавляйте fallback — если модель не уверена (вероятность 0.4–0.6), возвращайтесь к графу.
4. Забыть про гибридный поиск по комментариям и issue. Граф может не знать, что конкретная функция уже исправлена в соседнем PR — а гибридный RAG найдёт.
Никогда не запускайте enforcement layer в production без нагрузочного тестирования. Мы тестировали на 10 000 запросов/мин — Neo4j справился, ONNX тоже, а всё упёрлось в скорость гибридного RAG. Пришлось кэшировать частые запросы.
Будущее: от принуждения к обучению
Следующий шаг — enforcement layer, который не просто блокирует, а генерирует исправленный код. Например, вместо «не используй console.log» он сам заменяет на logger.debug() и объясняет агенту, почему так лучше. Но это уже тема для отдельной статьи. Пока что внедрите базовый слой с Neo4j и гибридным RAG — и количество «плохих» коммитов от AI-агента упадёт на 60–80%. Проверено на себе.