Разбираем 9 скрытых багов в бенчмарке A2A на Python, Go, Node и Rust. Узнайте, как одна строчка кода искажала результаты годами. Исправьте свои тесты прямо сейчас.
Представьте: вы запускаете бенчмарк, видите красивый график и уверены, что всё работает идеально. Но однажды вы замечаете странность — тест останавливается на N=22, хотя должен идти до 24. Вы копаете глубже и находите 9 багов, которые делали каждый график неверным. В этой статье мы разберём, как одна строчка кода может скрывать проблему годами, и научимся писать бенчмарки, которым можно доверять.
В репозитории a2a-benchmark лежал файл run_benchmark_1_22.py. Не 1_24, как ожидалось по логике теста, и не 1_26 по количеству известных экспонент Мерсенна. Просто 22. И подтверждающий график — a2a_latency_times_1_22.png. Кто-то решил, что бенчмарк должен заканчиваться на 22, зафиксировал это в коде и забыл.
Но истинная причина оказалась прозаичнее: краш. При N=24 Python-агент падал с ошибкой:
"Exceeds the limit (4300 digits) for integer string conversion;
use sys.set_int_max_str_digits() to increase the limit"
CPython 3.11 ввёл ограничение на преобразование int в str — 4300 цифр — как защиту от DoS-атак. А 24-е простое Мерсенна, 2^19937−1, содержит 6002 цифры. Агент преобразовывал каждое найденное простое в строку — и падал.
Самое смешное: эта строчка была декоративной. Результат преобразования нигде не использовался — агент отчитывался только о времени выполнения. Исправление заняло одну секунду: удалить str() и оставить сырое число. Go-версия имела точно такой же мёртвый код (val.String()), но ей просто повезло не упасть.
После удаления одной строчки появился столбец данных, которого никогда не существовало: N=24, Python, 2425.9 мс.
Харнес извлекал время выполнения Python из текста ответа LLM с помощью регулярного выражения r"It took ([\d\.\-e]+) seconds". Но Gemini в моих логах ни разу не сказала "It took" — она писала "Calculating the first 5 Mersenne primes took…" или "The calculation took…". Единственная причина, по которой бенчмарк вообще имел данные по Python, — fallback на чтение структурированного артефакта инструмента. Основной путь измерения был ставкой на речевые привычки языковой модели.
Node и Rust-агенты при запросе 100 простых чисел радостно отвечали "Found first 100 Mersenne primes", хотя на самом деле вычисляли только 26 — размер таблицы экспонент. А время они форматировали как %.2f ms, поэтому быстрейшие запуски Rust показывали 0.00ms, что на логарифмической шкале выглядело как отсутствие данных, а не ошибка.
График назывался "A2A Round-Trip Time (including LLM/Tool calling)", но два из четырёх агентов (Python и Go) шли через Gemini, а два других (Node и Rust) — напрямую через HTTP. Медианное время: ~2.6 мс и ~4.6 мс для прямых, ~1.6 с и ~1.8 с для Gemini. Разрыв в 400 раз был не сравнением языков, а сравнением архитектур.
После исправления Go-агент на N=1 отработал так быстро, что time.Duration переключил единицы — "Elapsed time: 836ns". А харнес умел парсить только µs, ms и s. Наносекунд не было. Исправление сделало код слишком быстрым для собственного бенчмарка.
После повторного прогона пропали три точки данных Go. Причина: харнес переиспользовал идентификаторы контекста, ADK хранит историю сессии, и Gemini, увидев старый разговор, отказался повторять работу: "I already did that. Do you want to do it again?" Зависимость бенчмарка от мнения LLM о повторении — это новый уровень хрупкости. Уникальные ID на каждый запуск решили проблему, и финальный прогон вернул 96/96 точек.
run_benchmark_1_22.py лежал в репозитории как окаменелость неисследованного краша. Как только вы переименовываете скрипт вместо чтения stack trace, вы решаете поставлять баг.Девять багов. Четыре PR (#1, #2, #3, #4). Один вопрос, который следовало задать год назад: почему 22?
Прямо сейчас откройте свой самый старый бенчмарк и задайте себе три вопроса:
Если ответ на любой вопрос "да" — исправьте это сегодня. Иначе ваш бенчмарк может тихо врать годами, а вы будете думать, что число 22 — это осознанное решение.
# Пример: как НЕ надо парсить время из LLM
import re
def parse_time_llm(text):
# Хрупко: зависит от формулировки LLM
match = re.search(r"It took ([\d\.\-e]+) seconds", text)
if match:
return float(match.group(1))
# fallback — тоже хрупко
return None
# Лучше: использовать структурированный артефакт
# (например, JSON-ответ инструмента)
def parse_time_structured(response):
return response.tool_output.elapsed_msХочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →