Statik Prompt'lardan Ajanik Zekaya: LangChain & LangGraph ile Production-Ready RAG Mimarisi
Basit API çağrılarından başlayıp LCEL zincirleri, hafıza yönetimi, vektör veritabanları, ReAct ajanları ve kendi kendini denetleyen (Self-Reflective) LangGraph mimarilerine uzanan, sunucuya deploy ile biten uçtan uca bir mühendislik rehberi.
Statik LLM Çağrılarının Ötesine Geçmek
Günümüzde Büyük Dil Modelleri (LLM — Large Language Models) ile uygulama geliştirmek denildiğinde akla gelen ilk şey OpenAI veya Gemini API'lerine doğrudan istek atmak oluyor[cite: 2]. Ancak prodüksiyon seviyesinde, ölçeklenebilir ve esnek bir yapay zeka mimarisi inşa etmek istiyorsanız, modelleri tek bir sağlayıcıya bağlamak uzun vadede büyük bir teknik borç getirir[cite: 2].
İşte bu noktada devreye yapay zeka ekosisteminin en güçlü orkestrasyon aracı olan LangChain giriyor[cite: 2]. Bu yazı serisinde LangChain ekosisteminin mimarisini kuracak, LCEL zincirleriyle modüler yapılar oluşturacak, sohbet hafızası ve streaming ekleyecek, kendi verilerimizle çalışan bir RAG (Retrieval-Augmented Generation) sistemi kuracak, LLM'e karar verme yeteneği kazandıran Ajan (Agent) mimarileri geliştirecek ve serinin zirvesinde LangGraph ile kendi kendini denetleyen, hatasını fark edip düzelten gelişmiş bir mimariyi sıfırdan inşa edeceğiz[cite: 2].
Ekosistemin Dört Temel Bileşeni
| Bileşen | Görevi |
|---|---|
| LangChain Core & Community | Model entegrasyonları, prompt şablonları ve zincir (chain) yapılarını içeren açık kaynak çekirdek[cite: 2]. |
| LangServe | Zincirleri ve ajanları FastAPI tabanlı bir web servisine dönüştürüp API endpoint'i olarak sunar[cite: 2]. |
| LangSmith | Modelin nasıl düşündüğünü, token harcamasını ve gecikmeyi trace/debug etmeyi sağlayan izleme platformu[cite: 2]. |
| LangGraph | Döngüsel (cyclical) ve çoklu ajan akışlarını yöneten gelişmiş durum makinesi (state machine) mimarisi[cite: 2]. |
Rasittekin18/ai-applications-langchain-rag[cite: 2]Serinin ilk adımı olan Bölüm 1'de LangChain ekosistemine giriş yapıyor, geliştirme ortamını kuruyor ve modelle ilk mesajlaşmamızı gerçekleştiriyoruz[cite: 2].
Giriş, LangChain Ekosistemi ve İlk Proje
1.1 — Neden LangChain? (Orkestrasyon Gücü)
Yapay zeka dünyası baş döndürücü bir hızla gelişiyor[cite: 2]. Bugün OpenAI'ın en gelişmiş modelini kullanırken, yarın Google Gemini veya Anthropic Claude projeniz için maliyet ya da performans açısından daha avantajlı bir hale gelebilir[cite: 2]. Eğer tüm kod mimarinizi tek bir SDK'ye bağımlı yazarsanız, model değiştirmek tam bir kabusa dönüşür[cite: 2].
LangChain sunduğu orkestrasyon katmanı sayesinde mimarinizi soyutlaştırır — modelinizi OpenAI'dan Gemini'a geçirmek sadece iki satır kod değişikliğine bakar[cite: 2]. Ayrıca bellek yönetimi, ajan yapıları, vektör veritabanı entegrasyonları ve süreç izleme araçlarıyla tam teşekküllü bir altyapı sunar[cite: 2].
1.2 — Geliştirme Ortamı ve Proje Kurulumu
Projelerimize başlamadan önce temiz ve izole bir Python ortamı oluşturmak kritik bir adımdır[cite: 2]. Python 3.10 veya üzeri bir sürüm önerilir[cite: 2].
- Bağımlılıkları tanımla
requirements.txtdosyasında gerekli tüm paketlerin sürümlerini sabitle[cite: 2]. - Sanal ortamı kur
pip install -r requirements.txtkomutuyla bağımlılıkları izole ortama yükle[cite: 2]. - .env dosyasını hazırlaAPI anahtarlarını ve LangSmith tracing ayarlarını proje kök dizinindeki
.enviçinde sakla[cite: 2].
# requirements.txt
langchain==0.2.16
langchain-community==0.2.16
langchain-core==0.2.38
langchain-openai==0.1.23
langchain-chroma
langchain-text-splitters==0.2.2
langserve==0.2.2
python-dotenv==1.0.1
chromadb
beautifulsoup4
tavily-python
pydantic
pip install -r requirements.txt
OPENAI_API_KEY=your_openai_api_key_here
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your_langsmith_api_key_here
LANGCHAIN_PROJECT=First_Project
LANGCHAIN_TRACING_V2 LangSmith üzerinde attığımız her isteğin adım adım izlenmesini sağlar; LANGCHAIN_PROJECT ise LangSmith panelinde bu projenin hangi isim altında loglanacağını belirler[cite: 2].
1.3 — İlk Kod: Model İle İletişim Kurmak
Kurulumları tamamladıktan sonra simple_message.py adında bir dosya açarak LangChain'in ChatOpenAI sınıfı ile ilk model örneğimizi oluşturalım[cite: 2].
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
# .env dosyasındaki API anahtarlarını yüklüyoruz
load_dotenv()
# Model konfigürasyonu
# temperature: 0'a yaklaştıkça daha kararlı/kesin, 1'e yaklaştıkça daha yaratıcı yanıtlar üretir
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.3
)
# Mesaj Yapısı — LangChain, dict yapısı yerine nesne yönelimli (OOP) nesneler sunar
messages = [
SystemMessage(content="You are an expert translator. Translate the following from English to Turkish."),
HumanMessage(content="Artificial intelligence and orchestration frameworks are changing the software industry.")
]
# Modeli tetikleme (Invoke)
response = model.invoke(messages)
print("--- Tam Yanıt Nesnesi ---")
print(response)
print("\n--- Sadece İçerik ---")
print(response.content)
Kodun anatomisi: SystemMessage ve HumanMessage standart JSON/dict yapısı yerine nesne tabanlı sınıflar sunarak okunabilirliği artırır[cite: 2]. invoke() metodu mesaj listesini modele gönderir[cite: 2]. Model geriye sadece string bir metin döndürmez; token sayısı, durdurma nedeni ve metadata bilgilerini içeren zengin bir AIMessage nesnesi döndürür — .content ile doğrudan metin çıktısına erişilir[cite: 2].
gpt-4o-mini ya da gpt-3.5-turbo hem daha hızlıdır hem de token maliyetlerinizi ciddi oranda düşürür[cite: 2]..env dosyanıza LANGCHAIN_TRACING_V2=true eklediğinizde, kodu her çalıştırdığınızda LangSmith panelinizde isteğin ne kadar sürdüğünü, kaç token yediğini ve gelen ham JSON yanıtını anlık olarak izleyebilirsiniz[cite: 2].Bölüm 1 ile LangChain ekosistemine sağlam bir giriş yaptık ve temel modeli ayağa kaldırdık[cite: 2]. Sırada LCEL, Output Parser'lar ve Prompt Template yapıları var[cite: 2].
Output Parser'lar, Prompt Template'ler ve LCEL
Gerçek hayat projelerinde LLM'den aldığımız cevabı sadece bir konsol çıktısı olarak ekrana bastırmakla kalmayız[cite: 2]. Çoğu zaman bu yanıtı temiz bir string, yapılandırılmış bir JSON veya farklı bileşenlerin aktığı dinamik bir boru hattı (pipeline) içinde işlemek isteriz[cite: 2]. Bu bölümde kodumuzu daha profesyonel ve esnek hale getirecek üç kritik kavramı öğreneceğiz: Output Parser'lar, Prompt Template'ler ve LCEL[cite: 2].
2.1 — Output Parser Nedir?
Modelimize istek attığımızda bize ham bir AIMessage nesnesi dönüyordu — içinde metin çıktısı, token harcama detayları ve durdurma nedeni gibi metadata bulunuyordu[cite: 2]. Sadece metin yanıtına odaklanmak istediğimizde her defasında response.content yazmak yerine Output Parser sınıflarını kullanırız[cite: 2].
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
# Modeli çağırıyoruz
raw_response = model.invoke(messages)
# Parser ile ham yanıtı işliyoruz
clean_text = parser.invoke(raw_response)
print(clean_text) # Çıktı doğrudan temiz metindir: "Hola Mundo"
2.2 — Prompt'ları Dinamik Hale Getirmek: ChatPromptTemplate
Uygulamalarımızda prompt'ları kodun içine hardcoded yazmak sürdürülebilir değildir[cite: 2]. LangChain'in ChatPromptTemplate sınıfı, tekrar kullanılabilir ve parametrik şablonlar oluşturmamıza olanak tanır[cite: 2].
from langchain_core.prompts import ChatPromptTemplate
# Süslü parantezler {} ile dinamik değişken alanlarımızı tanımlıyoruz
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are a professional translator. Translate the text into {language}."),
("user", "{text}")
])
# Şablonu değişkenlerle doldurarak mesaj listesini üretiyoruz
formatted_messages = prompt_template.invoke({
"language": "Italian",
"text": "Hello, how are you today?"
})
2.3 — LCEL ve Pipe (|) Operatörü
Geldik LangChain'in en güçlü yanına: LCEL (LangChain Expression Language)[cite: 2]. Yapay zeka uygulamalarında süreç genelde adım adımdır: girdiyi al ve prompt'u oluştur, prompt'u modele ver, model çıktısını parser ile işle[cite: 2]. Bu süreci Unix felsefesindeki pipe (|) operatörüyle bileşenleri zincir gibi birbirine bağlayarak kodlayabiliriz[cite: 2].
# 1. Şablon (Prompt)
prompt = ChatPromptTemplate.from_messages([
("system", "Translate the following text into {language}."),
("user", "{text}")
])
# 2. Model
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# 3. Output Parser
parser = StrOutputParser()
# --- LCEL İLE ZİNCİRİN (CHAIN) OLUŞTURULMASI ---
# Veri soldan sağa doğru akar: Prompt -> Model -> Parser
chain = prompt | model | parser
result = chain.invoke({
"language": "German",
"text": "Artificial intelligence is transforming software engineering."
})
print("Çeviri Yanıtı:", result)
LCEL mimarisi bize ne kazandırıyor? Akışın soldan sağa nasıl ilerlediği açıkça görünür (okunabilirlik), ara değişkenler ortadan kalkar (kod tasarrufu) ve tanımlanan tüm zincirler ekstra kod yazmadan paralelleştirilebilir, asenkron çalıştırılabilir veya kelime kelime ekrana aktarılabilir (streaming & async desteği)[cite: 2].
2.4 — LangServe ile API Endpoint'i Oluşturmak
Yazdığımız zinciri hızlıca bir web servisine dönüştürmek istersek LangServe ve FastAPI devreye girer[cite: 2].
from fastapi import FastAPI
from langserve import add_routes
import uvicorn
# 1. Zincirimizi tanımlıyoruz (prompt | model | parser)
# 2. FastAPI uygulamasını oluşturuyoruz
app = FastAPI(
title="LangChain Translator Server",
version="1.0",
description="LangChain LCEL mimarisi ile yapılmış çeviri servisi"
)
# 3. LangServe ile zincirimizi bir route/endpoint olarak ekliyoruz
add_routes(app, chain, path="/translate")
if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=8000)
Bu kodu çalıştırıp http://localhost:8000/translate/playground adresine gittiğinizde, LangServe sizin için otomatik olarak cURL istekleri atabileceğiniz, parametreleri test edebileceğiniz tam teşekküllü bir Playground arayüzü sunar[cite: 2].
| sembolü, LangChain sınıfları içinde aşırı yüklenerek (__or__ metodu override edilerek) LCEL akış hattını kuracak şekilde özelleştirilmiştir[cite: 2].Artık prompt şablonlarını yönetmeyi, LLM çıktılarını parse etmeyi ve LCEL ile modüler zincirler kurmayı öğrendik[cite: 2]. Sırada sohbet geçmişi, oturum yönetimi ve streaming var[cite: 2].
Mesaj Hafızası (Memory), Akış Yönetimi ve Streaming
Gerçek hayat uygulamalarında ve kullanıcı deneyiminin ön planda olduğu chatbot projelerinde iki kritik gereksinim ortaya çıkar: Hafıza (Memory) — kullanıcı "Benim adım ne?" diye sorduğunda sistemin önceki mesajlardaki bilgiyi hatırlaması — ve Akış Yönetimi (Streaming) — modelin tüm metni üretip bitirmesini beklemek yerine kelime kelime yanıt akışı sağlamak[cite: 2].
3.1 — LLM'lerde Hafıza Mantığı Nasıl Çalışır?
Büyük Dil Modelleri doğası gereği stateless (durumsuz) yapılardır — modele atılan her API isteği geçmişten bağımsız sıfır bir oturumdur[cite: 2]. ChatGPT veya Claude gibi sistemlerin bizi "hatırlaması", aslında arka planda her yeni mesaj gönderildiğinde tüm sohbet geçmişinin tekrar modele yollanmasıyla gerçekleşir[cite: 2].
3.2 — In-Memory Chat History ve RunnableWithMessageHistory
Bir oturum (Session ID) mimarisi kurarak kullanıcı geçmişini hafızada tutan profesyonel bir yapı inşa edelim[cite: 2].
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Oturum geçmişlerini tutacağımız In-Memory veri deposu (Store)
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
"""Verilen session_id'ye göre sohbet geçmişini döndürür; yoksa yeni oluşturur."""
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# Prompt Şablonu: 'history' alanı önceki sohbet mesajlarını dinamik olarak enjekte eder
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful and friendly AI assistant."),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
chain = prompt | model
# Zinciri Hafıza Mimarisi ile Sarmalıyoruz
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
# 1. Oturum Konfigürasyonu
config = {"configurable": {"session_id": "session_rasit_101"}}
# İlk mesajımızı atıyoruz
response1 = with_message_history.invoke(
{"input": "Merhaba, benim adım Raşit ve ben bir elektronik/yapay zeka mühendisiyim."},
config=config
)
# İkinci mesajda adımızı soruyoruz
response2 = with_message_history.invoke(
{"input": "Benim mesleğim ve adım neydi?"},
config=config
)
print("AI:", response2.content)
# Çıktı: "Adınız Raşit ve elektronik/yapay zeka mühendisisiniz." yanıtını hatasız alırız
3.3 — Streaming Yanıt Alımı
Özellikle uzun metinler üretirken kullanıcının ekran karşısında blok halinde tüm yanıtın gelmesini beklemesi kötü bir kullanıcı deneyimidir (UX)[cite: 2]. .invoke() yerine .stream() kullanarak kelime kelime yanıt alabiliriz[cite: 2].
user_input = "Yapay zekanın geleceği hakkında 2 paragraf detaylı açıklama yap."
print("AI (Streaming): ", end="", flush=True)
# Yanıt parça parça (chunk) geldikçe anlık ekrana basıyoruz
for chunk in with_message_history.stream({"input": user_input}, config=config):
print(chunk.content, end="", flush=True)
3.4 — Bellek Yönetimi ve Token Limitleri
Sohbet uzadıkça sohbet geçmişi büyür[cite: 2]. LLM'lerin bir Context Window (Bağlam Penceresi) limiti olduğu için sınırsız mesaj biriktirmek hem maliyeti katlar hem de bağlam sınırı aşıldığında hata verdirir[cite: 2]. İki temel strateji kullanılır[cite: 2]:
A. Mesaj Kırpma (Trim Messages)
- Son n mesajı tutup eski mesajları siler[cite: 2]
max_tokensveya mesaj adedine göre sınırlandırma[cite: 2]include_system=Trueile SystemMessage hiç silinmez[cite: 2]
B. Özetleme (Summarization)
- Geçmiş mesajlar tamamen silinmez[cite: 2]
- LLM'e ayrı bir istekle "önceki sohbeti özetle" talimatı verilir[cite: 2]
- Bağlam kaybı önlenir, token harcaması minimumda kalır[cite: 2]
from langchain_core.messages import trim_messages
# Mesaj geçmişini maksimum token sayısına göre sınırlandırma
trimmer = trim_messages(
max_tokens=500,
strategy="last", # En son mesajları koru
token_counter=model,
include_system=True # SystemMessage'ı asla silme
)
RedisChatMessageHistory veya PostgresChatMessageHistory sınıflarıyla tek satır değişiklikle veritabanı entegrasyonuna izin verir[cite: 2].session_id değerine sahip olduğundan emin olun; aksi halde farklı kullanıcıların sohbet geçmişleri birbirine karışabilir[cite: 2].Sohbet geçmişini yönetmeyi, streaming yanıtlar almayı ve bellek optimizasyonunu tamamladık[cite: 2]. Sırada kendi verilerimizle çalışmanın kapıları: Vektör Veritabanları ve RAG[cite: 2].
Vektör Veritabanları, Embedding ve RAG Konseptine Giriş
Uygulamalarımıza hafıza kazandırdık, zincirler kurduk ve LLM'lerle dinamik iletişim kanalları açtık[cite: 2]. Ancak gerçek dünyada yapay zeka projeleri geliştirirken çok geçmeden devasa bir duvarla karşılaşırız: modelin bilmediği özel veriler[cite: 2]. Şirketinizin dahili prosedürleri veya henüz geçen hafta yayınlanmış bir makale hakkında soru sorduğunuzda model ya mantıklı yanıt veremez ya da halüsinasyon görerek gerçek dışı bilgiler uydurur[cite: 2].
İşte bu noktada RAG (Retrieval-Augmented Generation) devreye girer: modelin kendi eğitim verisinde bulunmayan dış kaynaklardaki bilgileri bir vektör veritabanında saklayarak, gelen soruya özel bu bilgileri sorgulayıp modele bağlam (context) olarak sunma süreci[cite: 2].
4.1 — RAG Mantığı 3 Temel Adımdan Oluşur
- Retrieval (Erişim)Kullanıcı bir soru sorduğunda veritabanından anlamsal olarak en alakalı metin parçaları (chunks) bulunur[cite: 2].
- Augmentation (Zenginleştirme)Bulunan metinler kullanıcının orijinal sorusuyla birleştirilerek zengin bir prompt oluşturulur[cite: 2].
- Generation (Üretim)LLM kendisine sağlanan bağlama dayanarak doğru, güncel ve halüsinasyonsuz yanıtı üretir[cite: 2].
Kullanıcı Sorusu
│
▼
[ Retriever ] ──▶ Chroma DB'den en alakalı k adet chunk çekilir
│
▼
[ Augmentation ] ──▶ Soru + çekilen dokümanlar tek prompt'ta birleşir
│
▼
[ LLM Generation ] ──▶ Bağlama sadık, halüsinasyonsuz nihai yanıt
4.2 — Vektörler, Embedding ve Chroma DB
Embedding, her bir kelimeyi, cümleyi veya paragrafı yüzlerce/binlerce boyutlu bir uzayda sayısal koordinatlara dönüştürme işlemidir[cite: 2]. Bu sayede Semantic Search (Anlamsal Arama) mümkün olur: "köpek" ve "sadık dost" kelimeleri matematiksel uzayda birbirine çok yakın konuma düşer, birebir kelime eşleşmesi olmasa bile anlamsal benzerlik üzerinden veri bulunur[cite: 2]. Vektörleri saklamak ve yüksek hızda anlamsal arama yapmak için açık kaynaklı Chroma DB kullanacağız[cite: 2].
4.3 — Adım Adım Uçtan Uca RAG Uygulaması
Adım 1 — Web Sitesinden Veri Çekme: internetteki HTML içeriğini düzgün parse etmek için BeautifulSoup4 destekli WebBaseLoader kullanılır[cite: 2].
from bs4 import BeautifulSoup
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=["https://lilianweng.github.io/posts/2023-06-23-agent/"],
bs_kwargs=dict(
parse_only=BeautifulSoup.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
)
)
docs = loader.load()
Adım 2 — Metni Bölme: binlerce kelimelik tek bir makaleyi doğrudan vektör veritabanına atamayız; anlamsal bütünlüğü bozmadan küçük parçalara (chunk) böleriz[cite: 2].
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
splits = text_splitter.split_documents(docs)
Adım 3 — Vektör Veritabanı Oluşturma: parçalanmış metinleri OpenAIEmbeddings ile vektör uzayına taşıyıp Chroma DB'ye gömeriz[cite: 2].
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(
documents=splits,
embedding=OpenAIEmbeddings()
)
# Arama yapmak için Retriever (Getirici) nesnemizi oluşturuyoruz
retriever = vectorstore.as_retriever(search_kwargs={"k": 2}) # En alakalı ilk 2 parçayı getir
Adım 4 — RAG Zincirini Kurmak: LangChain Hub üzerinden standart RAG prompt şablonunu çekip LCEL zincirimizi kuruyoruz[cite: 2].
from langchain import hub
from langchain_core.runnables import RunnablePassthrough
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# LangChain Hub'dan RAG için özel yazılmış prompt'u çekiyoruz
prompt = hub.pull("rlm/rag-prompt")
def format_docs(docs):
"""Gelen doküman parçalarını tek bir metin bloğu haline getirir."""
return "\n\n".join(doc.page_content for doc in docs)
# LCEL ile RAG Zincirini Oluşturma
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
question = "What is Task Decomposition in Autonomous Agents?"
for chunk in rag_chain.stream(question):
print(chunk, end="", flush=True)
chunk_size çok küçük tutulursa anlam bütünlüğü bozulur, çok büyük tutulursa vektör aramasında spesifik noktaları yakalamak zorlaşır[cite: 2]. Genel pratik: 500-1000 karakter arası chunk ve %10-20 overlap ideal bir başlangıçtır[cite: 2].DocumentLoader sınıfını kullanabilirsiniz[cite: 2].RAG mantığını, vektör veritabanlarını ve retriever zincirlerini öğrendik[cite: 2]. Sırada sisteme akıl ve karar mekanizması katmak var: Ajan Teknolojileri[cite: 2].
Ajan (Agent) Teknolojileri, ReAct Mimarisi ve Dış Dünya Entegrasyonu
Şu ana kadar geliştirdiğimiz sistemler, belirli bir akış hattını sırasıyla takip eden deterministik zincirlerden oluşuyordu[cite: 2]. Ancak gerçek hayattaki karmaşık problemler her zaman tek bir doğrusal rota takip etmez — kullanıcının sorduğu soru statik bir dokümanda bulunmayabilir, o günün hava durumu veya anlık hisse fiyatları söz konusu olduğunda ne RAG ne de LLM'in kendi iç eğitimi yeterli olur[cite: 2].
İşte bu noktada devreye Ajan (Agent) mimarisi girer: LLM'i bir "beyin" olarak konumlandırıp, görevi başarmak için hangi adımları atacağına ve hangi araçları (tools) ne zaman çalıştıracağına kendi karar vermesini sağlarız[cite: 2].
5.1 — ReAct (Reasoning + Acting) Mimarisi
Ajanların mantıklı kararlar alabilmesinin arkasındaki temel yaklaşım, Google Research ve Princeton Üniversitesi araştırmacıları tarafından geliştirilen ReAct mimarisidir[cite: 2]. LLM'e adım adım bir düşünme döngüsü kazandırır[cite: 2]:
- Thought (Düşünce)Model kullanıcının girdisini ve mevcut durumu analiz eder: "Bu soruyu cevaplamak için web'de arama yapmam gerekiyor."[cite: 2]
- Action (Aksiyon)Kullanacağı aracı ve araca vereceği girdiyi seçer:
tavily_search_results_json[cite: 2]. - Observation (Gözlem)Çalıştırılan araçtan dönen yanıtı yakalar:
29.9°C, Sunny[cite: 2]. - Final Answer (Nihai Yanıt)Gözlemler hedefe ulaştığını gösteriyorsa döngü biter ve nihai yanıt kullanıcıya sunulur; yetersizse döngü tekrarlanır[cite: 2].
5.2 — Tavily Search API ile Dış Dünya Bağlantısı
LLM'lerin internete erişimini sağlamak için endüstri standardı haline gelen Tavily Search API'ı kullanacağız — arama sonuçlarını doğrudan LLM'lerin okuyabileceği rafine metinler halinde döndüren, yapay zeka ajanları için özel optimize edilmiş bir arama motoru[cite: 2].
from langchain_community.tools.tavily_search import TavilySearchResults
# Web Arama Aracı (Tavily) — max_results: her aramada en alakalı kaç sonuç getirileceği
search_tool = TavilySearchResults(max_results=2)
tools = [search_tool]
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage
# Model ve Araçları bağlayarak ReAct Ajanını oluşturuyoruz
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent_executor = create_react_agent(model, tools)
response = agent_executor.invoke({
"messages": [HumanMessage(content="İstanbul'da şu an hava kaç derece ve gökyüzü nasıl?")]
})
for message in response["messages"]:
print(f"[{message.type.upper()}]: {message.content}\n")
Bu kodu çalıştırdığınızda konsolda sırasıyla: kullanıcının sorusu (HUMAN), modelin aracı çağırma kararı (AI Tool Call), Tavily'den dönen canlı veri (TOOL) ve son olarak dönen veriyi harmanlayan nihai yanıtı (AI) görürsünüz[cite: 2].
5.3 — Structured Output & Pydantic ile Çıktı Tipini Zorlama
Bir ajanın kullanıcının sorusunu analiz edip bunu ya Vector Store'a ya da Web Search'e yönlendiren bir Router olmasını istiyorsak, modelin sadece belirlediğimiz veri tipini döndürmesini Pydantic ve with_structured_output ile garantiye alırız[cite: 2].
from pydantic import BaseModel, Field
from typing import Literal
# 1. Pydantic ile Beklenen Çıktı Şemasını Tanımlıyoruz
class RouteQuery(BaseModel):
"""Kullanıcı sorusunu en uygun veri kaynağına yönlendirir."""
datasource: Literal["vectorstore", "websearch"] = Field(
...,
description="Sorunun türüne göre 'vectorstore' veya 'websearch' değerini seç."
)
# 2. Modeli Pydantic Şeması ile Yapılandırıyoruz
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm_router = llm.with_structured_output(RouteQuery)
# 3. Yönlendirme Prompt Şablonu
system_prompt = """You are an expert at routing user questions to a vectorstore or websearch.
The vectorstore contains documents related to Agents, Prompt Engineering, and RAG architectures."""
route_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}")
])
question_router = route_prompt | structured_llm_router
# TEST 1: RAG/Ajan Konusu (Vectorstore seçmeli)
res1 = question_router.invoke({"question": "ReAct mimarisi nedir ve ajanlarda nasıl çalışır?"})
print(res1.datasource) # Çıktı: vectorstore
# TEST 2: Genel Kültür / Güncel Konu (Websearch seçmeli)
res2 = question_router.invoke({"question": "Evde lezzetli bir hamburger nasıl yapılır?"})
print(res2.datasource) # Çıktı: websearch
Field(description="...") ve sınıf içi docstring alanlarını çok net yazmalısınız — ajan hangi aracı veya hangi şema alanını seçeceğine bu açıklama metinlerini okuyarak karar verir[cite: 2].verbose=True verebilir veya LangSmith paneli üzerinden akışı anlık takip edebilirsiniz[cite: 2].Ajanların çalışma mantığını, ReAct döngüsünü ve Pydantic ile yapılandırılmış çıktıları öğrendik[cite: 2]. Sırada serinin zirve noktası: LangGraph ile Self-Reflective RAG[cite: 2].
İleri Seviye Ajanik RAG ve LangGraph Mimarisi
Geldik serimizin en heyecan verici, en gelişmiş ve prodüksiyon seviyesindeki zirve noktasına! Şu ana kadar geliştirdiğimiz RAG sistemlerinde doğrusal (linear) bir zincir takip ediyorduk[cite: 2]. Ancak gerçek dünya senaryolarında bu yaklaşım yetersiz kalır: çekilen dokümanlar soruyla alakasız olabilir, model uydurma (halüsinasyon) yanıt üretebilir ya da üretilen cevap kullanıcının orijinal sorusunu tam karşılamayabilir[cite: 2].
İşte bu problemleri çözmek için LangGraph mimarisini kullanarak kendi kendini denetleyen, hatalarını fark edip düzelten Self-Reflective / Corrective RAG sistemini inşa edeceğiz[cite: 2].
6.1 — LangGraph Nedir ve Neden İhtiyacımız Var?
LangGraph, döngüsel (cyclical) ve yönlü grafik (state machine) akışları kurmamızı sağlayan bir kütüphanedir[cite: 2]. Klasik LCEL zincirlerinde veri tek yönlü akar (A → B → C); durum makineleriyle çalışan LangGraph sayesinde şu döngüsel mantıkları kurabiliriz: "Eğer çekilen dokümanlar alakasızsa, dur ve Web Search yap", "Eğer üretilen yanıt dokümanlara dayanmıyorsa, yanıtı tekrar üret", "Eğer üretilen yanıt soruyu çözmediyse, süreci baştan başlat."[cite: 2]
6.2 — Sistemin Mimarisi (Görsel Akış)
+------------------+
| START / ROUTER |
+--------+---------+
│
+----------+----------+
│ │
▼ ▼
[ Vector Store ] [ Web Search ]
│ │
▼ │
[ Document Grader ] │
(Alakalı mı?) │
│ │ │
(Evet) (Hayır) │
│ └──▶ [ Web Search ] ◀──┘
│ │
└────────────┬─────────────┘
▼
[ Generate Node ]
│
▼
[ Hallucination Grader ]
(Halüsinasyon var mı?)
│ │
(Evet) (Hayır)
│ │
▼ ▼
[ Re-Generate ] [ Answer Grader ]
(Soru cevaplandı mı?)
│ │
(Evet) (Hayır)
│ │
▼ ▼
[ END ] [ Web Search / Restart ]
6.3 — LangGraph'ın Temel Yapı Taşları
- State (Durum)Ajanlar ve düğümler arasında paylaşılan merkezi veri deposudur[cite: 2].
- Nodes (Düğümler)Bir işlemi gerçekleştiren Python fonksiyonları veya zincirleridir (Retrieve, Generate, Web Search)[cite: 2].
- Edges (Kenarlar)
add_conditional_edgesile bir düğümden çıkan sonuca göre hangi düğüme geçileceği dinamik olarak belirlenir[cite: 2].
6.4 — Durum (State) Tanımlaması
from typing import List, TypedDict
class GraphState(TypedDict):
"""Grafik mimarimizin anlık durumunu (state) temsil eder."""
question: str # Kullanıcının sorduğu soru
generation: str # LLM tarafından üretilen nihai yanıt
web_search: bool # Web araması yapılıp yapılmayacağı bayrağı
documents: List[str] # Süreçte toplanan doküman parçaları
6.5 — Karar Düğümleri ve Denetleyiciler (Graders)
Retrieval Grader — çekilen dokümanların soruyla alakalı olup olmadığını denetler[cite: 2]:
class GradeDocuments(BaseModel):
"""Çekilen dokümanların soruyla alakalı olup olmadığını değerlendiren ikili skor."""
binary_score: str = Field(description="Dokümanlar alakalı ise 'yes', değilse 'no'")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm_grader = llm.with_structured_output(GradeDocuments)
system_prompt = """You are a grader assessing relevance of a retrieved document to a user question.
If the document contains keyword(s) or semantic meaning related to the question, grade it as 'yes'. Otherwise 'no'."""
grade_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "Retrieved document: \n\n {document} \n\n User question: {question}")
])
retrieval_grader = grade_prompt | structured_llm_grader
Hallucination Grader — üretilen yanıtın dokümanlardaki gerçeklere dayanıp dayanmadığını doğrular[cite: 2]:
class GradeHallucination(BaseModel):
"""Üretilen yanıtın dokümanlara sadık kalıp kalmadığını denetler."""
binary_score: str = Field(description="Yanıt dokümanlara dayanıyorsa 'yes', değilse 'no'")
structured_hallucination_grader = llm.with_structured_output(GradeHallucination)
hallucination_prompt = ChatPromptTemplate.from_messages([
("system", "You are a grader assessing whether an LLM generation is grounded in a set of retrieved facts."),
("human", "Set of facts: \n\n {documents} \n\n LLM generation: {generation}")
])
hallucination_grader = hallucination_prompt | structured_hallucination_grader
6.6 — Düğümlerin (Nodes) Fonksiyon Olarak Yazılması
Her bir düğüm, mevcut state verisini alır, işlemini yapar ve state'in güncellenmiş halini bir Python dict olarak döndürür[cite: 2].
from langchain_community.tools.tavily_search import TavilySearchResults
web_search_tool = TavilySearchResults(k=3)
def retrieve_node(state: GraphState):
"""Vektör veritabanından doküman çeker."""
print("--- [NODE] RETRIEVE: Dokümanlar Getiriliyor ---")
question = state["question"]
documents = retriever.invoke(question)
return {"documents": documents, "question": question}
def grade_documents_node(state: GraphState):
"""Çekilen dokümanları filtreler ve alakasızsa web araması bayrağını açar."""
print("--- [NODE] GRADE DOCUMENTS: Doküman Alakası Denetleniyor ---")
question = state["question"]
documents = state["documents"]
filtered_docs = []
web_search = False
for doc in documents:
score = retrieval_grader.invoke({"question": question, "document": doc.page_content})
if score.binary_score.lower() == "yes":
filtered_docs.append(doc)
else:
web_search = True # Alakasız doküman varsa web aramasına yönlendir
return {"documents": filtered_docs, "question": question, "web_search": web_search}
def web_search_node(state: GraphState):
"""Gerekli durumlarda Tavily API ile internetten canlı arama yapar."""
print("--- [NODE] WEB SEARCH: İnternetten Veri Aranıyor ---")
question = state["question"]
documents = state.get("documents", [])
docs = web_search_tool.invoke({"query": question})
web_results = "\n".join([d["content"] for d in docs])
documents.append(web_results)
return {"documents": documents, "question": question}
def generate_node(state: GraphState):
"""Toplanan dokümanları kullanarak nihai yanıtı üretir."""
print("--- [NODE] GENERATE: Yanıt Üretiliyor ---")
question = state["question"]
documents = state["documents"]
generation = rag_chain.invoke({"context": documents, "question": question})
return {"documents": documents, "question": question, "generation": generation}
6.7 — Grafiğin Oluşturulması ve Koşullu Bağlantılar
from langgraph.graph import END, StateGraph
# 1. Grafiği State Yapımızla Başlatıyoruz
workflow = StateGraph(GraphState)
# 2. Düğümleri (Nodes) Ekleme
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("grade_documents", grade_documents_node)
workflow.add_node("web_search", web_search_node)
workflow.add_node("generate", generate_node)
# 3. Akış Yönlendirme Fonksiyonu (Conditional Edge için)
def decide_to_generate(state: GraphState):
"""Dokümanlar alakasızsa Web Search'e, alakalıysa Generate düğümüne yönlendirir."""
if state["web_search"]:
return "web_search"
else:
return "generate"
# 4. Giriş Noktası ve Kenarların Bağlanması
workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "grade_documents")
# Koşullu Kenar: Grade Documents sonrasında karara göre ayrışma
workflow.add_conditional_edges(
"grade_documents",
decide_to_generate,
{"web_search": "web_search", "generate": "generate"}
)
workflow.add_edge("web_search", "generate")
workflow.add_edge("generate", END)
# Grafiği Derleme (Compile)
app = workflow.compile()
6.8 — Uygulamanın Test Edilmesi
# TEST 1: Veritabanımızda olan bir soru (Prompt Engineering / RAG)
inputs = {"question": "What is Prompt Engineering?"}
for output in app.stream(inputs):
for key, value in output.items():
print(f"Adım '{key}' Tamamlandı.")
# TEST 2: Veritabanımızda olmayan, internet araması gerektiren bir soru
inputs_general = {"question": "İstanbul'da şu an hava kaç derece?"}
result = app.invoke(inputs_general)
print("Nihai Yanıt:", result["generation"])
Test 2 incelemesi: sistem önce Vektör DB'ye bakar, doküman alakasız olduğu için grade_documents aşamasında web_search = True bayrağını tetikler[cite: 2]. Koşullu kenar devreye girerek akışı anında Tavily API ile internet aramasına aktarır, taze veriyi alır ve doğru yanıtı üretir[cite: 2].
app.get_graph().draw_mermaid_png() çıktısını bir dosyaya yazdırmanız yeterlidir[cite: 2].LangGraph kullanarak kendi kendini denetleyen, dinamik yönlendirmeli ve prodüksiyon seviyesinde bir yapay zeka mimarisini sıfırdan kurduk[cite: 2]. Son bölümde bu mimariyi canlıya alacağız[cite: 2].
Production, Deploy ve Canlıya Alma Süreçleri
Serimizin bu son bölümünde, local geliştirme ortamımızda tıkır tıkır çalışan bu gelişmiş yapay zeka uygulamasını gerçek dünyaya, yani canlı bir sunucuya (Production) nasıl taşıyacağımızı adım adım ele alıyoruz[cite: 2]. Bir projenin bilgisayarımızda çalışması harikadır; ancak onu son kullanıcılara hizmet veren esnek, ölçeklenebilir ve kesintisiz bir web servisi haline getirmek işin mühendislik boyutudur[cite: 2].
7.1 — Local'den Production'a Geçiş: Neden Önemli?
Bir projenin canlıya alınması sürecinde şu kritik ilkeleri dikkate almak gerekir[cite: 2]:
- Ortam İzolasyonu: sunucu üzerindeki ana Python ortamını kirletmeden, projeye özel izole bir sanal çevre kurmak[cite: 2].
- Gizli Bilgilerin Yönetimi: API anahtarlarını asla kamuya açık GitHub depolarına pushlamadan, sunucu üzerinde güvenli
.envdosyalarında saklamak[cite: 2]. - Kesintisiz Çalışma: terminal kapandığında uygulamanın durmasını engellemek ve arka planda sürekli çalışmasını sağlamak[cite: 2].
7.2 — Adım Adım Sunucuya Dağıtım (DigitalOcean)
Canlıya alma sürecinde pratikliği ve maliyet etkinliği nedeniyle DigitalOcean Droplet (Linux/Ubuntu) sunucularını tercih edebiliriz[cite: 2].
Öncelikle sunucumuzda çalışacak API uç noktamızı (main.py) hazırlayalım:
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
app = FastAPI(title="LangGraph RAG Server")
class ChatRequest(BaseModel):
question: str
@app.post("/chat")
async def chat_endpoint(req: ChatRequest):
result = app_graph.invoke({"question": req.question})
return {"response": result["generation"]}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
- Projeyi GitHub'a taşı
.gitignoredosyasının içinde.env'in ekli olduğundan emin olarak kodu commit'leyip repoya push et[cite: 2]. - Sunucu kurulumu ve SSH bağlantısı5-10$'lık temel bir Ubuntu Droplet oluşturup terminal üzerinden IP adresine bağlan[cite: 2].
- Projeyi klonla, venv kurSunucunun
/homedizinine klonla, sanal çevreyi aktifleştir, bağımlılıkları yükle[cite: 2]. - Sunucuda .env oluştur
nanoile API anahtarlarını içeren.envdosyasını sunucuda yeniden oluştur[cite: 2]. - Servisi arka planda çalıştır
nohupile SSH bağlantısı kapansa bile servisin 7/24 çalışmasını sağla[cite: 2].
# 1. GitHub'a push
git init
git add .
git commit -m "feat: Production ready LangGraph RAG mimarisi"
git remote add origin https://github.com/Rasittekin18/ai-applications-langchain-rag.git
git push -u origin main
# 2. Sunucuya SSH bağlantısı
ssh root@SUNUCU_IP_ADRESI
# Sistem paketlerini güncelle
sudo apt update && sudo apt upgrade -y
sudo apt install python3-venv python3-pip git -y
# 3. Projeyi klonla ve venv kur
cd /home
git clone https://github.com/Rasittekin18/ai-applications-langchain-rag.git
cd ai-applications-langchain-rag
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 4. .env dosyasını oluştur
nano .env
# OPENAI_API_KEY=... / TAVILY_API_KEY=... / LANGCHAIN_TRACING_V2=true ...
# CTRL+O (kaydet) ve CTRL+X (çık)
# 5. Servisi arka planda sürekli çalıştır
nohup python3 main.py > output.log 2>&1 &
Bu sayede terminalimizi kapatsak bile yazdığımız ajanik RAG servisi sunucumuzun belirlenen portunda (örn. 8080 veya 8000) 7/24 çalışmaya devam eder[cite: 2].
7.3 — Görsel Arayüz Entegrasyonu: Voiceflow & Web Widget
Sunucumuz canlıya geçtiğinde elimizde http://SUNUCU_IP:8080/chat şeklinde çalışan canlı bir REST API olur[cite: 2]. Front-End geliştirmekle vakit kaybetmek istemiyorsanız, Voiceflow gibi sürükle-bırak agent platformlarını arka plan sunucunuzla konfigüre edebilirsiniz[cite: 2]:
- Workflow oluşturVoiceflow Dashboard üzerinden yeni bir akış oluşturulur[cite: 2].
- HTTP Request bağlaAkış içindeki HTTP Request bloğuna canlı sunucunun IP adresi ve endpoint'i eklenir[cite: 2].
- Embed script'i yapıştırVoiceflow'un sunduğu tek satırlık JavaScript kodu rasittekin.com'a yapıştırılır[cite: 2].
Böylece saniyeler içinde sitenin sağ alt köşesinde, canlı sunucudaki LangGraph RAG mimarisiyle haberleşen şık bir Chat Widget'ı yayına alınmış olur[cite: 2].
.env dosyanızdaki LANGCHAIN_TRACING_V2=true ayarını aktif tutarak LangSmith panelinden anlık izleme yapabilirsiniz[cite: 2].Serinin Özeti ve Kapanış
Bu rehber serisi boyunca statik yapay zeka entegrasyonlarından başlayarak adım adım prodüksiyon seviyesinde ajanik sistemlere kadar uzanan devasa bir yol katettik[cite: 2].
- Giriş & LCELLangChain ekosistemini anladık,
ChatOpenAIyapılandırmasını öğrendik ve LCEL (|) mimarisiyle modüler zincirler kurduk[cite: 2]. - Memory & StreamingLLM'lerde sohbet geçmişini (Session ID) korumayı ve streaming yanıtlarla kullanıcı deneyimini zirveye taşımayı inceledik[cite: 2].
- Vektör Veritabanları & RAGChroma DB ile metinleri vektör uzayına taşıyıp anlamsal aramalar yaptık[cite: 2].
- ReAct & AgentsLLM'e düşünme, karar verme ve Tavily Search gibi araçları kullanma yeteneği kazandırdık[cite: 2].
- LangGraph & Self-Reflective RAGSistemimize durum makineleri entegre ederek halüsinasyon denetleyicileri, doküman puanlayıcıları ve kendi kendini düzelten mimariyi sıfırdan kurduk[cite: 2].
- Production & DeployTüm bu mimariyi Linux sunucularda canlıya alarak uçtan uca bir yapay zeka ürünü haline getirdik[cite: 2].
Geleceğin yazılım mimarileri artık sadece kod yazan değil; akıl yürüten, karar veren ve kendi hatalarını denetleyebilen Ajanik (Agentic) Sistemler üzerine kuruluyor[cite: 2].
Rasittekin18/ai-applications-langchain-rag[cite: 2]Sorularınız, katkılarınız veya kendi projelerinizde karşılaştığınız senaryolar olursa yorumlar kısmında veya sosyal medya hesaplarım üzerinden benimle iletişime geçmekten çekinmeyin![cite: 2]

