Память ИИ между чатами: как мы ищем решения и историю работ на MySQL
«Что агент MikroTik делал с роутерами в июле?»
Это вопрос, ради которого мы доработали память своего Codex. MikroTik здесь имя специализированного чата, которому мы поручаем задачи по RouterOS. В текущей документации можно найти, как сеть устроена сейчас. В переписке за июль могли остаться обсуждение, отменённый план и отчёт о выполненной работе. Если смешать их, получится убедительная история, в которой запланированное изменение уже выглядит выполненным.
Нам нужен был ответ с датами и источниками: кто сообщил о работе, что именно сообщил и чем это можно проверить. Для этого мы связали канонические документы, индекс MySQL, отчёты специализированных чатов и архивы переписки. Ниже описана наша действующая реализация на 2 октября 2026 года. Примеры запросов не содержат частных адресов, ключей или выгрузок из рабочих чатов.
Что у нас называется памятью
Новый чат получает доступ к внешним инструментам поиска. Мы не дообучаем модель после каждой задачи и не рассчитываем, что она сама вспомнит прошлую переписку. Нужные сведения находятся вне модели, а в её контекст попадает ограниченная выборка со ссылками на источники.
У этой системы несколько разных хранилищ.
Где лежат сведения: Канонические Markdown-документы
Что мы там храним: Архитектуру, действующие решения, инструкции, актуальный статус
Для чего читаем: Выяснить, что принято и как система должна работать
Где лежат сведения: MySQL, у нас Percona Server 8.0
Что мы там храним: Структурированные факты, связи, решения, проверки и индекс документов
Для чего читаем: Найти нужный источник и собрать короткий начальный контекст
Где лежат сведения: События координатора задач
Что мы там храним: Назначения специалистам и их итоговые отчёты
Для чего читаем: Найти сообщения о работе по владельцу и времени
Где лежат сведения: Локальные записи сессий и отдельный архив
Что мы там храним: Сохранившуюся переписку Codex
Для чего читаем: Разобрать старый эпизод, которого нет в структурированных отчётах
Где лежат сведения: Таблица activity_events
Что мы там храним: Отобранные краткие записи со ссылкой и отпечатком источника
Для чего читаем: Сохранить указатель на исторический отчёт без копирования его тела
Полные переписки не переезжают в таблицу активности. Архив хранится отдельно; мы открываем нужные сессии ограниченными выборками. События координатора уже находятся в MySQL, поэтому повторно импортировать их в ещё один журнал не требуется.
Канонические документы остаются основой для действующих решений. Индекс помогает их найти. Исторический отчёт объясняет, что автор сообщил в определённый момент, но не отменяет более позднего решения и не подтверждает состояние оборудования сегодня.
Например, запись «сервис работает» имеет смысл вместе с датой проверки. Решение «этот способ развёртывания отменён» должно сохраняться дольше одной задачи, иначе следующий чат может добросовестно возобновить отменённую работу.
Как новый чат получает нужный контекст
Для начала задачи мы используем инструмент memory_task_context. Он принимает владельца задачи, поисковый запрос и бюджет ответа.
Внутри выполняется поиск по структурированным фактам и индексу исходников. Для документов мы сохраняем отпечаток содержимого, сведения о версии и времени индексации; текст разбиваем на небольшие фрагменты. В ответ входят подходящие факты, выдержки из источников и предупреждения об устаревании.
Мы доработали ранжирование под рабочие вопросы. Канонический статус и инструкция должны находиться раньше случайного упоминания в тесте или каталоге. Подсказка о владельце помогает выбрать документы его области. Контролируемые русско-английские соответствия позволяют искать, например, материалы про backup по слову «бэкап».
Это словарное расширение и ранжирование, а не понимание всех возможных формулировок. Подсказка о владельце также не является разграничением доступа: права на инструмент и источники задаются отдельно.
В начальном пакете мы оставляем по одному результату на документ. Иначе три соседних фрагмента одной инструкции могут занять место трёх разных полезных источников. При нехватке бюджета сначала сокращаем выдержки. Ссылка, версия и предупреждение об устаревании важнее ещё одного абзаца текста.
Вызовы доступны через MCP. Протокол позволяет приложению обнаруживать инструменты сервера и вызывать их с описанными аргументами; правила отбора источников и пределы ответа реализованы уже в нашем сервере памяти. Это разделение соответствует архитектуре MCP.
В результате модель получает исходную точку для работы. Она может открыть конкретный документ, проверить нужный участок кода или запросить разрешённую проверку текущего состояния. В пакете явно остаётся признак live_check_required: найденный контекст не освобождает от проверки перед изменением системы.
«Что делали в июле» требует другого поиска
Поиск действующей инструкции и поиск прошлых действий отвечают на разные вопросы. Для истории мы добавили memory_activity: точный владелец, начальная и конечная даты, часовой пояс и тип записи.
Вот параметры вызова для нашего июльского примера. Здесь месяц намеренно задан по UTC. Для календаря в другом часовом поясе нужно передать его явно.
Параметры вызова memory_activity · JSON
JSON
{
"name": "memory_activity",
"arguments": {
"owner": "MikroTik",
"start": "2026-07-01",
"end": "2026-07-31",
"timezone": "UTC",
"kind": "all",
"limit": 50
}
}
Инструмент читает отобранные записи активности и итоговые события координатора. Для отчёта координатора проверяется связь с назначенной задачей: отчёт должен относиться к специализированному заданию, назначенному тому же автору. Одного упоминания MikroTik в заголовке чужой задачи недостаточно.
Если подходящих записей нет, возможен ограниченный поиск по сохранившимся локальным сессиям точного владельца. Полный поиск по локальной истории и архиву выполняется отдельным инструментом истории. Обычный вызов памяти не скачивает архив незаметно для вызывающей стороны.
Результаты различаются по типу:
Тип: reported_change
Как читать запись: Автор сообщил об изменении. Нужен исходный отчёт, если важны подробности и подтверждение
Тип: read_only
Как читать запись: В отчёте указана работа без изменений, например диагностика
Тип: discussion
Как читать запись: Найден материал обсуждения; выполненное действие из него не установлено
Отдельное поле хранит результат отчёта: reported_complete, reported_stop или unknown. «Сообщил о завершении» не равняется «независимо проверено». Классификация консервативная и основана на тексте и метаданных отчёта, а не на наблюдении за каждой командой специалиста.
Когда мы проверили сохранившиеся июльские материалы MikroTik, поиск по локальной истории и архиву сработал. Но старые сообщения вернулись кандидатами обсуждения: оснований объявить их структурированными отчётами о выполненных изменениях не было. Мы не стали задним числом заполнять журнал предполагаемыми действиями.
Для такого запроса полезный ответ выглядит так: «Нашёл обсуждения за выбранный период. Выполнение по этим записям не установлено; вот источники для разбора». Дальше можно открыть ограниченную выборку сообщений конкретной сессии, найти явный итог и сопоставить его с сохранившейся документацией. При необходимости проверить нынешнюю конфигурацию, но не выдавать сегодняшнее состояние за доказательство того, что изменение сделали именно в июле.
Что находится в записи активности
В activity_events входят владелец, время, тип, результат, короткий заголовок, ссылка на источник и его SHA-256. В таблице нет тела переписки, полей полного отчёта или конфигураций оборудования. Для повторного импорта используется стабильный ключ события, чтобы одна запись не размножалась при повторной обработке.
Поиск по периоду опирается на составной индекс (owner_chat, occurred_at, event_key). Сначала мы ограничиваем владельца, затем диапазон времени. Такой порядок соответствует использованию левого префикса составных индексов MySQL. Конкретный план выполнения всё равно следует проверять на своих данных.
Даты интерфейса включают оба выбранных календарных дня. Код переводит их в UTC и строит полуоткрытый интервал: от начала первого дня до начала дня после последнего. Это избавляет от угадывания последней секунды месяца.
У DATETIME нет автоматического преобразования часового пояса, которое MySQL выполняет для TIMESTAMP. Поэтому хранение UTC в нашем DATETIME(6) является договорённостью приложения, а не свойством типа, которое само исправит переданные локальные значения. Различие описано в документации MySQL о датах и времени.
Ниже показана только выборка из отобранной таблицы активности. Это иллюстрация SQL для июля по UTC, не полный код memory_activity: в нём дополнительно читаются события координатора и, при отсутствии записей, локальные кандидаты. Запрос имеет смысл в схеме с описанной таблицей; сам он ничего не создаёт и не меняет в ней.
Выборка отобранных событий за июль · SQL
SQL
SELECT occurred_at, kind, result, title, source_ref
FROM activity_events
WHERE owner_chat = 'MikroTik'
AND occurred_at >= '2026-07-01 00:00:00'
AND occurred_at < '2026-08-01 00:00:00'
ORDER BY occurred_at, event_key
LIMIT 50;
Отпечаток источника помогает заметить изменение или сопоставить сохранённую запись с исходным материалом. Он не доказывает правдивость отчёта, полномочия автора или успешность выполненной операции. Для этого нужны другие свидетельства.
Что действительно ускорилось
После доработки мы сравнили старый и новый поиск на двадцати фиксированных вопросах. Прогоны выполнялись последовательно на одном хосте и одном индексе. Для каждого вопроса заранее был определён ожидаемый источник.
Показатель: Ожидаемый источник среди первых трёх результатов
До: 10 из 20
После: 20 из 20
Показатель: Медиана первого поиска без готового пакета в кэше
До: 339,915 мс
После: 325,05 мс
Показатель: Медиана повторного поиска
До: 344,23 мс
После: 115,63 мс
Первый поиск стал лишь немного быстрее. Заметный выигрыш получили при повторном запросе: в этом прогоне медиана уменьшилась примерно втрое. Для нас не менее важно, что нужный источник перестал теряться за менее полезными совпадениями.
Кэш находится в памяти процесса MCP-сервера. Он ограничен 128 записями с временем жизни 60 секунд; ключ включает владельца, запрос и бюджет. Перед использованием готового пакета сервер всё равно обращается к MySQL и сравнивает ревизию индекса и фактов, включая состояние их срока действия.
При промахе мы объединяем получение ревизии, фактов и поисковых результатов в одну транзакцию чтения. Это сокращает отдельные обращения. Существующий клиент MySQL остаётся в пути запроса; новый постоянный пул соединений мы здесь не вводили.
Проверка ревизии относится к уже обновлённому индексу. Если Markdown-файл изменился на диске, а переиндексация ещё не прошла, кэш не узнает об этом сам. Плановое обновление индекса у нас идёт раз в 30 минут; для важных изменений его можно обновить отдельно. Короткий TTL не превращает индекс в мониторинг файлов или оборудования.
Максимальный новый пакет в тестовом наборе составил 796 приблизительных токенов. Бюджет начального контекста ограничен 800 такими единицами. Оценка основана на длине JSON, а не на токенизаторе конкретной модели: это инженерный ограничитель объёма, не точный расчёт стоимости.
Эти измерения проверяют поиск источников и задержку инструмента. Двадцать вопросов не доказывают универсальное качество поиска, а попадание нужного документа в выдачу ещё не измеряет правильность ответа LLM. Общий контекст разговора тоже может быть гораздо больше начального пакета.
Почему мы не отправляем модели весь архив
Переписка содержит варианты, которые никогда не применяли, старые проверки и более поздние отмены. Передача всего архива увеличивает объём чтения, но сама по себе не определяет, какое решение действует.
Есть и исследовательское основание проверять, как модель использует длинный контекст. В работе Lost in the Middle, опубликованной в 2023 году, Liu и соавторы изучали поиск значений и ответы по нескольким документам. У исследованных моделей результат зависел от положения нужной информации: середина длинного контекста часто оказывалась менее удачным местом, чем его начало или конец.
Это результат тех экспериментов, не характеристика всех нынешних моделей. Он не доказывает наш выбор бюджета в 800 единиц. Для нашей системы практический вывод скромнее: объём доступного контекста нельзя принимать за готовую проверку качества извлечения. Нужно отдельно измерять, найдены ли нужные источники и правильно ли они использованы в ответе.
Поэтому история читается как данные. Старая фраза «перезапусти сервис» не получает статус нового поручения только потому, что её нашли в июльском чате. Фильтрация источников и ограничения чтения уменьшают ненужный контекст, но не являются полной защитой от инструкций, спрятанных в содержимом.
Где память заканчивается
Указатель на источник полезен ровно настолько, насколько понятно его происхождение и покрытие поиска. Мы сохраняем это различие в нескольких местах.
Историческая проверка может уже устареть. Противоречащие документы требуют разбора версий и действующих решений; система не обещает автоматически разрешить любой конфликт. Отчёт специалиста остаётся сообщением специалиста, даже если в нём написано complete.
Пустая выдача также не означает, что работы не было. Чат могли переименовать, часть архива могла не сохраниться, старый итог могли написать в свободной форме. В ответе есть сведения о покрытии и усечении. Например, при превышении допустимого числа сопоставленных с владельцем сессий поиск останавливается с явным ограничением, а не молча выбирает первые двадцать.
Индекс документов можно перестроить из исходников. Исторические указатели требуют другого отношения: после потери исходной переписки они могут оказаться последним сохранившимся свидетельством. Поэтому автоматического удаления таких записей мы не вводили; срок хранения и удаление должны быть отдельным решением.
Для ответа на вопрос о прошлой работе мы теперь требуем дату, владельца, тип записи и источник. Если выполнение не установлено, так и пишем. Если важно состояние системы сейчас, делаем разрешённую текущую проверку и показываем её отдельно от истории.
Так вопрос «что делали в июле» становится проверяемым: можно открыть запись, отличить план от отчёта и увидеть пробелы. Именно этого нам не хватало больше, чем ещё одного уверенного пересказа старого чата.
Те же примеры JSON и SQL с подсветкой синтаксиса доступны в статье.
Больше технических разборов публикую на сайте.













