Стратегии для кода и 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 docsHTML: структура важнее количества символов
Документы 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 — локальная установка не требуется.
Все уроки этого курса
- Почему наивное разбиение вредит поиску
- Семантическое разбиение по сходству эмбеддингов
- Поиск по принципу «родительский фрагмент — дочерний» и от малого к большому
- Стратегии для кода и HTML с учётом типа документа