Macademia'nın robotik ve mühendislik programı · ücretsiz seviye tespiti ve kişisel öğrenme yolu

10. ünite / 10 · 4 saat

Akışlı web sohbet arayüzü (JavaScript)

Anahtarı sunucuda tutmak, Server-Sent Events, parça parça gelen baytları tamponlamak, durdurma düğmesi ve sohbet geçmişini sınırlamak.

Bu ünitenin sonunda

  • API anahtarının neden yalnız sunucuda durduğunu açıklamak
  • SSE olaylarının biçimini ve sırasını okumak
  • Bayt sınırından kesilen parçaları akışlı TextDecoder ve tamponla doğru birleştirmek
  • Durdurma, hata ve geçmiş sınırı olan bir sohbet istemcisi yazmak

Ders notları

Bir sohbet arayüzünde cevap birkaç saniye sürebilir. Akış (streaming) ile sunucu metni üretildikçe parça parça yollar; kullanıcı ilk kelimeyi hemen görür ve isterse cevabı yarıda durdurur.

Mimari

  • Tarayıcı: kullanıcının mesajını kendi sunucuna gönderir, gelen akışı okur ve ekrana yazar. API anahtarı burada asla bulunmaz; tarayıcıdaki her şey kullanıcı tarafından görülebilir.
  • Senin sunucun: anahtarı ortam değişkeninden okur, Anthropic SDK ile akışlı isteği açar, gelen metni Server-Sent Events olarak tarayıcıya iletir. Kimlik doğrulama, hız sınırı, kişisel veri maskeleme ve kayıt burada yapılır.
  • API: cevabı olaylar hâlinde yollar: message_start, content_block_delta (metin parçaları), message_delta (durma nedeni, kullanım), message_stop.

Baytlar, olaylar, metin

Ağ veriyi olay sınırına göre değil, rastgele bayt sınırlarından keser. Bir parça bir olayın ortasında bitebilir, hatta iki baytlık bir Türkçe harfi ortadan bölebilir. İstemci bu yüzden üç şey yapar: TextDecoder'ı stream: true ile kullanır (yarım karakteri bir sonraki parçaya saklar), metni boş satırla (\n\n) biten olaylara böler ve son yarım olayı tamponda bekletir.

baytlar → akışlı çözme → tampon → olaylar → metin

Kullanıcı deneyimi

Durdurma düğmesi bir AbortController ile isteği keser; sunucu da bağlantı kapanınca modele giden akışı durdurur ki boşa token harcanmasın. Sohbet geçmişi her istekte yeniden gönderildiği için uzunluğu sınırlanır ya da eski mesajlar özetlenir. Hata olduğunda yarım kalan cevap işaretlenir ve yeniden deneme seçeneği sunulur. Bu sayfadaki simülasyonda sunucu ve ağ sahtedir; ayrıştırıcı ise gerçek kodun aynısıdır.

Simülasyon

Öğretim modeli: bu sayfadaki simülasyonlar gerçek bir dil modeli değildir. Küçük, deterministik, tarayıcıda çalışan modellerdir; kavramı göstermek için yazıldılar. Site hiçbir dış LLM servisine istek göndermez.

Akışlı sohbet arayüzü: SSE olaylarından ekrandaki metne

Bir soru seç ve gönder: sahte sunucu cevabı Server-Sent Events olayları (message_start, content_block_delta, message_stop) olarak parça parça yollar; parçalar rastgele bayt sınırlarından kesilir. İstemci ayrıştırıcısı tamponlar, olayları çözer ve metni ekrana akıtır. Hızı değiştir, yarıda durdur, ham olay günlüğüne bak.

Kendin dene

Akışlı sohbet simülasyonunda bir soru gönder ve ham olay günlüğünde "karakter bölündü" yazan bir parça bul. Azami parça boyunu 4 bayta indir ve yeniden gönder: metin yine doğru birleşti mi? Cevabı yarıda durdur ve durma nedeninin neden boş kaldığını açıkla. Son olarak istemci kodunda stream: true olmasaydı hangi karakterlerin bozulacağını tahmin et.

Kod

Bunları kendi bilgisayarında çalıştır. Anahtar ANTHROPIC_API_KEY ortam değişkeninden okunur; koda asla yazma.

Node.js: kendi sunucun, akışı SSE olarak iletir
// server.mjs  (npm install @anthropic-ai/sdk express)
import Anthropic from "@anthropic-ai/sdk";
import express from "express";

const client = new Anthropic(); // anahtar sunucuda, ANTHROPIC_API_KEY ortam değişkeninde; tarayıcıya asla gitmez
const app = express();
app.use(express.json());

app.post("/api/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  const stream = client.messages.stream({
    model: "claude-sonnet-5-5",
    max_tokens: 4096,
    system: "Yalnız ders notlarına dayanarak cevap ver.",
    messages: req.body.messages, // istemcinin gönderdiği geçmiş: uzunluğunu sınırla
  });
  req.on("close", () => stream.abort()); // kullanıcı durdurdu ya da sekmeyi kapattı
  stream.on("text", (delta) => {
    res.write(`data: ${JSON.stringify({ type: "delta", text: delta })}\n\n`);
  });
  try {
    const final = await stream.finalMessage();
    res.write(`data: ${JSON.stringify({ type: "done", usage: final.usage })}\n\n`);
  } catch (err) {
    res.write(`data: ${JSON.stringify({ type: "error", message: "cevap alınamadı" })}\n\n`);
  }
  res.end();
});

app.listen(3000);
Tarayıcı JavaScript: akışı oku, tamponla, durdur
let controller = null;

async function send(messages, onText) {
  controller = new AbortController();
  const res = await fetch("/api/chat", { // kendi sunucun; API anahtarı burada yok
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages }),
    signal: controller.signal,
  });
  const reader = res.body.getReader();
  const decoder = new TextDecoder(); // çok baytlı karakterleri (ş, ğ) parçalar arasında doğru birleştirir
  let buffer = "";
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const events = buffer.split("\n\n");
      buffer = events.pop(); // son parça yarım olabilir: tamponda beklet
      for (const ev of events) {
        if (!ev.startsWith("data: ")) continue;
        const msg = JSON.parse(ev.slice(6));
        if (msg.type === "delta") onText(msg.text);
        if (msg.type === "error") onText("\n[hata] " + msg.message);
      }
    }
  } catch (err) {
    if (err.name !== "AbortError") throw err; // durdurma bir hata değildir
  }
}

document.querySelector("#stop").onclick = () => controller?.abort();

Sık yapılan hatalar

  • API anahtarını tarayıcı koduna ya da ön yüz ortam değişkenine koymak.
  • Her parçayı tek başına JSON sanıp ayrıştırmak: olay iki parçaya bölünmüş olabilir.
  • TextDecoder'ı stream: true olmadan kullanmak: bölünen Türkçe harfler bozuk görünür.
  • Kullanıcı durdurunca sunucudaki akışı kapatmamak: model boşa token üretmeye devam eder.

Yazılım mühendisliği bağlantısı

Akış ayrıştırıcısı küçük ama hataya açık bir durum makinesidir; deterministik testlerle sınanır.

  • Aynı SSE metnini farklı tohumlarla rastgele bayt sınırlarından kesip ayrıştıran bir test yaz; çıkan metin her seferinde aynı olmalı (bu dersin testi böyle).
  • Sunucu uç noktası için uçtan uca test: sahte bir akış sağlayıcısıyla durdurma, hata ve zaman aşımı durumlarını sına.

Kendini sına

  1. API anahtarı neden tarayıcı koduna konmaz?

  2. TextDecoder'da stream: true seçeneği ne işe yarar?

  3. SSE metninde bir olay neyle biter?

  4. Kullanıcı durdurma düğmesine bastığında sunucu ne yapmalı?

Bu üniteden sonra bitirme projesinde

  • Demo, maliyet raporu ve sunum: Akışlı arayüzle canlı ya da kayıtlı demo; G1–G6 kanıt tablosu; aylık maliyet ve gecikme ölçümü; 10 dakikalık sunum, final raporu ve depo.

Macademia'da ilgili

Esc
  • PanelAna sayfaSayfa
  • ModüllerTüm modüllerSayfa
  • ParçalarParça ansiklopedisiSayfa
  • Mühendislik yoluBüyükler için robotik: 128 parça, 7 seviyeSayfa
  • Robotik dersi (ESOF 323)Üniversite robotik dersi, 14 haftaSayfa
  • Drone dersiBüyükler için: parça, simülasyon, yazılım; 10 üniteSayfa
  • Blok atölyesiSerbest blok programlamaAraç
  • Devre laboratuvarıSanal Arduino ve devreAraç
  • Robot arenasıSanal robot sürüşüAraç
  • OyunlarBloklarla 8 öğretici oyunAraç
  • Mikro Kart atölyesiSanal LED kartı bloklarla programlaAraç
  • Robot atölyesiMikro Kart'ı robota tak, bloklarla görev yapAraç