AI Engineering Academy · Lekcja

Strategie dopasowane do dokumentów: kod i HTML

Stosuj wyspecjalizowane dzielenie: dla kodu Python używaj dzielników funkcji opartych na AST, dla HTML parserów uwzględniających tagi, a dla Markdown hierarchii nagłówków.

Lekcja 4 z 413 kroki

Strategie dopasowane do dokumentów: kod i HTML to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Dlaczego ogólne dzielenie nie sprawdza się w dokumentach specjalistycznych

Dzielenie tekstu zostało zaprojektowane z myślą o prozie, ale dane z rzeczywistego świata obejmują kod źródłowy, strony HTML i dokumentację w Markdown. Dzielenie kodu na granicy o stałej liczbie znaków może przeciąć funkcję w połowie jej treści, przez co fragment stanie się bezużyteczny podczas pobierania. Dokumenty specjalistyczne wymagają splitterów rozumiejących ich wewnętrzną strukturę, a nie tylko długość.

Dzielenie kodu Python na podstawie AST

Abstrakcyjne drzewo składniowe (AST) pliku Python przedstawia każdą funkcję, klasę i moduł jako ustrukturyzowany węzeł. Przechodząc po AST, można wyodrębnić każdą funkcję lub metodę jako osobny fragment, zachowując razem sygnaturę, docstring i ciało. Klasa PythonCodeTextSplitter w LangChain wewnętrznie korzysta z tego podejścia.

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

Dzielenie na granicach klas

W przypadku obiektowych baz kodu dzielenie na poziomie klasy często sprawdza się lepiej niż dzielenie na poziomie funkcji. Fragment klasy zachowuje relację między metodami a współdzielonym stanem, na którym operują. Można umieścić docstring klasy i treść wszystkich metod w jednym fragmencie, a następnie utworzyć osobne, bardziej szczegółowe fragmenty tylko dla długich metod.

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')

Dodawanie metadanych kodu do fragmentów

Surowe fragmenty kodu są tak użyteczne, jak ich metadane. Podczas przechowywania fragmentów kodu w bazie wektorowej należy uwzględnić ścieżkę pliku, nazwę funkcji, język programowania oraz zakres wierszy. Metadane te pozwalają retrieverowi filtrować dane według języka lub pliku, a LLM-owi wskazywać w odpowiedzi dokładną lokalizację źródła.

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: struktura ważniejsza niż liczba znaków

Dokumenty HTML mają hierarchiczną strukturę obejmującą nagłówki, sekcje, akapity i listy. Dzielenie HTML według liczby znaków często przecina znaczniki, tworząc niepoprawne fragmenty. Właściwe podejście polega na analizowaniu HTML za pomocą odpowiedniego parsera, takiego jak BeautifulSoup, i wyodrębnianiu elementów o znaczeniu semantycznym, takich jak znaczniki <article>, <section> i <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

Hierarchiczne dzielenie HTML według nagłówków

Bardziej zaawansowana strategia dla HTML grupuje treść pod najbliższym nagłówkiem. Każdy akapit i każda lista występujące po nagłówku <h2> należą do tej sekcji. Grupowanie tekstu z nadrzędnym nagłówkiem pozwala zachować kontekst tematu, który samodzielny akapit w przeciwnym razie by utracił. Klasa HTMLHeaderTextSplitter w LangChain realizuje to automatycznie.

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: zachowywanie hierarchii nagłówków

Dokumentacja Markdown jest uporządkowana za pomocą nagłówków #, ## i ###. Klasa MarkdownHeaderTextSplitter dzieli tekst na granicach nagłówków i zapisuje hierarchię nagłówków w metadanych. Dzięki temu każdy fragment zna pełną ścieżkę nagłówków, co znacznie poprawia trafność pobieranego kontekstu, gdy użytkownicy pytają o konkretne sekcje dokumentacji.

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()

Dodatkowe dzielenie po podziale według nagłówków

Po podziale według nagłówków poszczególne sekcje mogą nadal być zbyt długie dla limitu tokenów modelu embeddingowego. Zalecany wzorzec to dzielenie dwuetapowe: najpierw podział według hierarchii nagłówków w celu zachowania kontekstu semantycznego, a następnie zastosowanie splittera opartego na znakach do każdej sekcji przekraczającej ustalony limit rozmiaru fragmentu. Dzięki temu żaden fragment nie jest zbyt duży, a metadane nagłówków zostają zachowane.

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')

Dzielenie plików PDF z uwzględnieniem tabel

Pliki PDF wyodrębniane za pomocą narzędzi takich jak PyMuPDF lub pdfplumber często tracą strukturę tabel, w wyniku czego powstają zniekształcone wiersze tekstu. Aby sobie z tym poradzić, należy używać parserów PDF uwzględniających układ strony, które wykrywają obwiednie tabel i przed podziałem konwertują je do formatu Markdown lub CSV. Każdą tabelę należy traktować jako jeden fragment z ustrukturyzowanymi metadanymi wskazującymi, że jest to tabela, a nie proza.

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

Wykrywanie typu pliku w mieszanych korpusach

Firmowe bazy wiedzy często zawierają różne typy plików: skrypty Python, dokumentację API w HTML, opisy architektury w Markdown oraz eksporty danych w CSV. Solidny potok dzielenia powinien wykrywać typ pliku na podstawie rozszerzenia lub typu MIME i kierować każdy dokument do odpowiedniego specjalistycznego splittera. Pozwala to uniknąć stosowania logiki dzielenia kodu do prozy i odwrotnie.

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)

Zachowywanie kontekstu za pomocą sąsiednich wierszy

Podczas dzielenia kodu według funkcji często warto uwzględnić kilka wierszy otaczającego kontekstu, na przykład instrukcje importu z początku pliku lub definicję klasy zawierającej daną metodę. Taki kontekst pomaga LLM-owi zrozumieć, jakie biblioteki są dostępne i jaka jest rola funkcji w szerszym kontekście klasy, co poprawia jakość generowanych odpowiedzi.

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

Szybki sprawdzian

Sprawdź swoją wiedzę na temat strategii dzielenia zależnych od rodzaju dokumentu z tego modułu.

Podsumowanie modułu

W tym module nauczysz się, że: dzielenie na podstawie AST zachowuje granice funkcji i klas w Pythonie, HTMLHeaderTextSplitter i MarkdownHeaderTextSplitter respektują hierarchię nagłówków, aby zachować kontekst sekcji, a podejście dwuetapowe (podział według nagłówków, a następnie podział według znaków) obsługuje zbyt duże sekcje bez utraty metadanych strukturalnych. Następnie omówimy wyszukiwanie hybrydowe łączące pobieranie gęste i rzadkie.

Bezpłatny start

Ucz się Python dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
30
Lekcje
120

Często zadawane pytania

Czy lekcja „Strategie dopasowane do dokumentów: kod i HTML” jest bezpłatna?

Tak — pełny tekst „Strategie dopasowane do dokumentów: kod i HTML” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Strategie dopasowane do dokumentów: kod i HTML”?

Stosuj wyspecjalizowane dzielenie: dla kodu Python używaj dzielników funkcji opartych na AST, dla HTML parserów uwzględniających tagi, a dla Markdown hierarchii nagłówków. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Strategie dopasowane do dokumentów: kod i HTML”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego naiwne dzielenie na fragmenty pogarsza wyszukiwanie
  2. Dzielenie semantyczne na podstawie podobieństwa embeddingów
  3. Wyszukiwanie nadrzędny–podrzędny i od małego do dużego
  4. Strategie dopasowane do dokumentów: kod i HTML
← Powrót do AI Engineering Academy