Протестировали MiniMax M2 и GigaChat-2-Max на шести реальных инженерных задачах: от простых функций до LRU-кэша и полноценного парсера арифметических выражений. Результаты отличаются кардинально: MiniMax стабилен и аккуратен в кодогенерации, GigaChat даёт сильные решения в простых задачах, но ломается на сложных структурах данных.
Введение
Обе модели — MiniMax M2 и GigaChat-2-Max — активно используются в России, и обе заявляют высокие способности к reasoning и генерации качественного кода. Но насколько они соответствуют этим заявлениям в реальной разработке?
Чтобы ответить на этот вопрос честно, мы провели серию тестов, максимально приближённых к реальным задачам Python-разработчика:
- простые функции;
- задачи со списками и множествами;
- алгоритмы скользящего окна;
- структуры данных: LRUCache с двусвязным списком;
- полноценный рекурсивный парсер арифметических выражений.
Условия были одинаковы:
- единый
system_prompt; - единый
user_prompt; - сняты ограничения по токенам (
max_tokens → 10k); temperature = 0;- оба сервиса работали в «честном» режиме без примесей в ответ.
И вот что получилось.
T1 — is_palindrome(s: str) -> bool
Задача:
Игнорировать регистр, пробелы и не-буквы; проверить, палиндром ли строка.
MiniMax — ответ
def is_palindrome(s: str) -> bool:
cleaned = ''.join(filter(str.isalpha, s.lower()))
return cleaned == cleaned[::-1]
GigaChat — ответ
import re
def is_palindrome(s: str) -> bool:
# Remove spaces, convert to lowercase, keep only alphabetic chars
s_cleaned = re.sub(r'[^a-zA-Z]', '', s.lower())
# Check if cleaned version reads the same forwards and backwards
return s_cleaned == s_cleaned[::-1]
Вывод по T1:
Обе модели решили задачу корректно. MiniMax — без импортов, через str.isalpha, Giga — через re.sub.
С точки зрения поведения — паритет, с лёгким уклоном в «попроще и чище» у MiniMax.
T2 — класс Inventory
Задача:
Реализовать класс с методами add_item, remove_item, get_quantity, total_items и корректной логикой состояния.
MiniMax — ответ
class Inventory:
def __init__(self):
self.stock = {}
def add_item(self, name: str, quantity: int):
if quantity <= 0:
return
if name in self.stock:
self.stock[name] += quantity
else:
self.stock[name] = quantity
def remove_item(self, name: str, quantity: int):
if quantity <= 0:
return
if name not in self.stock:
return
if quantity >= self.stock[name]:
self.stock[name] = 0
else:
self.stock[name] -= quantity
def get_quantity(self, name: str) -> int:
return self.stock.get(name, 0)
def total_items(self) -> int:
return sum(self.stock.values())
GigaChat — ответ
class Inventory:
def init(self):
self.stock = {}
def add_item(self, name: str, quantity: int):
if quantity <= 0:
return
if name in self.stock:
self.stock[name] += quantity
else:
self.stock[name] = quantity
def remove_item(self, name: str, quantity: int):
if quantity <= 0 or name not in self.stock:
return
if quantity >= self.stock[name]:
del self.stock[name]
else:
self.stock[name] -= quantity
def get_quantity(self, name: str) -> int:
return self.stock.get(name, 0)
def total_items(self) -> int:
return sum(self.stock.values())
Вывод по T2:
MiniMax: рабочий класс, корректная логика, сигнатуры, структура — всё ок.
GigaChat:
def initвместо__init__— конструктор не вызывается;- методы вынесены вне класса → код нерабочий как объектная модель.
Тут MiniMax выигрывает в ноль.
T3 — filter_and_sort(nums, threshold)
Задача:
Оставить элементы > threshold, удалить дубли, отсортировать по возрастанию.
MiniMax — ответ
def filter_and_sort(nums: list[int], threshold: int) -> list[int]:
return sorted(set(num for num in nums if num > threshold))
GigaChat — ответ
def filter_and_sort(nums: list[int], threshold: int) -> list[int]:
nums_set = {num for num in nums if num > threshold}
sorted_nums = sorted(nums_set)
return sorted_nums
Вывод по T3:
Функционально решения одинаковы. MiniMax — one-liner, Giga — более развёрнутая версия, но тоже корректная. Паритет.
T4 — longest_unique_substring(s)
Задача:
Найти длину самой длинной подстроки без повторяющихся символов (классика sliding window).
MiniMax — ответ
def longest_unique_substring(s: str) -> int:
char_index = {}
left = 0
max_len = 0
for right, char in enumerate(s):
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1
char_index[char] = right
current_len = right - left + 1
if current_len > max_len:
max_len = current_len
return max_len
GigaChat — ответ
def longest_unique_substring(s: str) -> int:
seen_chars = {}
start_idx = max_len = 0
for idx, char in enumerate(s):
if char in seen_chars and start_idx <= seen_chars[char]:
start_idx = seen_chars[char] + 1
else:
curr_len = idx - start_idx + 1
max_len = max(max_len, curr_len)
seen_chars[char] = idx
return max_len
Вывод по T4:
Обе реализации корректны и эффективны (O(n)).
MiniMax пишет более «канонический» вариант скользящего окна — лучше читаемость и структура.
GigaChat — компактно, но логика чуть более запутана.
Опять оба прошли, с небольшим стилистическим перевесом MiniMax.
T5 — LRUCache
Задача:
Полноценный LRU-кэш с hashmap + двусвязный список, все операции O(1), без OrderedDict.
MiniMax — ответ
class Node:
__slots__ = ('key', 'value', 'prev', 'next')
def __init__(self, key: int, value: int):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _add_node(self, node: Node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node: Node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def _move_to_head(self, node: Node):
self._remove_node(node)
self._add_node(node)
def _pop_tail(self) -> Node:
node = self.tail.prev
self._remove_node(node)
return node
def get(self, key: int) -> int:
node = self.cache.get(key)
if not node:
return -1
self._move_to_head(node)
return node.value
def put(self, key: int, value: int) -> None:
node = self.cache.get(key)
if not node:
new_node = Node(key, value)
self.cache[key] = new_node
self._add_node(new_node)
if len(self.cache) > self.capacity:
tail = self._pop_tail()
del self.cache[tail.key]
else:
node.value = value
self._move_to_head(node)
GigaChat — ответ
from typing import Dict
class Node:
def init(self, key: int, value: int):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def init(self, capacity: int):
self.capacity = capacity
self.cache: Dict[int, Node] = {}
self.head = Node(-1, -1)
self.tail = Node(-1, -1)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node: Node):
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def _add_node_to_head(self, node: Node):
current_first = self.head.next
self.head.next = node
node.prev = self.head
node.next = current_first
current_first.prev = node
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
# Move accessed node to head (most recent)
self._remove_node(node)
self._add_node_to_head(node)
return node.value
def put(self, key: int, value: int):
if key in self.cache:
node = self.cache[key]
node.value = value
# Update existing entry's position
self._remove_node(node)
self._add_node_to_head(node)
else:
new_node = Node(key, value)
self.cache[key] = new_node
self._add_node_to_head(new_node)
if len(self.cache) > self.capacity:
lru_node = self.tail.prev
del self.cache[lru_node.key]
self._remove_node(lru_node)
Вывод по T5:
MiniMax: типовая, корректная реализация LRUCache из “учебника” — Node, двойной список, словарь, все операции O(1).
GigaChat:
- снова
initвместо__init__; - функции
_remove_node,_add_node_to_head,get,putопределены вне класса; - внутри
get/putон обращается кself._remove_node, но у класса такого метода нет.
Практически — код GigaChat вообще не работает, MiniMax — абсолютно рабочее решение.
T6 — eval_expr(expr: str) -> int
Задача:
Написать интерпретатор арифметических выражений:
- числа;
+,-,*,/;- скобки;
- приоритеты;
- деление с усечением к нулю;
- никакого
eval/exec.
MiniMax — ответ
def eval_expr(expr: str) -> int:
tokens = []
i = 0
n = len(expr)
while i < n:
if expr[i] == ' ':
i += 1
elif expr[i].isdigit():
j = i
while j < n and expr[j].isdigit():
j += 1
tokens.append(expr[i:j])
i = j
else:
tokens.append(expr[i])
i += 1
pos = 0
current_token = None
def advance():
nonlocal pos, current_token
if pos < len(tokens):
current_token = tokens[pos]
pos += 1
else:
current_token = None
advance()
def int_div(a, b):
if b == 0:
raise ZeroDivisionError("division by zero")
sign = 1
if a < 0:
a = -a
sign = -sign
if b < 0:
b = -b
sign = -sign
result = a // b
return sign * result
def parse_factor():
nonlocal current_token, pos
if current_token == '+':
advance()
return parse_factor()
elif current_token == '-':
advance()
return -parse_factor()
elif current_token == '(':
advance()
value = parse_expr()
if current_token != ')':
raise ValueError("Missing closing parenthesis")
advance()
return value
else:
if current_token is None:
raise ValueError("Unexpected end of expression")
try:
value = int(current_token)
except ValueError:
raise ValueError(f"Invalid token: {current_token}")
advance()
return value
def parse_term():
nonlocal current_token, pos
value = parse_factor()
while current_token in ['*', '/']:
op = current_token
advance()
right = parse_factor()
if op == '*':
value = value * right
else:
value = int_div(value, right)
return value
def parse_expr():
nonlocal current_token, pos
value = parse_term()
while current_token in ['+', '-']:
op = current_token
advance()
right = parse_term()
if op == '+':
value = value + right
else:
value = value - right
return value
result = parse_expr()
if current_token is not None:
raise ValueError("Unexpected token at end of expression")
return result
GigaChat — ответ
def eval_expr(expr: str) -> int:
def apply_op(operators, values):
op = operators.pop()
right = values.pop()
left = values.pop()
if op == '+':
values.append(left + right)
elif op == '-':
values.append(left - right)
elif op == '*':
values.append(left * right)
elif op == '/':
values.append(int(left / right)) # truncate towards zero
i = 0
n = len(expr)
operators = []
operands = []
while i < n:
c = expr[i].strip()
if not c:
i += 1
continue
if c.isdigit():
num = ''
while i < n and expr[i].isdigit():
num += expr[i]
i += 1
operands.append(int(num))
continue
elif c == '(':
operators.append(c)
elif c == ')':
while operators[-1] != '(':
apply_op(operators, operands)
operators.pop() # Remove opening parenthesis
elif c in ('+', '-', '*', '/'):
# Check precedence by popping lower/eq-precedence ops
while (operators and
((c in ('+', '-') and operators[-1] in ('*', '/', '(', '+' ,'-')) or
(c in ('*', '/') and operators[-1] in ('*', '/')))):
apply_op(operators, operands)
operators.append(c)
i += 1
while operators:
apply_op(operators, operands)
return operands[0]
Вывод по T6:
MiniMax:
- полноценный рекурсивный спуск,
- унарные
+/-, - корректный приоритет,
- своя реализация деления с усечением к нулю,
- проверки ошибок.
GigaChat:
- не поддерживает унарный минус,
- ломается на сочетании
'('и+/-из-за некорректной логики приоритетов, - парсер выдаёт некорректные результаты на ряде выражений.
Общий вывод
MiniMax M2 — неожиданно силён:
- аккуратный, чистый, структурный код;
- сложные структуры данных → PASS;
- парсер → PASS;
- нет галлюцинаций в коде;
- стабилен при большом
max_tokens.
GigaChat-2-Max — хорош в простых задачах, но ломается на инженерных:
- функции и простые алгоритмы → PASS;
- классы, структуры данных → ❌;
- LRUCache → ❌;
- парсер → ❌;
- проблемы с
__init__, структурой класса, приоритетами операторов.
Почему это важно
Эти тесты отлично подходят для реальной оценки reasoning-моделей, потому что:
- они приближены к задачам из реальной разработки;
- они легко проверяются;
- они показывают стабильность и архитектурное мышление.
Для Re:II Lab это отличный набор эталонных тестов.