0Pricing
AI Engineering Academy · Урок

Стратегии для кода и HTML с учётом типа документа

Применяйте специализированное разбиение: для кода Python — разделители функций на основе AST, для HTML — анализаторы с учётом тегов, а для Markdown — иерархию заголовков.

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

Почему универсальное разбиение не подходит для специализированных документов

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

Разбиение кода Python на основе AST

Абстрактное синтаксическое дерево (AST) файла Python представляет каждую функцию, класс и модуль в виде структурированного узла. Обходя AST, можно извлечь каждую функцию или метод в отдельный фрагмент, сохранив вместе сигнатуру, строку документации и тело. PythonCodeTextSplitter из LangChain использует этот подход внутри.

import ast
import textwrap

def extract_functions(source_code: str) -> list[dict]:
    tree = ast.parse(source_code)
    chunks = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            start = node.lineno - 1
            end = node.end_lineno
            lines = source_code.splitlines()[start:end]
            chunks.append({
                'name': node.name,
                'code': '\n'.join(lines),
                'start_line': node.lineno,
            })
    return chunks

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

Для объектно-ориентированных кодовых баз разбиение на уровне класса часто лучше, чем разбиение на уровне функции. Фрагмент класса сохраняет связь между методами и общим состоянием, с которым они работают. Можно включить строку документации класса и тела всех методов в один фрагмент, а затем создавать отдельные более мелкие фрагменты только для длинных методов.

from langchain_text_splitters import Language, RecursiveCharacterTextSplitter

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=100,
)

with open('my_module.py', 'r') as f:
    source = f.read()

chunks = python_splitter.create_documents([source])
print(f'Created {len(chunks)} code chunks')

Добавление метаданных кода во фрагменты

Необработанные фрагменты кода полезны лишь настолько, насколько полезны их метаданные. При сохранении фрагментов кода в векторной базе данных указывайте путь к файлу, имя функции, язык программирования и диапазон строк. Эти метаданные позволяют средству поиска фильтровать результаты по языку или файлу, а LLM — указывать в ответе точное расположение источника.

from langchain_core.documents import Document

def chunk_python_file(filepath: str) -> list[Document]:
    with open(filepath) as f:
        source = f.read()

    functions = extract_functions(source)  # from previous example
    docs = []
    for fn in functions:
        docs.append(Document(
            page_content=fn['code'],
            metadata={
                'source': filepath,
                'function': fn['name'],
                'language': 'python',
                'start_line': fn['start_line'],
            }
        ))
    return docs

HTML: структура важнее количества символов

Документы HTML имеют иерархическую структуру, состоящую из заголовков, разделов, абзацев и списков. Разбиение HTML по количеству символов часто проходит прямо по тегам, создавая некорректные фрагменты. Правильный подход — разобрать HTML с помощью полноценного анализатора, например BeautifulSoup, и извлечь семантически значимые элементы, такие как теги <article>, <section> и <p>.

from bs4 import BeautifulSoup

def chunk_html_by_section(html: str) -> list[dict]:
    soup = BeautifulSoup(html, 'html.parser')
    chunks = []
    for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'li']):
        text = tag.get_text(separator=' ', strip=True)
        if len(text) > 40:  # skip trivial fragments
            chunks.append({
                'tag': tag.name,
                'text': text,
            })
    return chunks

Иерархическое разбиение HTML по заголовкам

Более сложная стратегия для HTML группирует содержимое под ближайшим заголовком. Каждый абзац и список, следующие за заголовком <h2>, относятся к этому разделу. Группируя текст вместе с родительским заголовком, Вы сохраняете тематический контекст, который отдельный абзац иначе утратил бы. HTMLHeaderTextSplitter из LangChain реализует это автоматически.

from langchain_text_splitters import HTMLHeaderTextSplitter

headers_to_split_on = [
    ('h1', 'Header 1'),
    ('h2', 'Header 2'),
    ('h3', 'Header 3'),
]

splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

with open('page.html') as f:
    html = f.read()

sections = splitter.split_text(html)
for sec in sections[:3]:
    print(sec.metadata)
    print(sec.page_content[:200])
    print('---')

Markdown: сохранение иерархии заголовков

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

from langchain_text_splitters import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ('#', 'H1'),
    ('##', 'H2'),
    ('###', 'H3'),
]

md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

with open('README.md') as f:
    markdown = f.read()

docs = md_splitter.split_text(markdown)
for doc in docs[:2]:
    print('Metadata:', doc.metadata)
    print('Content:', doc.page_content[:300])
    print()

Вторичное разбиение после разбиения по заголовкам

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

from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter

header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[('#', 'H1'), ('##', 'H2')]
)
char_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)

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

header_chunks = header_splitter.split_text(md)
final_chunks = char_splitter.split_documents(header_chunks)
print(f'{len(final_chunks)} final chunks produced')

Разбиение PDF с учётом таблиц

PDF-файлы, извлечённые с помощью таких инструментов, как PyMuPDF или pdfplumber, часто теряют структуру таблиц, превращаясь в искажённые строки текста. Чтобы решить эту проблему, используйте анализаторы PDF с учётом макета, которые обнаруживают ограничивающие рамки таблиц и преобразуют их в формат Markdown или CSV перед разбиением. Обрабатывайте каждую таблицу как отдельный фрагмент со структурированными метаданными, указывающими, что это таблица, а не обычный текст.

import pdfplumber

def extract_pdf_chunks(pdf_path: str) -> list[dict]:
    chunks = []
    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages):
            # Extract tables separately
            for table in page.extract_tables():
                rows = ['|'.join(str(c) for c in row) for row in table]
                chunks.append({
                    'type': 'table',
                    'content': '\n'.join(rows),
                    'page': page_num + 1,
                })
            # Extract prose text
            text = page.extract_text()
            if text:
                chunks.append({'type': 'text', 'content': text, 'page': page_num + 1})
    return chunks

Определение языка для смешанных корпусов

Корпоративные базы знаний часто содержат файлы разных типов: скрипты Python, документацию API в HTML, архитектурные заметки в Markdown и экспортированные данные в CSV. Надёжный конвейер разбиения должен определять тип файла по расширению или типу MIME и направлять каждый документ подходящему специализированному средству разбиения. Это предотвращает применение логики разбиения кода к обычному тексту и наоборот.

from pathlib import Path

def route_document(filepath: str) -> list[dict]:
    ext = Path(filepath).suffix.lower()
    if ext == '.py':
        return chunk_python_file(filepath)
    elif ext in ('.html', '.htm'):
        with open(filepath) as f:
            return chunk_html_by_section(f.read())
    elif ext == '.md':
        # use MarkdownHeaderTextSplitter
        return chunk_markdown(filepath)
    elif ext == '.pdf':
        return extract_pdf_chunks(filepath)
    else:
        # fallback: plain text recursive splitter
        return chunk_plain_text(filepath)

Сохранение контекста с помощью окружающих строк

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

def chunk_with_imports(source_code: str, fn_node, lines: list[str]) -> str:
    # Gather top-of-file imports (first block before first non-import)
    import_lines = []
    for line in lines:
        stripped = line.strip()
        if stripped.startswith('import ') or stripped.startswith('from '):
            import_lines.append(line)
        elif stripped and not stripped.startswith('#'):
            break

    fn_body = '\n'.join(lines[fn_node.lineno - 1:fn_node.end_lineno])
    return '\n'.join(import_lines) + '\n\n' + fn_body

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

Проверьте, насколько хорошо Вы поняли стратегии разбиения документов для отдельных типов данных в этом уроке.

Итоги урока

В этом уроке Вы узнали, что разбиение на основе AST сохраняет границы функций и классов Python, HTMLHeaderTextSplitter и MarkdownHeaderTextSplitter учитывают иерархию заголовков, сохраняя связь контекста с разделом, а двухэтапный подход (разбиение по заголовкам с последующим разбиением по символам) обрабатывает слишком большие разделы без потери структурированных метаданных. Далее мы рассмотрим гибридный поиск, объединяющий плотный и разреженный поиск.

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

Урок «Стратегии для кода и HTML с учётом типа документа» бесплатный?

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

Чему я научусь в уроке «Стратегии для кода и HTML с учётом типа документа»?

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

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

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

Сколько времени занимает урок «Стратегии для кода и HTML с учётом типа документа»?

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

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

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

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

  1. Почему наивное разбиение вредит поиску
  2. Семантическое разбиение по сходству эмбеддингов
  3. Поиск по принципу «родительский фрагмент — дочерний» и от малого к большому
  4. Стратегии для кода и HTML с учётом типа документа
← Назад к AI Engineering Academy