Documentverwerking voor enterprise RAG
Een organisatie heeft zelden maar één document dat zij met een AI-systeem wil verbinden. Er zijn honderden beleidsdocumenten, contracten, gescande PDF’s, technische rapporten, presentaties, tabellen en Excel-werkmappen. Van buitenaf lijkt de opdracht eenvoudig: upload het bestand, extraheer de tekst, verdeel die in chunks en stuur ze naar een vectordatabase.
Precies hier begint de eerste grote pitfall.
Niet ieder bestand kan op dezelfde manier worden gelezen. Zelfs twee PDF’s met dezelfde extensie kunnen technisch volledig verschillend zijn. De ene bevat een nette tekstlaag, terwijl de andere uitsluitend uit gescande pagina-afbeeldingen bestaat. Het ene rapport gebruikt één kolom; een ander combineert meerdere kolommen, voetnoten, tabellen, grafieken en annotaties op verschillende plaatsen. Een mens begrijpt de leesvolgorde vrijwel intuïtief. Een parser moet die opnieuw construeren.
Voor DocLayNet waren 80.863 handmatig geannoteerde pagina’s en 11 layoutklassen nodig om de variatie in echte documenten te representeren.[1] Ook het TableFormer-onderzoek laat zien dat het herkennen van rijen, kolommen, samengevoegde cellen, ontbrekende waarden en complexe headers in een tabelafbeelding geen eenvoudige text-extractiontaak is.[2]
Documentverwerking is daarom geen kleine preprocessingstap binnen een RAG-pipeline. Het is een zelfstandige service die afzonderlijk moet worden ontworpen en geëvalueerd.
“Wij kunnen PDF’s lezen” is niet voldoende
PDF is een presentatieformaat en geen betrouwbaar logisch contentmodel. De leesvolgorde die op de pagina zichtbaar is, staat niet altijd expliciet in het bestand. Onder dezelfde PDF-extensie kunnen onder meer de volgende soorten inhoud voorkomen:
- Machine-readable structured text
- Unstructured text met een gebroken leesvolgorde
- Gescande printed text
- Pagina’s met meerdere kolommen
- Tabellen met samengevoegde cellen
- Grafieken en visuele annotaties
- Formulieren, checkboxes en handtekeningen
- Pagina’s waarop tekst en visuele structuur samen betekenis dragen
Docling biedt hier een sterk startpunt. Het Docling Technical Report beschrijft dat het systeem modellen op basis van DocLayNet gebruikt voor layout analysis en TableFormer voor table-structure recognition, terwijl het ontworpen is voor commodity hardware met een beperkt resourcebudget.[3] Het kan documenten exporteren als Markdown of als een gestructureerde representatie.
from pathlib import Path
from docling.document_converter import DocumentConverter
converter = DocumentConverter()
result = converter.convert(Path("documents/report.pdf"))
document = result.document
markdown = document.export_to_markdown()
print(markdown[:1500])
In productie is één parser echter niet voldoende. Binnen onze CPU-only documentverwerkingsservice beginnen we met standaard parsing via Docling. Wanneer de output leeg is, veel minder tekst bevat dan verwacht of niet door de layout-quality checks komt, markeren we het bestand niet stilzwijgend als succesvol. Het wordt naar een tweede verwerkingsroute gestuurd.
OCR-fallback voor pagina’s die Docling niet kan lezen
Sommige PDF’s hebben geen tekstlaag. Andere bevatten kapotte character maps, beschadigde fonts of een onbruikbare leesvolgorde. In zulke gevallen renderen we de relevante pagina’s als afbeeldingen en passen we OCR toe. PyMuPDF kan pagina’s als Pixmaps renderen; Tesseract kan printed text uit pagina-afbeeldingen halen.[4][5]
Deze fallback is nuttig, maar niet kosteloos. Een volledig archief op hoge DPI rasteriseren en op iedere pagina OCR uitvoeren, veroorzaakt aanzienlijke latency op een CPU-only systeem. Daarom hebben we niet alleen geprobeerd OCR sneller te maken, maar de workflow veranderd.
We hebben de documentservice zo ontworpen dat een agent deze als tool kan aanroepen. De agent bekijkt eerst metadata, headings, de beschikbare tekstlaag en reeds geëxtraheerde content. Wanneer voor een gebruikersvraag specifieke pagina’s nodig zijn, roept de agent read_pdf alleen voor die pagina’s aan. De service rendert de geselecteerde pagina’s, voert OCR uit en schrijft het resultaat naar sidecar storage. Bij een volgende vraag hoeft dezelfde pagina niet opnieuw te worden verwerkt.
from io import BytesIO
import pymupdf
import pytesseract
from PIL import Image
from langchain_core.tools import tool
@tool
def read_pdf(file_id: str, pages: list[int]) -> str:
"""Read selected pages from an authorised PDF using OCR fallback."""
path = resolve_authorized_file(file_id) # Server-side access check
extracted = []
with pymupdf.open(path) as pdf:
for page_number in pages[:10]:
page = pdf.load_page(page_number - 1)
pixmap = page.get_pixmap(dpi=200, alpha=False)
image = Image.open(BytesIO(pixmap.tobytes("png")))
text = pytesseract.image_to_string(image, lang="eng+nld")
sidecar.put(file_id, page_number, text)
extracted.append(
f"[Page {page_number}]\n{text}"
)
return "\n\n".join(extracted)
Het voorbeeld is bewust vereenvoudigd. In productie moet file_id via server-side autorisatie naar een werkelijk bestand worden vertaald. Een agent mag nooit willekeurige bestandspaden kunnen doorgeven. Ook het aantal pagina’s, DPI, de verwerkingstijd en outputgrootte moeten harde limieten hebben.
On-demand verwerking verwijdert de OCR-kosten niet. Zij zorgt ervoor dat de kosten op het juiste moment worden gemaakt. In plaats van tijdens de eerste ingestion latency te creëren voor duizenden pagina’s, verwerkt het systeem alleen de pagina’s die voor de actuele vraag nodig zijn. Document processing verandert zo van een passieve ETL-stap in een actieve en gecontroleerde kennis-tool.
Bij grote Excel-bestanden is het aantal rijen niet het echte probleem
De tweede belangrijke uitdaging die we tegenkwamen, waren grote Excel-werkmappen. Een bestand met 1.000 of 10.000 rijen aan verkoop-, operatie-, voorraad- of financiële data kan bijzonder waardevol zijn voor medewerkers.
Hier is een belangrijk onderscheid nodig: 1.000 rijen is niet groot voor pandas. Het probleem ontstaat wanneer alle rijen als tekst worden geserialiseerd en in de context van een taalmodel worden geplaatst.
Wanneer een volledige workbook naar het model wordt gestuurd, groeit het tokengebruik snel. Het model kan relaties tussen kolommen verliezen, fouten maken in aggregaties of patronen produceren die niet door de data worden ondersteund. Een groot context window lost dit niet automatisch op. “Lost in the Middle” laat zien dat modellen informatie niet op iedere positie binnen een lange context even betrouwbaar gebruiken; de prestaties kunnen duidelijk afnemen wanneer relevante informatie in het midden staat.[6]
Daarom behandelen we een Excel-werkmap als een bevraagbare databron en niet als promptcontext. pandas kan Excel-sheets in DataFrames laden en ondersteunt kolomselectie, filtering, aggregatie en statistische bewerkingen.[7] Deze mogelijkheden hebben we via een allowlisted data-analysis tool aan de agent gekoppeld.
from typing import Literal
import pandas as pd
from langchain_core.tools import tool
from pydantic import BaseModel, Field
class ExcelRequest(BaseModel):
file_id: str
sheet_name: str
operation: Literal["summary", "sum", "mean", "top"]
column: str | None = None
top_n: int = Field(default=10, ge=1, le=50)
@tool(args_schema=ExcelRequest)
def analyse_excel(
file_id: str,
sheet_name: str,
operation: str,
column: str | None = None,
top_n: int = 10,
) -> str:
"""Run an allowlisted analysis without placing the workbook in context."""
path = resolve_authorized_file(file_id)
frame = pd.read_excel(path, sheet_name=sheet_name)
if operation == "summary":
result = frame.describe(include="all").transpose().reset_index()
elif operation == "sum" and column:
result = {"column": column, "sum": float(frame[column].sum())}
elif operation == "mean" and column:
result = {"column": column, "mean": float(frame[column].mean())}
elif operation == "top" and column:
result = frame.nlargest(top_n, column).head(50)
else:
raise ValueError("Unsupported operation or missing column")
if isinstance(result, pd.DataFrame):
return result.head(50).to_json(orient="records", date_format="iso")
return str(result)
Wanneer de vraag luidt “Welke tien klanten hadden in 2025 de hoogste omzet?”, plaatst de agent niet de volledige workbook in de prompt. De agent stuurt een gestructureerd verzoek met de benodigde sheet, kolom en operatie. pandas voert de berekening deterministisch uit; alleen het resultaat en een beperkt aantal ondersteunende rijen komen in de context terecht.
Zo beschermen we het context window en verplaatsen we de berekening van het taalmodel naar een bibliotheek die voor data-analyse is ontworpen. Een agent mag daarbij geen willekeurige Python-code of eval-expressies uitvoeren. De toegestane operaties moeten met expliciete en gevalideerde toolschema’s worden begrensd.
Multimodale modellen voor documenten met visuele betekenis
OCR zet zichtbare tekens om in tekst. Op sommige pagina’s ligt de betekenis echter in visuele relaties. De richting van een lijn in een grafiek, de koppeling tussen kleuren en een legenda, de positie van een checkbox of de relatie tussen een tabelcel en de bovenliggende header kan in platte tekst verloren gaan.
Voor zulke pagina’s kan een multimodaal vision-language model als tweede fallback of enrichmentlaag dienen. Qwen2.5-VL-3B-Instruct is een compact voorbeeld. Qwen positioneert het 3B-model voor edge AI en benadrukt dat de modelfamilie documenten en diagrammen kan begrijpen.[8] Het technische rapport beschrijft dynamic-resolution processing en window attention voor het efficiënt verwerken van afbeeldingen met verschillende afmetingen.[9]
from transformers import pipeline
vision_model = pipeline(
task="image-text-to-text",
model="Qwen/Qwen2.5-VL-3B-Instruct",
device_map="auto",
)
messages = [{
"role": "user",
"content": [
{"type": "image", "url": "file:///tmp/page-12.png"},
{
"type": "text",
"text": (
"Extract the headings, printed text, table structure and "
"chart meaning. Return valid JSON and do not infer "
"values that are not visible."
),
},
],
}]
result = vision_model(text=messages, max_new_tokens=800)
print(result[0]["generated_text"][-1]["content"])
Een multimodaal model moet niet op ieder document worden toegepast. Het kan duurder en minder deterministisch zijn dan OCR. De beste use case is een geselecteerde pagina waarop klassieke parsing en OCR weinig vertrouwen geven en waar visuele structuur essentieel is voor het antwoord. JSON vragen is geen garantie voor correctheid. De output moet door schema validation gaan, terwijl numerieke waarden waar mogelijk met de bronafbeelding of een deterministic parser moeten worden gecontroleerd.
Aanvullende technieken voor een betrouwbare documentpipeline
1. Classificeer vóór het parsen
Vertrouw niet alleen op de extensie. Controleer MIME type, file signature, encryptie, pagina-aantal, dichtheid van de tekstlaag en verhouding tussen tekst en afbeeldingen. Routeer het bestand vervolgens naar een native parser, layout-aware parser, OCR-engine, spreadsheet-engine of multimodaal model.
2. Gebruik confidence-aware fallback
Een parser die geen exception teruggeeft, heeft niet automatisch een goed document opgeleverd. Character count, percentage lege pagina’s, aandeel beschadigde tekens, table-cell coverage en consistentie van de leesvolgorde kunnen als kwaliteitssignalen dienen. Output van lage kwaliteit moet automatisch een tweede methode activeren.
3. Behoud layout vóór chunking
Chunking op basis van een vast aantal tekens kan een heading van zijn alinea scheiden, een tabelheader losmaken van de rijen of een voetnoot van zijn referentie verwijderen. Behoud eerst de relaties tussen headings, paragrafen, lijsten, tabellen, figures en captions en maak daarna pas chunks.
4. Behoud provenance
Iedere chunk moet file_id, documentversie, paginanummer, section heading, parserversie en waar mogelijk bounding-boxcoördinaten bevatten. Een RAG-antwoord dat niet naar de oorspronkelijke bron kan terugkeren, is binnen een organisatie moeilijk te verifiëren.
5. Maak mislukte ingestion zichtbaar
Het gevaarlijkste resultaat is een beschadigde PDF die als lege content wordt geïndexeerd terwijl de pipeline succes rapporteert. Medewerkers denken dat het document beschikbaar is, maar retrieval kan het nooit vinden. Parsing coverage, mislukte pagina’s en fallbackresultaten moeten observeerbaar zijn.
flowchart TD
F["Incoming file"] --> R["Type and quality router"]
R --> D["Docling parser"]
D --> Q{"Quality sufficient?"}
Q -->|Yes| S["Structured sidecar"]
Q -->|No| O["Page render + OCR or VLM"]
O --> S
A["Agent"] --> T["read_pdf / analyse_excel tools"]
T --> S
Conclusie: documentverwerking is geen bestandsconversie
Retrievalkwaliteit binnen een enterprise-RAG-systeem begint niet nadat een document de vectordatabase heeft bereikt. Zij begint veel eerder: wanneer het bestand wordt gelezen, de structuur wordt behouden en het systeem beslist welke geëxtraheerde onderdelen betrouwbaar genoeg zijn.
Een robuuste documentverwerkingsservice dwingt niet ieder bestand door dezelfde parser. Zij herkent het bestand, kiest de juiste methode, meet de kwaliteit en gebruikt waar nodig OCR of een multimodale fallback. Grote tabellen worden met tools bevraagd in plaats van in het context window geplaatst. Dure bewerkingen worden on demand uitgevoerd en niet vooraf op het volledige archief toegepast.
Een groter context window, een beter embeddingmodel of een krachtiger LLM kan een slecht geparsed document niet redden.
LMXAI ontwerpt enterprise-AI-systemen over de volledige stack: ingestion, document intelligence, retrieval, security en inference.
Vorige artikel: Enterprise AI Integration: From Prototype to Production
Volgende artikel: Choosing the Right Vector Database
Sources
- Docling Technical Report
- DocLayNet: A Large Human-Annotated Dataset for Document-Layout Analysis
- TableFormer: Table Structure Understanding with Transformers
- Lost in the Middle: How Language Models Use Long Contexts
- Qwen2.5-VL official announcement
- Qwen2.5-VL Technical Report
- pandas.read_excel documentation
- PyMuPDF Pixmap documentation
- Tesseract OCR documentation