Как проверить память ИИ-агента на достоверность: скрипт на Python находит устаревшие факты. Узнайте, как избежать ложных срабатываний и улучшить надёжность.
Мой кодинг-агент хранит память в плоских markdown-файлах: один факт на файл, заголовок в frontmatter, индекс загружается в начале каждой сессии. Через две недели у меня накопилось 90 файлов и 25 сессий. Каждый файл содержит утверждения о моей машине: пути, команды, учётные данные. Утверждения устаревают, и формат не сообщает, когда это происходит. Поэтому я написал скрипт, который проверяет каждое утверждение на соответствие текущему состоянию системы. Первый запуск показал: 45 из 90 файлов — «гнилые». Пятьдесят процентов. Это число было неверным, и именно это оказалось интересным.
Не всё в файле памяти поддаётся проверке. «Пользователь предпочитает короткие ответы» — не фальсифицируемо для скрипта. Но удивительно многое можно проверить:
os.path.exists;shutil.which;Это неплохая сеть. Проблема в том, что она ловит лишнее.
Мой первый проход извлекал любой токен в кавычках и проверял, есть ли он в PATH:
RE_CMD = re.compile(r'`([a-z][a-z0-9_.-]{1,20})(?: [^`]*)?`')Файлы памяти пишутся агентом для агента, и кавычки используются как курсив в прозе. Аудитор сообщил о пропавших командах:
devto-comment-drafting-rules.md
missing commands: comprehensive
missing commands: robust
missing commands: leverage
missing commands: webdev
missing commands: fastapiЭто не команды. comprehensive и robust — слова, которые файл запрещает мне писать. webdev и fastapi — теги. Файл о фронтенд-геометрии дал h-11 и h-7 — это классы Tailwind. Исправление: не считать одиночное слово командой. Команда — это имя плюс аргумент, и имя должно быть либо известным CLI, либо содержать флаг:
RE_CMD = re.compile(r'`([a-z][a-z0-9_-]{1,20})\s+([^`]{1,60})`')
if c not in KNOWN_CLI and not re.match(r'-{1,2}[a-z]', rest.strip()):
continueЖёсткий список допустимых команд кажется читерством. Но альтернатива — инструмент, который сообщает sql duplicate как отсутствующий бинарник, а такой инструмент игнорируют через день.
Проверка путей ловила всё, что начинается со слэша. Файлы памяти полны API-маршрутов:
datadog-key-hidden-consumer-log-drain.md
dead paths: /health
dead paths: /v2/integrations/log-drainsНи то, ни другое не файл. Это эндпоинты на чужом сервере, и os.path.exists всегда скажет «нет». Я привязал паттерн к корням, существующим на этой машине:
RE_PATH = re.compile(r'`(~/[^`\s]{2,}|' +
r'/(?:Users|Applications|Volumes|opt|etc|usr|private|var|Library)/[^`\s]{2,})`')Относительные пути я полностью исключил. ./scripts/gate.sh был помечен как мёртвый, но файл существует. Он лежит в директории проекта, а аудитор разрешал его относительно своей рабочей директории. Относительный путь — это утверждение о контексте, которого у проверяющего нет, поэтому его нельзя проверить. Догадка даёт ровно один результат — уверенный неверный ответ.
Проверка дат помечала всё, что в прошлом. Но файл памяти может говорить о прошлом. Заметка о том, что что-то случилось 6 августа, корректна вечно. Заметка о том, что документ действителен до 6 августа, теряет смысл после этой даты. Одна и та же дата, противоположный смысл, и разница — в слове перед ней:
RE_DEADLINE = re.compile(r'(?:expires?(?:\s+on)?|valid\s+(?:until|through)|until|deadline|' +
r'renew\w*\s+by)\s*(\d{2}\.\d{2}\.20\d{2})', re.I)Я также исключил эти случаи из общего счётчика гнили. Истёкший срок годности — не ложное утверждение, это утверждение, которое перестало быть действенным. Смешение категорий раздувает пугающее число, а оно и так наносило достаточно вреда.
После трёх исправлений: 4 файла из 90.
ROTTEN: 4/90 files (4%)
------------------------------------------
dead paths: 3 file(s)
gone from 1Password: 1 file(s)Проблема с учётными данными выглядела серьёзно. Файл о секретах проекта ссылался на идентификатор, и менеджер паролей сказал, что такого элемента нет. Я проверил вручную, прежде чем поверить. Идентификатор называет хранилище, а не элемент. Хранилище живо и содержит шесть учётных записей. Мой скрипт спрашивал op item get, получал ошибку и делал вывод, что объект исчез, тогда как правильный вопрос — разрешается ли идентификатор как объект любого типа:
ok = False
for sub in (['item', 'get', i], ['vault', 'get', i]):
r = subprocess.run(['op'] + sub + ['--format=json'], capture_output=True, text=True)
if r.returncode == 0:
ok = True
breakТри реальные находки из девяноста файлов: две директории, которых больше нет, и путь к приложению с ошибкой в слове — /Applications/Telegram.app, тогда как на диске Telegram Desktop.app.
Ранее в тот же день я столкнулся с классической гнилью. Сохранённая инструкция говорила, что ключ лежит в конкретном хранилище под конкретным идентификатором. Я выполнил и получил:
could not find item 5wdv... in vault Private,
because it has been deleted or archived«Удалено или заархивировано». Я прочитал как «удалено», списал запись как устаревшую и пошёл искать, куда переехал ключ. Запись никогда не удалялась. Она заархивирована, всё ещё в том хранилище, переименована, чтобы отметить замену. op item get извлекает её по идентификатору без проблем. op read отказывается разрешать путь к архивированному элементу и сообщает об отказе через «или», содержащее правильный и неправильный ответы с равным весом. Моя память не была устаревшей. Устарел я, потому что доверился сообщению об ошибке, которое сказало две вещи и позволило мне выбрать.
Измеримая часть подтвердилась. Индекс, загружаемый в каждой сессии, занимает 2460 токенов на 89 записей — это 27 токенов постоянных накладных расходов на каждый запомненный факт. Это реальная цена файловой памяти агента, и она линейна. Пятьсот фактов — примерно четырнадцать тысяч токенов, потраченных до того, как кто-то поздоровается. Продавцы векторных хранилищ называют цифры в этом диапазоне, и на моих данных они не преувеличивают.
Структура оказалась здоровее, чем я ожидал для собранной за две недели: 180 ссылок между записями, ноль битых, ноль сирот. Так что заголовок, который я собирался написать — «файловая память тихо гниёт» — не подтверждается моей собственной машиной. За две недели она почти не сгнила.
Проверяльщик, который помечает половину файлов, не улучшают — его затыкают. Стоимость ложного срабатывания — не один потраченный взгляд, а доверие ко всем последующим находкам, включая настоящие, лежащие на три строки ниже. Я снизил процент с 50% до 3%, заставив инструмент отказываться от догадок: голое слово не команда, слэш не файл, если он не начинается с реального корня, относительный путь не разрешается относительно выдуманной директории, дата не дедлайн без слова, говорящего об этом. Каждое из этих решений — мой выбор: сообщить ничего лучше, чем сообщить что-то, похожее на ответ.
И последнее ложное срабатывание не поддалось лучшему регулярному выражению. Оно пало, потому что я проверил находку вручную и обнаружил, что мой собственный скрипт ошибается. Если вы строите такой инструмент, заложите бюджет на этот шаг. Аудитор — это утверждение о ваших утверждениях, и он гниёт так же.
Скрипт — 260 строк стандартной библиотеки Python, без зависимостей. Укажите ему директорию памяти:
memrot.py ~/.claude/projects/<project>/memory --opФлаг --op специфичен для 1Password и выполняет только чтение. Уберите его — остальное продолжит работать. Некоторые имена файлов в выводе выше изменены: память содержит личные записи, и важна была структура, а не содержимое.
Прямо сейчас: возьмите свою память агента, запустите подобную проверку и посмотрите, какие утверждения устарели. Начните с путей и команд — это самое простое и часто самое полезное. И помните: если проверка находит слишком много, сначала исправьте проверку, а не данные.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →