За последние несколько месяцев накопился опыт работы с LLM-агентами, особенно применительно к поиску уязвимостей.
Модели неплохо справляются с навигацией по большим кодовым базам, объяснением незнакомых подсистем и поиском потенциальных векторов атаки. Однако как только исследование затягивается на несколько часов, всплывает одна и та же проблема: модель постепенно теряет из виду то, что уже было установлено.
Она может предложить подход, который уже был отвергнут, забыть, что какое-то предположение оказалось ложным, или уверенно продолжать рассуждать на основе наблюдения, которое уже перестало быть верным. Сказать LLM, что что-то неверно, не значит автоматически заставить её перестать верить во всё, что из этого следовало :)
Изначально интерес к системам памяти возник из желания сделать LLM полезнее при сложном исследовании уязвимостей и снизить количество подобных галлюцинаций.
Разумеется, существует множество готовых решений для памяти LLM. Обычно это сводится к сохранению старых разговоров или наблюдений, их эмбеддингу и последующему извлечению наиболее релевантных фрагментов, когда модели снова требуется контекст.
Работает это вполне неплохо, но кое-что не давало покоя.
Во время сессии по исследованию уязвимости важно не просто чтобы модель помнила, что было сказано.
Нужно, чтобы она поддерживала актуальное состояние того, что известно на данный момент.
Представим, что в ходе исследования установлено следующее:
attacker controls object_a
object_a points to object_b
object_b is a kernel object
Отсюда можно заключить, что атакующий способен контролировать объект ядра.
Обычная система памяти могла бы сохранить все эти наблюдения и извлечь их снова, когда речь заходит об эксплуатируемости бага. LLM затем самостоятельно приходит к тому же выводу.
Отлично!
Но предположим, что двумя часами позже в LLDB выясняется, что object_a на самом деле не указывает на object_b, и предыдущее наблюдение было основано на неверном предположении.
В этот момент память может содержать что-то вроде:
object_a points to object_b
attacker can control object_b
object_a does not actually point to object_b
Теперь извлекается некое подмножество этих воспоминаний, и остаётся надеяться, что LLM правильно определит, какие выводы всё ещё справедливы.
Ситуация начала казаться слегка знакомой.
Это похоже на анализ программ
Значительная часть привычной работы связана с анализом программ.
При анализе программы обычно имеется набор фактов о ней и некоторые правила, выводящие из них дополнительные факты.
Например, пусть известно:
calls(foo, bar)
calls(bar, baz)
Можно определить правило: если одна функция вызывает другую, которая способна достичь третьей, то и первая функция способна её достичь.
В итоге вычисляется неподвижная точка, содержащая всё, что можно вывести из программы. Что важнее — если один из входных фактов меняется, существует масса техник для обновления только затронутых результатов, без пересчёта всего с нуля.
Это в точности то, чего хотелось от LLM при исследовании уязвимостей.
Если наблюдение меняется, не нужно, чтобы модель восстанавливала всё исследование заново по транскрипту разговора в надежде заметить все последствия. Нужно, чтобы затронутые выводы автоматически становились недействительными.
Взглянув на проблему под этим углом, возник вопрос: зачем вообще заставлять LLM снова и снова реконструировать своё состояние?
А что, если просто поддерживать его?
Так и получилось, что дело закончилось написанием движка Datalog для LLM :)
Datalog
Прежде чем продолжить, стоит вкратце пояснить, что такое Datalog.
Datalog — это декларативный язык логического программирования. Вместо инструкций, описывающих, как что-то должно вычисляться, описываются факты и правила, из которых можно вывести новые факты.
Например, можно сохранить следующие факты:
controls(attacker, object_a).
points_to(object_a, object_b).
kernel_object(object_b).
А затем определить правило:
controls_kernel_object(Attacker) :-
controls(Attacker, ObjectA),
points_to(ObjectA, ObjectB),
kernel_object(ObjectB).
Из имеющихся фактов движок может вывести:
controls_kernel_object(attacker).
Пока ничего особенно интересного.
Но предположим, позже выясняется, что:
points_to(object_a, object_b).
было неверно.
Если controls_kernel_object(attacker) был выведен из этого факта, точно известно, какой вывод зависит от только что изменившегося наблюдения, и его можно автоматически аннулировать.
Это гораздо удобнее, чем закладывать всю старую информацию в промпт и надеяться, что LLM заметит то же самое.
Lemmalog
Со временем это превратилось в Lemmalog.
Базовая идея в том, что LLM не обязана самостоятельно поддерживать собственные знания в согласованном состоянии. Задача разбивается на две части.
LLM отвечает за нечёткую часть:
"LLDB shows that the freed object is later reused
as the destination of the write."
|
v
freed(object_a)
reused_as(object_a, write_target)
А Lemmalog отвечает за детерминированную часть:
facts
|
v
rules
|
v
derived facts
Это значит, что LLM по-прежнему отвечает за понимание естественного языка, исходного кода, вывода отладчика и прочей неупорядоченной информации, возникающей в ходе исследования.
С этим LLM справляется весьма неплохо.
Но как только информация преобразована в структурированные факты, больше не нужно, чтобы модель раз за разом заново определяла все её следствия. Это может сделать база данных.
Ретракции
Одна из первых интересных проблем, с которой пришлось столкнуться, — удаление фактов.
Добавление факта в базу данных Datalog относительно простое: добавить новый факт и вычислить правила, которые теперь могут дать дополнительные результаты.
Удаление — задача чуть более неприятная.
Рассмотрим пример:
a.
b.
c :- a.
c :- b.
Здесь c истинно по двум независимым причинам.
Если убрать a, нельзя просто удалить c, потому что b по-прежнему обеспечивает другой вывод для него. Но если убрать и a, и b, c тоже должно исчезнуть.
Это оказывается весьма важным при исследовании уязвимостей, поскольку один вывод может опираться сразу на несколько наблюдений.
Например:
candidate_3_is_exploitable
может оставаться истинным, даже если один конкретный примитив эксплуатации перестал работать, потому что существует другой независимый путь к тому же результату.
Поэтому Lemmalog отслеживает, как были выведены факты, и обновляет их поддержку при изменениях.
Удобно, что это даёт ещё одно полезное свойство:
можно спросить, почему нечто истинно.
Почему?
Представим, что агент работал несколько часов над каким-то исследованием и в итоге заключил:
candidate_3_is_exploitable
Это хорошо, но хотелось бы ещё узнать почему.
Поскольку Lemmalog уже отслеживает зависимости выведенных фактов, у него можно запросить происхождение вывода. Концептуально результат может выглядеть так:
candidate_3_is_exploitable
|
+-- attacker_controls_pointer
| |
| +-- observation_41
|
+-- pointer_reaches_target
|
+-- observation_57
+-- rule_12
Если позже выяснится, что observation_41 ошибочен, становится известно, что этот вывод может быть больше не действителен, и, поскольку база данных знает об этом тоже, она может автоматически удалить затронутые заключения.
Изначально это было нужно в основном для корректной инкрементальной оценки, но выяснилось, что возможность спросить у ИИ-агента, почему он во что-то верит, тоже весьма полезна :)
Это также решает одну из самых раздражающих проблем, встречавшихся при исследованиях с помощью LLM. Иногда модель уверенно заявляет что-то вроде:
we already established that this pointer is attacker-controlled
хотя на самом деле это неверно.
Если вывод существует в Lemmalog, можно спросить, откуда он взялся. Если у него нет подтверждённого происхождения, значит он не является частью поддерживаемого состояния.
Разумеется, это не мешает LLM галлюцинировать на этапе извлечения фактов, но существенно затрудняет незаметное проникновение необоснованных выводов в исследование.
Факты тоже меняются со временем
Ещё одна проблема в том, что замена старых фактов не всегда равнозначна их удалению.
Допустим, изначально считается, что:
primitive_a is viable
а позже выясняется:
primitive_a is not viable
Для большинства текущих запросов важно, вероятно, только второе утверждение. Но если нужно понять, почему ранее исследовалась определённая стратегия эксплуатации, старое состояние по-прежнему полезно.
По этой причине Lemmalog может связывать факты с интервалами действительности.
Концептуально состояние можно представить так:
viable(primitive_a) [10:14, 12:37)
not_viable(primitive_a) [12:37, ...)
Это позволяет ответить и на вопрос:
Is primitive_a viable now?
и на вопрос:
Why did we think primitive_a was viable earlier?
не храня при этом два внешне противоречащих друг другу факта и не заставляя LLM решать, какой из них имелся в виду.
Опять же, это не совсем проблема языковой модели.
Это в основном проблема баз данных.
Почему бы просто не использовать векторную базу данных?
Векторные базы данных весьма полезны.
На вопрос:
What did we find earlier about this allocation path?
семантический поиск — вероятно, именно то, что нужно.
Но косинусное сходство «по ощущениям» и истина — не совсем одно и то же.
Векторная база данных способна извлечь:
object_a points to object_b
потому что это релевантно вопросу. Она не знает по своей природе, что это утверждение было опровергнуто два часа спустя или что от него зависели ещё пять выводов, которые из-за этого больше не считаются действительными.
Это навело на мысль, что под термином «память» на самом деле скрываются две разные проблемы.
Первая:
What information from the past is relevant to this question?
Вторая:
Given everything we have learned so far, what is currently true?
Извлечение отлично справляется с первой проблемой.
Lemmalog — это в основном эксперимент по решению второй.
Оба подхода можно комбинировать, что сейчас и делается.
Исследование уязвимости — по сути состояние анализа
Чем дольше велась эта работа, тем больше проявлялось сходств с анализом программ.
В ходе исследования уязвимости есть наблюдения:
this field is attacker-controlled
предположения:
this object survives until the second callback
отношения:
primitive_b depends on primitive_a
гипотезы:
this could become an arbitrary write
и выводы:
candidate_3 is exploitable
Это удивительно хорошо соотносится с тем, что уже делается в анализе программ.
Есть входные факты:
observations
правила:
relationships between observations
выведенные факты:
conclusions
неподвижная точка:
everything currently known
и при изменении входных данных выполняется инкрементальная оценка:
update affected conclusions
Поскольку зависимости отслеживаются, можно объяснить происхождение результатов:
provenance
В какой-то момент стало очевидно, что подход, сам того не подразумевая, свёлся к движку статического анализа.
Это также изменило взгляд на роль самой LLM.
Всю систему можно представить как своего рода необычный компилятор.
LLM выступает в роли фронтенда:
source code,
debugger output,
natural language notes
|
v
structured facts
Lemmalog — это промежуточное представление и движок анализа:
structured facts
|
v
deductive rules
|
v
maintained state
Ещё один вызов LLM в итоге может превратить это состояние обратно в естественный язык, предложить следующий эксперимент или использовать его для выполнения какого-то действия.
Забавно, что парсер здесь вероятностный, а всё, что идёт после него, не обязано таковым быть.
Делает ли это LLM действительно лучше?
Это, разумеется, главный вопрос.
Сам движок теперь поддерживает инкрементальную оценку, ретракции, отслеживание происхождения, темпоральные факты, агрегации, сведение сущностей, гибридное извлечение, запросы по требованию и ещё кучу возможностей, добавленных, вероятно, просто потому, что реализовывать фичи Datalog оказалось интереснее, чем ожидалось.
Есть также MCP-сервер, позволяющий агентам использовать Lemmalog напрямую.
Но всё это не имеет большого значения, если такая память для LLM на самом деле ничего не улучшает.
Поэтому Lemmalog был подключён к MemEval и протестирован на LongMemEval и LoCoMo с использованием их стандартных моделей-читателей и схемы оценки. Извлечение при индексации выполняется Claude Sonnet 4.6 (с чанкингом и кэшированием файлов, так что оплачивается один раз за разговор); всё после извлечения использует собственных стандартных читателей и судей бенчмарка.
Результаты оказались чуть лучше ожидаемого.
LongMemEval
LongMemEval проверяет, способна ли LLM отвечать на вопросы по информации, разбросанной по длинным историям разговоров. Использованный сплит содержит 102 вопроса, поровну разделённых между фактами о пользователе, фактами об ассистенте, предпочтениями, многосессионными вопросами, темпоральными рассуждениями и обновлениями знаний.
Поскольку 17 вопросов на категорию — не самая внушительная выборка, Lemmalog был прогнан три раза, а не один — чтобы не радоваться случайно удачному прогону.
Результат:
Lemmalog
F1: 0.463 +/- 0.010
Accuracy: 0.575 +/- 0.004
Для сравнения, опубликованные результаты других систем памяти:
PropMem 0.550
SimpleMem 0.480
Lemmalog 0.463 +/- 0.010
OpenClaw 0.244
Full Context 0.222
Собственный прогон с полным контекстом на GPT-4.1 показал 0.197 F1.
Таким образом, Lemmalog пока не обходит PropMem и немного уступает SimpleMem, но даёт более чем вдвое больший F1 по сравнению с передачей GPT-4.1 всего разговора целиком.
Забавнее другое: контекст, передаваемый отвечающей модели, оказывается примерно в 38 раз меньше.
Full context: ~104,000 tokens/question
Lemmalog: ~2,700 tokens/question
Похоже, поддержание состояния вместо повторного перечитывания всей истории действительно приносит пользу :)
Результаты по категориям для одного из показательных прогонов выглядели так:
| Система | SS-User | SS-Asst | Preference | Multi-Session | Temporal | K-Update |
|---|---|---|---|---|---|---|
| PropMem | 0.851 | 0.767 | 0.147 | 0.582 | 0.424 | 0.528 |
| SimpleMem | 0.752 | 0.566 | 0.126 | 0.382 | 0.578 | 0.475 |
| Lemmalog | 0.790 | 0.672 | 0.128 | 0.211 | 0.416 | 0.579 |
| OpenClaw | 0.401 | 0.432 | 0.127 | 0.082 | 0.185 | 0.234 |
| Full Context | 0.265 | 0.415 | 0.177 | 0.062 | 0.212 | 0.202 |
Самым интересным оказался результат в категории Knowledge Update.
Lemmalog набрал 0.579 против 0.528 у PropMem и 0.202 у полного контекста.
Knowledge Update — это, по сути, та самая ситуация, ради которой всё и затевалось:
we believed A
|
later we learn that A is no longer true
|
what should we believe now?
Так что вывод Lemmalog в лидеры по категории, наиболее близкой к поддержанию состояния программного анализа, оказался весьма приятным.
Однопессионная фактическая память тоже сработала на удивление хорошо. Lemmalog достиг 0.790 по фактам о пользователе и 0.672 по фактам об ассистенте, а темпоральные рассуждения достигли 0.416 — почти идентично 0.424 у PropMem в этом прогоне.
Очевидная оставшаяся проблема — многосессионные рассуждения:
PropMem 0.582
SimpleMem 0.382
Lemmalog 0.211
Диагностика этих провалов оказалась интересной: информация обычно не была неправильно связана — она просто никогда не извлекалась. Если экстрактор ни разу не создал факт о бронировании на Airbnb, никакая дедукция не поможет ответить на вопрос о нём.
Что подводит к одному из самых забавных моментов прогона бенчмарков.
Случайно научил систему не отвечать на вопросы
В какой-то момент результат LongMemEval внезапно упал до 0.371 F1.
Разбор провалов показал, что 32 из 102 вопросов получали отказ в ответе.
Все 32 были отвечаемы.
Вопросы вроде:
Which airline did I fly most?
или:
How many magazine subscriptions do I have?
получали в ответ:
Not mentioned.
Проблема крылась в инструкции, добавленной для снижения галлюцинаций. Читателю было указано убедиться, что ответ действительно подтверждён извлечёнными фактами, прежде чем отвечать.
К сожалению, модель истолковала это как:
Если ни один отдельный факт буквально не содержит итоговый ответ — отказывайся.
Разумеется, нет факта вида:
most_flown_airline(user, swiss)
если память вместо этого содержит:
flew(user, swiss, trip_1)
flew(user, swiss, trip_2)
flew(user, lufthansa, trip_3)
Ответ существует. Просто требует подсчёта.
Решением стало разделение двух случаев:
Если предпосылка отсутствует или неверно атрибутирована — отказ.
Если доказательства есть, но требуют подсчёта, сравнения, объединения или упорядочивания фактов — действительно рассуждать над ними.
После этой правки F1 восстановился до 0.429.
Остальная часть разрыва оказалась ещё коварнее: путь подсчёта был незаметно нерабочим всё это время. Строки со счётчиками проходили через фильтр релевантности перед показом читателю, а стеммер множественного числа, использовавшийся этим фильтром, сворачивал только слова длиннее четырёх символов. Поэтому owns никогда не сопоставлялось с own, все строки со счётчиками отбрасывались, и вопросы на подсчёт тихо оставались вовсе без чисел.
Исправление стеммера, отображение счётчиков вместе с фактами, которые они считают, и предварительное вычисление разности дат вместо надежды, что модель правильно вычтет одну дату из другой, подняли F1 до 0.463.
Это различие оказывается важным и на другом бенчмарке.
LoCoMo
Lemmalog также был прогнан на полном бенчмарке LoCoMo.
LoCoMo значительно крупнее: 10 длинных разговоров, содержащих 1986 вопросов, охватывающих фактологическое припоминание, темпоральные рассуждения, многошаговые вопросы, инференцию и состязательные вопросы с ложными предпосылками.
Этот бенчмарк оказался особенно полезен именно потому, что 1986 вопросов существенно затрудняют случайную радость от удачного сида.
И снова весь бенчмарк был прогнан три раза.
Lemmalog LoCoMo:
0.533 +/- 0.001 F1
Опубликованное сравнение выглядит так:
| Система | F1 |
|---|---|
| PropMem | 0.605 |
| OpenClaw | 0.557 |
| Full Context | 0.542 |
| Lemmalog | 0.533 ± 0.001 |
| Hindsight | 0.489 |
| Graphiti | 0.416 |
| Memory-R1 | 0.389 |
| SimpleMem | 0.358 |
Так что Lemmalog сейчас занимает третье место среди специализированных систем памяти в этом сравнении, уступая PropMem и OpenClaw.
Если засчитывать закидывание всего разговора в промпт как систему памяти, то четвёртое.
Что, пожалуй, справедливо :)
Что важнее, все три прогона оказались почти идентичны, так что ~0.53 выглядит как реальный результат, а не шум бенчмарка.
Результаты по категориям для финальной конфигурации выглядят так:
| Категория | Lemmalog | PropMem | Full Context |
|---|---|---|---|
| Factual | 0.399 | 0.431 | 0.517 |
| Temporal | 0.454 | 0.615 | 0.369 |
| Multi-hop | 0.545 | 0.599 | 0.674 |
| Inferential | 0.164 | 0.289 | 0.197 |
| Adversarial | 0.707 | 0.794 | 0.509 |
Здесь есть два результата, которые особенно нравятся.
Первый — темпоральные рассуждения.
Начальная версия Lemmalog набрала:
0.257
После исправления нормализации и извлечения временных данных:
0.454
Баг оказался довольно забавным.
В какой-то момент значения, похожие на даты, сравнивались как интернированные символы Datalog.
Оператор < движка для символов сравнивает их внутренние идентификаторы.
Внутренние идентификаторы, очевидно, не являются датами :)
После нормализации извлечённых дат в сравнимые целые числа и вывода happened_before из реальных временных меток темпоральная производительность подскочила почти на двадцать пунктов F1.
Второй понравившийся результат — состязательные вопросы.
Lemmalog набирает:
0.707
тогда как полный контекст набирает:
0.509
Эти вопросы намеренно содержат ложные или неверно атрибутированные предпосылки.
Например, разговор может содержать историю о том, как кто-то получил подарок, а затем вопрос приписывает тот же подарок другому человеку.
Языковая модель с гигантским транскриптом весьма склонна найти семантически похожую историю и всё равно ответить. Структурированная память вместо этого способна заметить, что просто нет подтверждающего факта о человеке из вопроса.
Другими словами:
no
оказывается вполне полезным ответом.
Фронтенд имеет большое значение
Первая реализация для LoCoMo дала 0.483.
Текущая даёт около 0.533.
Оценщик Datalog не стал внезапно на 10% умнее.
Большая часть улучшения пришла от исправления того, как информация попадает в состояние анализа и извлекается из него.
Например, разрешение сущностей оказалось весьма важным.
Представим следующие сессии:
Session 1:
"I bought a Honda Civic."
Session 3:
"My car broke down."
Session 7:
"The Civic is finally fixed."
Если извлечение даёт:
bought(user, honda_civic).
broke_down(car).
fixed(civic).
то движок Datalog делает ровно то, что от него требовали.
К сожалению, его попросили рассуждать о трёх разных объектах.
Поэтому теперь у Lemmalog есть проход сведения сущностей, связывающий локальные для эпизода упоминания с каноническими сущностями.
Чисто лексическое извлечение тоже вызывало забавные сбои. Вопрос, ссылающийся на:
"kitchen gadget"
необязательно извлекал факт про:
"Instant Pot"
хотя связь очевидна человеку.
Теперь извлечение сочетает BM25, усиление по графу/сущностям и эмбеддинги, а финальный контекст содержит как структурированные факты, так и исходные фрагменты текста, из которых они получены.
Это стало ещё одним напоминанием, что сложная часть архитектуры — не столько вычисление неподвижной точки.
Сложность — в построении хорошего информационного извлечения из естественного языка.
Что, опять же, подозрительно напоминает анализ программ.
Кое-что, вероятно, должно оставаться нечётким
Есть также область, где Lemmalog остаётся довольно слабым: инференция.
На LoCoMo:
PropMem 0.289
Lemmalog 0.164
Это объяснимо.
Предположим, кто-то говорит:
I usually prefer quiet restaurants, except when I'm travelling
with friends, when I quite like somewhere lively.
Сведение этого к:
prefers(user, quiet_restaurants).
отбрасывает половину полезной информации ещё до того, как Datalog её увидел.
Очевидное направление — не отказываться от структурированной памяти, а перестать притворяться, что каждый факт памяти безусловен.
Условное знание может оставаться условным:
prefers(User, lively_restaurants) :-
prefers_when(User, lively_restaurants, with_friends),
with_friends(User).
А исходный текст эпизода может оставаться доступным для ситуаций, где структурированное представление теряет полезные нюансы.
Поэтому полезная архитектура выглядит не столько как:
vector memory
OR
symbolic memory
а скорее как:
agent memory
|
+--------------+--------------+
| |
deductive state episodic memory
| |
facts / rules / time fuzzy context
provenance semantic retrieval
retractions source text
Что, к счастью, довольно близко к тому, чем Lemmalog в итоге и стал.
О токенах
Есть ещё одна часть результата, которая изначально не казалась настолько значительной.
Для LongMemEval отвечающая модель видит примерно:
Full context: ~104,000 tokens/question
Lemmalog: ~2,700 tokens/question
Около 38x меньше контекста.
Для LoCoMo:
Full context: ~18,900 tokens/question
Lemmalog: ~3,400 tokens/question
Около 6x меньше.
Разумеется, есть стоимость извлечения.
Разговор нужно прочитать один раз и превратить в факты, поэтому утверждение, что вся система просто в 38 раз дешевле, было бы нечестным.
Важное отличие в том, что извлечение происходит один раз.
Промптинг с полным контекстом заново оплачивает всю историю при каждом запросе.
При работе с постоянным агентом разница поэтому растёт со временем.
Концептуально:
| Ход | Полный контекст | Lemmalog |
|---|---|---|
| 50 | 100K/запрос | ~2.5K/запрос |
| 100 | 200K/запрос | ~2.5K/запрос |
| 500 | 1M/запрос | ~2.5K/запрос |
В какой-то момент версия с полным контекстом не просто становится дорогой.
Она перестаёт помещаться в окно контекста.
Контекст запросов Lemmalog не растёт вместе со всем транскриптом, потому что извлекается только релевантное поддерживаемое состояние.
В этом, собственно, и была изначальная идея.
Доказывает ли это что-нибудь?
Пока не совсем.
LongMemEval — это 102 вопроса, а LoCoMo всё ещё бенчмарк памяти разговоров, а не исследование уязвимости.
PropMem также по-прежнему опережает Lemmalog в целом по обоим стандартизированным сравнениям.
Так что заявлять, что Datalog решил проблему памяти LLM, преждевременно :)
Но результатов достаточно, чтобы показать: идея не совсем глупая.
По трём прогонам LongMemEval Lemmalog показывает:
0.463 +/- 0.010 F1
0.575 +/- 0.004 accuracy
А на LoCoMo:
0.533 +/- 0.001 F1
Система особенно конкурентоспособна, когда задача вознаграждает именно то, ради чего архитектура создавалась: обновления знаний, темпоральное состояние, многошаговые отношения и отклонение необоснованных предпосылок.
Но, пожалуй, самый интересный результат — не финальное число.
Первая стандартизированная конфигурация LongMemEval показала:
0.226
Текущая показывает:
0.463
Более чем вдвое выше.
Большая часть этого улучшения пришла от разбора отдельных провалов и обнаружения довольно конкретных проблем computer science:
- идентичность сущностей была разорвана
- даты представлялись некорректно
- извлечение упускало семантические алиасы
- агрегация существовала, но не отображалась
- стеммер множественного числа не считал, что «owns» соответствует «own»
- читатель случайно был обучен отказываться от синтеза ответа
Ничто из этого не требовало увеличения языковой модели.
Требовалось лишь поддерживать вокруг неё более качественное состояние.
Результат довольно забавный, учитывая, ради чего проект вообще затевался.
Следующий эксперимент — тот, который действительно интересен.
Дать агенту сложное исследование уязвимости, позволить ему работать долго и посмотреть, перестанет ли поддержание состояния анализа воскрешать мёртвые гипотезы и порождать галлюцинированные связи между наблюдениями.
Это, вероятно, окажется интереснее, чем запоминание того, где работает Алиса :)
Заключение
Цель была не столько в том, чтобы дать LLM память получше.
Цель — чтобы модель перестала забывать, почему во что-то верила.
Если агент уже установил, что:
A implies B
B implies C
а позже узнаёт, что A больше не истинно, не должно требоваться подсовывать ему пятьдесят старых сообщений и просить разобраться, стоит ли по-прежнему доверять C.
Точно так же, если стратегия эксплуатации зависит от предположения, только что опровергнутого в отладчике, не хочется, чтобы модель предложила ту же стратегию снова два часа спустя просто потому, что старый разговор оказался семантически релевантным.
Уже известно, как решать проблемы, связанные с фактами, зависимостями, инвалидацией и неподвижными точками. Их решают в базах данных и анализе программ десятилетиями.
Результаты бенчмарков как минимум показывают, что это не только красивая идея в теории.
Lemmalog уже конкурентоспособен по сравнению со специализированными системами памяти LLM, существенно превосходит полный контекст в некоторых задачах, для которых создавался, и делает это, передавая читателю лишь малую долю исходной истории.
Остаётся ещё множество вещей, с которыми система справляется плохо.
Но, возможно, не каждый раз, когда агент что-то забывает, нужно большее окно контекста.
Иногда достаточно просто поддерживать состояние.
Исходный код Lemmalog доступен здесь.