Transformer un PDF complexe en markdown vraiment RAG-ready
Un PDF technique posé sur un disque dur, c’est de la donnée morte. Un PDF technique correctement converti, indexé et interrogeable via RAG, c’est de la connaissance active. Entre les deux, il y a une étape qu’on évoque rarement parce qu’elle ressemble à du travail manuel à l’ancienne. C’est pourtant elle qui décide de la qualité de tout ce qui suit.
J’ai un projet RAG en cours sur la documentation moteur BMW. Le corpus de référence est le Funktionsrahmen du calculateur MSS60 — 448 pages d’allemand technique sec produites par Siemens en 2005, qui décrivent à la virgule près le fonctionnement du calculateur moteur des BMW M3 E92 (S65) et M5/M6 E60 (S85). Le genre de document que les forums BMW citent par fragments depuis quinze ans sans qu’aucune base unifiée n’ait jamais été construite.
Pour démarrer, j’ai isolé un seul chapitre : le Modulbeschreibung Momentenmanagement, 51 pages, qui détaille la gestion du couple moteur. Suffisamment représentatif pour calibrer la méthodologie, suffisamment compact pour itérer rapidement.
Ce qui devait être une petite tâche s’est transformé en deux soirées de travail dense, parce que j’ai découvert au passage que le travail intéressant n’est pas celui que je croyais. La sortie d’un parser automatique — même un bon — est une matière première brute. La transformer en quelque chose qu’un système RAG va vraiment savoir exploiter demande des décisions sémantiques que ni LlamaParse, ni Docling, ni Voyage AI ne peuvent prendre à votre place.
Ce que je vais raconter ici, c’est ce qui se passe après le parsing automatique. L’étape qui conditionne plus la qualité finale du retrieval que tous les paramètres de chunking réunis.
Trois caractéristiques du PDF qui mettent les parsers en échec
Dans mon article précédent sur le parsing visuel, j’ai expliqué pourquoi les PDF complexes sabotent les pipelines RAG quand on s’en remet à des extractions naïves. LlamaParse et ses alternatives modernes corrigent une partie du problème en lisant les PDF visuellement plutôt que via leur structure interne souvent corrompue. C’est un saut qualitatif réel.
Mais sur le MSS60, j’ai pu mesurer concrètement les limites de ces outils, même les plus avancés. Trois caractéristiques du document mettent les parsers en difficulté.
Les diagrammes sont des schémas-blocs Simulink rasterisés. Pas de texte récupérable, pas de structure XML sous-jacente, juste des images bitmap embarquées dans le PDF. Un parser visuel moderne arrivera à OCR-iser les labels (md_eta_zw_ve, →, Limiter MIN) mais produira au mieux une transcription linéaire qui perd la logique de flux. Or c’est précisément cette logique — quel signal entre où, qui produit quoi, dans quel ordre — qui fait toute la valeur du document. Un chunk qui contient md_eta_zw_ve → Limiter MIN(md_max_begr) → md_ind_wunsch_begr sans phrase descriptive autour est un chunk sémantiquement pauvre, malgré sa densité informationnelle apparente.
Les formules sont entrelacées avec du texte allemand technique et des labels code. Une expression comme md_ind_ve = (md_wi_opt_korr * md_eta_zw_ve) * K_RF_HUBVOLUMEN / 0.012566 est lisible pour un humain qui sait lire un calculateur moteur. Pour un parser, c’est un mélange ambigu de mots, d’identifiants, d’opérateurs et de constantes magiques qui finit souvent saucissonné en morceaux décontextualisés.
Les tableaux des paramètres applicables (chapitres 16 et 17 du document) sont denses et pauvres en redondance — chaque ligne fait deux ou trois mots, le contexte n’est porté que par le titre du tableau lointain. Un bon parser les transcrira en markdown propre, mais découpés sans précaution par un chunker classique, ces tableaux produiront des chunks orphelins du type K_MD_SK_PWGMIN | Seuil PWG sous lequel surveillance souhait couple nul active sans que la moindre information de contexte ne survive. Pour la recherche sémantique, c’est une perte sèche.
Aucun parser automatique ne va décider à votre place qu’il faut éclater ces tableaux en sous-sections individuelles avec un rappel de contexte par entrée. C’est une décision sémantique, propre à votre cas d’usage, qui demande qu’un humain (ou un LLM bien briefé) comprenne ce que ces tableaux contiennent et comment ils seront interrogés en aval.
Pourquoi j’ai gardé l’allemand au lieu de traduire (et pourquoi c’est contre-intuitif)
Ma première intuition fausse a été de vouloir traduire. Le document est en allemand, mes requêtes seront en français, donc traduisons à la conversion. Cette intuition est doublement mauvaise.
D’abord, parce que les modèles d’embedding modernes sont cross-lingual, pas seulement multilingues. C’est une distinction qu’on lit rarement dans les guides RAG francophones et qui change tout. Un modèle multilingue sait traiter plusieurs langues. Un modèle cross-lingual place les concepts équivalents — Zündwinkel allemand et angle d'allumage français — dans une zone proche de l’espace vectoriel. Une requête française active alors les bons chunks même quand ces chunks sont en allemand. J’avais discuté ce point dans l’article sur les modèles d’embedding : voyage-3-large fait partie des modèles qui maintiennent ~85-90% du recall mono-lingual quand on croise les langues sur des benchmarks comme MIRACL.
Ensuite, parce que traduire un terme technique, c’est figer une interprétation discutable dans un corpus qui en croisera d’autres. Schleppmoment peut se rendre par « couple traînée », « couple de traînée moteur », « couple résistant moteur entraîné », « couple de friction étendu ». Aucune de ces traductions n’est canonique. Si je traduis dans le .md du MSS60, et que j’ingère plus tard un autre PDF BMW où je laisse le mot allemand, je fragmente mon espace vectoriel pour rien. Mes embeddings de « Schleppmoment » et « couple traînée » seront proches, mais pas identiques. Je crée artificiellement de la dispersion.
J’ai donc fait l’inverse : préserver l’allemand comme corps principal du document, et ajouter une couche d’annotations françaises ciblées. Cette décision a structuré toute la suite.
Concrètement, ça donne :
- Le texte technique reste en allemand, fidèle au PDF source
- Les premières occurrences des termes-clés sont annotées en français entre parenthèses :
Das Reibmoment (couple de friction moteur entraîné)— cette annotation suffit à activer la cross-linguality au moment de la requête - Un glossaire complet français des termes techniques, en section markdown dédiée au début du document, embeddable comme le reste
- Des résumés français en blockquote en tête de chaque chapitre majeur — qui jouent le rôle de « summary embedding » implicite et capturent les requêtes conceptuelles haut niveau
Cette stratégie a un effet non trivial sur le retrieval : une requête comme « comment fonctionne la régulation Vmax » en français trouve son chunk pertinent même si le contenu du chunk est intégralement en allemand, parce que le résumé français en tête de section et le glossaire ancrent le vocabulaire français dans l’espace vectoriel partagé.
Pour ceux qui veulent creuser le pourquoi mathématique de cette mécanique, j’avais retracé l’odyssée du concept de vecteur du XVIIᵉ siècle aux embeddings modernes. C’est exactement la propriété qui rend possible la recherche cross-linguale : des langues différentes activent des dimensions communes dans l’espace latent quand elles décrivent les mêmes concepts.
Quatre erreurs structurelles de la V1 que rien ne signalait
J’ai produit une première version du markdown qui me semblait propre. Hiérarchie de titres respectée, glossaire en frontmatter YAML, formules dans des blocs de code, tableaux markdown standard. À l’œil, ça ressemblait à un bon document RAG-ready.
À la deuxième relecture, j’ai compris que cette V1 contenait quatre problèmes structurels qui dégraderaient sensiblement la qualité du retrieval. Aucun n’aurait été détecté par un test fonctionnel basique — il faut comprendre comment les chunkers et les embedders traitent réellement le markdown pour les voir.
Le glossaire dans le frontmatter YAML était un mort-né. La plupart des chunkers markdown standard — MarkdownHeaderTextSplitter de LangChain, les splitters de LlamaIndex — ignorent ou retirent le frontmatter avant de chunker. Mon glossaire de 60 termes techniques, soigneusement rédigé, n’aurait été présent dans aucun chunk. Embedding inexistant, retrieval impossible. C’est typiquement le genre d’erreur qui se paie en silence : aucune erreur ne remonte, simplement les requêtes sur des termes français comme « couple traînée » ou « régulateur de ralenti » tombent à plat sans qu’on comprenne pourquoi.
Le bilinguisme français/allemand mélangé phrase par phrase produisait une fragmentation de l’espace vectoriel. Embedder un chunk où chaque paragraphe alterne entre une explication française et un détail allemand, ça brouille le centre de gravité sémantique. Les vecteurs résultants sont moins discriminants que des vecteurs monolingues qui auraient gardé une cohérence de langue par chunk.
Les diagrammes étaient transcrits en pseudo-code seul. Quelque chose comme :
md_ind_wunsch_filter
→ Limiter MAX(md_ind_vmax)
→ Limiter MIN(md_max_begr)
→ md_ind_wunsch_begr
C’est lisible humainement, c’est même trop lisible — on a l’impression que l’information est là. Mais embeddé, ces flèches → et ces parenthèses sont des tokens parasites qui ne portent quasiment aucun signal sémantique. Une requête en langage naturel comme « comment la limitation Vmax interagit avec la protection boîte de vitesse » matchera mal un chunk constitué uniquement de pseudo-code, parce qu’aucun mot du chunk ne porte l’idée d’interaction, de protection ou de limitation Vmax — il faut déduire tout ça de la lecture du flux, ce qu’un embedder ne sait pas faire.
Les longs tableaux des chapitres 16 et 17 allaient produire des chunks orphelins. Avec une chunk_size de 5000 caractères, un tableau de 80 lignes serait coupé en plein milieu, donnant un chunk dont le header | Nom | Description | se trouve dans le chunk précédent et dont les 30 dernières lignes flottent sans aucun contexte de définition. Pour une requête sur un paramètre précis comme K_MD_BEZUG_ZW, le chunk retourné serait au mieux ambigu, au pire inutile.
La V2 a corrigé tout ça par quatre transformations.
Le glossaire est passé du frontmatter à une section markdown dédiée en début de document, garantissant qu’il sera dans son propre chunk indexable. J’ai ajouté à l’occasion une sous-section sur la convention de nommage des paramètres BMW (K_ pour les constantes, KL_ pour les courbes 1D, KF_ pour les cartographies 2D, B_ pour les booléens) — convention qu’aucun document du domaine ne formalise et qui aide énormément à interpréter les chunks récupérés.
Le corps du document est revenu à l’allemand fidèle, avec annotations françaises uniquement sur les premières occurrences des termes-clés. Plus de fragmentation de la cohérence linguistique au sein des chunks.
Les diagrammes sont systématiquement doublés : une description en prose française qui raconte le flux en langage naturel (« Le couple souhaité filtré traverse cinq Limiters en série. Le premier impose une borne haute via la régulation Vmax… »), suivie du pseudo-code structuré pour le lecteur ou le LLM downstream qui veut le détail technique. La prose est pleinement embeddable, le pseudo-code reste là pour ceux qui le liront.
Les tableaux des chapitres 16 et 17 ont été éclatés en sous-sections individuelles au niveau ####. Chaque constante, chaque cartographie, chaque variable a maintenant son propre chunk autonome avec un rappel de contexte. Au lieu d’avoir un chunk orphelin avec quinze lignes de tableau, j’ai quinze chunks chacun de ~200 caractères contenant : le nom du paramètre, sa définition, et le numéro de chapitre où il intervient. C’est pile ce qu’il faut pour une requête paramétrique précise.
J’ai ajouté en fin de document une section de synthèses transversales : six paraphrases de haut niveau qui croisent les concepts du document (séparation chemin remplissage / chemin allumage, principe de coordination DSC, protection multi-couches, calcul du couple réel, fonction Katheizen, régulation Vmax). Ces synthèses sont là pour capter les requêtes conceptuelles qui n’utilisent aucun nom de variable précis. Elles n’apparaissent pas dans le PDF source — c’est de la valeur ajoutée par le LLM au moment de la conversion. Et c’est précisément ce qu’aucun parser automatique ne saurait produire.
Visuellement, l’écart entre les deux versions est massif :
| Métrique | V1 (naïve) | V2 (optimisée) |
|---|---|---|
| Poids | 66 Ko | 119 Ko |
| Mots | 8 700 | 14 400 |
| Lignes | 1 414 | 2 100 |
| Glossaire | Frontmatter (invisible aux chunkers) | Section markdown indexable |
| Bilinguisme | Mélangé phrase par phrase | Allemand fidèle + annotations FR ciblées |
| Diagrammes | Pseudo-code seul | Prose française + pseudo-code |
| Tableaux denses | Bruts (chunks orphelins) | Éclatés en sous-sections #### |
| Synthèses transversales | Absentes | 6 paraphrases en fin de doc |
| Retrouvabilité paramétrique | Faible | Haute |
| Retrouvabilité conceptuelle | Médiocre | Forte (résumés FR + synthèses) |
Le document a quasiment doublé de taille, mais cette densité supplémentaire est entièrement de la redondance utile au retrieval : annotations, résumés, paraphrases croisées, contextes répétés. Embedder cette densité coûte 0,01 € de plus en API Voyage. La gagner en qualité de retrieval rentabilise ce coût dès la première heure d’utilisation.
Six patterns réutilisables sur tout PDF technique
En faisant ce travail, j’ai identifié six patterns qui me semblent généralisables à n’importe quelle conversion de PDF technique vers du markdown RAG-ready. Je les liste ici pour qu’ils servent à ceux qui s’attaqueront à des documents similaires.
Le glossaire vit dans une section markdown, pas dans le frontmatter. Le frontmatter YAML est utile pour les métadonnées de filtrage (doc_id, version, tags), mais tout ce qui doit être retrouvé par recherche sémantique doit être dans le corps du document.
Les résumés en blockquote en tête de section jouent le rôle de summary embedding. Avec une chunk_size de 5000 caractères et un splitter qui respecte les headers, ces résumés se retrouvent toujours dans le premier chunk de leur section. Ils captent les requêtes conceptuelles que les chunks détaillés rateraient.
Les diagrammes doivent être doublés en prose française et en pseudo-code structuré. La prose est pour l’embedding, le pseudo-code est pour le LLM downstream qui devra restituer la précision technique. Aucun des deux ne suffit seul.
Les tableaux denses doivent être éclatés en sous-sections individuelles. Un paramètre = un chunk = un embedding distinct. C’est le seul moyen de garantir un retrieval précis sur des requêtes paramétriques.
Les synthèses transversales en fin de document captent les requêtes conceptuelles haut niveau. Cinq à dix paraphrases qui croisent les concepts principaux du document. Elles ne dupliquent pas le contenu — elles le reformulent dans un autre vocabulaire, créant des points d’entrée alternatifs dans l’espace vectoriel.
Les métadonnées riches dans le frontmatter ne servent qu’au filtrage à la requête, pas au retrieval sémantique. doc_id, version, chapitre, mots_cles sous forme d’array. Ces métadonnées seront ajoutées à chaque chunk au moment de l’upsert dans la base vectorielle et permettront de filtrer les recherches ultérieures (par version du document, par chapitre, etc.). Mais elles ne participent pas à la similarité cosinus.
Il existe sans doute d’autres patterns selon les types de documents. Pour des documents juridiques, pour de la poésie, pour du code, les bonnes pratiques diffèrent. Mais sur de la documentation technique d’ingénierie — manuels, datasheets, specs — ces six patterns me semblent un socle solide.
Pourquoi le travail manuel ne se paie qu’une fois et rapporte indéfiniment
L’objection évidente, quand on lit ce qui précède, c’est : « OK mais ça ne scale pas. Si tu dois faire ça à la main pour chaque document, ton pipeline est mort dès qu’il y aura plus de cinq PDF dans le corpus. »
C’est une objection qu’il faut prendre au sérieux et démonter méthodiquement.
Premièrement, ce travail n’est pas vraiment « manuel ». Il est LLM-assisté. Sur les 51 pages du chapitre Momentenmanagement, j’ai produit la V2 en deux soirées de travail. Pas en lisant et retapant 51 pages. En dialoguant avec un LLM qui a ingéré le PDF complet et qui a produit le markdown structuré sous mes directives sémantiques. Mon travail consistait à : décider de la stratégie globale, valider les choix de structuration, repérer les erreurs de la V1, demander les corrections en V2, vérifier les passages critiques. Un travail d’éditeur, pas de typiste.
Deuxièmement, et c’est là que l’argument de scalabilité s’effondre vraiment, l’effort baisse de façon spectaculaire à partir du deuxième document du même corpus. La conversion du premier chapitre est une phase d’exploration : il faut découvrir les patterns du domaine, calibrer le glossaire, décider de la stratégie linguistique, identifier les structures récurrentes. Sur le chapitre Momentenmanagement, j’ai passé environ 70% du temps à construire la méthodologie et 30% à l’appliquer. Pour le deuxième chapitre — Modulbeschreibung Lambdaregelung, par exemple — la méthodologie est posée. Le glossaire BMW commun est déjà construit. Les conventions sont stabilisées. Le LLM peut reproduire 70 à 80% du travail en autonomie sous une simple directive du type « applique la même structuration que sur le Momentenmanagement, rapporte-moi les ambiguïtés ». Mon travail se réduit à la validation et à la résolution des cas limites.
Pour les 397 pages restantes du Funktionsrahmen MSS60, je vais probablement passer entre 15 et 20 heures au total avec cette méthodologie pour produire l’ensemble — pas 51 fois 4 heures. Une journée et demie de travail effectif pour transformer une documentation technique unique au monde en base de connaissance interrogeable à perpétuité.
Troisièmement, ce travail ne se fait qu’une fois par document. Le PDF MSS60 a été produit en 2005, il est gravé dans le marbre, il ne changera pas. La V2 markdown que j’ai produite vivra dix, quinze, vingt ans dans mon corpus. Le coût marginal par requête est zéro. L’investissement initial s’amortit dès qu’on commence à utiliser le RAG, et continue de payer indéfiniment.
Quatrièmement — et c’est l’argument économique qui devrait clore le débat —, le coût d’un mauvais retrieval ne se mesure pas en factures Voyage ou Pinecone. Il se mesure en heures-utilisateur. Quand un système RAG retourne des chunks médiocres, l’utilisateur perd du temps à filtrer le bruit, à reformuler ses requêtes, à recouper avec d’autres sources, à corriger les hallucinations que le LLM downstream a brodées sur du contexte ambigu. Au bout de quelques semaines, il abandonne le système et retourne aux signets et aux PDF ouverts en parallèle. L’embedding coûte 0,01 € par document. Le coût caché d’un mauvais embedding peut atteindre des centaines d’heures-utilisateur sur la durée de vie du système, plus la perte de confiance qui rend l’outil inutilisable. Économiser deux soirées sur la conversion peut coûter des semaines de tâtonnements en aval pour comprendre pourquoi le système répond mal. Et comme je l’ai documenté ailleurs, la dégradation s’aggrave à mesure que le corpus grossit.
Le vrai sujet n’est pas « le travail manuel ne scale pas ». Le vrai sujet est : où dans le pipeline RAG l’investissement humain rapporte-t-il le plus.
La réponse, après avoir construit ce pipeline, me semble claire. L’investissement à la conversion documentaire rapporte beaucoup. L’investissement au choix du modèle d’embedding rapporte un peu. L’investissement aux paramètres de chunking rapporte marginalement. L’investissement au reranking, au prompt downstream, à la base vectorielle elle-même : marginal.
C’est un retournement de perspective par rapport à ce que la blogosphère RAG francophone met en avant en 2026. On parle beaucoup de Pinecone vs Qdrant, de OpenAI vs Voyage, de chunk_size 500 vs 1500. Très peu de la qualité du markdown qui rentre dans le pipeline. C’est pourtant là que se joue 60% de la qualité finale. Le reste est du réglage fin sur des dizaines de pourcents.
Une autre tendance qui éclaire ce point : le glissement progressif du RAG vers le CAG (Cache-Augmented Generation), qu’a permis l’explosion des fenêtres de contexte des LLM modernes. Si demain mes 448 pages tiennent intégralement dans le contexte d’un appel à Claude ou GPT, je n’aurai même plus besoin de chunker ni d’embedder. Mais — et c’est le point important — j’aurai toujours besoin que le markdown source soit bien structuré.
Il y a une croyance répandue selon laquelle un contexte de 2 millions de tokens règle le problème de la qualité documentaire. Les benchmarks Needle In A Haystack, qui mesurent la capacité d’un LLM à retrouver une information précise noyée dans un long contexte, montrent l’inverse : la performance d’attention dégrade significativement au milieu des longs contextes (le fameux « lost in the middle »), et cette dégradation est plus prononcée sur des documents mal structurés que sur des documents bien organisés. Un LLM avec une fenêtre de 10 millions de tokens lit toujours mieux un document avec des résumés en tête de section, des termes glossarisés, et une hiérarchie claire qu’un dump brut. La qualité éditoriale du document est l’invariant qui survit à toutes les évolutions de l’infrastructure. C’est ce qui rend cet investissement, contrairement aux choix de stack technique qui se périment, profondément durable.
Voilà pourquoi, au final, je trouve que cette étape de conversion intelligente est la plus rentable et la plus pérenne de tout le pipeline. Et c’est paradoxalement celle dont on parle le moins.
Pour les 397 pages restantes du Funktionsrahmen MSS60, je vais répliquer la méthodologie chapitre par chapitre, en affinant les patterns au fur et à mesure. Quand le corpus complet sera ingéré, j’aurai pour la première fois une base de connaissance interrogeable couvrant la totalité du calculateur moteur du S65 et du S85 — quelque chose qui n’existe nulle part ailleurs. Les forums BMW citent des fragments depuis quinze ans sans qu’aucune base unifiée n’ait jamais été construite. Le RAG n’est pas une fin, c’est l’outil qui rend cette unification possible. Et le markdown bien fait, c’est la matière première qui rend le RAG honnête.