Чанкінг документів для RAG (Recursive, Semantic, Sentence-level)

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1566 послуг
Чанкінг документів для RAG (Recursive, Semantic, Sentence-level)
Середній
від 1 дня до 3 днів
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1284
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1196
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    901
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    586
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

Чанкування документів для RAG (Рекурсивне, Семантичне, на рівні речень)

Чанкування — розбиття документів на фрагменти для індексації у векторну БД. Розмір і межі чанків критично впливають на якість RAG: занадто малі фрагменти втрачають контекст, занадто великі — знижують точність пошуку та перевищують контекстне вікно моделі.

Стратегії чанкування

Чанкування фіксованого розміру — найпростіше, найгірше:

def fixed_size_chunk(text: str, chunk_size: int = 500,
                     overlap: int = 50) -> list[str]:
    tokens = text.split()  # Спрощено
    chunks = []
    for i in range(0, len(tokens), chunk_size - overlap):
        chunk = ' '.join(tokens[i:i + chunk_size])
        chunks.append(chunk)
    return chunks

Проблема: розрізає речення та абзаци посередині.

Рекурсивний розбиває текст символами (LangChain) — розбиває за ієрархією розділювачів:

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # ~250 слів
    chunk_overlap=200,     # 50-словне перекриття
    separators=[
        "\n\n",  # Параграфи (пріоритет)
        "\n",    # Рядки
        ". ",    # Речення
        ", ",    # Частини речень
        " ",     # Слова (останній варіант)
        ""       # Символи
    ]
)

chunks = splitter.create_documents(
    texts=[document_text],
    metadatas=[{"source": "document.pdf", "page": 1}]
)

Семантичне чанкування — розбиття за смисловими межами:

from sentence_transformers import SentenceTransformer
import numpy as np

class SemanticChunker:
    def __init__(self, model_name: str = 'all-MiniLM-L6-v2',
                 threshold: float = 0.7):
        self.model = SentenceTransformer(model_name)
        self.threshold = threshold

    def chunk(self, text: str) -> list[str]:
        # Розбиття на речення
        sentences = self._split_into_sentences(text)
        if len(sentences) < 2:
            return [text]

        # Вбудовування речень
        embeddings = self.model.encode(sentences)

        # Пошук смислових розривів
        chunks = []
        current_chunk = [sentences[0]]

        for i in range(1, len(sentences)):
            # Косинусна подібність сусідніх речень
            sim = np.dot(embeddings[i], embeddings[i-1]) / (
                np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1])
            )

            if sim < self.threshold:
                # Смисловий розрив — створюємо новий чанк
                chunks.append(' '.join(current_chunk))
                current_chunk = []

            current_chunk.append(sentences[i])

        if current_chunk:
            chunks.append(' '.join(current_chunk))

        # Об'єднання занадто малих чанків
        return self._merge_small_chunks(chunks, min_words=50)

Чанкування з урахуванням структури документа — збереження ієрархії документа:

class StructureAwareChunker:
    def chunk_markdown(self, text: str, max_chunk_tokens: int = 300) -> list[dict]:
        """Розбиття з дотриманням заголовків Markdown"""
        sections = re.split(r'\n(#{1,3}\s+.+)', text)
        chunks = []
        current_section_header = "Introduction"

        for part in sections:
            if re.match(r'#{1,3}\s+', part):
                current_section_header = part.strip()
            else:
                # Розбиваємо розділ на під-чанки, якщо він великий
                sub_chunks = self._split_section(part, max_chunk_tokens)
                for sub_chunk in sub_chunks:
                    if sub_chunk.strip():
                        chunks.append({
                            'text': sub_chunk,
                            'section': current_section_header,
                            # Хлібні крошки для атрибуції
                            'breadcrumb': current_section_header
                        })

        return chunks

Оптимальні параметри чанкування за типом контенту

Тип документа Стратегія Розмір чанку Перекриття
Технічна документація Структурне 500-1000 100-200
Наукові статті Семантичне 800-1500 150-300
FAQ / Q&A За запитаннями 100-300 0
Код За функціями Змінний 0
Новини/блоги Рекурсивне 400-800 80-150
Чати За сесіями 300-700 50

Метадані чанків та індексація батько-дитина

Пошук від малого до великого — індексуємо малі чанки для точного пошуку, але передаємо великі батьківські чанки в контекст:

class ParentChildIndexer:
    def index(self, document: str) -> list[dict]:
        # Батьківські чанки (великі, для контексту)
        parent_splitter = RecursiveCharacterTextSplitter(
            chunk_size=2000, chunk_overlap=200
        )
        parents = parent_splitter.split_text(document)

        all_chunks = []
        for p_idx, parent in enumerate(parents):
            # Дочірні чанки (малі, для пошуку)
            child_splitter = RecursiveCharacterTextSplitter(
                chunk_size=300, chunk_overlap=50
            )
            children = child_splitter.split_text(parent)

            for child in children:
                all_chunks.append({
                    'child_text': child,       # Для вбудовування та пошуку
                    'parent_text': parent,     # Для передачі в LLM
                    'parent_idx': p_idx
                })

        return all_chunks

Правильний вибір стратегії чанкування покращує релевантність пошуку на 15-30% порівняно з наївним підходом фіксованого розміру.