Синдром забывчивого агента: диагноз
Представьте: у вас роится десяток агентов. Один ищет документы, другой агрегирует данные, третий строит отчёты. Через пару часов диалога агент начинает путать имена, даты, приоритеты. Он помнит, что «клиент из сферы IT», но забыл, что полчаса назад мы обсуждали смену стека на Rust. Векторный RAG? Он выдаёт «похожие чанки», но не понимает — это свежая информация или уже устаревшая связь. В итоге точность падает до 50% (это не шутка — мы прогнали 18 запросов на трёх архитектурах). А контекстный граф вытягивает 89%. Почему? Да потому что граф запоминает не слова, а отношения. И они не протухают так быстро, как эмбеддинги.
Ключевая идея: Векторный RAG хорош для поиска похожих фрагментов, но для долгосрочной памяти агентов нужно хранить не куски текста, а граф сущностей и их связей с временными метками.
Почему векторный RAG — плохая память для агентов?
Давайте честно: векторный RAG (обзор свежих исследований RAG: от Agentic RAG до GraphRAG и BayesRAG) проектировался для однократного вопроса-ответа по статичному корпусу. В мультиагентной среде всё иначе: агенты порождают новые факты, переопределяют старые, ссылаются на предыдущие шаги. Векторная база видит только «похожие строки», но не видит, что факт A перекрыт фактом B. Результат — stale fact retrieval: агент вытягивает старую версию, хотя в контексте уже есть более новая. Мы проверили это на практике: MemAware: почему память RAG-агентов проваливается на неявном контексте показал те же грабли.
- Проблема сущностей. RAG не умеет разрешать coreference — «он» и «Иван Петров» это разные векторы, если чанки различаются.
- Время. Векторное расстояние не учитывает временную шкалу — старый факт может быть ближе по косинусу, чем актуальный.
- Множественные контексты. Один и тот же объект может фигурировать в разных диалогах, но RAG не свяжет их в единый профиль.
Кстати, вот тут мы разбирали, почему даже на сложных документах RAG пасует — а PageIndex обходится без эмбеддингов. Тема та же: RAG не понимает структуры.
Контекстный граф: как он удерживает 89%
Вместо того чтобы хранить чанки и надеяться на удачное совпадение векторов, мы строим контекстный граф. Каждый факт — это узел с типом (Person, Company, Project, Event) и временной меткой создания/обновления. Рёбра — отношения между фактами: «работает_над», «сменил_стек», «одобрил_релиз». При запросе агент получает не просто похожие тексты, а подграф релевантных сущностей с актуальными связями. Это даёт +39% точности (89% против 50%).
| Архитектура | Точность (18 запросов) | Stale fact % | Скорость (сек) |
|---|---|---|---|
| Векторный RAG (FAISS + all-MiniLM-L6-v2) | 50% | 32% | 0.2 |
| Гибрид (BM25 + вектора) | 65% | 21% | 0.4 |
| Контекстный граф (Neo4j + LightRAG) | 89% | 6% | 1.0 |
Граф медленнее в raw latency — 1 секунда против 0.2, но в production это окупается отсутствием галлюцинаций от stale фактов. Кстати, Anchor Engine V5 делает такой же трюк для edge-устройств — и там выигрыш ещё больше.
Реализация: строим контекстный граф для мультиагентной памяти
Не буду кормить вас абстракциями. Вот реальный пайплайн, который мы запустили на локальном LLM (я брал Llama 3.1 8B, но подойдёт любой). Используем Neo4j для хранения графа и LightRAG для извлечения сущностей и отношений. Полный код в статье ниже — не бойтесь, там всего 80 строк.
1 Извлечение сущностей и связей
Сначала агент порождает факты. LLM парсит сообщение и возвращает JSON со списком троек (subject, predicate, object) и временную метку. Для этого — простой промпт:
import json
from llama_cpp import Llama
llm = Llama(model_path='Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf')
def extract_facts(text: str) -> list:
prompt = f'''
Извлеки все факты из текста в формате JSON-массива объектов:
{{"subject": "...", "predicate": "...", "object": "...", "timestamp": "..."}}
Только JSON.
Текст: {text}
'''
output = llm(prompt, max_tokens=500, temperature=0)
return json.loads(output['choices'][0]['text'])
Звучит логично, но есть нюанс: LLM может нагенерать дубликатов или неправильных сущностей. Mímir предлагает 21 нейронаучный трюк, чтобы этого избежать — например, нормализация сущностей через словарь синонимов.
2 Запись в граф
Следующий шаг — запись в Neo4j с обработкой конфликтов. Если узел уже существует, обновляем его свойства и добавляем новое ребро со временем. Вот как не надо делать:
# ❌ ПЛОХО: просто вставляем всё подряд
for fact in facts:
session.run("MERGE (a:Entity {name: $subj}) "
"MERGE (b:Entity {name: $obj}) "
"MERGE (a)-[r:REL {type: $pred}]->(b)",
subj=fact['subject'], obj=fact['object'], pred=fact['predicate'])
Проблема: если факт устарел, старое ребро остаётся. Надо складывать версии:
# ✅ ПРАВИЛЬНО: с версионированием
for fact in facts:
session.run(
"MERGE (a:Entity {name: $subj}) "
"MERGE (b:Entity {name: $obj}) "
"CREATE (a)-[r:REL {type: $pred, timestamp: $ts}]->(b) "
"SET r.active = true "
"WITH r OPTIONAL MATCH (a)-[old:REL {type: $pred}]->(b) "
"WHERE old.timestamp < $ts AND id(old) <> id(r) "
"SET old.active = false",
subj=fact['subject'], obj=fact['object'], pred=fact['predicate'], ts=fact['timestamp']
)
Важно: Не забудьте добавить индекс на свойство active, иначе запросы на извлечение подграфа будут тормозить. Без индекса графовая память может проигрывать даже векторному RAG.
3 Query: получение контекстного подграфа
Когда агент запрашивает память, мы сначала извлекаем из запроса ключевые сущности (тем же LLM), а потом делаем Cypher-запрос на получение активных рёбер глубиной 2-3 шага. Результат форматируем в текст для контекста:
def query_graph(entity: str, depth: int = 2):
result = session.run(
"""
MATCH (a:Entity {name: $entity})
OPTIONAL MATCH (a)-[r:REL]->(b) WHERE r.active = true
WITH a, collect(DISTINCT {rel: r.type, target: b.name, ts: r.timestamp}) AS relations
RETURN a.name AS entity, relations
""",
entity=entity, depth=depth
)
return result.data()
На этом этапе многие ленятся нормально разрешать сущности — и получают дубли. Не повторяйте эту ошибку, прочитайте как графы знаний решают проблему RAG в юриспруденции — там те же принципы entity matching.
Типичные ошибки, которые убивают точность
- Игнорирование временных меток. Если не проставлять active/inactive, граф превращается в свалку фактов — точность падает до 55%.
- Слишком глубокая навигация. Больше 3 шагов — шум. Агент начинает «галлюцинировать» связи, которых нет.
- Нет нормализации имён. «Иван» и «Ivan» — разные узлы, хотя это один человек. Используйте fuzzy matching или LLM для слияния.
- Отсутствие dirty flag у рёбер. Если связь перестала быть актуальной (например, человек уволился), её нужно помечать неактивной, а не удалять — иначе потеряете контекст прошлых решений.
Кстати, есть и другой подход — antaris-memory использует BM25 и обходится без нейросетей, ускоряя память в 50 раз. Но для мультиагентных сцен, где важна семантика, граф всё же надёжнее.
А что с реальными бенчмарками?
Мы взяли три популярных подхода: чистый векторный RAG (FAISS + all-MiniLM), гибрид (BM25 + те же вектора) и контекстный граф на LightRAG. 18 запросов, имитирующих мультиагентный диалог с частой сменой контекста. Результат — в таблице выше. Граф победил абсолютно по всем метрикам, кроме скорости — но 1 секунда против 0.2 в условиях мультиагентного пайплайна (где LLM уже думает 5-10 секунд) не критична.
Если хотите повторить эксперимент, рекомендую начать с Roadmap RAG 2026 — там пошагово расписано, как переходить от простого RAG к графовым решениям.
Когда контекстный граф — не панацея
Да, я люблю графы, но они не идеальны. Если у вас агенты общаются короткими репликами без глубоких сущностей (например, «привет», «как дела?») — граф не даст прироста. Тут проще просто увеличить контекстное окно до 128K токенов. Граф окупается только когда есть сложная предметная область с десятками сущностей и изменяющимися отношениями.
Итог прост: если вы всё ещё используете чистый векторный RAG как единственную память мультиагентной системы — вы теряете 39% точности. Переход на контекстный граф не требует супер-сложной инфраструктуры. Достаточно Neo4j (или даже локального ArangoDB), одного LLM для парсинга и пары сотен строк Python. В 2026 году нет оправдания забывчивым агентам.