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

в 14:00, , рубрики: AI, chatgpt, claude, llm, qwen, ИИ, машинное обучение, нейросети, языковые модели
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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js