Multi-agent Sistemler: Handoff, Group Chat, Orchestration ve A2A
Birden fazla agent uzmanlaşma, izolasyon ve paralellik getirir ama token çarpanı ve koordinasyon hatasıyla ödenir. MAF'ın sequential, concurrent, handoff, group chat ve Magentic orkestrasyonlarını Bookstore'da koşturuyor; uzmanın tool çağırmadan geri devrettiği handoff'u ve bitmeyen group chat'i gerçek çıktılarla görüyor, başka servisteki agent'a A2A ile iş veriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.Extensions.AI 10.11.0, OllamaSharp 5.5.2, Microsoft.Agents.AI 1.28.0, Microsoft.Agents.AI.Workflows 1.28.0, Microsoft.Agents.AI.A2A ve Microsoft.Agents.AI.Hosting.A2A.AspNetCore 1.28.0-preview.261106.1 ile derlenip yerel Ollama 0.34.4 üzerindeki qwen3:4b ve gemma3:4b modelleriyle denenmiştir.
Önce Tahmin Et
Bookstore desteğini üç agent'a bölüyoruz: gelen mesajı yönlendiren bir triage agent, siparişlere bakan bir order agent (elinde get_order_status var) ve katalogla ilgilenen bir catalog agent. Müşteri soruyor: "Has my order 1044 shipped yet?" 1044'ün gerçek durumu Processing, yani henüz kargoya verilmedi.
Tahmin et: cevap doğru mu gelir? Cevabı hangi agent yazar? Ve kaç tool çağrısı yapılır? Tek agent'lı bir tasarımda bu soru tek bir tool çağrısıyla doğru cevaplanırdı. Bu yazıda multi-agent tasarımın bu soruyu nasıl yanlış cevaplayabildiğini gerçek bir koşuda göreceğiz.
Neden Birden Fazla Agent?
Multi-agent'ın üç meşru gerekçesi var. Uzmanlaşma: her agent'ın kendi talimatı ve az sayıda tool'u olur; on beş tool'lu tek bir agent yerine dörder tool'lu üç agent, her birinin doğru tool'u seçmesini kolaylaştırır. İzolasyon: bir agent'ın context'i diğerininkini kirletmez; dördüncü yazıdaki sub-agent örneğinde gördüğümüz gibi araştırmayı yapan agent'ın bütün tool çıktıları lead agent'ın context'ine taşınmaz. Paralellik: birbirinden bağımsız işler aynı anda yürür.
Bedeli de üç kalem. Token çarpanı: her agent kendi talimatını, kendi tool tanımlarını ve devraldığı konuşmayı modele yeniden gönderir; aynı iş tek agent'a göre birkaç kat token tüketir. Koordinasyon hatası: agent'lar arasında bilgi kaybolur, görev yanlış agent'a gider, iki agent birbirine iş atar. Debug zorluğu: bir hata artık tek bir loop'un trace'inde değil, agent'lar arası mesajlaşmanın içinde. İlk yazıdaki kural burada da geçerli: tek bir agent ve iyi tasarlanmış tool'larla çözülebilen işi multi-agent yapma.

MAF'ın Hazır Orkestrasyonları
Microsoft Agent Framework (MAF), sık görülen multi-agent yapıları için hazır workflow builder'ları getiriyor. Microsoft.Agents.AI.Workflows 1.28.0 paketindeki AgentWorkflowBuilder şu girişleri sunuyor:
| Orkestrasyon | Giriş noktası | Ne yapar |
|---|---|---|
| Sequential | BuildSequential, CreateSequentialBuilderWith | Agent'lar sırayla çalışır, her biri öncekinin çıktısını alır |
| Concurrent | BuildConcurrent, CreateConcurrentBuilderWith | Aynı girdi bütün agent'lara gider, cevaplar toplanır |
| Handoff | CreateHandoffBuilderWith | Agent, konuşmayı bir handoff tool'u çağırarak başka bir agent'a devreder |
| Group chat | CreateGroupChatBuilderWith | Bir manager (ör. RoundRobinGroupChatManager) sırayı belirler, agent'lar ortak bir sohbete yazar |
| Magentic | CreateMagenticBuilderWith | Bir orchestrator plan yapar, ilerlemeyi bir ledger'da izler ve gerektiğinde yeniden planlar |
Hepsi önceki yazıda gördüğümüz workflow altyapısının üzerinde çalışıyor: superstep'ler, event'ler, checkpoint. Sequential ve concurrent, beşinci yazıdaki prompt chaining ve parallelization'ın agent'larla yazılmış hali. Yeni olanlar, kontrolün agent'lar arasında el değiştirdiği handoff ve group chat.
Handoff: Triage ve Uzmanlar
Handoff'ta her agent'a, devredebileceği agent'lar için bir tool verilir. Model bu tool'u çağırdığında konuşmanın kontrolü diğer agent'a geçer:
ChatClientAgent triage = qwen.AsAIAgent(
instructions: "You route Bookstore customers. Always hand off: order questions to order_agent, book questions to catalog_agent.",
name: "triage_agent",
description: "Routes the customer to the right specialist.");
ChatClientAgent orders = qwen.AsAIAgent(
instructions: "You answer questions about the customer's orders with get_order_status. One sentence.",
name: "order_agent",
description: "Specialist for order status and delivery questions.",
tools: [tools.ReadTools()[2]]);
ChatClientAgent catalog = qwen.AsAIAgent(
instructions: "You answer questions about books with search_books and check_stock. One sentence.",
name: "catalog_agent",
description: "Specialist for book search and stock questions.",
tools: [tools.ReadTools()[0], tools.ReadTools()[1]]);
Workflow workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triage)
.WithHandoffs(triage, [orders, catalog])
.WithHandoffs([orders, catalog], triage)
.Build();
Workflow'u streaming çalıştırıp event'leri izliyoruz: AgentResponseUpdateEvent içindeki tool çağrılarını ve sondaki WorkflowOutputEvent'teki mesajları yazdırıyoruz. İlk soru için gerçek çıktı:
> Has my order 1044 shipped yet?
[triage_agent_49afb5c3d97f4a38bb371fd3051edd3a] calls handoff_to_1
[order_agent_c3a48ec1fc124295b9f280353f29eefa] calls handoff_to_1
[triage_agent] result "Transferred."
[order_agent] result "Transferred."
triage_agent: Your order 1044 has been shipped and is expected to arrive by October 15, 2023.
model calls=3, input tokens=1627, output tokens=2486, time=285,1s
Tahmin sorusunun cevabı: cevap yanlış ve tamamen uydurma. 1044 kargoya verilmedi, "October 15, 2023" diye bir tarih de hiçbir yerde yok. Trace ne olduğunu gösteriyor: triage, soruyu doğru şekilde order agent'a devretti (handoff_to_1); order agent ise get_order_status'u çağırmak yerine kendi handoff tool'unu çağırıp konuşmayı triage'a geri verdi. Hiç tool çağrısı yapılmadı ve cevabı, sipariş verisine hiç erişimi olmayan triage agent yazdı. Bu koşuyu iki kez yaptık; ilkinde de aynı yol izlenmiş ve cevap "Your order 1044 has been shipped." olmuştu. Tek agent'lı tasarımda bir tool çağrısıyla çözülen bir soru, üç agent'a bölününce iki handoff, üç model çağrısı ve yanlış bir cevap üretti.
> Do you have Winter in Kyoto in stock?
[triage_agent_49afb5c3d97f4a38bb371fd3051edd3a] calls handoff_to_2
[triage_agent] result "Transferred."
model calls=2, input tokens=1087, output tokens=366, time=55,7s
İkinci soruda triage doğru agent'ı seçti (handoff_to_2, catalog agent), ama workflow kullanıcıya hiçbir metin döndürmeden bitti: catalog agent ne tool çağırdı ne de görünür bir cevap yazdı. İki koşuda iki farklı hata: biri yanlış cevap, diğeri cevapsızlık. İkisi de yalnızca event'leri tek tek yazdırdığımız için görünür oldu; son mesajı kullanıcıya göstermekle yetinen bir uygulama ilk durumda yanlış bilgi, ikincisinde boş ekran gösterirdi. Handoff'u production'a almadan önce uzman agent'ların gerçekten işini yaptığını (tool çağrısı, boş olmayan cevap) kodla kontrol etmen gerekiyor.
Concurrent: Aynı Metne İki Göz
Concurrent orkestrasyon, aynı girdiyi birden fazla agent'a verip cevapları toplamak için. Bir kampanya metnini pazarlama ve hukuk açısından iki gemma3:4b agent'ına inceletiyoruz:
// Aynı metin, iki bağımsız inceleyici, sonuçlar birlikte toplanıyor
AIAgent marketing = gemma.AsAIAgent(instructions: "You review promo texts for a bookstore. Give one short improvement.", name: "marketing");
AIAgent legal = gemma.AsAIAgent(instructions: "You check promo texts for promises the store cannot keep. Reply in one sentence.", name: "legal");
Workflow workflow = AgentWorkflowBuilder.BuildConcurrent([marketing, legal]);
await RunAndPrintAsync(workflow, [new ChatMessage(ChatRole.User, "Promo: Order today and every book arrives tomorrow, guaranteed, anywhere in the world!")]);
marketing: Here’s one short improvement: **Promo:** Order today and every book arrives tomorrow, guaranteed – **worldwide delivery!** (Adding “worldwide delivery” is more concise and immediately clarifies the benefit.)
legal: That's an unrealistic promise as worldwide shipping and guaranteed same-day delivery are rarely achievable for all books.
İki bakış açısı aynı anda geldi ve birbirini tamamlıyor: pazarlama agent'ı metni kısaltırken yerine getirilemeyecek sözü daha da parlattı, hukuk agent'ı tam o sözü yakaladı. Concurrent orkestrasyonun değeri burada: tek bir agent'a "hem pazarlama hem hukuk açısından bak" demek, çoğu zaman bir bakış açısının diğerini bastırmasıyla sonuçlanır. Toplanan cevapları kimin birleştireceği ise senin kararın; burada iki cevabı olduğu gibi yazdırdık, gerçek bir akışta hukuk itirazı varsa metin yayınlanmamalı.
Group Chat: Sırayla Konuşan Agent'lar
Group chat'te agent'lar ortak bir sohbete sırayla yazar; sırayı bir manager belirler. RoundRobinGroupChatManager en basiti: katılımcıları sırayla dolaşır ve MaximumIterationCount'a ulaşınca durur:
AIAgent writer = gemma.AsAIAgent(instructions: "You write and revise a one sentence newsletter line about the book 'Winter in Kyoto' by Ken Sato.", name: "writer");
AIAgent editor = gemma.AsAIAgent(instructions: "You edit newsletter lines. Point out one concrete problem, or reply APPROVED.", name: "editor");
Workflow workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 })
.AddParticipants(writer, editor)
.Build();
Gerçek çıktı (writer'ın ilk mesajını kısalttım):
writer: Okay, here are a few options for a one-sentence newsletter line promoting "Winter in Kyoto" by Ken Sato, suitable for next week's newsletter, with slightly different tones: **Option 1 (Warm & Evocative):** ... **To help me tailor the line even better for *your* newsletter, could you tell me:** ...
editor: APPROVED
writer: Great! I’m glad we got there. 😊 If you need anything else related to this newsletter line or promoting "Winter in Kyoto," just let me know.
editor: Fantastic! Thanks so much for your collaboration. 😊
Bu koşu group chat'in iki tipik sorununu birden gösteriyor. Birincisi, writer tek cümle yerine üç seçenek yazdı ve kullanıcıya soru sordu; editor bunların hangisini onayladığını söylemeden "APPROVED" dedi. İkincisi, onaydan sonra sohbet bitmedi: round-robin manager'ın bildiği tek durma koşulu sayaç, o yüzden iki agent iki tur boyunca birbirine teşekkür etti. Bu tam olarak "agent'lar arası sonsuz konuşma" tuzağı; burada MaximumIterationCount = 4 olduğu için dört mesajda bitti. Gerçek bir senaryoda "APPROVED" gördüğünde sohbeti bitiren bir manager yazmalısın (GroupChatManager'dan türeterek) ve onaylanan şeyin tek ve belirli bir metin olmasını zorlamalısın. Beşinci yazıdaki writer-critic workflow'u bu iş için daha uygun: durma koşulu kodda.
Süreçler Arası: A2A Protokolü
Buraya kadarki bütün agent'lar aynı process'teydi. Peki stok sorusunu cevaplayan agent başka bir ekibin, başka bir serviste, kendi modeli ve kendi tool'larıyla çalışan agent'ıysa? A2A (Agent2Agent) protokolü bunun için var. Google'ın Nisan 2025'te duyurduğu protokol, 23 Haziran 2025'te Linux Foundation'a devredildi; bu yazıda kullandığımız .NET paketleri protokolün 1.0 sürümünü konuşuyor (ProtocolVersion = "1.0"). Agent, kendini /.well-known/agent-card.json adresindeki bir Agent Card ile tanıtıyor: adı, açıklaması, yetenekleri (skill'ler) ve hangi transport'larla konuştuğu.
Depo ekibinin servisi, kendi tool'larıyla bir InventoryAgent'ı ASP.NET Core üzerinden A2A ile yayınlıyor:
// Depo ekibinin servisi: kendi process'i, kendi tool'ları, kendi modeli
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
const string Url = "http://localhost:19801";
HttpClient http = new() { BaseAddress = new Uri("http://localhost:11434"), Timeout = TimeSpan.FromMinutes(8) };
BookstoreTools tools = new(new BookstoreData(), currentCustomerId: 7);
AIAgent inventoryAgent = new OllamaApiClient(http, "qwen3:4b").AsAIAgent(
instructions: "You answer stock questions for the Bookstore warehouse. Use the tools. Reply in one sentence.",
name: "InventoryAgent",
tools: [tools.ReadTools()[0], tools.ReadTools()[1]]);
AgentCard card = new()
{
Name = "InventoryAgent",
Description = "Answers stock and availability questions about Bookstore books.",
Version = "1.0.0",
DefaultInputModes = ["text/plain"],
DefaultOutputModes = ["text/plain"],
Capabilities = new() { Streaming = false, PushNotifications = false },
Skills =
[
new()
{
Id = "stock-lookup",
Name = "Stock lookup",
Description = "Tells how many copies of a book are in the warehouse.",
Tags = ["inventory"],
Examples = ["How many copies of Winter in Kyoto are in stock?"],
},
],
SupportedInterfaces = [new() { Url = Url, ProtocolBinding = ProtocolBindingNames.JsonRpc, ProtocolVersion = "1.0" }],
};
builder.AddA2AServer(inventoryAgent);
WebApplication app = builder.Build();
app.MapA2AJsonRpc(inventoryAgent, "/");
app.MapWellKnownAgentCard(card);
app.Run(Url);
Bookstore tarafı Agent Card'ı okuyor ve uzak agent'ı sıradan bir AIAgent gibi kullanıyor; hem doğrudan hem de kendi concierge agent'ına bir tool olarak (AsAIFunction) veriyor:
// Uzak agent'ı card'ından keşfet, sonra her AIAgent gibi kullan
A2ACardResolver resolver = new(new Uri("http://localhost:19801"));
AgentCard card = await resolver.GetAgentCardAsync();
Console.WriteLine($"card: {card.Name} - {card.Description} - skills: {string.Join(", ", card.Skills.Select(s => s.Id))}");
// Uzak agent dakikalarca düşünebilir: HttpClient'ın varsayılan timeout'u 100 saniye
HttpClient a2aHttp = new() { Timeout = TimeSpan.FromMinutes(10) };
AIAgent remote = card.AsAIAgent(a2aHttp);
AgentResponse direct = await remote.RunAsync("How many copies of Winter in Kyoto are in stock?");
Console.WriteLine($"direct: {direct.Text.Trim()}");
AIAgent concierge = qwen.AsAIAgent(
instructions: "You are the Bookstore concierge. For any stock question ask the InventoryAgent tool. Be brief.",
name: "Concierge",
tools: [remote.AsAIFunction()]);
AgentResponse answer = await concierge.RunAsync("I want to buy two copies of A Short History of Maps. Are there enough?");
İlk denemede HttpClient vermemiştik. Card okundu, ama uzak agent'ın cevabı gelmeden istemci düştü:
card: InventoryAgent - Answers stock and availability questions about Bookstore books. - skills: stock-lookup
Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
...
at A2A.A2AClient.SendMessageAsync(SendMessageRequest request, CancellationToken cancellationToken)
at Microsoft.Agents.AI.A2A.A2AAgent.RunCoreAsync(...)
Uzak agent bir HTTP (HyperText Transfer Protocol) isteği değil, kendi modeli ve tool'larıyla çalışan bir döngü; paylaşılan Ollama'da bu döngü 100 saniyeyi rahatça aştı. Timeout'u açıkça uzattıktan sonraki koşu:
card: InventoryAgent - Answers stock and availability questions about Bookstore books. - skills: stock-lookup
direct: There are 7 copies of Winter in Kyoto in stock.
concierge -> InventoryAgent {"query":"How many copies of A Short History of Maps are available?"}
concierge: Yes, there are 3 copies available, so you can purchase two.
local model: model calls=2, input tokens=877, output tokens=636, time=130,7s
İki cevap da doğru: Winter in Kyoto'dan 7, A Short History of Maps'ten 3 adet var. Concierge, uzak agent'ı tek argümanlı bir tool olarak gördü ve soruyu kendi cümlesiyle yeniden yazıp gönderdi ({"query":"How many copies of A Short History of Maps are available?"}); sonra "iki adet yeter mi" kısmını kendisi yorumladı. Sunucunun log'unda iki A2A isteğinin süresi de görünüyor: 75 ve 197 saniye. "local model" satırı yalnızca concierge'in kendi model çağrılarını sayıyor; uzak agent'ın harcadığı token ve süre başka bir servisin hesabında ve bu taraftan görünmüyor. Maliyet ve gözlem açısından A2A'nın en önemli özelliği bu: iş başka bir sistemin içinde yapılıyor.
MCP (Model Context Protocol) ile A2A'nın farkı bu örnekte netleşiyor. MCP, bir agent'ı tool'lara ve veriye bağlar: karşı taraf bir fonksiyondur, ne yapacağına sen (modelin aracılığıyla) karar verirsin. A2A, bir agent'ı başka bir agent'a bağlar: karşı taraf kendi kararlarını veren, kendi modeli olan, cevabı dakikalar sürebilen bir sistemdir ve sen yalnızca görevi tarif edersin. A2A'nın resmi sitesi de ikisini rakip değil tamamlayıcı olarak konumluyor. Serinin ikinci yarısı MCP'ye ayrılmış durumda.
Üretimde Dikkat
Token çarpanını ölç. Her handoff, devralan agent'ın talimatını, tool tanımlarını ve o ana kadarki bütün konuşmayı modele yeniden gönderir. Multi-agent bir akışı yayına almadan önce aynı senaryoyu tek agent'la çalıştırıp model çağrısı ve token sayısını karşılaştır; fark büyükse ve kalite artışı yoksa tek agent'ta kal.
Agent'lar arası her mesajı trace'le. Hangi agent'ın hangi handoff'u ne zaman yaptığı, devredilen konuşmanın ne içerdiği ve son cevabı kimin yazdığı log'da görünmeli. Bu yazıdaki ilk handoff koşusunun hatasını ancak event'leri yazdırdığımız için gördük.
Uzak agent'a bir dış servis gibi davran. A2A çağrısının timeout'u, retry politikası, kimlik doğrulaması (authentication) ve yetkilendirmesi (authorization) olmalı; uzak agent'ın cevabı da senin modelin için güvenilmeyen bir girdi. Uzak agent'ın Agent Card'ında yazan yeteneklere körü körüne güvenme, sonucu doğrula.
Paket olgunluğunu hesaba kat. Bu yazıdaki A2A paketleri (Microsoft.Agents.AI.A2A, Microsoft.Agents.AI.Hosting.A2A.AspNetCore, A2A 1.0.0-preview4) hâlâ preview; API'ler (Application Programming Interface) sürümden sürüme değişebilir.
Sık Düşülen Tuzaklar
Tek agent ve iyi tool'larla çözülecek işi multi-agent yapmak. Bu yazıdaki sipariş sorusu tek agent'la tek tool çağrısında çözülüyordu; üç agent'a bölmek yeni bir hata türü getirdi.
Uzmanın, uzman olduğu işi yapmadan konuşmayı geri devretmesi. Handoff tool'u da bir tool; küçük bir model işi yapmak yerine "devret" demeyi seçebilir. Uzman agent'ın talimatına "devretmeden önce tool'unu çağır" yazmak, ya da handoff'u yalnızca belirli koşullarda açmak gerekiyor.
Agent'lar arası sonsuz konuşma. Group chat'te "APPROVED"dan sonra teşekkürleşen iki agent gördük; sayaç olmasaydı sohbet sürecekti. Her orkestrasyonun kodda bir bitiş koşulu olmalı.
Uzak agent'ı yerel bir fonksiyon gibi düşünmek. Varsayılan 100 saniyelik HttpClient timeout'u bir agent için kısa kaldı. Uzak agent yavaş, pahalı ve senin kontrolünde olmayan bir sistem.
Küçük Bir Deneme
★☆☆ Handoff örneğinde order agent'ın talimatına "Always call get_order_status before answering. Never hand off without calling it." ekle ve aynı soruyu tekrar sor. Cevap ve tool log'u değişiyor mu?
★★☆ Aynı iki soruyu, üç agent'ın bütün tool'larını alan tek bir agent'la çalıştır. Model çağrısı, token ve doğruluk açısından handoff sürümüyle karşılaştır.
★★★ GroupChatManager'dan türeyen ve son mesajda "APPROVED" geçtiğinde sohbeti bitiren bir manager yaz. Writer'ın tek bir cümle yazmasını da kodla kontrol et (birden fazla seçenek varsa editor'a gitmeden reddet).
Multi-agent sistemlerde kontrol modelde ve agent'lar arasında dağılıyor. Bir sonraki yazıda kontrolün bir kısmını bilinçli olarak insana geri veriyoruz: yazma yapan tool'ları onaya bağlamak, onay saatler sürdüğünde agent'ı ve workflow'u checkpoint'ten sürdürmek.