Mert Özen Her Satırda Daha İleri
Yapay Zeka AI Agent'ları ve MCP — #01

AI Agent Nedir? Workflow, Agent ve "Burada Agent Gerekmez" Kararı

Mert Özen 8 Kas 2026 11 dk 1 görüntülenme
AI Agent Nedir? Workflow, Agent ve "Burada Agent Gerekmez" Kararı

Agent, kontrol akışını modelin yönettiği sistemdir; workflow'da yolu kod çizer. Bookstore'dan üç senaryoyu (sipariş durumu, iade, kitap bul ve sepete ekle) aynı kodla qwen3:4b üzerinde koşturup model çağrısı, token ve süreyi ölçüyor, agent'ın bedelini ve "burada agent gerekmez" kararının kriterlerini çıkarıyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.Extensions.AI 10.11.0 ve OllamaSharp 5.5.2 ile derlenip yerel Ollama 0.34.4 üzerindeki qwen3:4b modeliyle denenmiştir.

Önce Tahmin Et

Bookstore'un destek ekranına aynı gün üç mesaj düşüyor:

  • "Where is my order 1043?"
  • "The cover of Coffee and Compilers arrived torn. I want to send order 1040 back."
  • "I like quiet novels. Find me one under 20 dollars that is in stock and add one copy to my cart."

Üçü de bir LLM (Large Language Model) ile çözülebilir. Soru şu: hangisi için gerçekten bir agent gerekir? Her birinde modelin kaç kez çağrılacağını, kaç token harcanacağını ve cevabın kaç saniye süreceğini tahmin et. Yazının sonunda üç senaryoyu aynı kod tabanında gerçekten koşturup sayıları yan yana koyacağız. İpucu: üçüncüsü, ilk ikisinin toplamından çok daha pahalı çıkıyor.

Workflow ve Agent: İki Tanım

"Agent" kelimesi son iki yılda neredeyse her LLM özelliğine yapıştırıldı: chatbot'a agent, tek bir prompt'a agent, bir cron job'a bile agent dendiğini gördüm. Mühendislik konuşabilmek için daha dar bir tanım lazım. Bu seride Anthropic'in 19 Aralık 2024 tarihli "Building effective agents" yazısındaki ayrımı kullanacağız, çünkü sektörde en çok atıf alan ve en kullanışlı ayrım bu:

  • Workflow: LLM'ler ve tool'lar, senin kodunda önceden çizilmiş yollar üzerinden çalıştırılır. Sıradaki adımı kod seçer.
  • Agent: LLM kendi adımlarını ve tool kullanımını dinamik olarak yönetir. Hangi tool'un, hangi sırayla, kaç kez çağrılacağına ve ne zaman duracağına model karar verir.

İki tanımın ortak yapı taşı aynı: tool'lar, retrieval ve memory ile güçlendirilmiş bir LLM çağrısı. Bu çağrının mekaniğini LLM Temelleri serisinin 11. ve 12. yazılarında gördük: model fonksiyon çalıştırmaz, çağrı ister; çağrıyı senin kodun çalıştırır ve sonucu modele geri verir. Agent'ı workflow'dan ayıran şey tool kullanmak değil, kontrolün kimde olduğu. Workflow'da kontrol akışı (control flow) C# kodunda, agent'ta modelin her turda ürettiği kararlarda.

Bu ayrımın pratik bir sonucu var: workflow'u okuyarak ne yapacağını bilirsin, agent'ı ancak çalıştırarak görürsün. Test, debug ve maliyet tahmini bu farktan doğuyor.

Otonomi Yelpazesi

Gerçek sistemler bu iki uçtan birine tam oturmaz; bir yelpazenin üzerinde durur. Soldan sağa gittikçe modele daha çok karar bırakırsın:

01-autonomy-spectrum-tr.png

En solda tek bir model çağrısı var: özet çıkar, sınıflandır, çevir. Bir adım sağında tek tool çağrısı: model hangi tool'u hangi argümanla çağıracağını seçer ama akış yine tek tur. Ortada workflow: birden çok LLM adımı var, ama adımların sırası ve dallanması kodda. En sağda agent: model bir döngü içinde tool çağırır, sonucu gözlemler, sonraki adımı kendisi seçer ve "bitti" dediğinde durur.

Anthropic'in yazısındaki en önemli cümle bence tanımlar değil, tavsiye: önce mümkün olan en basit çözümü ara, karmaşıklığı ancak ölçülebilir bir kazanç getirdiğinde ekle. Agent'lar daha iyi sonuç için gecikme ve maliyetten taviz verir; bu takasın her senaryoda mantıklı olduğunu varsayma. Yelpazede sağa doğru her adım bir bedel, ve bu yazının geri kalanı o bedeli ölçmekle ilgili.

Ortak Kod: Bookstore Tool'ları ve Bir Sayaç

Üç senaryo aynı veriyi ve aynı tool'ları kullanıyor: sekiz kitaplık bir katalog, birkaç sipariş ve bir sepet (veri seri boyunca aynı kalacak). Tool'lar bir class'ın method'ları; adları AIFunctionFactory.Create ile snake_case veriliyor, çünkü serinin MCP (Model Context Protocol) yazılarında da aynı adları kullanacağız:

public AIFunction[] ReadTools() =>
[
    AIFunctionFactory.Create(SearchBooks, name: "search_books"),
    AIFunctionFactory.Create(CheckStock, name: "check_stock"),
    AIFunctionFactory.Create(GetOrderStatus, name: "get_order_status"),
];

public AIFunction AddToCartTool() => AIFunctionFactory.Create(AddToCart, name: "add_to_cart");

Bedeli ölçmek için modelin önüne bir sayaç koyuyoruz. DelegatingChatClient'tan türeyen bu client, içinden geçen her model çağrısını sayıyor, Ollama'nın döndürdüğü token sayılarını topluyor ve süreyi ölçüyor:

public sealed class MeteringChatClient(IChatClient inner) : DelegatingChatClient(inner)
{
    public int Calls { get; private set; }
    public long InputTokens { get; private set; }
    public long OutputTokens { get; private set; }
    public TimeSpan Elapsed { get; private set; }

    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        Stopwatch sw = Stopwatch.StartNew();
        ChatResponse response = await base.GetResponseAsync(messages, options, cancellationToken);
        Calls++;
        InputTokens += response.Usage?.InputTokenCount ?? 0;
        OutputTokens += response.Usage?.OutputTokenCount ?? 0;
        Elapsed += sw.Elapsed;
        return response;
    }

    // Reset() ve ToString() burada kısaltıldı
}

Pipeline'da sayacın yeri önemli. UseFunctionInvocation tool döngüsünü yürüten katman; sayacı onun altına koyduğumuz için döngünün yaptığı her model çağrısı ayrı ayrı sayılıyor:

HttpClient http = new() { BaseAddress = new Uri("http://localhost:11434"), Timeout = TimeSpan.FromMinutes(5) };
MeteringChatClient meter = new(new OllamaApiClient(http, "qwen3:4b"));
IChatClient toolClient = new ChatClientBuilder(meter)
    .UseFunctionInvocation(configure: f => f.MaximumIterationsPerRequest = 8)
    .Build();

Sipariş Durumu: Tek Tool Çağrısı

İlk mesaj için modele yalnızca get_order_status tool'unu veriyoruz:

// 1) Tek tool çağrısı: model yalnızca get_order_status'u seçiyor
ChatOptions options = new() { Temperature = 0, Seed = 42, Tools = [tools.ReadTools()[2]] };
ChatResponse r = await toolClient.GetResponseAsync("Where is my order 1043?", options);

Gerçek çıktı:

--- order status
model calls=2, input tokens=501, output tokens=405, time=25,4s, tool calls=1
Your order #1043 is currently **Shipped** with tracking number **TRK-88213**. You can use this tracking number to monitor the delivery status with the carrier.

Tek tool çağrısı bile iki model çağrısı demek: biri çağrıyı istemek, biri sonucu cümleye dökmek için. 405 output token'ın büyük kısmı qwen3:4b'nin cevaptan önce ürettiği reasoning metni; ekranda tek cümle görüyorsun ama model yüzlerce token düşündü. Süredeki 25 saniyenin bir kısmı da bu makinedeki Ollama'yı başka işlerle paylaşmamızdan geliyor; mutlak sayılara değil, senaryolar arasındaki orana bak.

Bu senaryo yelpazenin solunda ve orada kalmalı. Hatta daha da sola çekilebilir: sipariş numarası zaten bir form alanından ya da URL'den (Uniform Resource Locator) geliyorsa model hiç gerekmez, GetOrderStatus(1043) bir veritabanı sorgusudur. Modelin tek katkısı serbest metinden sipariş numarasını çekip cevabı cümleye dökmek.

İade Başvurusu: Yolu Kod Çiziyor

İkinci mesajın adımları baştan belli: iade sebebini sınıflandır, uygunluğu kontrol et, müşteriye cevap yaz. Belli olan adımları modele keşfettirmenin anlamı yok. Kod yolu çiziyor, model yalnızca iki kutuyu dolduruyor:

// 2) Workflow: yol kodda sabit, model iki adımı dolduruyor
string message = "The cover of Coffee and Compilers arrived torn. I want to send order 1040 back.";
ChatResponse<ReturnReason> classified = await meter.GetResponseAsync<ReturnReason>(
    $"Classify the return reason of this customer message: {message}",
    new ChatOptions { Temperature = 0, Seed = 42 });
ReturnReason reason = classified.Result;

Order order = data.Orders.Single(o => o.Id == 1040);
DateOnly today = new(2026, 11, 8);
bool eligible = order.DeliveredOn is { } d && today.DayNumber - d.DayNumber <= 14
    && reason.Category != ReturnCategory.ChangedMind;

ChatResponse reply = await meter.GetResponseAsync(
    $"Write a two sentence reply to a customer. Return {(eligible ? "approved" : "rejected")}, reason: {reason.Category}.",
    new ChatOptions { Temperature = 0, Seed = 42 });
public enum ReturnCategory { Damaged, WrongItem, ChangedMind, Other }

public sealed record ReturnReason(
    ReturnCategory Category,
    [property: Description("One short sentence quoting the customer's problem.")] string Summary);

Dikkat edilecek nokta, iadenin uygun olup olmadığına modelin karar vermemesi. 14 gün kuralı bir iş kuralı; C# ile yazılır, unit test'i yazılır, değiştiğinde code review'dan geçer. Model yalnızca kendisinin iyi yaptığı iki işi yapıyor: serbest metni enum'a çevirmek (structured output) ve cevabı yazmak. Çıktı:

--- return workflow (category=Damaged, eligible=True)
model calls=2, input tokens=69, output tokens=1412, time=84,0s, tool calls=0
Your return has been approved because the item was damaged. Please send the damaged item back using the return label we provided in your email.

İki model çağrısı, sıfır tool çağrısı. Input token'ın bu kadar düşük olması tool tanımı göndermediğimiz için; buna karşılık output tarafında 1412 token var, yine reasoning yüzünden. Bu tür sınıflandırma ve kısa metin işleri için reasoning yapmayan daha küçük bir model çoğu zaman yeter; beşinci yazıdaki routing pattern'i tam olarak bunu yapacak. Bir şeye daha dikkat et: model "return label we provided in your email" diyor, oysa böyle bir e-posta yok. Workflow'da bunu yakalamak kolay, çünkü cevabı üreten adım tek ve yerini biliyorsun; prompt'a "do not promise anything not listed" eklemek ya da cevabı bir şablondan üretmek yeter.

"Bana Kitap Bul ve Sepete Ekle": Agent

Üçüncü mesajda adım sayısı baştan belli değil. Kaç arama yapılacağı, hangi kitabın stokuna bakılacağı, stok yoksa ne yapılacağı aramanın sonucuna bağlı. Burada kontrolü modele bırakıyoruz: dört tool, bir talimat ve en fazla sekiz tur:

// 3) Agent: hangi tool'un hangi sırayla çağrılacağına model karar veriyor
ChatOptions options = new()
{
    Temperature = 0,
    Seed = 42,
    Instructions = "You are the Bookstore shopping assistant. Use the tools. Never add a book that is out of stock.",
    Tools = [.. tools.ReadTools(), tools.AddToCartTool()],
};
ChatResponse r = await toolClient.GetResponseAsync(
    "I like quiet novels. Find me one under 20 dollars that is in stock and add one copy to my cart.", options);
--- shopping agent
model calls=4, input tokens=7422, output tokens=2342, time=287,2s, tool calls=3
  tool: search_books(query: quiet novel, genre: novel, maxPrice: 20)
  tool: check_stock(bookId: 7)
  tool: add_to_cart(bookId: 7, quantity: 1)
Your cart now includes **The Quiet Orchard** (Mira Holt) at $16.50. ✅  
This book is in stock and has been added to your cart. Let me know if you'd like to proceed with checkout!
cart: The Quiet Orchard x1

Dört model çağrısı, üç tool çağrısı. Model aramayı kendisi daralttı (genre: novel, maxPrice: 20), listenin başındaki kitabın stokunu kontrol etti ve ekledi. Bu sırayı kodda hiçbir yere yazmadık; model bu koşuda böyle seçti, başka bir koşuda farklı seçebilir.

Katalogda 20 doların altında iki roman var: 18,90 dolarlık The Last Lighthouse (stok 0) ve 16,50 dolarlık The Quiet Orchard (stok 15). Bu koşuda arama sonucunun ilk sırasına The Quiet Orchard çıktığı için model tuzağa hiç yaklaşmadı. Aynı istekte arama The Last Lighthouse'u öne çıkarsaydı, stok kontrolü ve add_to_cart'ın hata cevabı devreye girecekti; ikinci yazıda tam bu durumu koşturacağız. Agent'ın değeri bu tür belirsizliği kendi başına çözebilmesi; bedeli de aşağıdaki tablo.

Üretimde Dikkat: Bedeli Ölçmek

Üç senaryonun sayıları yan yana:

SenaryoModel çağrısıTool çağrısıInput tokenOutput tokenSüre
Sipariş durumu (tek tool)2150140525,4 sn
İade (workflow)2069141284,0 sn
Kitap bul ve ekle (agent)4374222342287,2 sn

Agent'ın input token'ı diğer ikisinin toplamının on katından fazla. Sebebi döngünün doğası: her turda bütün geçmiş (talimat, tool tanımları, önceki tool çağrıları ve sonuçları) modele yeniden gönderiliyor. Tur sayısı arttıkça maliyet doğrusal değil, birikerek büyüyor. Bulut sağlayıcıda bu doğrudan fatura demek; yerel modelde gecikme ve paylaşılan GPU (Graphics Processing Unit) zamanı demek.

Maliyetin yanında üç bedel daha var. Birincisi güvenilirlik: her adımın %95 doğru olduğu bir zincirde on adımın hepsinin doğru gitme olasılığı 0,95'in onuncu kuvveti, yani yaklaşık %60. Agent'larda hatalar birikir; Anthropic de agent'ların daha yüksek maliyet ve birikerek büyüyen hata riski taşıdığını, bu yüzden sandbox'ta kapsamlı test ve guardrail istediğini yazıyor. İkincisi gecikme: kullanıcı her turu bekliyor. Üçüncüsü debug edilebilirlik: workflow'da bir hata hangi adımda olduğunu söyler, agent'ta önce modelin o gün hangi yolu seçtiğini bulman gerekir. Bu yüzden ikinci yazıda loop'u yazarken ilk iş her adımı loglamak olacak.

Production'da bu sayıları tahmin etme, ölç. Buradaki MeteringChatClient gibi bir katman (ya da .NET ile Yapay Zeka serisinde göreceğimiz OpenTelemetry entegrasyonu) her istek için model çağrısı, token ve süreyi kaydetmeli. Bir agent'ı yayına alırken sorulacak soru "çalışıyor mu" değil, "istek başına ortalama ve en kötü durumda kaç tur, kaç token" olmalı.

Karar için kullandığım kısa liste şöyle. Adımlar baştan biliniyorsa workflow yaz. Adım sayısı girdiye göre değişiyor ama yollar sayılabiliyorsa (üç dört dal) routing'li bir workflow yaz. Yalnızca adım sayısı ve sırası gerçekten öngörülemiyorsa, hata maliyeti düşükse ya da bir insan onayıyla sınırlanabiliyorsa ve bütçe tur başına ödemeyi kaldırıyorsa agent'a geç. Bu seride sırasıyla agent loop'unu sıfırdan yazacağız (02), tool tasarımını (03) ve context yönetimini (04) konuşacağız, workflow ve multi-agent pattern'lerine (05, 06) ve human-in-the-loop'a (07) geçeceğiz; ikinci yarıda aynı tool'ları MCP server'ı olarak C# ve TypeScript'te yayınlayıp güvenlik ve evaluation ile kapatacağız.

Sık Düşülen Tuzaklar

Her LLM özelliğine "agent" demek. Tek prompt'la ürün açıklaması üreten bir endpoint agent değil; böyle adlandırınca ekip ona agent muamelesi yapmaya, gereksiz döngü ve framework eklemeye başlıyor. Neyin ne olduğunu doğru adlandır.

Framework'le başlamak. Microsoft Agent Framework (MAF) gibi framework'ler kurulumu kolaylaştırır ama prompt'ları ve cevapları bir soyutlama katmanının arkasına saklar. Anthropic'in tavsiyesi de bu yönde: önce LLM'in API'siyle (Application Programming Interface) doğrudan çalış, framework kullanacaksan altında ne yaptığını bil. Bir sonraki yazıda loop'u önce framework'süz yazmamızın sebebi bu.

İş kuralını modele bırakmak. İade uygunluğu, indirim oranı, yetki kontrolü gibi kararlar kodda kalmalı. Modelin cevabı bir girdi; karar senin.

Durma koşulu koymamak. Agent "bitti" demediği sürece döner. MaximumIterationsPerRequest gibi bir üst sınır ve token/zaman bütçesi olmadan bir agent'ı production'a çıkarma.

Yalnızca mutlu yolu denemek. Yukarıdaki agent bu koşuda doğru kitabı seçti; aynı prompt'u stok verisi değişmiş halde, belirsiz bir istekle ya da yanlış yazılmış bir yazar adıyla da dene. Agent'ın kalitesi tek bir koşuyla ölçülmez; on dördüncü yazıda bunu sistematik hale getireceğiz.

Küçük Bir Deneme

★☆☆ Sipariş durumu senaryosunu modelsiz yaz: kullanıcı mesajından sipariş numarasını bir regular expression ile çek, GetOrderStatus'u doğrudan çağır ve cevabı string interpolation ile oluştur. Süre ve token farkını not et; hangi mesajlarda bu yaklaşım bozuluyor?

★★☆ İade workflow'unda sınıflandırma adımını gemma3:4b ile, cevap adımını qwen3:4b ile çalıştır. Output token ve süre nasıl değişiyor? Sınıflandırma kalitesi düşüyor mu? "I just don't like it anymore" gibi bir mesajla da dene.

★★★ Agent senaryosunda add_to_cart'ı hata durumunda error yerine boş bir object döndürecek şekilde değiştir ve isteği "add The Last Lighthouse" diye ver. Model kullanıcıya ne söylüyor? Ardından MaximumIterationsPerRequest'i 2'ye indir ve agent'ın yarım kalan işi nasıl raporladığına bak.

Bu yazıda döngüyü UseFunctionInvocation'a bıraktık ve içini görmedik. Bir sonraki yazıda aynı döngüyü framework'süz, kendi elimizle yazacağız: plan, tool çağrısı, gözlem, durma koşulu, adım bütçesi ve her adımın trace'i; sonra aynı işi FunctionInvokingChatClient ve MAF'ın ChatClientAgent'ıyla yapıp nelerin soyutlandığını göreceğiz.