MiniMax vs GigaChat: большой инженерный тест

MiniMax M2 vs GigaChat 2 Max: сравнили на шести инженерных задачах — алгоритмы, структуры данных, парсер

~5 мин чтения

Протестировали 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 это отличный набор эталонных тестов.

QR Telegram

Подписывайтесь на наш Telegram

Новости, сводки и разборы

Читайте также