← Tüm yazılar

Enterprise RAG için Doküman İşleme

5 Eylül 2026 8 dk okuma ##RAG##DokumanIsleme##PDF##docling

Bir şirketin AI sistemine bağlamak istediği tek bir doküman yoktur. Yüzlerce politika dosyası, sözleşme, taranmış PDF, teknik rapor, sunum, tablo ve Excel çalışma kitabı vardır. Dışarıdan bakınca yapılacak iş kolay görünür: dosyayı yükle, metni çıkar, chunk’lara böl ve vector database’e gönder.

İşte ilk büyük pitfall tam burada başlar.

Her dosya aynı şekilde okunamaz. Aynı uzantıya sahip iki PDF bile teknik olarak tamamen farklı olabilir. Birinde düzgün bir text layer bulunurken diğeri yalnızca taranmış sayfa görüntülerinden oluşabilir. Bir rapor tek kolonlu metin taşırken başka bir raporda iki kolon, dipnot, tablo, grafik ve sayfanın farklı bölgelerine yerleştirilmiş açıklamalar olabilir. İnsan sayfaya baktığında okuma sırasını sezgisel olarak anlar. Parser ise bu ilişkiyi yeniden kurmak zorundadır.

Document layout analysis üzerine geliştirilen DocLayNet’in 80.863 manuel olarak işaretlenmiş sayfaya ve 11 farklı layout sınıfına ihtiyaç duyması, bu problemin ne kadar değişken olduğunu gösteriyor.[1] Tablo yapısını görüntüden çıkarmak için geliştirilen TableFormer çalışması da satır, sütun, birleşik hücre, eksik değer ve karmaşık header yapılarını çözmenin basit bir text extraction problemi olmadığını ortaya koyuyor.[2]

Bu nedenle doküman işleme, RAG pipeline’ının küçük bir preprocessing adımı değildir. Başlı başına tasarlanması ve ölçülmesi gereken bir servistir.

“PDF okuyoruz” demek yeterli değildir

PDF bir içerik modeli değil, bir sunum formatıdır. Sayfada görünen metnin mantıksal okuma sırası dosyanın içinde her zaman açık biçimde bulunmaz. Üstelik aşağıdaki içeriklerin hepsi aynı PDF uzantısı altında gelebilir:

  • Makine tarafından okunabilen structured text
  • Dağınık veya hatalı reading order’a sahip unstructured text
  • Taranmış printed text
  • Çok kolonlu sayfalar
  • Birleşik hücreler içeren tablolar
  • Grafikler ve görsel açıklamalar
  • Formlar, checkbox’lar ve imzalar
  • Metin ile görselin anlamı birlikte taşıdığı sayfalar

Docling bu noktada güçlü bir başlangıç sağlar. Docling Technical Report, sistemin layout analysis için DocLayNet tabanlı modellerden ve table structure recognition için TableFormer’dan yararlandığını; düşük kaynak bütçesiyle commodity hardware üzerinde çalışacak şekilde tasarlandığını açıklıyor.[3] Docling ayrıca dokümanı Markdown veya yapılandırılmış bir temsil olarak dışa aktarabilir.

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

Fakat production ortamında tek bir parser’a güvenmek yeterli değildir. Biz CPU-only doküman işleme servisimizde önce Docling ile standart parsing yapıyoruz. Çıktı boşsa, beklenenden çok az metin içeriyorsa veya layout kalite kontrollerini geçemiyorsa dosyayı sessizce “başarılı” kabul etmiyoruz. İkinci işleme yoluna yönlendiriyoruz.

Docling’in okuyamadığı sayfalarda OCR fallback

Bazı PDF’lerde text layer hiç yoktur. Bazılarında ise karakterler bozulmuş, font mapping hatalı veya okuma sırası kullanılamaz durumdadır. Böyle durumlarda ilgili PDF sayfalarını görüntüye çevirip OCR uyguluyoruz. PyMuPDF sayfaları Pixmap olarak render edebilir; Tesseract ise görüntü üzerindeki printed text’i OCR ile çıkarabilir.[4][5]

Bu yaklaşım kurtarıcıdır ama bedelsiz değildir. Bütün bir arşivi yüksek DPI ile görüntüye dönüştürmek ve her sayfada OCR çalıştırmak CPU-only bir sistemde ciddi latency doğurur. Bu nedenle problemi yalnızca “daha hızlı OCR” ile çözmeye çalışmadık. İş akışını değiştirdik.

Doküman servisini agent tarafından tool olarak çağrılabilecek şekilde tasarladık. Agent önce dokümanın metadata’sını, başlıklarını, mevcut text layer’ını veya daha önce çıkarılmış içeriği inceliyor. Kullanıcının sorusunu cevaplamak için belirli sayfalara ihtiyaç duyduğunda read_pdf tool’unu yalnızca o sayfalar için çağırıyor. Servis seçilen sayfaları render ediyor, OCR sonucunu çıkarıyor ve çıktıyı sidecar storage’a yazıyor. Bir sonraki sorguda aynı sayfayı yeniden işlemek gerekmiyor.

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)

Bu kod mimariyi göstermek için sadeleştirilmiştir. Production uygulamasında file_id mutlaka server-side authorization üzerinden gerçek dosyaya çözülmeli; agent’ın serbest dosya yolu göndermesine izin verilmemelidir. Sayfa sayısı, DPI, işlem süresi ve çıktı boyutu da sınırlandırılmalıdır.

On-demand yaklaşımı OCR maliyetini ortadan kaldırmaz. Doğru anda ödenmesini sağlar. İlk ingestion sırasında binlerce sayfa için latency üretmek yerine, yalnızca sorunun gerektirdiği sayfaları işleriz. Böylece document processing servisi arka planda çalışan pasif bir ETL adımı olmaktan çıkar ve agent’ın kontrollü biçimde kullandığı aktif bir bilgi aracına dönüşür.

Büyük Excel dosyalarında problem satır sayısı değil, context’tir

Karşılaştığımız ikinci önemli problem büyük Excel çalışma kitaplarıydı. Şirket çalışanları için 1.000 veya 10.000 satırlık satış, operasyon, stok ya da finans verisi çok kıymetli bir bilgi kaynağıdır.

Burada küçük ama önemli bir ayrım yapmak gerekir: 1.000 satır pandas için büyük bir veri değildir. Problem, bu satırların tamamını metne dönüştürüp language model context’ine koymaya çalıştığımızda başlar.

Bütün workbook context’e gönderildiğinde token kullanımı hızla büyür. Model kolon ilişkilerini kaybedebilir, aggregation işlemlerinde hata yapabilir veya cevabı olmayan bir örüntü üretebilir. Uzun context sunabilen modeller de bu sorunu otomatik olarak çözmez. “Lost in the Middle” araştırması, modellerin uzun context içindeki bilgiyi her konumda aynı güvenilirlikle kullanamadığını; ilgili bilgi ortalarda olduğunda performansın belirgin biçimde düşebildiğini gösteriyor.[6]

Bu nedenle Excel dosyasını context olarak değil, sorgulanabilir bir veri kaynağı olarak ele aldık. pandas, Excel dosyalarını DataFrame’e okuyabilir; belirli sheet ve kolonları seçmeye, filtreleme, aggregation ve istatistiksel analiz yapmaya imkân verir.[7] Bu yetenekleri allowlist edilmiş bir data analysis tool üzerinden agent’a bağladık.

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)

Agent örneğin “2025 yılında en yüksek ciroya sahip 10 müşteriyi bul” sorusunu aldığında bütün workbook’u prompt’a koymuyor. Tool’a hangi sheet, kolon ve operasyonun gerektiğini belirten yapılandırılmış bir istek gönderiyor. pandas hesaplamayı deterministik olarak yapıyor; yalnızca sonuç ve gerekli birkaç satır context’e dönüyor.

Bu tasarım iki sorunu aynı anda çözüyor: context penceresini koruyor ve hesaplamayı dil modelinden alıp bu iş için geliştirilmiş bir veri işleme kütüphanesine veriyor. Burada agent’ın serbest Python kodu veya eval çalıştırmasına izin vermek yerine, izin verilen operasyonları sınırlı ve doğrulanabilir tool şemalarıyla tanımlamak kritik önemdedir.

Görsel anlam taşıyan dokümanlarda multimodal model

OCR yalnızca görüntüdeki karakterleri metne dönüştürür. Ancak bazı sayfalarda cevap karakterlerin kendisinde değil, görsel ilişkidedir. Bir grafikte çizginin yönü, legend ile renklerin eşleşmesi, bir formdaki checkbox konumu veya bir tablodaki hücrenin hangi üst başlığa bağlı olduğu yalnızca düz metinle kaybolabilir.

Bu tür sayfalarda multimodal bir vision-language model ikinci bir fallback veya enrichment katmanı olarak kullanılabilir. Kompakt bir örnek Qwen2.5-VL-3B-Instruct modelidir. Qwen’in resmî yayını 3B sürümünü edge AI için konumlandırıyor ve model ailesinin dokümanlar ile diagramları anlama yeteneğini özellikle vurguluyor.[8] Teknik rapor da modelin farklı çözünürlüklerdeki görüntüleri işlemek için dynamic resolution ve window attention kullandığını açıklıyor.[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"])

Multimodal model her dokümana uygulanmamalıdır. OCR’a göre daha maliyetli ve daha az deterministik olabilir. En mantıklı kullanım, klasik parser ve OCR’ın düşük güven verdiği; görsel yapının cevabın parçası olduğu seçilmiş sayfalardır. Modelden JSON istemek de tek başına doğruluk garantisi değildir. Çıktı schema validation’dan geçirilmeli, sayısal değerler mümkünse kaynak görüntü veya deterministic parser ile çapraz kontrol edilmelidir.

Sağlam bir document processing pipeline için birkaç teknik daha

1. Önce dosyayı sınıflandırın

Extension’a güvenmek yerine MIME type, file signature, encryption durumu, sayfa sayısı, text layer yoğunluğu ve görüntü oranı kontrol edilmelidir. Dosya önce uygun işleme yoluna yönlendirilmelidir: native parser, layout-aware parser, OCR, spreadsheet engine veya multimodal model.

2. Confidence-aware fallback kullanın

Parser’ın exception üretmemesi başarılı sonuç aldığı anlamına gelmez. Çıkarılan karakter sayısı, boş sayfa oranı, bozuk karakter oranı, table cell coverage ve reading order tutarlılığı gibi sinyaller değerlendirilmelidir. Düşük kaliteli çıktı otomatik olarak ikinci yönteme yönlendirilmelidir.

3. Layout’u chunking’den önce koruyun

Chunking işlemi ham karakter sayısına göre yapılırsa başlık ile paragraf, tablo başlığı ile satırlar veya dipnot ile referans ayrılabilir. Önce heading, paragraph, list, table, figure ve caption ilişkileri korunmalı; chunk’lar bu yapı üzerinden oluşturulmalıdır.

4. Provenance’ı kaybetmeyin

Her chunk ile birlikte file_id, document version, page number, section heading, parser version ve mümkünse bounding box bilgisi tutulmalıdır. Kaynağa geri dönemeyen bir RAG cevabı kurumsal kullanımda kolayca doğrulanamaz.

5. Başarısız ingestion’ı görünür hale getirin

Sistem bozuk bir PDF’yi boş içerikle indexleyip işlemi başarılı gösteriyorsa en tehlikeli hata oluşur: kullanıcı dokümanın sistemde olduğunu düşünür ama retrieval hiçbir zaman o içeriği bulamaz. Parsing coverage, failed pages ve fallback sonuçları gözlemlenebilir olmalıdır.

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

Sonuç: doküman işleme bir dosya dönüştürme işi değildir

Enterprise RAG sistemlerinde retrieval kalitesi doküman vector database’e ulaştıktan sonra başlamaz. Çok daha önce, dosyanın nasıl okunduğu, yapısının nasıl korunduğu ve hangi parçaların gerçekten güvenilir kabul edildiği anda başlar.

İyi bir document processing service her dosyayı aynı parser’dan geçirmeye çalışmaz. Dosyayı tanır, uygun yöntemi seçer, kaliteyi ölçer ve gerektiğinde OCR veya multimodal fallback uygular. Büyük tabloları context’e doldurmak yerine onları araçlarla sorgulatır. Yüksek maliyetli işlemleri bütün arşive peşinen uygulamak yerine agent’ın ihtiyacına göre on demand çalıştırır.

Kötü parse edilen bir dokümanı daha büyük context window, daha iyi embedding modeli veya daha güçlü LLM kurtaramaz.

LMXAI olarak enterprise AI sistemlerini yalnızca model katmanında değil; ingestion, document intelligence, retrieval, security ve inference katmanlarıyla birlikte tasarlıyoruz.

Önceki yazı: Enterprise AI Integration: From Prototype to Production

Sonraki yazı: Choosing the Right Vector Database


Sources