Aller au contenu principal

Module 4 — Chargeurs de documents et découpage

Un modèle n'ingère pas un PDF, il ingère du texte. Entre les deux, deux étapes que LangChain uniformise : le chargement (fichier, URL, source bureautique) qui produit un objet Document, et le découpage qui coupe ce document en morceaux prêts à être indexés. Notre assistant de notes de frais aura besoin de deux flux : les justificatifs individuels (un PDF par ticket) et la politique de remboursement (un PDF unique de 40 pages). Les deux passent par le même patron.

L'objet Document​

Un Document est un couple page_content: str et metadata: dict. Le contenu, c'est le texte extrait. Les métadonnées, c'est tout ce qui va rendre la recherche exploitable : source, page, date, type, employe_id.

from langchain_core.documents import Document

doc = Document(
page_content="Article 4.2 — Plafond hôtel Paris : 180 EUR TTC par nuit.",
metadata={"source": "politique-2026.pdf", "page": 12, "section": "hebergement"},
)

Trois règles pour les métadonnées. Elles doivent être filtrables : les stocker en dict plat, valeurs scalaires (nombres, chaînes, booléens), jamais de liste ou d'objet imbriqué. Elles doivent être présentes dès le chargement : il est plus facile d'annoter la source dès l'ouverture du fichier que d'y revenir plus tard. Enfin, une clé source est attendue par la plupart des affichages de traçabilité — la respecter.

Charger un PDF​

Deux chargeurs PDF s'affrontent selon le besoin.

from langchain_community.document_loaders import PyPDFLoader, PDFPlumberLoader

# Rapide, une page = un Document, métadonnées {source, page}
pages = PyPDFLoader("politique-2026.pdf").load()

# Plus lent, préserve la mise en page (colonnes, tableaux)
pages_precises = PDFPlumberLoader("politique-2026.pdf").load()

PyPDFLoader traite un PDF de 100 pages en une seconde. PDFPlumberLoader (via pdfplumber) préserve mieux les tableaux et le multi-colonnes, au prix d'un facteur trois sur le temps. Pour un ticket scanné (image dans un PDF), aucun des deux ne suffit : il faut passer par UnstructuredPDFLoader avec option OCR, ou un service d'OCR externe (Tesseract, AWS Textract) et se ramener à un Document texte.

Charger d'autres sources​

Le catalogue langchain-community couvre l'essentiel du monde bureautique :

SourceChargeurNote
Page webWebBaseLoaderRespecte robots.txt, filtrage bs4.
Multi-URL avec rendu JSPlaywrightURLLoaderUtile pour du contenu généré côté navigateur.
.docxDocx2txtLoader, UnstructuredWordDocumentLoaderLe second garde titres et listes.
.xlsx, .csvUnstructuredExcelLoader, CSVLoaderUne ligne = un Document.
Confluence, Notion, Google DriveConfluenceLoader, NotionDBLoader, GoogleDriveLoaderAuthentification par jeton.
Répertoire entierDirectoryLoaderWrapper qui applique un chargeur à un motif glob.

Un exemple direct sur notre politique interne hébergée :

from langchain_community.document_loaders import WebBaseLoader

pages_web = WebBaseLoader(
["https://intranet.exemple.fr/rh/notes-frais"],
header_template={"User-Agent": "notes-frais-bot/1.0"},
).load()

Pourquoi découper​

Un Document de 40 pages ne peut pas alimenter un récupérateur : les plongements calculés seraient dilués, la recherche ne retrouverait rien de précis, et l'insertion dans la fenêtre de contexte gaspillerait les jetons. On coupe donc en morceaux — les chunks — de taille ciblée sur ce que le modèle sait traiter.

Deux paramètres gouvernent le compromis :

  • Taille du morceau (chunk_size) : entre 300 et 1 000 caractères pour du texte administratif dense. Trop petit, on perd le contexte ; trop grand, la recherche perd en précision.
  • Chevauchement (chunk_overlap) : 10 à 20 % de la taille. Il évite qu'une phrase clef ne soit coupée entre deux morceaux.

Le séparateur récursif​

RecursiveCharacterTextSplitter est le découpeur par défaut de LangChain. Il essaie une liste de séparateurs, du plus grand au plus petit, jusqu'à obtenir des morceaux sous la limite :

from langchain_text_splitters import RecursiveCharacterTextSplitter

decoupeur = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=80,
separators=["\n\n", "\n", ". ", " ", ""],
)

morceaux = decoupeur.split_documents(pages)

Il coupe d'abord sur les doubles retours (paragraphes), puis sur les retours simples, puis sur la ponctuation, enfin sur les espaces. Résultat : les morceaux respectent les frontières naturelles du texte quand elles existent. Pour du code source, RecursiveCharacterTextSplitter.from_language(Language.PYTHON) fournit une liste de séparateurs adaptée (fonction, classe, ligne).

Un découpeur alternatif, MarkdownHeaderTextSplitter, coupe selon la hiérarchie #, ##, ### et injecte les titres dans les métadonnées — irremplaçable pour une politique structurée en articles.

Le fil rouge, chargé et découpé​

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

pages = PyPDFLoader("politique-notes-frais-2026.pdf").load()
for p in pages:
p.metadata["type"] = "politique"
p.metadata["annee"] = 2026

morceaux = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80).split_documents(pages)
print(len(morceaux), "morceaux prêts à indexer")

Chaque morceau conserve source, page, type, annee : le récupérateur du module 5 pourra filtrer sur type == "politique" et annee == 2026 avant même la recherche vectorielle.

Le piège du morceau vide

Une extraction PDF produit régulièrement des pages presque vides (numéros de page seuls, en-têtes). Ces morceaux polluent l'index et remontent parfois en tête. Un filtre [m for m in morceaux if len(m.page_content.strip()) > 50] avant indexation évite plusieurs heures de recherche du bogue au module 5.

En résumé​

  • Un Document est un couple page_content + metadata ; les métadonnées doivent être plates et filtrables dès le chargement (source, page, type, date).
  • PyPDFLoader va vite, PDFPlumberLoader préserve les tableaux, l'OCR est nécessaire pour les tickets scannés — un Document texte reste la cible finale.
  • RecursiveCharacterTextSplitter avec chunk_size entre 300 et 1 000 et chunk_overlap de 10 à 20 % couvre 90 % des cas ; MarkdownHeaderTextSplitter garde la hiérarchie dans les métadonnées.
  • Filtrer les morceaux trop courts avant indexation évite un bogue silencieux au module suivant.

Module suivant : brancher ces morceaux à une base vectorielle et bâtir un récupérateur RAG minimal sur la politique de remboursement.