Два Саши, одна память: откуда в диалоге взялся чужой выбор
В первом вымышленном диалоге Саша выбрал синюю кружку, во втором другой Саша — зелёную. Общая переменная вернула первому зелёную. В выполненном учебном коде ключ «диалог + сессия» сохранил синюю для первого и зелёную для второго.
Условие: выбор действует только в текущей сессии. После её завершения новая начинается без предпочтения. Я проверил чтения и записи в словаре, без нейросети и реальных покупателей. Ошибка относится к способу хранения; поведение LLM здесь не тестировалось. Ниже — полный журнал событий, по которому можно повторить проверку.
Проблема появляется ещё до генерации ответа
Представим помощника, который запоминает выбранный цвет товара между сообщениями. Для первого прототипа удобно завести одну переменную: «последнее предпочтение». На единственном разговоре она может выглядеть вполне рабочей.
Но если второй разговор пишет в ту же ячейку, значение первого заменяется. Следующее чтение возвращает последнюю запись независимо от того, кто сейчас спрашивает. Красивый текст ответа эту ошибку хранения не исправляет.
В опыте я вообще не генерировал ответы. Программа только записывала выбранную кружку и возвращала сохранённое значение. Так видно, где именно исчезла связь с разговором.
Какие границы я задал
Есть два независимых диалога — C1 и C2. В обоих отображается имя «Саша». Сначала каждый работает в своей сессии S1. Идентификатор S1 здесь уникален только внутри диалога, поэтому одинаковые номера сессий не означают один общий разговор.
Для C1 позднее начинается S2. По условиям опыта это новая сессия без автоматически перенесённого выбора. Настройка, заданная в S1, не является постоянным предпочтением аккаунта.
Я сравнил четыре способа адресовать память: одну общую ячейку, имя, идентификатор диалога и пару «диалог + сессия». Все получили одни и те же события в одном порядке. Это последовательная симуляция перемежения сообщений, а не испытание настоящей базы под параллельной нагрузкой.
Полный журнал: где изменилось значение
В строках чтения сначала указан результат общей ячейки, затем — результат хранения по паре идентификаторов. Второй совпадает с заранее заданным ожиданием на каждом чтении.
- 1. C1/S1: открыть сессию; отображаемое имя — Саша.
- 2. C1/S1: сохранить «синяя кружка».
- 3. C2/S1: открыть другую сессию; имя тоже Саша.
- 4. C2/S1: сохранить «зелёная кружка».
- 5. C1/S1, чтение: общая память — зелёная; по паре — синяя.
- 6. C2/S1, чтение: общая память — зелёная; по паре — зелёная.
- 7. C1/S1: изменить выбор на «белая кружка».
- 8. C2/S1, чтение: общая память — белая; по паре — зелёная.
- 9. C1/S1, чтение: общая память — белая; по паре — белая.
- 10. C1/S1: завершить сессию.
- 11. C1/S2: открыть новую сессию того же диалога.
- 12. C1/S2, чтение: общая память — белая; по паре — «не задано».
- 13. C2/S1, чтение: общая память — белая; по паре — зелёная.
- 14. C1/S2: сохранить «красная кружка».
- 15. C1/S2, чтение: общая память — красная; по паре — красная.
- 16. C2/S1, чтение: общая память — красная; по паре — зелёная.
По этому журналу можно повторить опыт даже вручную: взять один лист для общей ячейки и три подписанные карточки C1/S1, C2/S1, C1/S2. Каждую запись отправлять в соответствующее место, затем сравнивать чтения.
Почему имя не помогло
Отдельно прогнал вариант «храним по имени». Обе записи получили один ключ «Саша», и смешивание повторилось. Имя выглядит понятным человеку, но в наших входах оно не различает два диалога.
Здесь недостаточно проверить, что код использует словарь. Важно посмотреть, чем он адресует запись и может ли этот ключ совпасть у независимых разговоров.
При переходе на C1 и C2 чужие изменения перестали попадать в соседний диалог. Но осталась другая ошибка: на шаге 12 новая C1/S2 получила белую кружку от завершённой C1/S1. Идентификатор разговора разделил собеседников, но не разделил жизненные циклы их сессий.
Новая сессия — отдельное решение
Пара C1/S2 не существовала в хранилище до её первой записи. Поэтому чтение на шаге 12 возвращает состояние «не задано». Программа не подставляет ни белую кружку из прошлого разговора, ни зелёную из соседнего.
После явного выбора красной на шаге 14 она сохраняется до следующего чтения в C1/S2. При этом C2/S1 продолжает возвращать зелёную. Это важная положительная проверка: память действительно удерживает нужное значение внутри своей области, пока рядом происходят изменения.
В учебном коде завершение помечает сессию закрытой, но не стирает её запись физически. Новый ключ обеспечивает отсутствие автоматического переноса. Удаление старых данных этим опытом не проверено.
Заполненная карточка приёмки
- Что храним: выбор кружки только для текущего диалога и сессии.
- Ключ: пара conversation_id и session_id, например C2/S1. Имя «Саша» остаётся подписью.
- Разрешённое изменение: запись C1/S1 меняет только C1/S1; после выбора белой соседний C2/S1 остаётся зелёным.
- Новая сессия: C1/S2 сначала возвращает «не задано», затем сохраняет явно выбранную красную.
- Проверка сохранения: C2/S1 возвращает зелёную на шагах 6, 8, 13 и 16, несмотря на изменения в C1.
- Критерий ошибки: хотя бы одно чтение получило значение другой области либо перенесло выбор вопреки условию новой сессии.
Идентификаторы в этом опыте заданы как доверенные входы. Проверка прав доступа, подделки ID, одновременных записей в одну сессию, восстановления после перезапуска и хранения в настоящей базе сюда не входила.
Если вашему продукту нужны постоянные предпочтения пользователя, задайте для них отдельную область и явное правило переноса. Тогда ожидание для новой сессии будет другим. Главное — определить его до проверки, чтобы старое случайно оставшееся значение не стало «задуманной памятью» задним числом.
На чём держится память вашего помощника: на имени, диалоге или явно определённой сессии?
Подписывайтесь, если полезны такие разборы: показываю небольшие ошибки в автоматизации через полный журнал и понятный критерий исправления.