AI Engineering Academy · Урок

Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия

Реализуйте и сравните разделители текста фиксированного размера, по границам предложений и рекурсивные разделители, а также поймёте, как размер фрагмента и перекрытие влияют на качество поиска.

Урок 2 из 413 шагов

«Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.

Почему качество разбиения на фрагменты важно

Разбиение на фрагменты — это процесс разделения загруженных документов на небольшие части, которые помещаются в контекстное окно LLM и могут индексироваться и извлекаться по отдельности. Способ разбиения влияет на качество поиска сильнее, чем почти любой другой фактор. Если ответ разделён между двумя частями, ни одна из них по отдельности не будет достаточной для ответа на вопрос. Если фрагмент объединяет две несвязанные темы, он будет найден по вопросам о каждой из них, но не окажется полезным ни для одной.

Разбиение на фрагменты фиксированного размера

Разбиение на фрагменты фиксированного размера разделяет текст на фрагменты ровно по N символов или N токенов, независимо от границ предложений и абзацев. Это самая простая стратегия, которую легко реализовать. Главный недостаток заключается в том, что она часто разрывает предложения пополам, создавая фрагменты, начинающиеся или заканчивающиеся в середине мысли. Такой подход приемлем для плотного текста с единообразным форматированием, например экспортированных данных из базы, но даёт низкое качество поиска для повествовательной прозы или технической документации.

def fixed_size_chunks(text, chunk_size=500, overlap=50):
    '''Split text into fixed-size character chunks with overlap'''
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        start += chunk_size - overlap  # overlap keeps context at boundaries
    return chunks

example = 'This is a long document. ' * 100
chunks = fixed_size_chunks(example, chunk_size=200, overlap=20)
print(f'Produced {len(chunks)} chunks, first: {chunks[0][:80]}...')

Добавление перекрытия к фрагментам фиксированного размера

Ключевое улучшение разбиения на фрагменты фиксированного размера — это перекрытие: каждый фрагмент содержит N символов из предыдущего фрагмента. Благодаря этому информация около границы фрагмента присутствует в обоих соседних фрагментах. При перекрытии в 50–100 токенов предложение, пересекающее границу, будет полностью сохранено хотя бы в одном из двух фрагментов. Большее перекрытие улучшает охват, но увеличивает размер индекса и количество дублирующегося содержимого в результатах поиска.

# Overlap example with a sentence at the boundary
text = 'A B C D E F G H I J'
chunk_size = 6  # characters
overlap = 2

i = 0
while i < len(text):
    print(repr(text[i:i+chunk_size]))
    i += chunk_size - overlap

# Output shows overlapping content:
# 'A B C D'
# 'C D E F'
# 'E F G H'
# Each adjacent pair shares 2 chars

Разбиение по границам предложений

Разбиение по границам предложений использует библиотеки NLP, например nltk или spacy, чтобы определить окончания предложений до разделения текста. Это гарантирует, что ни одно предложение не будет разорвано пополам. Предложения накапливаются до тех пор, пока добавление следующего не превысит целевой размер, после чего начинается новый фрагмент. В результате фрагменты всегда содержат завершённые мысли, что обеспечивает значительно более высокое качество векторных представлений по сравнению с разбиением по фиксированному числу символов.

import nltk
nltk.download('punkt', quiet=True)

def sentence_chunks(text, max_tokens=300):
    sentences = nltk.sent_tokenize(text)
    chunks = []
    current = []
    current_len = 0

    for sent in sentences:
        sent_tokens = len(sent.split())
        if current_len + sent_tokens > max_tokens and current:
            chunks.append(' '.join(current))
            current = []
            current_len = 0
        current.append(sent)
        current_len += sent_tokens

    if current:
        chunks.append(' '.join(current))
    return chunks

Рекурсивное разбиение по символам

Рекурсивное разбиение по символам — наиболее широко используемая стратегия в промышленных системах RAG. Она последовательно проверяет иерархию разделителей: сначала разрывы абзацев (\n\n), затем переводы строк (\n), точки в конце предложений, пробелы и, наконец, отдельные символы. Текст разделяется по самой крупной осмысленной границе, при которой фрагмент не превышает целевой размер, поэтому структура документа сохраняется настолько, насколько это возможно.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,       # target size in characters
    chunk_overlap=200,     # overlap between consecutive chunks
    separators=['\n\n', '\n', '. ', '! ', '? ', ' ', ''],
    length_function=len
)

with open('document.txt') as f:
    text = f.read()

chunks = splitter.split_text(text)
print(f'Split into {len(chunks)} chunks')
for i, chunk in enumerate(chunks[:3]):
    print(f'Chunk {i}: {len(chunk)} chars — {chunk[:60]}...')

Разбиение с учётом токенов

Количество символов — неточный заменитель количества токенов. Фрагмент из 1000 символов может содержать 200 или 400 токенов в зависимости от длины слов и языка. Для точного контроля разделяйте текст по количеству токенов с помощью tiktoken. Это важно для подгонки фрагментов под контекстное окно модели и точной оценки стоимости. TokenTextSplitter из LangChain обеспечивает разбиение с учётом токенов, используя tiktoken.

from langchain_text_splitters import TokenTextSplitter
import tiktoken

# Split by actual token count, not characters
splitter = TokenTextSplitter(
    encoding_name='cl100k_base',  # GPT-4 tokenizer
    chunk_size=256,               # target tokens per chunk
    chunk_overlap=32              # overlap in tokens
)

chunks = splitter.split_text(text)

# Verify token count
enc = tiktoken.get_encoding('cl100k_base')
for chunk in chunks[:3]:
    tokens = len(enc.encode(chunk))
    print(f'Chunk: {tokens} tokens')

Выбор подходящего размера фрагмента

Размер фрагмента — важный гиперпараметр. Небольшие фрагменты (100–200 токенов) точны: они содержат узко сфокусированную информацию, для которой хорошо создаются векторные представления. Однако они теряют окружающий контекст, поэтому LLM может не получить достаточно информации для ответа. Большие фрагменты (500–1000 токенов) дают больше контекста, но их векторные представления усредняются по более широкой теме, из-за чего такие фрагменты сложнее находить по конкретным запросам. В большинстве промышленных систем используется 256–512 токенов, а окончательный выбор проверяется на собственных данных.

Добавление контекста к фрагментам

Мощное улучшение — это контекстные заголовки фрагментов: добавляйте название документа и заголовок раздела в начало текста каждого фрагмента перед созданием векторного представления. Благодаря этому векторное представление отражает не только содержимое фрагмента, но и его место в документе. Фрагмент Преимущества и политика отпусков: сотрудники ежегодно получают 15 дней... будет значительно точнее найден по вопросам о политике отпусков, чем тот же текст без заголовка.

def create_contextual_chunks(doc, splitter):
    title = doc['metadata'].get('title', '')
    section = doc['metadata'].get('section', '')
    text = doc['text']

    raw_chunks = splitter.split_text(text)
    contextual_chunks = []
    for chunk in raw_chunks:
        # Prepend document context to each chunk
        context_header = f'{title}\n{section}\n\n' if title else ''
        contextual_chunks.append({
            'text': context_header + chunk,
            'metadata': doc['metadata']
        })
    return contextual_chunks

Сравнение стратегий на собственных данных

Ни одна стратегия разбиения на фрагменты не является универсально лучшей. Проведите быструю оценку: возьмите 20 вопросов, ответы на которые вам известны, выполните поиск с каждой стратегией разбиения и измерьте, как часто правильный фрагмент попадает в первые 5 результатов (доля попаданий@5). Такая проверка занимает час, но избавляет от недель догадок. Часто оказывается, что конкретный формат документов (плотный юридический текст или структурированная техническая документация) явно отдаёт предпочтение одной стратегии перед другими.

def evaluate_chunking_strategy(questions_and_answers, retriever):
    hits = 0
    for qa in questions_and_answers:
        results = retriever.retrieve(qa['question'], top_k=5)
        result_texts = [r['text'] for r in results]
        # Check if answer text appears in any retrieved chunk
        if any(qa['answer'] in text for text in result_texts):
            hits += 1
    hit_rate = hits / len(questions_and_answers)
    print(f'Hit rate@5: {hit_rate:.1%} ({hits}/{len(questions_and_answers)})')
    return hit_rate

Работа с особыми типами документов

Некоторым типам документов требуется специальное разбиение. Файлы с кодом следует разделять по границам функций или классов, а не по количеству символов. Файлы Markdown следует разделять по границам заголовков, чтобы каждый фрагмент соответствовал одному разделу. Таблицы нужно сохранять целиком в одном фрагменте (разделение внутри таблицы делает содержимое непонятным). Планируйте стратегию разбиения с учётом типов документов в вашей коллекции, а не применяйте один универсальный разделитель.

from langchain_text_splitters import MarkdownHeaderTextSplitter

# Split Markdown by header hierarchy
md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ('#', 'h1'),
        ('##', 'h2'),
        ('###', 'h3')
    ]
)

with open('documentation.md') as f:
    md_text = f.read()

# Each chunk gets metadata from its heading hierarchy
chunks = md_splitter.split_text(md_text)
for chunk in chunks[:3]:
    print('Section:', chunk.metadata)
    print('Text:', chunk.page_content[:80])
    print()

Отслеживание происхождения фрагментов

Каждому фрагменту нужен стабильный уникальный идентификатор, полученный из исходного документа и позиции фрагмента. Используйте этот идентификатор, чтобы обновлять отдельные фрагменты при изменении документов, не переиндексируя всю коллекцию. Хорошо подходит детерминированный хеш пути к источнику и индекса фрагмента. Также сохраняйте в метаданных позицию фрагмента (фрагмент 3 из 12 документа X) — это помогает повторно собрать полные разделы, когда извлечены несколько соседних фрагментов.

import hashlib

def assign_chunk_ids(chunks, source_path):
    for i, chunk in enumerate(chunks):
        key = f'{source_path}::chunk_{i}'
        chunk_id = hashlib.sha256(key.encode()).hexdigest()[:16]
        chunk['id'] = chunk_id
        chunk['metadata']['chunk_index'] = i
        chunk['metadata']['total_chunks'] = len(chunks)
    return chunks

# IDs are stable across re-runs if source and position match
chunks = sentence_chunks(text)
chunks = [{'text': c, 'metadata': {}} for c in chunks]
assign_chunk_ids(chunks, 'docs/policy_v3.pdf')

Быстрая проверка

Проверьте своё понимание концепций инженерии ИИ из этого урока.

Итоги урока

В этом уроке вы узнали, что разбиение на фрагменты фиксированного размера просто, но разрывает предложения пополам; разбиение по границам предложений сохраняет завершённые мысли; рекурсивное разбиение по символам учитывает структуру документа и чаще всего выбирается для промышленных систем; а также об продвинутых методах, включая разбиение с учётом токенов, контекстные заголовки, специальные разделители для Markdown и кода и стабильные идентификаторы фрагментов для поэтапных обновлений. Далее мы создадим векторные представления этих фрагментов и сохраним их в векторной базе данных.

Можно начать бесплатно

Изучай Python с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
30
Уроки
120

Часто задаваемые вопросы

Урок «Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия» бесплатный?

Да — полный текст урока «Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.

Чему я научусь в уроке «Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия»?

Реализуйте и сравните разделители текста фиксированного размера, по границам предложений и рекурсивные разделители, а также поймёте, как размер фрагмента и перекрытие влияют на качество поиска. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Engineering Academy?

Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Engineering Academy?

Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Загрузка документов и извлечение текста
  2. Стратегии разбиения: фиксированные фрагменты, предложения и рекурсия
  3. Индексирование: создание и сохранение фрагментов
  4. Запрос, поиск и генерация
← Назад к AI Engineering Academy