
На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — 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-Банка использовала многоступенчатый подход к обучению:
Первичный претрейн на 100B токенов русскоязычных данных из Common Crawl, книг, кода и проприетарных датасетов
Вторичный претрейн на 40B токенов с фокусом на инструктивные данные
SFT (Supervised Fine-Tuning) на 1B токенов для улучшения следования инструкциям
Финальная настройка предпочтений также на 1B токенов
Такой подход позволил создать модели, которые не просто понимают русский язык, но и способны эффективно работать в различных доменах — от написания кода до ведения диалогов. По заявлению разработчиков, T-Lite стала лучшей русскоязычной опенсорс-моделью в классе до 10 млрд параметров, а T-Pro показывает впечатляющие результаты в сравнении даже с более крупными моделями.
Технические особенности
Обе модели сохраняют полное контекстное окно в 32k токенов, унаследованное от базовой модели Qwen 2.5, с возможностью масштабирования до 128k. Команда сохранила оригинальный токенизатор 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
От бенчмарков к реальности

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

В этом тесте мы предложили моделям создать 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

Этот тест был особенно интересен, так как требовал не только написания команд, но и понимания принципов обработки видео- и аудиопотоков, а также размеров и временных меток. Задача заключалась в том, чтобы видео с длиной, заранее указанной как 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 должна обрабатывать ошибки и не падать } } }
Разбор тестов:
should collect users successfully
Проверяет, что UserViewModel корректно собирает пользователей из Flow репозитория.
should handle empty user list
Убеждается, что UserViewModel корректно обрабатывает пустой список.
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-технологий и внедряет их в свои решения, а также участвует в их разработке, мы рады видеть появление качественных отечественных моделей с открытым исходным кодом и прозрачной лицензией. Особенно впечатляет то, что эти решения не просто существуют на бумаге, а показывают реальную применимость в повседневных задачах разработчиков, при этом оставаясь доступными даже для тех, кто не располагает мощными вычислительными ресурсами.
Если же вы хотите улучшить бизнес-процессы в вашей компании с помощью нейросетей, но не уверены в том, как лучше это сделать, вы можете напрямую обратиться к нам за консультацией.
