T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях. ai.. ai. chatgpt.. ai. chatgpt. Claude.. ai. chatgpt. Claude. llm.. ai. chatgpt. Claude. llm. Open source.. ai. chatgpt. Claude. llm. Open source. qwen.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ. искусственный интеллект.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ. искусственный интеллект. Машинное обучение.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ. искусственный интеллект. Машинное обучение. нейросети.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ. искусственный интеллект. Машинное обучение. нейросети. т-банк.. ai. chatgpt. Claude. llm. Open source. qwen. Блог компании Doubletapp. ИИ. искусственный интеллект. Машинное обучение. нейросети. т-банк. языковые модели.
T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях - 1

На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — T-Банк представил свои языковые модели T-Lite и T-Pro, основанные на китайской LLM Qwen 2.5. И хотя анонсов «революционных» нейросетей в последнее время становится всё больше, этот случай действительно заслуживает пристального внимания — перед нами не очередной наспех слепленный форк с громкими заявлениями, а результат полугодовой работы над полноценным решением с открытой лицензией Apache 2.0.

Что такое T-Lite и T-Pro?

T-Банк представил две модели разного масштаба: T-Lite на 7 миллиардов параметров и T-Pro на 32 миллиарда параметров. Обе модели построены на базе Qwen 2.5 и прошли серьёзное дообучение для работы с русским языком. Особенно интересен сам процесс их создания — команда T-Банка использовала многоступенчатый подход к обучению:

  1. Первичный претрейн на 100B токенов русскоязычных данных из Common Crawl, книг, кода и проприетарных датасетов

  2. Вторичный претрейн на 40B токенов с фокусом на инструктивные данные

  3. SFT (Supervised Fine-Tuning) на 1B токенов для улучшения следования инструкциям

  4. Финальная настройка предпочтений также на 1B токенов

Такой подход позволил создать модели, которые не просто понимают русский язык, но и способны эффективно работать в различных доменах — от написания кода до ведения диалогов. По заявлению разработчиков, T-Lite стала лучшей русскоязычной опенсорс-моделью в классе до 10 млрд параметров, а T-Pro показывает впечатляющие результаты в сравнении даже с более крупными моделями.

Технические особенности

Обе модели работают с контекстным окном в 8k токенов, хотя базовая модель Qwen 2.5 поддерживает до 32k. Команда сохранила оригинальный токенизатор Qwen 2.5, что означает сохранение его плотности токенизации, хотя и оставляет возможность для самостоятельной адаптации пользователями.

Бенчмарки и позиционирование

T-Pro показывает результаты, сопоставимые с GPT-4o по многим метрикам:

  • MERA: 0.629 (vs 0.642 у GPT-4o)

  • MaMuRAMu: 0.841 (vs 0.874)

  • ruMMLU: 0.768 (vs 0.792)

T-Lite, несмотря на свой компактный размер, демонстрирует впечатляющие результаты в своём классе:

  • MERA: 0.552

  • MaMuRAMu: 0.775

  • ruMMLU: 0.664

От бенчмарков к реальности

T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях - 2

Бенчмарки и метрики — это замечательно, но как разработчики, глубоко погруженные в практическое применение ML-технологий, мы в Doubletapp прекрасно понимаем, что реальная ценность модели проявляется именно в боевых условиях. Красивые цифры в тестах не всегда транслируются в удобство использования и практическую применимость, особенно когда речь идёт о решении конкретных задач разработки.

Именно поэтому мы решили провести собственное тестирование в условиях, максимально приближенных к реальному применению. Для T-Pro мы использовали облачную инфраструктуру runpod.io с Nvidia A40 (40GB VRAM), что позволило нам развернуть модель без каких-либо ограничений по памяти. T-Lite же мы намеренно тестировали на более скромном железе — ноутбуках с RTX 3050Ti (4GB VRAM, TDP 60W), ведь именно такая конфигурация чаще всего встречается у разработчиков в реальной работе. Отдельно ещё отметим, что обе модели будут квантизированными до 4-битной точности для имитации приближенных к реальным условий, где в силу экономии ресурсов инференс моделей в полную точность не очень экономически целесообразен.

В качестве тестовых сценариев мы выбрали задачи, с которыми регулярно сталкиваются разработчики:

  • Написание и отладка скриптов для автоматизации рутинных задач

  • Работа с мультимедиа через консольные утилиты

  • Анализ потенциально вредоносного контента

  • Генерация и рефакторинг кода

Давайте посмотрим, как справились модели с каждой из этих задач.

Результаты тестирования

Тест 1: Автоматизация работы с файлами

T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях - 3

В этом тесте мы предложили моделям создать bash-скрипт для сортировки файлов по типам в различные директории. Интересно было наблюдать, как разные модели подходят к обработке потенциальных ошибок и краевых случаев.

Claude 3.5 Sonnet

Время отклика составило в среднем около ~25 секунд, с поправкой на длину ответа, аналогичные результаты далее будут и у ChatGPT 4o от OpenAI. В итоге получился следующий скрипт:

Скрипт
#!/bin/bash

# Определяем базовую директорию

BASE_DIR="/home/marat/Downloads"

# Создаем директории для разных типов файлов

IMAGES_DIR="$BASE_DIR/images"

PDFS_DIR="$BASE_DIR/pdfs"

VIDEOS_DIR="$BASE_DIR/videos"

# Функция для создания директории, если она не существует

create_dir() {

    if [ ! -d "$1" ]; then

        mkdir -p "$1"

        echo "Создана директория: $1"

    else

        echo "Директория уже существует: $1"

    fi

}

# Создаем необходимые директории

create_dir "$IMAGES_DIR"

create_dir "$PDFS_DIR"

create_dir "$VIDEOS_DIR"

# Функция для копирования файлов

copy_files() {

    local source_pattern="$1"

    local dest_dir="$2"

    local file_type="$3"

    

    # Находим файлы и копируем их

    find "$BASE_DIR" -maxdepth 1 -type f -iname "$source_pattern" -exec cp -v {} "$dest_dir/" ;

    

    # Проверяем, были ли скопированы файлы

    if [ $? -eq 0 ]; then

        echo "Файлы типа $file_type скопированы в $dest_dir"

    else

        echo "Не найдены файлы типа $file_type или произошла ошибка при копировании"

    fi

}

# Копируем изображения (PNG и JPG)

copy_files "*.png" "$IMAGES_DIR" "PNG"

copy_files "*.jpg" "$IMAGES_DIR" "JPG"

copy_files "*.jpeg" "$IMAGES_DIR" "JPEG"

# Копируем PDF файлы

copy_files "*.pdf" "$PDFS_DIR" "PDF"

# Копируем видео файлы

copy_files "*.mp4" "$VIDEOS_DIR" "MP4"

echo "Сортировка файлов завершена!"

Как можно заметить, скрипт получился достаточно массивным, однако работающим. Далее посмотрим на результаты T-Pro.

T-Pro

При генерации кода T-Pro показала скорость 23.3 токена в секунду, что обеспечило быстрый отклик. Модель предложила более компактное, но не менее эффективное решение:

Скипт
#!/bin/bash

# Определяем базовую директорию

BASE_DIR="/home/marat/Downloads"

# Создаем необходимые поддиректории, если они не существуют

mkdir -p "$BASE_DIR/images"

mkdir -p "$BASE_DIR/pdf"

mkdir -p "$BASE_DIR/videos"

# Копируем файлы в соответствующие папки

echo "Копирование изображений (PNG и JPG)..."

find "$BASE_DIR" -maxdepth 1 ( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" ) -exec cp {} "$BASE_DIR/images/" ;

echo "Копирование PDF файлов..."

find "$BASE_DIR" -maxdepth 1 -iname "*.pdf" -exec cp {} "$BASE_DIR/pdf/" ;

echo "Копирование видео (MP4) файлов..."

find "$BASE_DIR" -maxdepth 1 -iname "*.mp4" -exec cp {} "$BASE_DIR/videos/" ;

echo "Организация завершена."

ChatGPT 4o

ChatGPT 4o предложил следующее решение:

Скрипт
#!/bin/bash

# Указание пути к папке Downloads

DOWNLOADS_DIR="/home/marat/Downloads"

# Создание папок для PNG/JPG, PDF и MP4 файлов

mkdir -p "$DOWNLOADS_DIR/Images"

mkdir -p "$DOWNLOADS_DIR/Documents"

mkdir -p "$DOWNLOADS_DIR/Videos"

# Копирование PNG и JPG файлов в папку Images

find "$DOWNLOADS_DIR" -maxdepth 1 -type f ( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" ) -exec mv {} "$DOWNLOADS_DIR/Images/" ;

# Копирование PDF файлов в папку Documents

find "$DOWNLOADS_DIR" -maxdepth 1 -type f -iname "*.pdf" -exec mv {} "$DOWNLOADS_DIR/Documents/" ;

# Копирование MP4 файлов в папку Videos

find "$DOWNLOADS_DIR" -maxdepth 1 -type f -iname "*.mp4" -exec mv {} "$DOWNLOADS_DIR/Videos/" ;

echo "Файлы успешно распределены по папкам!"

Сработало оно так же хорошо, как и предыдущие.

T-Lite

Младший брат T-Pro справился с генерацией за 44 секунды, со скоростью генерации 11.8 токенов в секунду. Ответ, как и у старшего собрата, и коллег по цеху, оказался не менее рабочим.

Скрипт
#!/bin/bash

# Путь к Downloads

downloads_dir="/home/marat/Downloads"

# Проверяем существование директории и создаем её если не существует

if [ ! -d "$downloads_dir" ]; then

    echo "Directory $downloads_dir does not exist. Creating it..."

    mkdir "$downloads_dir"

fi

# Создаем папки для различных типов файлов

mkdir -p "$downloads_dir"/PNG_JPG

mkdir -p "$downloads_dir"/PDF

mkdir -p "$downloads_dir"/MP4

# Функция для копирования файлов по расширению в соответствующие папки

move_files() {

    local extension="$1"

    local target_dir="$2"

    for file in "$downloads_dir"/*."$extension"; do

        if [ -f "$file" ]; then

            echo "Moving $file to $target_dir..."

            mv -v "$file" "$downloads_dir/$target_dir/"

        fi

    done

}

# Копируем файлы по расширениям

move_files "png" "PNG_JPG"

move_files "jpg" "PNG_JPG"

move_files "pdf" "PDF"

move_files "mp4" "MP4"

echo "All files have been moved to the respective directories."

Тест 2: Работа с FFmpeg

T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях - 4

Этот тест был особенно интересен, так как требовал не только написания команд, но и понимания принципов обработки видео- и аудиопотоков, а также размеров и временных меток. Задача заключалась в том, чтобы видео с длиной, заранее указанной как 3 минуты 32 секунды, было ускорено в 2 раза и разделено на 2 части с помощью FFmpeg.

Claude 3.5 Sonnet

ffmpeg -i "/home/marat/Downloads/videos/Not_a_Rickroll.mp4" 

       -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" 

       -map "[v]" -map "[a]" -c:v libx264 -c:a aac "/tmp/temp_speedup.mp4" && 

ffmpeg -i "/tmp/temp_speedup.mp4" 

       -t 53 -c copy "/home/marat/Downloads/videos/Not_a_Rickroll_part1.mp4" && 

ffmpeg -i "/tmp/temp_speedup.mp4" 

       -ss 53 -c copy "/home/marat/Downloads/videos/Not_a_Rickroll_part2.mp4"

Модель точно рассчитала временные отрезки и, из интересного, сама догадалась использовать промежуточный, временный файл.

T-Pro

ffmpeg -i "/home/marat/Downloads/videos/Not_a_Rickroll.mp4" 

       -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" 

       -map "[v]" -map "[a]" -c:v libx264 -c:a aac -f segment -segment_time 106 "/home/marat/Downloads/videos/Not_a_Rickroll_part%d.mp4"

Тут T-pro справлялся с задачей лишь на 50%, либо деля видео только на 2 равные части, либо только ускоряя его, как в этом примере.

ChatGPT 4o

Как и с Claude, обошлось без сюрпризов, сначала ChatGPT выдал команду для ускорения видео в 2 раза:

ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll.mp4 -filter:v "setpts=0.5*PTS" -an /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4

А затем отделил одну часть от уже ускоренного:

ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4 -ss 0 -t 53 /home/marat/Downloads/videos/Not_a_Rickroll_part1.mp4

И потом вторую:

ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4 -ss 53 -t 53 /home/marat/Downloads/videos/Not_a_Rickroll_part2.mp4

T-Lite

T-Lite себя тут показал хуже, хотя и видно, что пытался, и вместо того, чтобы решить задачу несколькими командами, он попытался сделать всё одной монструозной и неизбежно провалился.

ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll.mp4 

       -filter_complex "[0:v]setpts=0.5*PTS,split=2[v1][v2];[0:a]atempo=2[a1][a2]" 

       -map "[v1]" -map "[a1]" /home/marat/Downloads/videos/Not_a_Rickroll_sped_up_1.mp4 

       -map "[v2]" -map "[a2]" /home/marat/Downloads/videos/Not_a_Rickroll_sped_up_2.mp4

Тест 3: на анализ потенциально опасных сообщений

Тестовое сообщение 1

“Привет! У тебя интересные фотки, чем ещё увлекаешься?”

Claude 3.5 Sonnet

  • Вердикт: Не спам

  • Анализ: Детально объяснил признаки нормального общения — персонализация, естественность вопроса

  • Особенности: Сохранил контекст диалога, предложил варианты безопасного продолжения общения

T-Pro

  • Вердикт: Не спам

  • Анализ: Четкое определение признаков нормального общения

  • Особенности: Краткий, но информативный анализ безопасности сообщения

T-Lite

  • Вердикт: (Ушла от задачи анализа)

  • Анализ: Начала вести диалог вместо анализа

  • Особенности: Полностью проигнорировала роль спам-фильтра

ChatGPT 4o

  • Вердикт: Не спам

  • Анализ: Отсутствует

  • Особенности: Минималистичный ответ без объяснений

Тестовое сообщение 2

“А чем ты занимаешься? Я вот в спортзал хожу и инвестициями увлекаюсь, хочешь и тебе расскажу как?”

Claude 3.5 Sonnet

  • Вердикт: Спам

  • Анализ: Подробно описал признаки мошеннической схемы, выделил паттерны романтического развода

  • Особенности: Привел полный анализ рисков и подозрительных маркеров

T-Pro

  • Вердикт: Спам

  • Анализ: Указала на конкретные признаки потенциального мошенничества

  • Особенности: Четкое выделение подозрительных элементов в сообщении

T-Lite

  • Вердикт: (Ушла от задачи анализа)

  • Анализ: Развернутый ответ о пользе спорта и инвестиций

  • Особенности: Полностью проигнорировала роль спам-фильтра

ChatGPT 4o

  • Вердикт: Спам

  • Анализ: Отсутствует

  • Особенности: Категоричный ответ без объяснений

Тестовое сообщение 3

“Слушай, тут сейчас такой концерт интересный проходит, давай вместе сходим, билеты вот тут продаются – [ссылка].”

Claude 3.5 Sonnet

  • Вердикт: Спам

  • Анализ: Глубокий разбор рисков, связанных с внешними ссылками

  • Особенности: Предложил альтернативные безопасные варианты общения

T-Pro

  • Вердикт: Спам

  • Анализ: Подробное объяснение рисков с практическими рекомендациями

  • Особенности: Структурированный список мер предосторожности

T-Lite

  • Вердикт: Частично соответствует роли

  • Анализ: Смешанный подход — советы по безопасности + диалог

  • Особенности: Неполное следование роли фильтра

ChatGPT 4o

  • Вердикт: Спам

  • Анализ: Отсутствует

  • Особенности: Быстрая классификация без обоснования

Тестовое сообщение 4

“У тебя в анкете указано что ты любишь погулять, может сходим куда-нибудь сегодня/завтра?”

Claude 3.5 Sonnet

  • Вердикт: Не спам

  • Анализ: Отметил персонализацию, учет информации из профиля

  • Особенности: Оценил естественность развития диалога

T-Pro

  • Вердикт: Не спам

  • Анализ: Подчеркнула важность контекста и персонализации

  • Особенности: Акцент на признаках нормального общения

T-Lite

  • Вердикт: (Ушла от задачи анализа)

  • Анализ: Советы по планированию прогулки

  • Особенности: Игнорирование роли спам-фильтра

ChatGPT 4o

  • Вердикт: Не спам

  • Анализ: Отсутствует

  • Особенности: Точная, но не обоснованная классификация

В этой задаче, по непонятным причинам, справились все, кроме малютки T-Lite, которая, вместо того, чтобы анализировать спам, стабильно игнорировала заданный сообщением ранее запрос и на «спам» отвечала как собеседнику. T-Pro же показала себя достаточно схоже с Claude 3.5 Sonnet, детально разбирая и анализируя всё, а вот ChatGPT 4o был максимально краток, отвечая только — спам, не спам.

Сравнительная таблица

Критерий

Claude 3.5

T-Pro

T-Lite

ChatGPT 4o

Следование роли

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐

⭐⭐⭐⭐⭐

Качество анализа

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐

⭐⭐⭐⭐⭐

Полезность рекомендаций

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐

⭐

Понимание контекста

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐⭐

Стабильность ответов

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐

⭐⭐⭐⭐⭐

Бонусная задача: RAG в боевых условиях, или как LLM справляются с реальной разработкой

После всех этих тестов с bash-скриптами и FFmpeg’ом самое время взглянуть на то, как наши подопытные справляются с повседневными задачами разработчиков. И тут мы решили быть максимально практичными — взяли типичный паттерн из Android-разработки: приложение с Room-базой данных, пользователями и стандартной архитектурой.

Наша задача для моделей звучала просто: проанализировать существующую кодовую базу и написать юнит-тесты для UserViewModel. В фокусе — работа с корутинами, Flow и обработка ошибок. По сути, то, с чем Android-разработчики сталкиваются каждый день.

Почему именно такой сценарий? Во-первых, это реальный код, который можно встретить практически в любом проекте. Во-вторых, здесь нужно не просто сгенерировать что-то с нуля, а разобраться в существующей структуре. И в-третьих, это отличный способ оценить, насколько модели понимают современный стек Android-разработки.

RAG-контекст для тестирования

В качестве исходного кода мы взяли типичную реализацию работы с базой данных в Android-приложении через Room. Вот наша кодовая база :

Кодовая база
// Начнём с сущности пользователя — классика для любого приложения

@Entity(tableName = "users")

data class User(

    @PrimaryKey val uid: Int,

    @ColumnInfo(name = "first_name") val firstName: String,

    @ColumnInfo(name = "last_name") val lastName: String,

    @ColumnInfo(name = "email") val email: String,

    @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()

)

// DAO для работы с базой — стандартный набор CRUD-операций

@Dao

interface UserDao {

    @Query("SELECT * FROM users")

    fun getAll(): Flow<List<User>>

    @Query("SELECT * FROM users WHERE uid IN (:userIds)")

    fun loadAllByIds(userIds: IntArray): Flow<List<User>>

    @Insert

    suspend fun insertAll(vararg users: User)

    @Delete

    suspend fun delete(user: User)

}

// Room Database — ничего необычного, просто связываем всё воедино

@Database(entities = [User::class], version = 1)

abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

}

// Repository — классический паттерн для абстракции работы с данными

class UserRepository @Inject constructor(

    private val userDao: UserDao

) {

    fun getAllUsers(): Flow<List<User>> = userDao.getAll()

    suspend fun addUser(user: User) = userDao.insertAll(user)

    suspend fun deleteUser(user: User) = userDao.delete(user)

}

// И наконец, ViewModel — здесь вся бизнес-логика и состояние UI

@HiltViewModel

class UserViewModel @Inject constructor(

    private val userRepository: UserRepository

) : ViewModel() {

    private val _users = MutableStateFlow<List<User>>(emptyList())

    val users: StateFlow<List<User>> = _users.asStateFlow()

    init {

        viewModelScope.launch {

            userRepository.getAllUsers()

                .collect { users ->

                    _users.value = users

                }

        }

    }

}

А теперь давайте посмотрим, как модели справились с задачей написания юнит-тестов. 

Claude 3.5 Sonnet

Claude выдал максимально полный набор тестов, затрагивающий не только базовый функционал, но и edge-cases:

Claude
@Test

fun initial state should be empty list() = runTest {

    val usersFlow = flowOf(emptyList<User>())

    coEvery { repository.getAllUsers() } returns usersFlow

    viewModel = UserViewModel(repository)

    assertEquals(emptyList<User>(), viewModel.users.value)

}

@Test

fun should collect users from repository() = runTest {

    val testUsers = listOf(

        User(1, "John", "Doe", "john@example.com"),

        User(2, "Jane", "Smith", "jane@example.com")

    )

    val usersFlow = flowOf(testUsers)

    coEvery { repository.getAllUsers() } returns usersFlow

    viewModel = UserViewModel(repository)

    testDispatcher.scheduler.advanceUntilIdle()

    assertEquals(testUsers, viewModel.users.value)

}

@Test

fun should handle repository errors gracefully() = runTest {

    val errorFlow = flow<List<User>> { 

        throw IOException("Network error") 

    }

    coEvery { repository.getAllUsers() } returns errorFlow

    viewModel = UserViewModel(repository)

    testDispatcher.scheduler.advanceUntilIdle()

    assertEquals(emptyList<User>(), viewModel.users.value)

}

Примечательно, что модель сразу использует StandardTestDispatcher и структурирует тесты по паттерну Given-When-Then. Особенно порадовало внимание к обработке ошибок — критичный момент для реальных приложений, который часто упускают из виду. Тесты покрывают основные сценарии использования ViewModel и демонстрируют понимание как Room, так и корутин с Flow.

ChatGPT 4o

Творение OpenAI выдало в чате следующее:

ChatGPT
import app.cash.turbine.test

import io.mockk.*

import kotlinx.coroutines.Dispatchers

import kotlinx.coroutines.ExperimentalCoroutinesApi

import kotlinx.coroutines.flow.Flow

import kotlinx.coroutines.flow.MutableStateFlow

import kotlinx.coroutines.flow.flow

import kotlinx.coroutines.test.*

import org.junit.Assert.*

import org.junit.Before

import org.junit.Rule

import org.junit.Test

import org.junit.rules.TestRule

import org.junit.runner.RunWith

import org.mockito.junit.MockitoJUnitRunner

@ExperimentalCoroutinesApi

@RunWith(MockitoJUnitRunner::class)

class UserViewModelTest {

    private lateinit var userRepository: UserRepository

    private lateinit var userViewModel: UserViewModel

    private val testDispatcher = StandardTestDispatcher()

    @get:Rule

    val rule: TestRule = TestCoroutineRule()

    @Before

    fun setup() {

        userRepository = mockk()

        Dispatchers.setMain(testDispatcher)

        userViewModel = UserViewModel(userRepository)

    }

    @Test

    fun should collect users successfully() = runTest {

        val users = listOf(

            User(1, "John", "Doe", "john@example.com"),

            User(2, "Jane", "Doe", "jane@example.com")

        )

        val userFlow: Flow<List<User>> = flow { emit(users) }

        coEvery { userRepository.getAllUsers() } returns userFlow

        

        userViewModel = UserViewModel(userRepository)

        

        userViewModel.users.test {

            assertEquals(users, awaitItem())

        }

    }

    @Test

    fun should handle empty user list() = runTest {

        val emptyUserFlow: Flow<List<User>> = flow { emit(emptyList()) }

        coEvery { userRepository.getAllUsers() } returns emptyUserFlow

        

        userViewModel = UserViewModel(userRepository)

        

        userViewModel.users.test {

            assertTrue(awaitItem().isEmpty())

        }

    }

    @Test

    fun should handle repository error() = runTest {

        val errorFlow: Flow<List<User>> = flow { throw RuntimeException("Database error") }

        coEvery { userRepository.getAllUsers() } returns errorFlow

        

        userViewModel = UserViewModel(userRepository)

        

        userViewModel.users.test {

            assertTrue(awaitItem().isEmpty()) // ViewModel должна обрабатывать ошибки и не падать

        }

    }

}

Разбор тестов:

  1. should collect users successfully

    • Проверяет, что UserViewModel корректно собирает пользователей из Flow репозитория.

  2. should handle empty user list

    • Убеждается, что UserViewModel корректно обрабатывает пустой список.

  3. should handle repository error

    • Симулирует ошибку в Flow и проверяет, что UserViewModel не падает.

Используем Turbine для тестирования StateFlow и mockk для мокирования зависимостей. Если что-то улучшить – пиши! 🚀

ChatGPT 4o выдал максимально подробные тесты, включив не только стандартные проверки, но и весь необходимый бойлерплейт — от аннотаций до импортов. Использование Turbine для тестирования Flow и TestCoroutineRule показывает, что модель в курсе актуальных практик тестирования в Android.

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

T-Lite

Bash
Анализируя кодовую базу, мы видим, что UserViewModel использует корутины и потоки данных (Flow`) для  

получения списка пользователей из базы данных. Давайте напишем юнит-тесты, чтобы убедиться в правильности  

работы с этими механизмами.

### Шаг 1: Подготовка тестовой среды

Для начала нам нужно подготовить тестовую среду для UserViewModel. Это может включать создание моков и  

инжекцию их в UserViewModel для изоляции тестирования. Мы будем использовать Mockito и Mockk.

### Шаг 2: Написание юнит-тестов

Давайте напишем несколько тестов, чтобы проверить работу UserViewModel.

```kotlin

import androidx.arch.core.executor.testing.InstantTaskExecutorRule

import kotlinx.coroutines.Dispatchers

import kotlinx.coroutines.test.runBlockingTest

import org.junit.Assert.assertEquals

import org.junit.Before

import org.junit.Rule

import org.junit.Test

import org.mockito.kotlin.mock

import org.mockito.kotlin.whenever

class UserViewModelTest {

   @get:Rule

   val instantTaskExecutorRule = InstantTaskExecutorRule()

   private lateinit var userRepositoryMock: UserRepository

   private lateinit var userViewModel: UserViewModel

   @Before

   fun setUp() {

       userRepositoryMock = mock()

       userViewModel = UserViewModel(userRepositoryMock)

   }

   @Test

   fun getAllUsers should return empty list when no users are in database() = runBlockingTest {

       // Given

       val emptyUserList = listOf<User>()

       whenever(userRepositoryMock.getAllUsers()).thenReturn(emptyFlow())

       // When

       val users = userViewModel.users.first()

       // Then

       assertEquals(emptyUserList, users)

   }

   @Test

   fun getAllUsers should return list of users when there are users in database() = runBlockingTest {

       // Given

       val userList = listOf(User(uid = 1, firstName = "John", lastName = "Doe", email =  

"john.doe@example.com"))

       whenever(userRepositoryMock.getAllUsers()).thenReturn(flowOf(userList))

       // When

       val users = userViewModel.users.first()

       // Then

       assertEquals(userList, users)

   }

   private fun flowOf(list: List<User>): Flow<List<User>> {

       return flow {

           emit(list)

       }

   }

   private fun emptyFlow(): Flow<List<User>> {

       return flow {}

   }

}

```

### Объяснение тестов

1. Тест на пустой список пользователей: Мы проверяем, что при отсутствии данных в базе, UserViewModel  

корректно возвращает пустой список.

2. Тест на заполненный список пользователей: В этом тесте мы подменяем реальный поток данных из базы на  

мок-поток с конкретными данными и проверяем, что UserViewModel правильно обрабатывает эти данные.

### Шаг 3: Дополнительные тесты

Мы также можем добавить тесты для работы с ошибками:

```kotlin

@Test

fun getAllUsers should throw exception when repository throws an error() = runBlockingTest {

   // Given

   whenever(userRepositoryMock.getAllUsers()).doReturn(emptyFlow())

   // When & Then

   assertFailsWith<Throwable> {

       userViewModel.users.first()

   }

}

```

### Замечания

- Обработка ошибок: Добавьте обработку ошибок в ваш UserViewModel, чтобы корректно уведомлять  

пользователей о возможных проблемах с базой данных.

  

- Использование TestCoroutineDispatcher: Для более сложного тестирования вы можете использовать  

TestCoroutineDispatcher для контроля выполнения корутин.

```kotlin

private val testDispatcher = TestCoroutineDispatcher()

@Before

fun setup() {

   Dispatchers.setMain(testDispatcher)

}

@AfterEach

fun teardown() {

   testDispatcher.cleanupTestCoroutines()

}

```

Эти тесты помогут вам убедиться, что UserViewModel работает корректно с использованием потоков данных и  

корутин.

При анализе ответа T-Lite видим классический пример «младшей» модели — базовая структура на месте, но в деталях начинаются проблемы. Устаревший runBlockingTest вместо современного runTest, странное смешение Mockito и Mockk, наивная работа с Flow через first(). В сравнении с решениями Claude 3.5 и ChatGPT 4o, где используются StandardTestDispatcher, правильная обработка StateFlow и чёткое разделение тестовых фаз, разница в понимании современного Android-стека очевидна. Особенно это заметно в обработке ошибок, где T-Lite предлагает assertFailsWith вместо корректной проверки поведения ViewModel.

7B параметров против больших моделей, у которых этих параметров сотни миллиардов — разница в понимании контекста и объём знаний, увы, неизбежно несравнимы. 

T-Pro

Скрытый текст
Для написания юнит-тестов для UserViewModel, которые включают работу с корутинами, Flow` и обработку ошибок, мы начнем с создания мока (например, используя библиотеку Mockito или MockK)

для UserDao. Затем напишем тесты, чтобы покрыть основные функциональности ViewModel.

### Шаг 1: Подготовка зависимостей и настройка окружения

Убедитесь, что у вас есть необходимые зависимости в вашем build.gradle файле:

```groovy

dependencies {

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

    implementation "androidx.room:room-runtime:$room_version"

    kapt "androidx.room:room-compiler:$room_version"

    testImplementation 'junit:junit:4.13.2'

    testImplementation "org.mockito:mockito-core:4.x.x"

    testImplementation "io.mockk:mockk:1.12.x"

    testImplementation "androidx.arch.core:core-testing:2.1.0" // Для тестирования корутин и Flow

}

```

### Шаг 2: Создание ViewModel

Предположим, у нас есть UserViewModel, которая использует Flow для получения данных из базы:

```kotlin

import androidx.lifecycle.ViewModel

import androidx.lifecycle.viewModelScope

import kotlinx.coroutines.flow.*

import kotlinx.coroutines.launch

class UserViewModel(private val userDao: UserDao) : ViewModel() {

    val users = userDao.getAll().catch { e ->

        emit(emptyList()) // Логика обработки ошибок

    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    fun loadUsersByIds(userIds: IntArray) {

        viewModelScope.launch {

            try {

                userDao.loadAllByIds(userIds).collect { users ->

                    // Обработка списка пользователей по ID

                }

            } catch (e: Exception) {

                // Логика обработки ошибок

            }

        }

    }

    fun addUser(user: User) = viewModelScope.launch {

        try {

            userDao.insert(user)

        } catch (e: Exception) {

            // Обработка исключения

        }

    }

}

```

### Шаг 3: Написание тестов

Теперь мы можем написать юнит-тесты:

```kotlin

import kotlinx.coroutines.ExperimentalCoroutinesApi

import kotlinx.coroutines.flow.flowOf

import kotlinx.coroutines.test.runTest

import org.junit.Assert.assertEquals

import org.junit.Before

import org.junit.Test

import org.mockito.kotlin.*

@OptIn(ExperimentalCoroutinesApi::class)

class UserViewModelTest {

    private lateinit var userDao: UserDao

    private lateinit var viewModel: UserViewModel

    @Before

    fun setUp() {

        userDao = mock()

        viewModel = UserViewModel(userDao)

    }

    @Test

    fun test getAll users() = runTest {

        val userList = listOf(User(1, "John", "Doe", "john@doe.com"))

        whenever(userDao.getAll()).thenReturn(flowOf(userList))

        assertEquals(userList, viewModel.users.first())

    }

    @Test

    fun test loadUsersByIds success() = runTest {

        val userIds = intArrayOf(1, 2)

        val userList = listOf(User(1, "Jane", "Doe", "jane@doe.com"), User(2, "Bob", "Smith", "bob@smith.com"))

        whenever(userDao.loadAllByIds(any())).thenReturn(flowOf(userList))

        viewModel.loadUsersByIds(userIds)

        // Проверка того, что корутинная логика сработала корректно

    }

    @Test

    fun test getAll error handling() = runTest {

        val exception = Exception("Database error")

        whenever(userDao.getAll()).thenReturn(flow { throw exception })

        assertEquals(emptyList<User>(), viewModel.users.first())

    }

    @Test

    fun test addUser success() = runTest {

        val user = User(3, "Alice", "Johnson", "alice@johnson.com")

        viewModel.addUser(user)

        verify(userDao).insert(user)

    }

    @Test

    fun test addUser error handling() = runTest {

        val user = User(4, "Charlie", "Brown", "charlie@brown.com")

        whenever(userDao.insert(any())).thenThrow(RuntimeException("Insert failed"))

        viewModel.addUser(user)

        // Проверить логику обработки ошибок

    }

}

```

### Объяснение

1. runTest: Используется для тестирования корутинных функций в изолированной среде.

2. Mocking: Мы используем mockk для создания мока объекта UserDao, чтобы проверить, как ViewModel взаимодействует с ним.

3. Error handling: Проверяем, что ошибка корректно обрабатывается и заменяется на пустой список или другое подходящее значение.

Таким образом мы обеспечиваем покрытие основных функциональностей ViewModel через тесты.

В отличие от T-Lite, здесь мы видим более структурированный подход: отдельный build.gradle с зависимостями, полноценную реализацию ViewModel и набор тестов, покрывающих разные сценарии.

Однако даже T-Pro допускает некоторые неточности. Например, использование first() для тестирования Flow (как и младший брат) вместо более надёжного решения через Turbine, которое мы видели у ChatGPT 4o. Также отсутствует явная работа с TestDispatcher, что может привести к проблемам при тестировании корутин.

С другой стороны, T-Pro предлагает более полное покрытие функционала — тестирование не только базовых операций получения данных, но и обработки ошибок, работы с ID и добавления пользователей. Это показывает более глубокое понимание реальных сценариев использования ViewModel.

В целом, результат T-Pro ближе к решениям старших моделей, хотя и требует некоторой доработки в части работы с корутинами и Flow.

Заключение

Проведенное тестирование показало интересные результаты, особенно в контексте соотношения производительности и требований к ресурсам. T-Pro, несмотря на более скромные требования к оборудованию по сравнению с некоторыми конкурентами (достаточно Nvidia A40 с 40GB VRAM), продемонстрировала впечатляющие результаты, практически на равных конкурируя с более «тяжелыми» моделями в реальных задачах разработки.

T-Lite, хотя и показала менее стабильные результаты в тестах, представляет собой интересное решение для случаев, когда ресурсы ограничены. Возможность её запуска на обычном, далеко не свежем и не топовом ноутбуке открывает новые возможности для локальной разработки и тестирования. Да, модель чаще отклонялась от заданной роли и показывала менее стабильные результаты, но при этом демонстрировала неплохое понимание контекста и генерацию связных ответов.

Как компания, которая активно следит за развитием ML-технологий и внедряет их в свои решения, а также участвует в их разработке, мы рады видеть появление качественных отечественных моделей с открытым исходным кодом и прозрачной лицензией. Особенно впечатляет то, что эти решения не просто существуют на бумаге, а показывают реальную применимость в повседневных задачах разработчиков, при этом оставаясь доступными даже для тех, кто не располагает мощными вычислительными ресурсами.

Если же вы хотите улучшить бизнес-процессы в вашей компании с помощью нейросетей, но не уверены в том, как лучше это сделать, вы можете напрямую обратиться к нам за консультацией.

Автор: maratts_doubletapp

Источник

Рейтинг@Mail.ru
Rambler's Top100