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.