Разбираем реальный баг с async with в Python-бенчмарке: как найти, исправить и не наступить на грабли. Читайте, применяйте!
Вы когда-нибудь запускали чужой код и получали TypeError ещё до первого запроса? Мы — да, и это стало отличным уроком. Разберём реальный баг в публичном бенчмарке Agent Memory Leaderboard: как он возник, как мы его нашли и исправили. Вы узнаете, почему async with не дружит с обычными файлами, и как избежать подобных граблей в своём коде.
Мы разрабатываем систему памяти для ИИ-агентов и решили участвовать в открытом бенчмарке Agent Memory Leaderboard, где соревнуются Tencent, Mem0 и другие. По нашему правилу: перед отправкой результатов мы всегда запускаем официальный пайплайн оценки, а не полагаемся на собственную имитацию. И вот — пайплайн упал на первой же команде, даже не сделав ни одного API-вызова.
async with и синхронный файлВ трёх из пяти публичных пайплайнов бенчмарка встречается такой код:
async with httpx.AsyncClient(timeout=120) as client, \
output.open("a", encoding="utf-8") as handle:
for item in items:
...В многоэлементном async with каждый контекстный менеджер должен быть асинхронным. Но Path.open() возвращает обычный синхронный файловый объект — у него нет __aenter__ и __aexit__. Поэтому Python гарантированно выбрасывает ошибку:
TypeError: '_io.TextIOWrapper' object does not support the asynchronous context manager protocolЭто не особенность окружения, а гарантия на уровне языка: такой код в принципе не мог работать. Ошибка возникает в функциях answer() и evaluate() в пайплайнах locomo-refined, beam и longmemeval-s. Два других набора тестов не используют этот паттерн и работают нормально.
Прежде чем обвинять чужой код, мы проверили тремя независимыми способами:
После этого мы создали issue с минимальным воспроизведением и предложили фикс.
Решение простое: разделяем составной оператор на вложенные менеджеры — асинхронный клиент снаружи, синхронный файл внутри. Семантика не меняется, только переиндентация тела:
# было — падает до первого запроса
async with httpx.AsyncClient(timeout=120) as client, \
output.open("a", encoding="utf-8") as handle:
for item in items:
...
# стало — работает
async with httpx.AsyncClient(timeout=120) as client:
with output.open("a", encoding="utf-8") as handle:
for item in items:
...Мы исправили шесть блоков в трёх файлах. Изменения: +67/−61 строк, поведение не изменилось, кроме того, что код теперь вообще выполняется.
Мы не просто заменили строки, а проверили:
py_compile.locomo-refined выполнил этап ответов и этап оценки на реальных данных и выдал вердикт.beam, где блоки включают многострочные вызовы — каждая строка сдвинута ровно на один уровень отступа, и ничего больше не изменилось.Весь смысл публичного бенчмарка в том, что каждый может его запустить. Лидерборд, чьи пайплайны падают из коробки, вредит всем участникам, включая тех, кого мы хотим обогнать. Наше правило простое: мы не соревнуемся с теми, кому не готовы помочь. И здесь есть эгоистичная выгода: запустив их настоящий пайплайн (через драйвер с чистым импортом, пока фикс ждёт апстрима), мы уверены в своих локальных результатах. Если бы мы молча обошли падение и никому не сказали, каждое наше сравнение имело бы звёздочку, которую никто другой не увидел бы.
Если вы работаете с асинхронным Python, запомните: в async with можно использовать только асинхронные контекстные менеджеры. Для обычных файлов используйте синхронный with внутри асинхронного блока. И всегда запускайте чужой код перед тем, как на него полагаться — даже если это официальный бенчмарк. А если нашли баг — не стесняйтесь предлагать фикс, это плюс и вам, и сообществу.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →