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 :
| Source | Chargeur | Note |
|---|---|---|
| Page web | WebBaseLoader | Respecte robots.txt, filtrage bs4. |
| Multi-URL avec rendu JS | PlaywrightURLLoader | Utile pour du contenu généré côté navigateur. |
.docx | Docx2txtLoader, UnstructuredWordDocumentLoader | Le second garde titres et listes. |
.xlsx, .csv | UnstructuredExcelLoader, CSVLoader | Une ligne = un Document. |
| Confluence, Notion, Google Drive | ConfluenceLoader, NotionDBLoader, GoogleDriveLoader | Authentification par jeton. |
| Répertoire entier | DirectoryLoader | Wrapper 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.
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
Documentest un couplepage_content+metadata; les métadonnées doivent être plates et filtrables dès le chargement (source,page,type,date). PyPDFLoaderva vite,PDFPlumberLoaderpréserve les tableaux, l'OCRest nécessaire pour les tickets scannés — unDocumenttexte reste la cible finale.RecursiveCharacterTextSplitteravecchunk_sizeentre 300 et 1 000 etchunk_overlapde 10 à 20 % couvre 90 % des cas ;MarkdownHeaderTextSplittergarde 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.