Почему обычный RAG ломается на русском. nlp.. nlp. python.. nlp. python. qa.. nlp. python. qa. rag.. nlp. python. qa. rag. большие языковые модели.. nlp. python. qa. rag. большие языковые модели. Машинное обучение.
Почему обычный RAG ломается на русском - 1

RAG (Retrieval-Augmented Generation) — это не одна технология, а архитектурный приём: мы соединяем поиск по базе знаний (retrieval) с генерацией текста (generation).
На английском всё работает прилично, а вот на русском начинаются приключения.

Причины банальны:

  • Морфология. У нас «книга», «книги», «книгой», «о книгах» — это всё одно слово, но простая векторная модель не понимает, что это так. Без лемматизации теряется до 40% качества поиска.

  • Токенизация. Большинство моделей обучались на латинице. Кириллица часто рвётся на абсурдные куски.

  • Дефицит данных. Русскоязычные корпуса и бенчмарки появились недавно — ruMTEB, RusBEIR, DRAGON — но выбор всё ещё ограничен.

Если просто подключить базовый RAG через LangChain и E5, получится система, которая в лучшем случае “угадывает”.
На DRAGON-бенчмарке такие модели показывают faithfulness ≈ 0.55, то есть почти половина ответов содержит галлюцинации.
Для серьёзного продакшена это неприемлемо.

Advanced RAG

Чтобы модель не врала, нужно не просто “прикрутить” поиск, а выстроить полноценный pipeline с несколькими уровнями контроля.

Ключевые компоненты:

  1. Семантическое чанкирование.
    Вместо тупого деления по 1000 символов используем sentence-aware разбиение (например, с razdel) и перекрытие на 150 токенов. Это даёт до +18% контекстного recall без роста времени ответа.

  2. Русскоязычные эмбеддинги.
    Используем не просто multilingual-e5, а специализированные модели вроде BorisTM/bge-m3_en_ru — прирост Recall@10 до 83%.
    Да, они тяжелее, но выигрыш огромный.

  3. Гибридный поиск (BM25 + векторы).
    Классический поиск по ключевым словам и семантический поиск должны работать вместе. Это особенно помогает при запросах с редкой терминологией.

  4. Кросс-энкодер для rerank-а.
    После того как найдено 50 кандидатов, мы переоцениваем их моделью, которая “понимает” контекст.
    Прирост точности по Top-1 — до 20%, а задержка — всего 30 мс на RTX 4090.

  5. RAG-Fusion для сложных (“multi-hop”) вопросов.
    Когда нужно соединить несколько фактов, обычный RAG путается.
    Трюк: генерируем несколько перефразов запроса, ищем по каждому и объединяем результаты с помощью Reciprocal Rank Fusion.
    Faithfulness растёт ещё на 10 пунктов.

Пример из практики

Представьте себе базу знаний, содержащую предложение: «Инструкция по сборке лежит на столе». Пользователь спрашивает: «Где инструкция по сборке?». Наивная система RAG, использующая базовую многоязычную модель встраивания, может извлечь нерелевантные документы об офисной мебели, поскольку вектор для слова «столе» (на столе, предложный падеж) недостаточно близок к подразумеваемому в запросе слову «стол» (стол, именительный падеж). В этом случае система LLM, учитывая неточный контекст, вынуждена либо заявить, что не знает ответа, либо, что ещё хуже, фантазировать. Это классический пример низкого качества поиска, критического состояния сбоя в российских системах RAG.

Практическое занятие

В этом разделе представлен основной код для построения нашей системы RAG, отражающий структуру сопутствующего репозитория. Полный план проекта включает отдельные скрипты и блокноты для каждого этапа.Основа2

Прием и очистка данных

Сначала мы загружаем и очищаем наши русскоязычные документы из каталога.

# This code assumes you have installed the libraries from requirements.txt
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, UnstructuredFileLoader
from razdel import sentenize
import re

print("Libraries imported successfully.")

# Path to the directory containing your Russian knowledge base
DATA_PATH = 'data/knowledge_base'

# Using DirectoryLoader to load documents. It can be configured to handle different file types.
# For this example, we'll focus on PDFs.
loader = DirectoryLoader(DATA_PATH, glob="**/*.pdf", loader_cls=PyPDFLoader, show_progress=True)

documents = loader.load()
print(f"Loaded {len(documents)} documents from {DATA_PATH}")

# A simple text cleaning function
def clean_text(text):
 # Remove excessive newlines and spaces
 text = re.sub(r'n+', 'n', text)
 text = re.sub(r's+', ' ', text)
 return text.strip()

# Clean the content of each document
for doc in documents:
 doc.page_content = clean_text(doc.page_content)

Семантический фрагментатор сrazdel

Затем мы применяем нашу передовую стратегию фрагментации, чтобы разбить документы, соблюдая при этом границы предложений.

# Initialize the text splitter
# chunk_size: The maximum size of a chunk (in characters). Tune this based on your embedding model's context window.
# chunk_overlap: The number of characters to overlap between chunks. This helps maintain context across chunks.
text_splitter = RecursiveCharacterTextSplitter(
 chunk_size=1000,
 chunk_overlap=150,
 length_function=len, # We use character length here, but token length can also be used.
 separators=["nn", "n", ". ", " ", ""] # Tries to split on paragraphs, then lines, then sentences.
)

# Split the documents into chunks
chunks = text_splitter.split_documents(documents)

print(f"Split {len(documents)} documents into {len(chunks)} chunks.")

# Let's inspect a chunk to see the result
if chunks:
 print("n--- Example Chunk ---")
 print(chunks[0].page_content)
 print("n--- Metadata ---")
 print(chunks[0].metadata)

Встраивание и индекс FAISS

Теперь мы выбираем нашу модель встраивания, генерируем встраивания для фрагментов и сохраняем их в хранилище векторов FAISS.

import torch
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings

# Define the device to use (GPU if available, otherwise CPU)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Using device: {device}")

# Define the embedding model from Hugging Face
# 'intfloat/multilingual-e5-large' is a strong choice for multilingual tasks including Russian.
model_name = "intfloat/multilingual-e5-large"

# It's important to normalize embeddings for this model
model_kwargs = {'device': device}
encode_kwargs = {'normalize_embeddings': True}

embeddings = HuggingFaceEmbeddings(
 model_name=model_name,
 model_kwargs=model_kwargs,
 encode_kwargs=encode_kwargs
)

print(f"Embedding model '{model_name}' loaded successfully.")

# Define the path to save the FAISS index
DB_FAISS_PATH = 'vectorstore/db_faiss'

# Create the FAISS vector store from the document chunks and embeddings
print("Creating FAISS vector store... This may take a while.")
vectordb = FAISS.from_documents(documents=chunks, embedding=embeddings)

# Save the vector store locally
vectordb.save_local(DB_FAISS_PATH)

print(f"FAISS index created and saved to {DB_FAISS_PATH}")

Конструкция цепи LCEL RAG

Когда компоненты готовы, мы используем язык выражений LangChain (LCEL) для построения финального конвейера.

from langchain_community.llms import LlamaCpp
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser

# --- Load Components ---
vectordb = FAISS.load_local(DB_FAISS_PATH, embeddings, allow_dangerous_deserialization=True)
retriever = vectordb.as_retriever(search_kwargs={'k': 4}) # Retrieve top 4 chunks

llm = LlamaCpp(
 model_path="/path/to/your/model.gguf", # Provide path to your GGUF model
 n_gpu_layers=-1, n_ctx=4096, f16_kv=True, temperature=0.1
)

# --- Create Prompt Template ---
prompt_template_str = """
Используй следующий контекст, чтобы ответить на вопрос. Если ты не знаешь ответ, просто скажи, что не знаешь. Не пытайся выдумать ответ. Отвечай на русском языке.

Контекст: {context}

Вопрос: {question}

Ответ:
"""
prompt = PromptTemplate(template=prompt_template_str, input_variables=["context", "question"])

# --- Build RAG Chain ---
def format_docs(docs):
 return "nn".join(doc.page_content for doc in docs)

rag_chain = (
 {"context": retriever | format_docs, "question": RunnablePassthrough()}
 | prompt
 | llm
 | StrOutputParser()
)

print("RAG chain created successfully.")

Выполнение вашего первого запроса

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

# Example query in Russian
query = "Что такое эффект Доплера и где он применяется?"

print(f"nQuerying the RAG chain with: '{query}'")

# Invoke the chain to get the answer
answer = rag_chain.invoke(query)

print("n--- Generated Answer ---")
print(answer)

requirements.txt

torch==2.1.0
transformers==4.36.2
sentence-transformers==2.2.2
accelerate==0.25.0

langchain==0.1.0

unstructured==0.12.0
pypdf==3.17.4

razdel==0.5.0
pymorphy2==0.9.1

# Vector store
faiss-gpu==1.7.2

# LLM inference
llama-cpp-python==0.2.20
bitsandbytes==0.41.3

ragas==0.0.22
python-dotenv==1.0.0

Спасибо за прочтение!

Автор: Nikuson

Источник

Rambler's Top100