LLM yanıtlarını kullanıcıya token token göstermek, algılanan yanıt süresini önemli ölçüde düşürür. ASP.NET Core SignalR, bu streaming davranışını gerçek zamanlı çift yönlü iletişimle WebSocket üzerinden tarayıcıya taşır.

SignalR Hub: Streaming AI Yanıtı

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.AI;

public class AiHub(IChatClient chatClient) : Hub
{
    // IAsyncEnumerable döndüren hub metodu — SignalR otomatik stream yapar
    public async IAsyncEnumerable<string> SoruSor(
        string soru,
        [EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var guncelleme in chatClient.CompleteStreamingAsync(soru, cancellationToken: ct))
        {
            if (guncelleme.Text is { Length: > 0 } metin)
                yield return metin;
        }
    }

    // Sohbet geçmişiyle — multi-turn
    public async IAsyncEnumerable<string> KonusSohbet(
        List<ChatMesajiDto> gecmis,
        string yeniMesaj,
        [EnumeratorCancellation] CancellationToken ct)
    {
        var mesajlar = gecmis
            .Select(m => m.Rol == "user"
                ? new ChatMessage(ChatRole.User, m.Icerik)
                : new ChatMessage(ChatRole.Assistant, m.Icerik))
            .ToList();

        mesajlar.Add(new ChatMessage(ChatRole.User, yeniMesaj));

        await foreach (var chunk in chatClient.CompleteStreamingAsync(mesajlar, cancellationToken: ct))
            if (chunk.Text is { Length: > 0 } t)
                yield return t;
    }
}

Program.cs Konfigürasyonu

// Program.cs
builder.Services
    .AddOpenAIClient(opts => opts.ApiKey = config["OpenAI:ApiKey"]!)
    .AddChatClient("gpt-4o");

builder.Services.AddSignalR();

app.MapHub<AiHub>("/hub/ai");

JavaScript İstemcisi

// npm install @microsoft/signalr
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hub/ai")
    .withAutomaticReconnect()
    .build();

await connection.start();

const ciktiAlani = document.getElementById("cikti");
ciktiAlani.textContent = "";

// Streaming akışını başlat
const stream = connection.stream("SoruSor", "C# async/await nasıl çalışır?");

const abortController = new AbortController();

stream.subscribe({
    next:     (token) => { ciktiAlani.textContent += token; },
    error:    (err)   => console.error("Hata:", err),
    complete: ()      => console.log("Tamamlandı")
});

// İptal etmek için
document.getElementById("iptalBtn").onclick = () => {
    abortController.abort();
    stream.dispose();
};

Blazor İstemcisi

@inject NavigationManager Nav
@inject IJSRuntime JS

@code {
    private HubConnection? hubConnection;
    private string cikti = "";
    private CancellationTokenSource? cts;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Nav.ToAbsoluteUri("/hub/ai"))
            .WithAutomaticReconnect()
            .Build();

        await hubConnection.StartAsync();
    }

    private async Task SoruSor(string soru)
    {
        cikti = "";
        cts = new CancellationTokenSource();

        var akis = hubConnection!.StreamAsync<string>("SoruSor", soru, cts.Token);

        await foreach (var token in akis.WithCancellation(cts.Token))
        {
            cikti += token;
            StateHasChanged(); // UI'ı güncelle
        }
    }

    private void Iptal() => cts?.Cancel();

    public async ValueTask DisposeAsync()
    {
        cts?.Dispose();
        if (hubConnection is not null)
            await hubConnection.DisposeAsync();
    }
}

Bağlantı Yönetimi ve Grup Odaları

// Her kullanıcıyı kendi odasına ekle — sadece o kullanıcıya yayın
public class AiHub(IChatClient chatClient) : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Uzun süreli görevlerde ilerleme bildirimi
    public async Task BelgeOzetle(string icerik)
    {
        await Clients.Caller.SendAsync("IlerlemeGuncellendi", "Analiz başlatılıyor...");

        var ozet = await chatClient.CompleteAsync(
            $"Şu metni 5 cümleyle özetle:\n\n{icerik}");

        await Clients.Caller.SendAsync("OzetHazir", ozet.Message.Text);
    }
}

SignalR ile LLM streaming kombinasyonu, kullanıcı deneyimini dramatik biçimde iyileştirir. İlk token genellikle 200-500ms içinde gelir; kullanıcı tam yanıt yerine akan içeriği görür. Blazor ve JavaScript istemcileri aynı hub API'sini kullandığından platform geçişi kolaylaşır.