Обновить

Комментарии 14

да, очень круто. Хабр все же иногда еще торт!

Спасибо, очень крутая работа. Расскажите, как вы тестируете. Какие критерии, что и с чем сравниваете?

Кроме обычных юнит-тестов есть ручные end-to-end сценарии. Память приходится часто сбрасывать в ноль и заполнять пошагово фактами, конфликтными фактами, проверять на edge cases. Пока вручную, хочу автоматизировать с готовыми эмбеддингами. Кроме понятных простых сценариев есть те, которые показывают преимущества такой архитектуры памяти:
1. Дается несколько последовательных факта в разных сообщениях, затем агент должен восстановить их хронологию, а не просто выдать все три факта пачкой. Это проверка temporal ребер.
2. Дается большим набором сразу куча фактов - агент должен сохранить их как отдельные. Затем дается конфликтный факт - в памяти должен поменяться только 1 факт. Это проверка supersedes.
3. Агенту дается цепочка последовательных фактов вида: "прод упал", потом "причина в в памяти - повысили лимит", потом в новой сессии "почему мы повысили лимит памяти?" - агент должен назвать корневую причину "потому что прод упал". Это графовый обход по causal ребру, он может быть многошаговым.
4. Проверка сущности и ее связей - это может быть человек, проект, что угодно. В разных сессиях дается разная информация об одной сущности, при этом с разными видами упоминаний вида "Алексей, Леша, Леха, Мой руководитель". Например, "Я вчера с Лехой играл в шахматы", "Алексей мой начальник", "Босс поручил мне проект B2B". В памяти собираются связи с сущностью и при запросе сущности должна выводиться вся связанная информация. "Кто такой Алеша?". Ответ должен быть вида "Это твой руководитель, ты с ним играл в шахматы и он тебе поручил проект B2B".

Ключевой критерий производительности - на сколько механизм памяти предугадывает контекст. Агенту по сути обращаться к поиску в памяти надо только в крайнем случае, у него в системном промпте с помощью механизма Context Injection уже есть что-то полезное и в 80-90% случаев этого хватает для ответов.

К сожалению проблема не в фактах и не в их пониманием моделью, когда они в контекстном окне, а в поиске нужных среди большого их объема и составлении индексов.

Максимально верный способ поиска нужного факта, это когда к текущему контексту беседы (можно использовать саморизацию), добавляется следующий факт из базы по очереди, с вопросом - нужен ли он на текущий момент, с ответом да/нет, затем все нужные факты подсовываются в основной контекст беседы.

Дорого, медленно..

Есть RAG, но, к сожалению, делает это хоть и быстрее на пару порядков, но хуже и главное, лишний мусор... т.е. в вашем примере вместе с фактами о том что леша руководитель, вылезут анекдоты про Алешу, факты про руководителя соседнего отдела, а факт про порученный проект B2B затеряется в списке проектов, которые босс поручал леше, его коллегам, или просто чем занимается компания, где работает Леша... потому что не существует красивого способа остановиться и переставить валить в общую кучу факты из индекса RAG.

Очень похожую схему реализовывал на Neo4j. Намного проще получилось

да, с графовой БД всё сильно проще, но одна из целей была минимум зависимостей для локального однопользовательского агента. Мне зашел Memgraph - неплохая альтернатива Neo4j.

Neo4j имеет смысл брать, если у тебя количество связей переваливает за миллионы и глубина поиска уходит дальше 5-6 прыжков

Могу раз в сутки, потому в одном комментарии.

Статья интересная. Многое не понял(не мой профиль), но заставило местами пояндексить и почитать глубже. Интересно.

Про neo4j мнение, не критики ради, а мнение моё субъективное.

Может на текущий момент всё иначе(сомневаюсь). Кто-нибудь сравнивал скорость работы в одинаковых условиях neo4j(или иной графовой) с обычной реляционкой?

В году 16 в контору пришёл новый тимлид с пачкой сертификатов и сразу начал продвигать Neo4j как таблетку от всех наших бед. Мы исторически сидели на msmsql как основной СУБД

Я скептически отнёсся к этой теме и мы с коллегой(ну чтобы мой субъективизм кто-то придержал и быстрее тестовую среду создать) для интереса провели эксперимент.

1) Две БД, neo4j и mssql, развёрнутые из коробки методом далее, далее, далее

2) В mssql две таблицы [сущность] и [связь сущности с другой сущностью] + расширенная процедура с алгоритмом Дейкстры на C#(максимально т.е не оптимально по вызовам)

3) Данные одни и те же залиты в обе БД.

Запросы одинаковые c разной глубиной между объектами "Показать связь между объектом A и X".

neo4j стабильно проигрывала по скорости в разы. На тот момент это было ожидаемо, т.к любой граф, ложится на 2 таблицы, а уж математическая скорость работы в реляционных БД рассчитана, доказана и за десятилетия вылизана по скорости. Что могли предложить этому "молодые" СУБД тех лет - не понятно.

ЗЫ: "но в neo4j можно навешать свойства на связи". Так и в том решении на 2-х таблицах навешать свойства на связи и объекты можно. Вопрос это уже архитектурный от задачи.

Я правильно понимаю, что "долгоживущий" агент, как отправная точка, выбран потому что нужна высокая скорость ответа и ожидание загрузки модели при запуске нового агента нужно минимизировать?
Просто если нет, тогда непонятно, что мешает перезапустить агента и скормить ему conventions.md ну или данные из rag-базы?

Да, в SQLite однопоточная запись. Но если установить busyTimeout - то попытка записи будет ждать указанное время своей очереди (хорошо работает с WAL-режимом).
Так что отправлять все записи в одну очередь нет особого смысла.
Единственное - транзакцию на запись рекомендую начинать как BEGIN IMMEDIATE - иначе запись начнется в момент реальной записи и есть риск получить ошибку SQLITE_BUSY.

P.S. впрочем, писать в БД через какой-нибудь примитив синхронизации потоков надежнее будет, чем busyTimeout.

Спасибо! Особенно ценен кусок про единую очередь записи, потому что уже на ступал на грабли с конкурентной записью в WAL-режиме SQLite при асинхронной работе с агентами)

Мне очень понравилась ваша статья, хотел бы уточнить один пару моментов, а именно увидел, что в Вашей архитектуре разрешение конфликтов реализовано через LLM-консолидацию на запись (supersedes-рёбра + soft-delete), а не через pre-load детектирование противоречий на уровне векторного сходства, как в некоторых GraphRAG-подходах. Как вы обеспечиваете семантическую точность при слиянии фактов из разнородных источников (например, клинические рекомендации с разным уровнем доказательности), если: (1) векторный поиск использует сокращённые Matryoshka-эмбеддинги (256 dim), которые могут терять нюансы для коротких фактов, (2) гибридный RRF-фьюжн объединяет результаты с несопоставимыми скорами (BM25 vs. cosine vs. graph rank), и (3) механизм Эббингауза ускоряет «забывание» старых фактов, что может преждевременно удалить важный, но редко используемый контекст? Есть ли в вашей схеме нативный способ взвешивать рёбра по уровню доверия к источнику (аналог GRADE), или это требует кастомной логики поверх write-path агента - и как это влияет на latency при масштабировании до сотен тысяч узлов в одном SQLite-файле?

Векторный поиск, матрёшка и RRF скорее как механизм подбора кандидатов, а не как финальный выбор истины. Они полезны для поиска, но не для того, чтобы автоматически понять, какой из конфликтующих фактов правильнее. В таких случаях я бы скорее опирался на источник, актуальность и отдельные правила приоритета. Ну и “забывание” может оказаться вредным, если применять его ко всему одинаково. Важные факты, особенно из сильных источников, лучше держать отдельно и не давать им пропадать просто потому, что к ним давно не обращались. Но это всё уже как развитие. При работе агента я уже сталкиваюсь с разной "ценностью" обсуждаемой информации. Есть беседа пользователя с агентом, а есть новости или научные материалы, найденные в интернете. Ценность источника везде разная, а устаревание и модификация проходят одинаково. Это уже как раз тонкая настройка алгоритмов, это уже следующий слой системы - не столько retrieval, сколько политика работы со знанием и памятью.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации