PDF ve Word Belgelerini LangChain ile İşleme
Kurumsal ortamlarda belge yönetimi her zaman can sıkıcı bir iş olmuştur. Yüzlerce PDF raporu, Word dökümanı, sözleşme ve teknik belge arasında bilgi aramak saatler alabilir. İşte tam bu noktada LangChain devreye giriyor ve bu belgeleri anlamlı bir şekilde işleyip sorgulanabilir hale getiriyor. Bu yazıda, gerçek dünya senaryolarını göz önünde bulundurarak PDF ve Word belgelerini LangChain ile nasıl işleyeceğimizi adım adım inceleyeceğiz.
Neden LangChain ile Belge İşleme?
Klasik yöntemlerle belge araması yaparken genellikle anahtar kelime eşleştirmesine güvenirsiniz. Ama “geçen ay sunucu bakım maliyetleri ne kadardı?” diye sorduğunuzda, sistem size “maliyet”, “bakım”, “sunucu” kelimelerini içeren tüm belgeleri döker. LangChain ise bu soruyu anlamsal olarak anlayıp doğru belgeyi bulmanızı sağlar.
LangChain’in belge işleme konusundaki gücü şu bileşenlerden gelir:
- Document Loaders: Farklı dosya formatlarını yükleyen bileşenler
- Text Splitters: Büyük metinleri parçalara bölen araçlar
- Embeddings: Metinleri vektörlere dönüştüren modeller
- Vector Stores: Vektörleri saklayan ve sorgulayan veritabanları
- Chains: Tüm bu bileşenleri bir araya getiren iş akışları
Gerekli Kurulumlar
Başlamadan önce gerekli kütüphaneleri kuralım. Python 3.9 veya üzeri bir sürüm kullandığınızı varsayıyorum.
# Temel LangChain kurulumu
pip install langchain langchain-community langchain-openai
# PDF işleme için
pip install pypdf pdfminer.six pymupdf
# Word belgesi işleme için
pip install python-docx docx2txt
# Vektör veritabanı için
pip install chromadb faiss-cpu
# Diğer yardımcı kütüphaneler
pip install tiktoken unstructured[docx,pdf]
# Tüm bağımlılıkları tek seferde kurmak isterseniz
pip install langchain langchain-community langchain-openai pypdf python-docx chromadb tiktoken unstructured
OpenAI API anahtarınızı ortam değişkeni olarak ayarlayın:
export OPENAI_API_KEY="sk-your-api-key-here"
# Ya da .env dosyası kullanıyorsanız
echo "OPENAI_API_KEY=sk-your-api-key-here" >> .env
PDF Belgelerini Yükleme
LangChain’de PDF yüklemek için birden fazla yöntem var. Her birinin kendine özgü avantajları bulunuyor.
PyPDFLoader ile Basit PDF Yükleme
En yaygın ve kullanımı en kolay yöntem PyPDFLoader’dır:
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import PyMuPDFLoader
# Temel PDF yükleme
loader = PyPDFLoader("sunucu_bakım_raporu_2024.pdf")
documents = loader.load()
# Her sayfa ayrı bir Document nesnesi olarak gelir
print(f"Toplam sayfa sayısı: {len(documents)}")
print(f"İlk sayfanın içeriği: {documents[0].page_content[:500]}")
print(f"Metadata: {documents[0].metadata}")
# PyMuPDF daha hızlı ve daha iyi metin çıkarımı sağlar
loader_mupdf = PyMuPDFLoader("teknik_dokuman.pdf")
docs_mupdf = loader_mupdf.load()
# Tüm sayfaları tek bir string olarak birleştirme
full_text = "nn".join([doc.page_content for doc in docs_mupdf])
print(f"Toplam karakter sayısı: {len(full_text)}")
Büyük PDF Klasörlerini Toplu İşleme
Gerçek dünyada genellikle tek bir belgeyle değil, yüzlerce belgeyle uğraşırsınız. Aşağıdaki örnek, bir klasördeki tüm PDF’leri otomatik olarak işler:
import os
from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_community.document_loaders import UnstructuredPDFLoader
def pdf_klasoru_yukle(klasor_yolu: str) -> list:
"""Belirtilen klasördeki tüm PDF dosyalarını yükler."""
# DirectoryLoader ile toplu yükleme
loader = DirectoryLoader(
klasor_yolu,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True, # Paralel işleme
max_concurrency=4
)
documents = loader.load()
# Her belgeye özel metadata ekleyelim
for doc in documents:
dosya_adi = Path(doc.metadata.get('source', '')).stem
doc.metadata['dosya_adi'] = dosya_adi
doc.metadata['islem_tarihi'] = '2024-01-15'
print(f"Toplam {len(documents)} sayfa yüklendi.")
return documents
# Kullanım
belgeler = pdf_klasoru_yukle("/var/belgeler/raporlar/")
# Hangi dosyalardan kaç sayfa geldiğini görelim
from collections import Counter
dosya_sayfalari = Counter([
Path(doc.metadata['source']).name
for doc in belgeler
])
for dosya, sayfa in dosya_sayfalari.most_common(10):
print(f"{dosya}: {sayfa} sayfa")
Word Belgelerini Yükleme
Word belgelerini işlemek için Docx2txtLoader veya UnstructuredWordDocumentLoader kullanabilirsiniz.
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
# Basit .docx yükleme
loader = Docx2txtLoader("sistem_prosedürleri.docx")
documents = loader.load()
print(documents[0].page_content[:1000])
# Daha gelişmiş yükleme - tablolar ve görseller dahil
loader_unstructured = UnstructuredWordDocumentLoader(
"sla_sozlesmesi.docx",
mode="elements" # Her elementi ayrı ayrı işler
)
elements = loader_unstructured.load()
# Element tiplerini filtreleyelim
basliklar = [e for e in elements if e.metadata.get('category') == 'Title']
tablolar = [e for e in elements if e.metadata.get('category') == 'Table']
paragraflar = [e for e in elements if e.metadata.get('category') == 'NarrativeText']
print(f"Başlık sayısı: {len(basliklar)}")
print(f"Tablo sayısı: {len(tablolar)}")
print(f"Paragraf sayısı: {len(paragraflar)}")
Metinleri Parçalara Bölme (Text Splitting)
Büyük dil modelleri sınırlı bir bağlam penceresine sahiptir. Bu yüzden belgelerimizi anlamlı parçalara bölmemiz gerekiyor. Bu adım, sistemin kalitesini doğrudan etkiler.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.text_splitter import TokenTextSplitter
# RecursiveCharacterTextSplitter - en yaygın kullanılan
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # Her parçanın maksimum karakter sayısı
chunk_overlap=200, # Parçalar arası örtüşme (bağlamı korumak için)
length_function=len,
separators=["nn", "n", " ", ""] # Bölme öncelikleri
)
# Yüklenen belgeleri bölelim
parcalar = text_splitter.split_documents(belgeler)
print(f"Toplam parça sayısı: {len(parcalar)}")
print(f"Ortalama parça uzunluğu: {sum(len(p.page_content) for p in parcalar) / len(parcalar):.0f} karakter")
# Token bazlı bölme - OpenAI modellerinde daha hassas
token_splitter = TokenTextSplitter(
chunk_size=500,
chunk_overlap=50,
encoding_name="cl100k_base" # GPT-4 için kullanılan encoding
)
token_parcalar = token_splitter.split_documents(belgeler)
print(f"Token bazlı parça sayısı: {len(token_parcalar)}")
Vektör Veritabanı Oluşturma
Parçaladığımız metinleri vektörlere dönüştürüp bir veritabanında saklayacağız. Bu işlem, semantik aramayı mümkün kılar.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.vectorstores import FAISS
import os
# Embedding modeli
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # Maliyet/performans dengesi için
chunk_size=1000
)
# ChromaDB ile kalıcı vektör veritabanı oluşturma
persist_directory = "./vektordb/belgeler"
vectorstore = Chroma.from_documents(
documents=parcalar,
embedding=embeddings,
persist_directory=persist_directory,
collection_name="sirket_belgeleri"
)
print("Vektör veritabanı oluşturuldu!")
# Daha sonra mevcut veritabanını yükleme
mevcut_vectorstore = Chroma(
persist_directory=persist_directory,
embedding_function=embeddings,
collection_name="sirket_belgeleri"
)
# FAISS alternatifi - daha hızlı, bellek içi
faiss_vectorstore = FAISS.from_documents(parcalar, embeddings)
faiss_vectorstore.save_local("./faiss_index")
# FAISS index'i yükleme
faiss_yuklu = FAISS.load_local(
"./faiss_index",
embeddings,
allow_dangerous_deserialization=True
)
Soru-Cevap Zinciri Kurma
Artık en heyecan verici kısma geldik. Belgelerimizden sorular sorabileceğimiz bir sistem kuracağız.
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts import PromptTemplate
# LLM modeli
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0, # Deterministik cevaplar için 0
max_tokens=2000
)
# Özel prompt template - Türkçe cevaplar için
prompt_template = """
Sen bir belge analiz asistanısın. Sana verilen bağlam bilgisini kullanarak
soruları Türkçe olarak yanıtla. Eğer cevabı bilmiyorsan, "Bu konuda belgede
bilgi bulunamadı" de. Tahmin yürütme.
Bağlam:
{context}
Soru: {question}
Cevap:
"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# Retriever oluşturma
retriever = mevcut_vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance - çeşitlilik için
search_kwargs={
"k": 5, # Kaç parça getirilecek
"fetch_k": 20, # MMR için aday havuzu
"lambda_mult": 0.7 # Çeşitlilik/alaka dengesi
}
)
# Basit QA zinciri
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
chain_type_kwargs={"prompt": PROMPT},
return_source_documents=True # Kaynakları da döndür
)
# Soru soralım
soru = "2024 yılında sunucu bakım maliyetleri ne kadar olmuştur?"
sonuc = qa_chain.invoke({"query": soru})
print(f"Cevap: {sonuc['result']}")
print("n--- Kaynak Belgeler ---")
for i, kaynak in enumerate(sonuc['source_documents'], 1):
print(f"n{i}. Kaynak: {kaynak.metadata.get('source', 'Bilinmiyor')}")
print(f" Sayfa: {kaynak.metadata.get('page', 'N/A')}")
print(f" İçerik: {kaynak.page_content[:200]}...")
Çok Turlu Konuşma Sistemi
Gerçek kullanımda kullanıcılar genellikle takip soruları sorar. Bunun için hafıza özelliği ekleyelim:
from langchain.memory import ConversationBufferWindowMemory
# Pencereli hafıza - son 5 konuşmayı hatırla
hafiza = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=5,
output_key="answer"
)
# Konuşmalı retrieval chain
konusmali_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=hafiza,
return_source_documents=True,
verbose=False
)
def belge_asistani():
"""İnteraktif belge sorgulama asistanı."""
print("Belge Asistanı hazır. Çıkmak için 'quit' yazın.n")
while True:
soru = input("Sorunuz: ").strip()
if soru.lower() in ['quit', 'exit', 'çık']:
print("Görüşmek üzere!")
break
if not soru:
continue
try:
yanit = konusmali_chain.invoke({"question": soru})
print(f"nCevap: {yanit['answer']}")
# Kaynak belgeler
if yanit.get('source_documents'):
print("nKaynaklar:")
kaynaklar = set()
for doc in yanit['source_documents']:
kaynak = doc.metadata.get('source', 'Bilinmiyor')
sayfa = doc.metadata.get('page', 'N/A')
kaynaklar.add(f" - {os.path.basename(kaynak)}, Sayfa {sayfa}")
for k in kaynaklar:
print(k)
print()
except Exception as e:
print(f"Hata oluştu: {e}n")
# Asistanı başlat
belge_asistani()
Gerçek Dünya Senaryosu: IT Dokümantasyon Sistemi
Şimdi tüm bu parçaları bir araya getirelim. Aşağıdaki senaryo, bir sysadmin ekibinin tüm prosedür belgelerini, SLA sözleşmelerini ve olay raporlarını sorgulanabilir hale getirdiği gerçekçi bir kullanım durumunu gösteriyor:
import os
import json
from pathlib import Path
from datetime import datetime
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferWindowMemory
class ITDokumantasyonSistemi:
"""IT ekipleri için belge sorgulama sistemi."""
def __init__(self, belge_dizini: str, vektordb_dizini: str):
self.belge_dizini = belge_dizini
self.vektordb_dizini = vektordb_dizini
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
self.vectorstore = None
self.chain = None
def belgeleri_yukle(self) -> list:
"""PDF ve Word belgelerini yükle."""
tum_belgeler = []
# PDF yükleme
pdf_loader = DirectoryLoader(
self.belge_dizini,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)
# Word yükleme
docx_loader = DirectoryLoader(
self.belge_dizini,
glob="**/*.docx",
loader_cls=Docx2txtLoader,
show_progress=True
)
try:
pdf_belgeler = pdf_loader.load()
print(f"PDF: {len(pdf_belgeler)} sayfa yüklendi")
tum_belgeler.extend(pdf_belgeler)
except Exception as e:
print(f"PDF yükleme hatası: {e}")
try:
docx_belgeler = docx_loader.load()
print(f"Word: {len(docx_belgeler)} belge yüklendi")
tum_belgeler.extend(docx_belgeler)
except Exception as e:
print(f"Word yükleme hatası: {e}")
# Metadata zenginleştirme
for doc in tum_belgeler:
kaynak = Path(doc.metadata.get('source', ''))
doc.metadata['dosya_turu'] = kaynak.suffix.lower()
doc.metadata['klasor'] = kaynak.parent.name
doc.metadata['yuklenme_zamani'] = datetime.now().isoformat()
return tum_belgeler
def indeks_olustur(self, yeniden_olustur: bool = False):
"""Vektör indeksi oluştur veya mevcut indeksi yükle."""
if not yeniden_olustur and os.path.exists(self.vektordb_dizini):
print("Mevcut indeks yükleniyor...")
self.vectorstore = Chroma(
persist_directory=self.vektordb_dizini,
embedding_function=self.embeddings
)
print(f"İndeks yüklendi. Koleksiyon boyutu: {self.vectorstore._collection.count()}")
else:
print("Yeni indeks oluşturuluyor...")
belgeler = self.belgeleri_yukle()
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=150,
separators=["nn", "n", ". ", " "]
)
parcalar = splitter.split_documents(belgeler)
print(f"Toplam {len(parcalar)} parça oluşturuldu")
self.vectorstore = Chroma.from_documents(
documents=parcalar,
embedding=self.embeddings,
persist_directory=self.vektordb_dizini
)
print("İndeks oluşturuldu ve kaydedildi!")
def zincir_kur(self):
"""Sorgulama zincirini kur."""
hafiza = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=5,
output_key="answer"
)
retriever = self.vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "fetch_k": 15}
)
self.chain = ConversationalRetrievalChain.from_llm(
llm=self.llm,
retriever=retriever,
memory=hafiza,
return_source_documents=True
)
def sorgula(self, soru: str) -> dict:
"""Belgeleri sorgula ve sonuçları döndür."""
if not self.chain:
self.zincir_kur()
yanit = self.chain.invoke({"question": soru})
# Kaynakları düzenle
kaynaklar = []
for doc in yanit.get('source_documents', []):
kaynak_bilgi = {
"dosya": os.path.basename(doc.metadata.get('source', 'Bilinmiyor')),
"sayfa": doc.metadata.get('page', 'N/A'),
"tur": doc.metadata.get('dosya_turu', 'N/A'),
"ozet": doc.page_content[:150] + "..."
}
if kaynak_bilgi not in kaynaklar:
kaynaklar.append(kaynak_bilgi)
return {
"soru": soru,
"cevap": yanit['answer'],
"kaynaklar": kaynaklar,
"zaman": datetime.now().isoformat()
}
# Sistemi kullanma
sistem = ITDokumantasyonSistemi(
belge_dizini="/opt/it-docs/",
vektordb_dizini="/opt/it-docs/vektordb/"
)
sistem.indeks_olustur(yeniden_olustur=False)
sistem.zincir_kur()
# Örnek sorgular
sorgular = [
"Sunucu yeniden başlatma prosedürü nedir?",
"Disaster recovery planımızda RTO hedefi ne kadar?",
"Güvenlik açığı bulunduğunda kim bilgilendirilmeli?"
]
for sorgu in sorgular:
sonuc = sistem.sorgula(sorgu)
print(f"nSoru: {sonuc['soru']}")
print(f"Cevap: {sonuc['cevap'][:300]}...")
print(f"Kaynak sayısı: {len(sonuc['kaynaklar'])}")
Performans İpuçları ve Yaygın Hatalar
Belge işleme sistemleri üretime alındığında bazı sorunlarla karşılaşırsınız. İşte dikkat etmeniz gerekenler:
Chunk boyutu seçimi kritik önem taşır. Çok küçük chunk’lar bağlamı kaybettirir, çok büyük chunk’lar ise maliyeti artırır. Teknik belgeler için 800-1200 karakter, kısa notlar için 400-600 karakter genellikle işe yarar.
Embedding maliyetlerini kontrol edin. Büyük belgeler için text-embedding-3-small modeli, text-embedding-3-large’a kıyasla 5 kat daha ucuzdur ve kalite farkı çoğu kullanım durumunda fark edilmez.
Vektör veritabanını düzenli güncelleyin. Belgeler değiştiğinde eski parçaları silip yenilerini eklemek için bir güncelleme mekanizması kurun. Chromadb’de belge ID’si kullanarak bu işlemi otomatize edebilirsiniz.
Türkçe belgeler için özel dikkat gerekir. Türkçe metinlerde ek ayrıştırma, bölme işlemlerini zorlaştırabilir. nn ile bölme yaparken Türkçe karakterlerin doğru encode edildiğinden emin olun.
OCR gereken PDF’ler için pypdf yerine pytesseract veya Azure Document Intelligence kullanmayı düşünün. Taranmış belgeler düz metin içermediğinden standart loader’lar işe yaramaz.
Sonuç
LangChain ile PDF ve Word belgelerini işlemek, sysadminlerin en çok zaman harcadığı dokümantasyon araştırmalarını dramatik biçimde kısaltabilir. Yüzlerce sayfayı manuel olarak taramak yerine, sistemin sizin için doğru bilgiyi bulmasını sağlayabilirsiniz.
Bu yazıda anlattığımız yapıyı bir adım öteye taşıyarak Slack entegrasyonu ekleyebilir, ekip içi bir chatbot haline getirebilirsiniz. Ya da periyodik olarak yeni belgeler eklendiğinde indeksi otomatik güncelleyen bir cron job kurabilirsiniz. Temel mimari aynı kalır: yükle, böl, vektörleştir, sorgula.
Önemli bir nokta olarak şunu vurgulamak isterim: Bu sistemler mükemmel değildir. LLM’ler bazen yanlış bilgi üretebilir, retrieval her zaman en alakalı parçayı getiremeyebilir. Bu yüzden kritik kararlar için her zaman kaynak belgeyi de kontrol edin. Sistemi bir asistan olarak kullanın, kesin kaynak olarak değil.
Kod örneklerini kendi ortamınıza uyarlarken chunk boyutlarıyla ve retriever parametreleriyle denemeler yapmanızı öneririm. Her belge seti farklı davranır ve doğru ayarları bulmak biraz deneme yanılma gerektirir. Ama bir kez iyi ayarladığınızda, ekibinizdeki herkesin bilgiye erişim hızı ciddi oranda artacak.
