
На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — 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 показывает впечатляющие результаты в сравнении даже с более крупными моделями.
Технические особенности
Обе модели работают с контекстным окном в 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
От бенчмарков к реальности

Бенчмарки и метрики — это замечательно, но как разработчики, глубоко погруженные в практическое применение 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.mp4T-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 для мокирования зависимостей. Если что-то улучшить – пиши!
