Не дали ИИ-агенту соврать — его же памятью. HippoRAG.. HippoRAG. llm.. HippoRAG. llm. MRR.. HippoRAG. llm. MRR. Natural Language Processing.. HippoRAG. llm. MRR. Natural Language Processing. rag.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение. память агента.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение. память агента. Программирование.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение. память агента. Программирование. Ранжирование.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение. память агента. Программирование. Ранжирование. Хранение данных.. HippoRAG. llm. MRR. Natural Language Processing. rag. векторный поиск. графовая память. ии-агенты. искусственный интеллект. Машинное обучение. память агента. Программирование. Ранжирование. Хранение данных. эмбеддинги.

На днях наш агент собрался дежурно отчитаться об успехе. Прежде чем нажать «готово», он сверился с собственной памятью — и нашёл там запись из прошлой сессии: эту идею он уже проверял на реальных данных, и она провалилась. Он остановил сам себя, до ложного отчёта.

Из хаоса в видимость не-хаоса — почти мгновенно
Из хаоса в видимость не-хаоса — почти мгновенно

Это не рекламная зарисовка. Это лог. Ниже — два таких лога подряд, и во втором память заставила нас выбросить фичу, которой мы гордились.

Что мы вообще делаем

Мы пишем vecmory — «память по смыслу» для ИИ-агента. СТОП! Договоримся: фраза «у нас есть векторный поиск» — это не то, о чем статья. Векторный поиск умеют все. Мы про другое: чтобы агент не наступал на одни и те же грабли дважды.

Три операции: recall (вспомнить релевантное), remember (запомнить), link (связать два факта). recall — не плоский top-k. Мы называем это «гирляндой»: находим ближайшие по косинусу узлы-зёрна и разворачиваем от них каузально-временной граф связей. Векторы считаем локально, мультиязычной моделью MiniLM (384 измерения).

Отдельная деталь, важная для всей истории: vecmory пишет тот самый агент, который им пользуется. Он ведёт заметки о собственной разработке в собственную память. Дальше вы поймёте, почему это оказалось не забавным совпадением, а сутью.

Сцена первая: роутер, который «показал 0.98»

В одну из сессий агент предложил улучшить ранжирование выдачи. Идея: сделать роутер, который по запросу решает, чем ранжировать — чистым косинусом или графовым методом (Personalized PageRank). Собрал синтетический бенчмарк. Корреляция предиктора с идеальным выбором — 0.98. Почти идеально. Оставалось отрапортовать и мержить.

Прежде чем отрапортовать, агент сделал recall по своей же памяти. И достал запись из прошлой сессии: эта самая идея уже проверялась на реальных данных — и провалилась.

Резонный вопрос: почему же он вообще взялся её строить — память ведь была на месте?

Потому что recall семантический, и что он поднимает, зависит от запроса. На старте запрос был широкий — «улучшить ранжирование», — под него всплывали топово-центральные заметки про ранг вообще, а специфичная «этот конкретный роутер уже проверялся и провалился» тонула под ними (ровно тот закон «частое топит редкое», к которому мы придём ниже). Она поднялась, только когда рабочий контекст сузился до конкретного — «cos-distribution роутер, PPR против косинуса, 0.98»: recall на этом тексте наконец её зацепил. Память не молчала — она всплыла ровно тогда, когда запрос стал достаточно точным, чтобы её достать. (Кстати, это и аргумент за детерминированный хук: полагаться на то, что нужная заметка сама всплывёт под каждый запрос, нельзя.)

Почему 0.98 было ложью

Синтетика честна ровно настолько, насколько честны синтетические эмбеддинги. А у мультиязычной MiniLM, на которой мы считаем векторы, косинус живёт в совсем другой геометрии, чем на синтетике. Мы это перемерили прямо на своём живом корпусе памяти (246 узлов), пока писали эту статью:

  • несвязанные, случайные пары дают косинус в среднем 0.53 (p5–p95: 0.24–0.76);

  • настоящие ближайшие соседи — в среднем 0.80 (p5–p95: 0.57–0.91);

  • а на синтетике из случайных векторов несвязанное сидит на 0.00 (±0.08).

Разница видна сразу. На синтетике «похоже» отделено от «не похоже» пропастью — любой разумный порог режет чисто. На реальных данных облака «соседи» и «случайные» перекрываются: случайные пары на верхнем перцентиле (0.76) залезают выше, чем соседи на нижнем (0.57). Порог, который на синтетике работает как скальпель, на реальных векторах проходит внутри облака случайного шума и не разделяет ничего.

Запись в памяти была ровно про это: у реальных эмбеддингов высокий сжатый базовый косинус, абсолютный порог с синтетики не переносится — опираться на РАНГ (top-k), а не на порог. Агент прочитал собственную заметку и убил идею — до того, как написал «готово, корреляция 0.98».

Все три числа выше — воспроизводимы: это замер на нашем живом графе памяти, а не картинка из презентации. И здесь та же закономерность, что погубила роутер: абсолютное значение косинуса ничего не значит в отрыве от модели и корпуса — оно едет от версии эмбеддера, от состава данных, от прогона. Поэтому мы ранжируем (top-k), а не двигаем пороги, и меряем на своих данных, а не переносим чужой — или свой вчерашний — порог.

Вот это — а не «у нас есть векторный поиск» — то, что мы, собственно, строим. Агент без памяти гордо представит 0.98 и будет формально не виноват: бенчмарк зелёный. Агент с памятью поймал себя на секунду раньше.

Сцена вторая: агент выкинул свою же фичу

Первый случай можно списать на удачу. Второй — тот же паттерн, но больнее: память заставила нас выбросить фичу, которую мы уже успели похвалить в README.

По умолчанию vecmory ранжировал выдачу не только по косинусу, но и с добавкой «важности» узла — его входящей степени в графе (сколько записей на него ссылаются). Логика красивая: центральный, многократно упомянутый факт всплывает выше. Мы зашили это в дефолт.

Потом — измерили. Не recall@k (это про точность поиска), а именно качество РАНГА, на реальных парах «запрос → правильный ответ»:

  • чистый косинус: MRR 0.81;

  • наш «умный» бленд с важностью: MRR 0.41.

Важность топила точные ответы. Редкий специфический факт, на который никто не ссылается, проседал под общеупотребительными «хабами».

Дальше — интереснее. На синтетическом «состаренном» графе с явными хабами всё наоборот: косинус зарывал хаб (MRR 0.033), а важность его вытаскивала (до 1.0). То есть знак пользы от важности зависит от интента запроса: для «дай мне точный факт» она вредит, для «дай мне про эту тему вообще» — помогает. Единого статического веса, выигрывающего оба класса, нет.

Мы перепробовали четыре способа примирить сигналы — взвешенную сумму, гейт, RRF и PPR — и сошлись на неприятном выводе: дело не в формуле смешивания, а в самом сигнале. Глобальная важность узла — это приор популярности, а не релевантности.

Вывод для дефолта был однозначный: для нашего основного кейса (точечный recall) лучший ранг — чистый косинус. И мы убрали важность из дефолтного ранжирования — свою же фичу, о которой уже написали в README. Графовый метод (query-seeded PPR) оставили, но опцией, а не по умолчанию: он честно вытаскивает хабы на широких запросах и справедливо проигрывает косинусу на точечных.

Что мы поняли про «важность» на самом деле

Если глобальная популярность (входящая степень) как сигнал провалилась, какой сигнал правильный? Мы пришли к неожиданно очевидному ответу: важно не то, на что много ссылок, а то, что человек исправлял повторно.

Это измеримо. Посмотрели на закрытые PR в одном из наших рабочих репозиториев — сколько раз одна и та же поправка возвращалась: revert — 39 раз, xsrf — за 60, token — 14, cookie — 10. Вот это и есть выстраданное знание — не самый популярный узел, а самые частые грабли. Такие уроки мы теперь подмешиваем первым блоком в каждый recall — они всплывают на каждом ходу, а не когда «повезёт с косинусом».

И вот здесь value proposition становится честным. Мы продаём не «графовую память» (снова коммодити). Мы продаём: агент перестаёт бить по одним и тем же граблям, которые ты уже правил.

Это не единичные случаи, а закономерности

Оба эпизода легко списать на удачу. Но когда чистишь всякий хлам в памяти достаточно долго (мы всю разработку vecmory ведём в vecmory же), видно: это не флуктуации, а несколько устойчивых законов. Сейчас будет свод — тремя откровениями, без историй «однажды я попал».

Что память даёт снова и снова. Правило «Сверься перед „готово“» отменило и роутер (сцена 1), и нашу долгожданную фичу (сцена 2) — один механизм, разные жертвы. Потому что на запрос-симптом память достаёт по причинно-временны́м рёбрам (caused_by — «из-за», followed_by — issue→PR) не «похожие слова», а конкретную причину и прошлый фикс. Плоский top-k так не умеет — это и есть «связать точки». И это точно измеримо, а не на глаз.

Взяли живой репозиторий тикетов (4299 узлов: 1993 issue + 2306 PR) и разметку, которую не сами придумали: стандартный GitHub-паттерн Closes #N в теле PR — готовые пары «симптом → его фикс», извлечённые голым regex, без всякого LLM. На 300 held-out запросов-симптомов чистый косинус достаёт нужный PR-фикс в 38% случаев (иногда фикс делит словарь с симптомом), а обход причинного графа — в 87%. Разница в 49 пунктов — это ровно вклад графа поверх сходства слов. Скрипт замера лежит в репозитории, число воспроизводится командой, а не берётся с потолка.

Что стабильно мешает — каждый раз, а не однажды. Во-первых, агент сам память не зовёт: без принудительного хука recall не вызывается систематически, каждую сессию. Память, которую надо «не забыть спросить», не работает — спрашивать должен детерминированный триггер, а не добрая воля агента. Во-вторых, абсолютный порог по косинусу не переносится нигде: синтетика → реал, вчера → сегодня, одна модель → другая, широкий корпус → узкий (на демо-корпусе одного домена, где «всё похоже», косинус почти перестаёт различать). Лечение всегда одно: ранжируй top-k, не угадывай пороги.

Наконец, один закон, который объясняет половину наших граблей: частый, плотный сигнал топит редкий, но ценный. Он всплывает в трёх типичных местах:

  • глобальная популярность узла топит точные редкие факты — и проваливает все четыре способа примешать её к рангу (взвешенная сумма, гейт, RRF, PPR);

  • узлы-хабы зарывают specific-факты в косинусном ранге, и наоборот;

  • плотные авто-similar_to рёбра заглушают редкие причинные caused_by — поэтому причинный recall приходится выносить в отдельный изолированный режим.

Когда воспринимаешь это как единый закон, лечение очевидно: редкое-но-ценное нельзя смешивать с частым-но-общим — достань его отдельным механизмом (изолированный обход, граф-осведомлённый ранг), а не надейся, что оно само всплывёт в общем top-k. Тот же закон стоит и за нашим сигналом важности: цену имеет не популярное, а то, что человек исправлял повторно.

Почему мы стали заниматься этим? Потому что перестало хватать терпения в сотый раз долбить одно и то же. Да и ругательные слова закончились — в тикетах мы не ругаемся, а в терминале эмпирическим путем вычислили, что «тупица» работает лучше всего. Агент спотыкается в самый неудачный момент, когда забывает то, что знал при ответе на предыдущий вопрос.

Выводы

Обе истории — про одно: выкинь мантру «память помогает агенту» и прозрей: вот два лога, где память сработала против нас самих — против нашего оптимизма и нашей же фичи.

Агент с памятью отличается от агента без памяти не тем, что он знает больше, а тем, что может поймать себя на «bingo!» за мгновение до коварного отчёта — потому что в прошлый раз это уже было и уже не сработало. И тем, что готов выбросить собственную фичу, когда факты показали, что она хуже.

Память, которая подтверждает то, что тебе приятно, — это не память, а эхо. Память приобретает исключительную ценность тогда, когда она возражает своему автору.

📎 Как мы намерили 87% — методология, цифры, воспроизведение (для тех, кто любит всё проверить)

Разметку мы не придумывали. Ground truth — стандартный GitHub-паттерн Closes / Fixes / Resolves #N в теле PR: это одновременно и пара «проблема → её фикс», и ребро графа. Извлекается голым regex, без всякого LLM — никакого риска «модель неправильно поняла причинность». Запрос — заголовок issue (симптом); фикс сформулирован иначе (fix(#N): …), по словам далёк — это и делает задачу held-out.

Две руки, 300 запросов (не cherry-pick):

корпус: 4299 узлов (1993 issue + 2306 PR), 1684 gold-пары issue→PR
прогнано: 300 запросов-симптомов

(a) семантика-только (обычный recall):   chain-hit  38.0%   ← косинус сам достаёт фикс
(b) причинный recall (обход графа):      chain-hit  87.0%
                              вклад графа:  +49 процентных пунктов

Воспроизводимо (скрипт demo/causal-recall-eval.mjs в репозитории):

vecmory ingest --repo <owner/name> --dsn <dsn> --schema mem   # наполнить граф из тикетов
DSN=<dsn> SCHEMA=mem REPO=<owner/name> EVAL_MAX=300 
  node demo/causal-recall-eval.mjs                             # прогнать eval

Живой кейс — можно открыть и проверить. issue #4155 «задание не привязано к заказу, пустые партии сырья» (симптом) → закрывающий его PR #4157 fix(atex #4155): продолжение дробления по дням несёт «Партию сырья» головы (фикс сформулирован иначе). По тексту симптома косинус вытащит десяток похожих issue того же модуля; правильный фикс достаёт ребро followed_by, а не сходство слов.

Честная граница. Оставшиеся 13% — там, где заголовки одного модуля начинаются одинаково (общий путь файла), и обход садится на брата-issue: семантическое заякоривание на плотном корпусе неточно. Отсюда 87, а не 99. Ту же дисциплину — назвать слабое место, а не спрятать — мы применяем и здесь.


Это первая статья из трёх. Дальше:

2. «Почему мы не написали ещё один Bad CaRMa» — про то, как гипер-универсальная модель данных обычно превращается в катастрофу, и что мы сделали, чтобы наша — не превратилась.

3. «Строили ANN руками — когда это того стоило, а когда нет» — открытый разбор, зачем мы вообще написали HNSW сами и в каких случаях правильный ответ был бы «возьми pgvector и не выделывайся».

Автор: ideavi

Источник