
На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — 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-технологий и внедряет их в свои решения, а также участвует в их разработке, мы рады видеть появление качественных отечественных моделей с открытым исходным кодом и прозрачной лицензией. Особенно впечатляет то, что эти решения не просто существуют на бумаге, а показывают реальную применимость в повседневных задачах разработчиков, при этом оставаясь доступными даже для тех, кто не располагает мощными вычислительными ресурсами.
Если же вы хотите улучшить бизнес-процессы в вашей компании с помощью нейросетей, но не уверены в том, как лучше это сделать, вы можете напрямую обратиться к нам за консультацией.
ссылка на оригинал статьи https://habr.com/ru/articles/879556/
Добавить комментарий