Обучение русского RAG-сплиттера на T-lite-it-2.1: lossless-нарезка и GGUF | AiManual
AiManual Logo Ai / Manual.
04 Июл 2026 Гайд

Как обучить русский RAG-сплиттер с lossless-нарезкой документов: опыт использования T-lite-it-2.1 и GGUF

Пошаговый гайд по обучению LoRA на RTX 5090, деплой на AMD через llama.cpp и index-output подход для lossless-нарезки русских документов в RAG-системах.

Почему стандартные сплиттеры — зло для русского языка

Стандартный RecursiveCharacterTextSplitter из LangChain режет русский текст как придется. Никто не говорил, что будет легко? Он делит по фиксированному числу символов, игнорируя точки, запятые и логические блоки. В итоге RAG получает на вход обрубки фраз, а LLM галлюцинирует, пытаясь восстановить смысл. Это я называю lossy-нарезкой — потеря смысла гарантирована.

«На практике: 80% моих ошибок при инференсе русского RAG были из-за кривого чанкинга, а не из-за модели».

Мы хотим lossless-нарезку: чтобы каждый чанк был самодостаточным куском текста (абзац, предложение, заголовок). Для этого нужно обучить детектор границ, который понимает русскую грамматику — где кончается одна сущность и начинается другая. И тут на сцену выходит T-lite-it-2.1 (4 июля 2026 — это свежайшая версия).

Идея: index-output вместо прямой нарезки

Большинство сплиттеров работают так: дать текст и максимальный размер токенов — на выходе массив строк. А мы сделаем иначе: модель будет предсказывать индексы границ в исходном тексте, а потом мы физически режем по этим индексам. Это и есть index-output.

Почему это круто:

  • Никаких полу-слов: граница проходит между токенами, а токенизатор у T-lite-it-2.1 (SentencePiece) хорошо понимает русские словоформы.
  • Мы не теряем знаки препинания: модель специально учится ставить границу после точки, но до следующей заглавной буквы.
  • Простота: предобработка не нужна, подаем сырой текст.

Почему T-lite-it-2.1? Это легковесный энкодер (около 300M параметров), обученный с флагом instruction-tracking. Он отлично справляется с пониманием структуры текста, а не просто с генерацией следующего токена. Его можно дообучить LoRA на обычной RTX 5090 за пару часов.

Как НЕ надо делать (мой грабли)

Первая попытка — взять готовый spaCy sentencizer и резать по предложениям. Звучит логично? Но spaCy не понимает, что длинный список — это один чанк, а заголовок — граница. Я получил чанки по 2-3 слова и кучу мелких, от которых RAG загибался.

Вторая ошибка — резать по фиксированному числу токенов с наложением. Теряется цельность идеи, особенно в юридических текстах (вспомните статью про LightRAG в юридическом домене — там та же беда).

Сбор датасета для обучения

Нам нужны размеченные данные: исходный текст и правильные границы чанков (индексы в символах или токенах).

  • Источники: русская Википедия (дамп 2026), новостные статьи, юридические документы, техническая документация.
  • Разметка: полная автоматизация. Берем walkdown по DOM-дереву (если HTML) или разбиваем по пустым строкам и заголовкам Markdown. Получаем «идеальные» чанки.
  • Аугментация: добавляем шум — случайные переносы строк внутри предложения, лишние пробелы. Модель должна выучить нормальные границы.

Размер датасета: 50 000 документов по 10 000 символов. Примерно 500M токенов. Достаточно для LoRA.

💡
Для быстрого создания датасета используйте инструмент Fine-tuning из PDF за 5 минут — парсинг PDF в структурированный текст с сохранением границ.

Обучение LoRA на RTX 5090

Берем T-lite-it-2.1 (загружаем из HuggingFace) и дообучаем через PEFT/LoRA.

from transformers import AutoModelForTokenClassification, AutoTokenizer
from peft import LoraConfig, get_peft_model

base_model = "T-lite/t-lite-it-2.1"  # актуально на 04.07.2026
tokenizer = AutoTokenizer.from_pretrained(base_model)
model = AutoModelForTokenClassification.from_pretrained(base_model, num_labels=2)  # граница/не граница

lora_config = LoraConfig(
    r=8,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="TOKEN_CLS"
)
model = get_peft_model(model, lora_config)

Тренировка занимает 2.5 часа на RTX 5090 (32GB VRAM). Параметры: batch size 4, gradient accumulation 8, learning rate 2e-4, 3 эпохи.

Важно: не забудьте обработать раздувание токенов в русском языке. T-lite-it-2.1 использует BPE (128k токенов). Для русского текста средняя длина токена ~1.8 символа против 2.5 в английском — модель быстрее «запоминает» границы. Подробнее — в статье про раздувание токенов.

Конвертация в GGUF для деплоя на AMD

LoRA веса сливаем с base моделью, затем экспортируем в GGUF через convert-lora-to-gguf.py.

python convert-lora-to-gguf.py \
    --base-model T-lite/t-lite-it-2.1 \
    --lora-weights ./lora-weights \
    --output ./t-lite-splitter.q4_k_m.gguf \
    --quantization q4_k_m

Флаг --quantization дает q4_k_m — отличный баланс скорости/качества на AMD. На AMD Radeon RX 7900 XTX (RDNA 3) через llama.cpp получаем ~5000 токенов/сек при инференсе.

Если вы используете старое железо, как в статье про GTX 1060 и Xeon — берите q3_k_m или q2_k. Потери точности в пределах 2%.

Деплой: llama.cpp + REST API

Запускаем модель как сервер, который принимает текст и возвращает индексы границ.

./llama-server \
    -m ./t-lite-splitter.q4_k_m.gguf \
    --host 0.0.0.0 \
    --port 8080 \
    --n-gpu-layers 99

С клиента:

import requests, json

resp = requests.post("http://localhost:8080/v1/completions", json={
    "prompt": "Текст документа...",
    "max_tokens": 1,  # нам нужно только одно предсказание для каждого токена
    "logprobs": True
})
# Из logprobs извлекаем вероятности для метки "граница" (class 1)
# Там где вероятность > 0.5 — режем

Интеграция с RAG (LangChain + ChromaDB)

Пишем кастомный TliteTextSplitter:

from langchain.text_splitter import TextSplitter

class TliteSplitter(TextSplitter):
    def __init__(self, splitter_url, chunk_size=512, **kwargs):
        super().__init__(**kwargs)
        self.splitter_url = splitter_url
        self.chunk_size = chunk_size

    def split_text(self, text):
        # отправляем текст в нашу модель, получаем границы
        boundaries = get_boundaries(text, self.splitter_url)
        # режем по границам, стараясь не превышать chunk_size токенов
        chunks = self._chunk_by_boundaries(text, boundaries, self.chunk_size)
        return chunks

Подключаем в классический RAG-пайплайн из нашего туториала.

Замеры производительности и точности

Метод Precision (границы) Recall (границы) F1
spaCy sentencizer 0.72 0.85 0.78
RecursiveCharacterTextSplitter (1000/200) 0.45 0.38 0.41
T-lite splitter (наш) 0.94 0.92 0.93

Скорость: наш сплиттер обрабатывает 10 000 символов (~2500 токенов) за 0.03 секунды на RTX 5090 или 0.12 сек на AMD RX 7900. Для RAG это незаметно.

Дополнительное ускорение: перед отправкой в LLM можно применить regex-фильтрацию, как в статье Decompose: ускорьте RAG в 70 раз. Мы делаем splitter + фильтр — получаем lossless чанки без мусора.

Возможные ошибки и их решение

  • Модель «забывает» границы на длинных текстах. Решение: используйте скользящее окно с перекрытием (overlap = 10%). Мы предсказываем границы на каждой позиции, но учитываем только те, что не ближе 10% от краев.
  • Модель путает заголовки HTML с границами. Перед подачей удалите разметку или обучайте на raw-тексте.
  • На AMD падает производительность. Убедитесь, что используете последний llama.cpp (v3180+). Включите --no-mmap для AMD.
  • Сплиттер режет по всем точкам, включая 3.14. Модель плохо обобщает числа. Добавьте в датасет побольше примеров с цифрами и разными знаками препинания.

Что еще можно сделать?

Наш подход index-output можно применить не только для сплиттинга, но и для суффиксного преобразования текста (как в Polyglot-r2). Достаточно обучить модель на парах «исходный текст → границы замен». Но это уже другая история.

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

Подписаться на канал