Главная / Статьи / Почему память чата находит лишь 25% фактов и как починить поиск

Почему память чата находит лишь 25% фактов и как починить поиск

Реальное фриланс-задание: система памяти с идеальным хранением, но полнотой поиска 25%. Переписывание запросов, HyDE, реранкинг и многошаговый поиск, плюс совет — чинить или менять.

987 слов

Память чата, которая держится в пределах одной сессии, а потом ничего не помнит о том, что было больше нескольких месяцев назад, — картина, которую в приложениях-компаньонах на базе ИИ видишь снова и снова. Фриланс-заказ, опубликованный на Upwork, превращает эту размытую жалобу в измерения, поэтому разбирать его полезнее любого выдуманного примера.

Цифры клиента

Собственный подсчёт клиента: примерно 5000 сохранённых сообщений, 300 эпизодических событий и около 3500 семантических выводов. Они выбрали восемь конкретных фактов, о которых пользователь говорил ещё в июне, и спросили о них так, как спросил бы реальный пользователь сегодня. Вернулись только два, то есть 25% попаданий, хотя каждое исходное сообщение по-прежнему хранилось слово в слово. При записи воспоминаний ничего не теряется.

К последнему утверждению стоит отнестись серьёзно. Что бы ни ломалось, хранилище тут ни при чём.

И это был не прототип, собранный за выходные. Стек работал на Postgres с pgvector, поиск шёл в два этапа с реранкером, а оркестрация и код извлечения были написаны под заказ. Система хранила несколько видов памяти (эпизодические события, семантические выводы, реестр людей из жизни пользователя и процедурный стиль), и у каждого был свой маршрут поиска. Каждый ход логировался: что было найдено, что отброшено и что в итоге попало в модель. Команда также прогнала mem0 на 120 своих сообщениях и держала экземпляр Hindsight в теневом режиме. Разумные эксперименты, а полнота поиска так и осталась на уровне 25%.

Где на самом деле ломается

Одна проверка делает проблему очевидной. Введите "какие слова использует мой папа", и система вернёт прошлые сообщения, в которых пользователь жалуется на отца. Единственное сообщение, где тот процитирован, так и не появляется.

Это не причуда конкретного кода. Поиск по сходству эмбеддингов в логе чата ведёт себя именно так, потому что ближе всего к вопросу почти всегда оказывается другой вопрос. Запрос и ответ на него живут в разных районах одного векторного пространства, и косинусное сходство не может понять, что они связаны. Так что с хранилищем памяти всё в порядке. Слабое место — в оркестрации, то есть в том, что отбирается, ранжируется и упаковывается в контекст в момент генерации.

Четыре исправления по возрастанию усилий

Переписывайте запрос. Не стройте эмбеддинг по исходному вопросу. Пусть LLM сгенерирует несколько формулировок в том виде, в каком выглядел бы ответ: точные фразы, которые говорит отец пользователя, слова, которыми он его называет, прямые цитаты с его авторством. Тогда поиск нацелен на то, как выглядит ответ, а не на то, как выглядит вопрос.

HyDE. Гао и соавторы в 2022 году предложили пойти ещё дальше. Не превращайте вопрос в поисковые термины, а попросите LLM написать гипотетический ответ и постройте эмбеддинг по нему. Придуманный ответ оказывается гораздо ближе к настоящему, чем сам вопрос. Для именно этого сбоя это, пожалуй, самое прямое лекарство.

Реранжируйте не только по сходству. Когда пул кандидатов уже собран, сортировка только по смысловой близости воспроизводит исходную ошибку в меньшем масштабе. Оценивайте совпадение сущностей, вероятность того, что фрагмент является ответом, соседство с ходом, который правдоподобно его содержит, и временной контекст. Но помните о потолке: реранкер лишь переставляет то, что ему отдал поиск, и не может вытащить то, что так и не всплыло.

Ищите в несколько шагов. Один проход справляется с вопросами об одном факте. На агрегирующих он ломается. Ответ на вопрос вроде "какие прозвища давал мне папа" разбросан по пяти сообщениям за три месяца. Чтобы с этим справиться, нужен широкий первый поиск, группировка кандидатов по типу, проверка, достаточно ли собрано, и ещё один поиск, если нет. Это та же схема «найти, подумать, решить, искать ли снова», что и у Self-RAG и Corrective RAG (CRAG). Дайте ей жёсткую остановку — ограничение числа шагов или проверку уверенности, — иначе она будет крутиться вечно или сделает каждый ответ дорогим.

Минимальный цикл выглядит так:

def retrieve_memory(user_query, vector_db, llm, max_hops=2):
    rewrites = llm.rewrite(
        query=user_query,
        instruction=(
            "Turn this question into 3-5 retrieval-oriented phrasings "
            "that describe what the ANSWER would say, not what the "
            "question says."
        ),
    )

    candidates = []

    for q in rewrites:
        candidates += vector_db.search(q, top_k=20)
    ranked = []
    for hop in range(max_hops):
        ranked = rerank(
            user_query,
            candidates,
            factors=[
                "semantic_relevance",
                "entity_match",
                "answer_likelihood",
                "conversational_relationship",
                "temporal_context",
            ],
        )
        if llm.is_sufficient(user_query, ranked[:10]):
            break
        follow_up = llm.next_query(user_query, ranked[:10])
        candidates += vector_db.search(follow_up, top_k=20)
    return ranked[:10]

Ничего экзотического тут нет. Важна последовательность: сначала почините полноту поиска и построение запроса, потом реранкинг, и только после этого думайте о новой инфраструктуре.

Совет клиенту

Запрос на аудит состоял из трёх частей: где память утекает между хранением и генерацией, выдержит ли архитектура в десять раз больше данных и честная рекомендация — чинить то, что есть, или переходить на что-то новое.

Где утекает. Между поиском и генерацией, а точнее — на этапе построения запроса, ещё до того, как реранкинг вообще может помочь.

При 10-кратном росте. Архитектура с несколькими типами памяти и логирование надёжны и будут масштабироваться. Однопроходный поиск по эмбеддингам — нет, потому что с ростом данных за те же места среди кандидатов конкурирует всё больше похожих вопросов. Слои переписывания и многошагового поиска превратятся из желательных в обязательные задолго до этой отметки.

Чинить или менять. Чинить своё. mem0 и Hindsight — оба слои хранения памяти. Ни один не касается оркестрации, а именно там и пропадают недостающие 75%. Смена базы данных не поможет запросу, который по-прежнему ищет не то.

Иногда система памяти вовсе ничего не забывает. Она отвечает на вопрос, которого никто не задавал, потому что запрос и доказательства с самого начала искали разное.