ГлавнаяБлогКак я нашел баг с распаковкой словарей в smolagents
Python

Как я нашел баг с распаковкой словарей в smolagents

Распаковка словарей (**) в smolagents вызывает ложную ошибку NoneType. Узнайте, как фаззинг помог найти и исправить баг, и что делать, чтобы избежать циклов.

Al
Редакция Algolitalgolit.ru
5 мин чтения20 июля 2026 г.

Почему стоит прочитать эту статью

Вы используете Python-агентов, которые генерируют код? Тогда вы могли столкнуться с ситуацией, когда агент зацикливается, а сообщение об ошибке указывает на несуществующий None. В этой статье я расскажу, как фаззинг помог найти скрытый баг в песочнице smolagents, связанный с распаковкой словарей, и как его исправить.

Баг: распаковка словарей ломает песочницу

Одна из самых частых операций, которую генерирует LLM — слияние словарей через **. Например:

config = {
    **{"temperature": 0.7, "max_tokens": 512},
    "top_p": 0.9
}

В smolagents этот код вызывает ошибку:

InterpreterError: NoneType is not supported

Проблема в том, что None в коде нет. Сообщение об ошибке вводит в заблуждение, заставляя искать несуществующее значение.

Почему это происходит

В абстрактном синтаксическом дереве Python (ast) литерал словаря хранит ключи и значения в двух параллельных списках. Для обычной записи ключ — это узел AST. Для распаковки (**mapping) ключ равен None — это маркер, сигнализирующий, что это не пара ключ-значение, а распаковка.

Песочница LocalPythonExecutor проходила по expression.keys и вызывала evaluate_ast(key, ...) для каждого ключа. Когда ключ оказывался None, вызов не попадал ни в одну ветку isinstance и проваливался в общий обработчик, который выбрасывал InterpreterError. Таким образом, маркер распаковки интерпретировался как выражение, и модель получала ошибку о несуществующем None.

Почему молчание дорого обходится

Агент перехватывает ошибку и отправляет модели сообщение «вот что пошло не так, попробуй снова». Но поскольку сообщение указывает на NoneType, а в коде его нет, модель не может исправить ошибку. Она повторяет тот же самый синтаксис снова и снова. Каждый шаг тратит реальный вызов LLM и слот в бюджете шагов, пока выполнение не завершится неудачей.

Один баг, одно вводящее в заблуждение сообщение, три одинаковых сбоя. Без инструмента вроде Sentry, который считает события, вы бы никогда не увидели цикл — только тихо деградирующий запуск.

Исправление

Решение заключается в том, чтобы обрабатывать словарь парами, а не слепо вычислять ключи. None в ключе означает «слить это отображение»:

result = {}
for key_node, value_node in zip(expression.keys, expression.values):
    if key_node is None:
        value = evaluate_ast(value_node, *common_params)
        if not hasattr(value, "keys"):
            raise InterpreterError(f"'{type(value).__name__}' object is not a mapping")
        result.update(value)
    else:
        key = evaluate_ast(key_node, *common_params)
        result[key] = evaluate_ast(value_node, *common_params)
return result

Это полностью соответствует поведению CPython: распаковки сливаются по порядку, более поздние ключи перезаписывают ранние, а попытка распаковать не-отображение вызывает ошибку 'list' object is not a mapping.

Ревьюер подловил меня на излишней строгости

Изначально я проверял распаковку через isinstance(value, Mapping). Но ревьюер от OpenAI Codex указал: CPython не требует наличия ABC Mapping, достаточно объекта с методом keys(). Поскольку песочница позволяет пользователям определять свои классы, утиная типизация с keys() и __getitem__() была бы несправедливо отвергнута. Я заменил проверку на hasattr(value, "keys") и добавил тест для этого случая. ИИ написал код, ИИ проверил код, я просто вёл счёт.

После исправления

На обновлённой версии тот же код просто работает:

app: step 1 ok, config = {'temperature': 0.7, 'max_tokens': 512, 'top_p': 0.9}

Один шаг, никакого цикла, никакого призрачного None.

Цифры

  • 4 неотмеченных бага найдено фаззингом валидного Python через песочницу; это первое исправление
  • Вводящая в заблуждение ошибка NoneType воспроизводится на текущем main и версии 1.26.0
  • 3 потраченных шага агента на одно вхождение, заметных только благодаря подсчёту событий в Sentry
  • 9 новых тестов: распаковки, двойные распаковки, порядок переопределения в обе стороны, класс с утиной типизацией, пустая распаковка, отвержение не-отображения
  • 406 тестов проходят, ruff чист

Практический вывод

Худшие баги агентов не бросают в вас красный стектрейс. Они передают модели правдоподобное, но неверное сообщение и позволяют ей вежливо ошибаться снова и снова. Считайте свои события. Используйте инструменты мониторинга, чтобы видеть циклы, которые иначе остались бы незамеченными. А если вы пишете песочницу для выполнения кода — не забывайте про распаковку словарей.

#smolagents#баги#песочница#фаззинг#распаковка словарей
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →