Чанкування документів для 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% порівняно з наївним підходом фіксованого розміру.







