ms-ai-architect/skills/ms-ai-security/references/performance-scalability/streaming-response-patterns.md
Kjell Tore Guttormsen 3a73eeafdc refactor(ms-ai-architect): R13 del 1 — nøytraliser Cosmo-personaen i ref-korpusets headinger, etter å ha rettet en gate som var målt usann to ganger
Ordre 20260912T193441Z-7358817909. Steg 1 var ikke transformen, men å rette
roadmapens R13-gate og få den ratifisert. Gaten `grep -rl "Cosmo"
skills/*/references -> 0` var usann på to uavhengige måter:

1. Ordren fanget den første: 451 av forekomstene er Azure Cosmos DB, ekte
   produktinnhold. Diskriminatoren er ikke bokstaven «s» — `Cosmos <norsk
   substantiv>` er genitiv av personaen (`### Cosmos tonalitet`), mens
   `Cosmos DB`/`CosmosClient`/`cosmos_ru` er produkt.
2. Denne økten fant den andre: 132 persona-forekomster ligger i prosa,
   tabeller, dialog-replikker og proveniens-linjer. Heading-nøytralisering
   kan ikke nå dem, så «0 persona» er uoppnåelig også under den ratifiserte
   formen. Operatøren ratifiserte alternativ A: gaten speiler formen, og de
   132 bokføres til R13b/R14.

Tre korreksjoner av premisser som sto i ordren og STATE:
  «ca 320 produkt»   -> 451 (case-sensitivt nett manglet 327 lowercase
                        TOC-ankre + 99 identifikatorer; sann nevner 1 638)
  «169 headinger»    -> 401. 169 var `^## For Cosmo`-prefikset (168) og var
                        internt inkonsistent med sin egen topp-variant (204)
  «417 matcher ingen
   populasjon»       -> 417 er cosmo-headinger utenfor kodefences; briefens
                        nevner var reell hele tiden

Fence-bevissthet er målt skadelig, ikke nødvendig: begge toggle-regler er
gale på dette korpuset (naiv toggle skjuler en ekte heading i
chain-of-thought-prompting.md, CommonMark-regelen ubalanserer
service-level-documentation-dr.md). Fence-agnostisk deteksjon finner 401
heading-linjer i nøyaktig de samme 40 variantene som fence-bevisst finner
400 i — ingen kodeblokk-linje er byte-identisk til en persona-heading. Derfor
nøkles transformen på 40 enumererte heading-tekster og ignorerer fences. En
ukjent variant kaster; en slug-kollisjon kaster. Ingenting auto-fikses.

TOC-en regenereres ikke, den rettes kirurgisk: alle 327 persona-lenker hadde
lenketekst lik én av de 40 heading-tekstene og anker lik slugify av den
(327/327, 0 avvik), så heading og TOC-entry skrives i samme operasjon og
ingen mellomtilstand etterlater en død lenke.

Ratifisert målform: `For Cosmo`, `For Cosmo Skyberg` og `For arkitekten
(Cosmo)` konvergerer på `For arkitekten`. To filer kolliderte og er adjudisert
ved å lese dem, ikke ved regel.

Verifisering (alle 7 kriterier fra ordren):
  G1 persona på heading-linjer   401 -> 0
  G2 døde fragmentlenker         1 -> 1 (pre-eksisterende, unntatt)
  G3 produkt-forekomster         451 -> 451; `Cosmos DB|Azure Cosmos` 308 = 308
  de 3 kun-produkt-filene        byte-identiske
  nettet validert begge veier    injisert persona feller G1; genitiv feller G1;
                                 produkt-heading og de 3 filene passerer
  hele diffen                    802 heading-linjer + 654 TOC-linjer, ANNET = 0
  linjeantall                    728 lagt til = 728 slettet
  suite                          1120/1120 (1097 + 23 nye)
  validate-plugin                250 PASS / 0 FAIL
  stikkprøve                     10 filer, alle 5 skills, inkl. de 3 mest
                                 produkt-tunge (26/20/19) — kun heading+TOC

Utenfor scope, urørt: de 4 SKILL.md, de 23 commands, CLAUDE.md, README.md,
NOTICE.md, docs/ (alt R14).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:12:28 +02:00

648 lines
20 KiB
Markdown

# Streaming Response Patterns
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
**Type:** reference
---
## Innhold
- [Introduksjon](#introduksjon)
- [Server-Sent Events (SSE) Grunnleggende](#server-sent-events-sse-grunnleggende)
- [Grunnleggende Streaming-implementasjon](#grunnleggende-streaming-implementasjon)
- [Chunked Transfer Encoding](#chunked-transfer-encoding)
- [Client-Side Stream Handling](#client-side-stream-handling)
- [Error Recovery in Streams](#error-recovery-in-streams)
- [Nar bruke streaming vs. non-streaming](#nar-bruke-streaming-vs-non-streaming)
- [Avanserte monstre](#avanserte-monstre)
- [Ytelsesmal for streaming](#ytelsesmal-for-streaming)
- [For arkitekten](#for-arkitekten)
## Introduksjon
Streaming av AI-responser er en kritisk teknikk for a forbedre brukeropplevelsen i interaktive AI-applikasjoner. Istedenfor a vente pa at hele responsen genereres for den vises, lar streaming brukeren se svaret bygges opp token for token. For norsk offentlig sektor, der innbyggerportaler og saksbehandlingssystemer i okende grad integrerer AI, er streaming avgjorende for akseptabel responstid.
Azure OpenAI stotter streaming gjennom Server-Sent Events (SSE)-protokollen, som er en enkel, unidireksjonell strommingsmekanisme over HTTP. Denne tilnaermingen er spesielt verdifull for chat-grensesnitt, dokumentgenerering og andre bruksomrader der brukeren forventer umiddelbar tilbakemelding.
Denne referansen dekker arkitekturmonstre for streaming i Azure OpenAI-baserte applikasjoner, fra grunnleggende SSE-implementasjon til avansert feilhandtering og mellomlag-konfigurasjon.
## Server-Sent Events (SSE) Grunnleggende
### Hva er SSE?
Server-Sent Events er en W3C-standard for enveis stromming fra server til klient over HTTP:
| Egenskap | SSE | WebSocket | Long Polling |
|----------|-----|-----------|--------------|
| Retning | Server -> Klient | Bidireksjonell | Klient -> Server -> Klient |
| Protokoll | HTTP/1.1+ | WebSocket (ws://) | HTTP |
| Automatisk reconnect | Ja (innebygd) | Nei (manuell) | Nei |
| Kompleksitet | Lav | Hoy | Middels |
| Azure OpenAI-stotte | Ja | Ja (Realtime API) | Nei |
### SSE-format
Azure OpenAI returnerer data i SSE-format:
```
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-cache
Connection: keep-alive
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Hei"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
**Viktige SSE-regler:**
- Hver hendelse er prefixet med `data: `
- Hendelser separeres med to linjeskift (`\n\n`)
- Siste hendelse er alltid `data: [DONE]`
- `delta`-feltet inneholder inkrementelt innhold (ikke kumulativt)
- `finish_reason` er `null` til generering er ferdig
## Grunnleggende Streaming-implementasjon
### Python med Azure OpenAI SDK
```python
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
def stream_chat_response(user_message: str) -> str:
"""Stream en chat completion og bygg opp komplett respons."""
full_response = ""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Du er en hjelpesom assistent."},
{"role": "user", "content": user_message}
],
stream=True,
max_tokens=500
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
print(content, end="", flush=True) # Vis inkrementelt
print() # Ny linje etter ferdig streaming
return full_response
```
### Async Python Streaming
```python
from openai import AsyncAzureOpenAI
import asyncio
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def stream_async(user_message: str):
"""Asynkron streaming for hoy-throughput applikasjoner."""
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
stream=True,
max_tokens=500
)
collected_content = []
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
collected_content.append(content)
yield content # Yield for videre prosessering
return "".join(collected_content)
```
### TypeScript/JavaScript Streaming
```typescript
import { AzureOpenAI } from "openai";
const client = new AzureOpenAI({
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key",
apiVersion: "2025-03-01-preview",
});
async function* streamChatResponse(
userMessage: string
): AsyncGenerator<string> {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
stream: true,
max_tokens: 500,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
// Bruk i en web-handler
async function handleStreamRequest(req: Request): Promise<Response> {
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const token of streamChatResponse("Hva er GDPR?")) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: token })}\n\n`));
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readableStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}
```
## Chunked Transfer Encoding
### HTTP-konfigurasjon for streaming
For at streaming skal fungere gjennom hele infrastrukturen, ma alle mellomlag konfigureres korrekt:
| Komponent | Nodvendig konfigurasjon |
|-----------|------------------------|
| Azure OpenAI | `stream: true` i request |
| API Management | `buffer-response="false"` i forward-request |
| Application Gateway | Deaktiver response buffering |
| Azure Front Door | Route-spesifikk konfigurasjon |
| Klient (browser) | `Accept: text/event-stream` header |
### API Management for SSE
```xml
<!-- APIM policy for SSE pass-through -->
<policies>
<inbound>
<base />
</inbound>
<backend>
<!-- KRITISK: buffer-response="false" for streaming -->
<forward-request timeout="120"
fail-on-error-status-code="true"
buffer-response="false" />
</backend>
<outbound>
<base />
<!-- VIKTIG: Deaktiver body-logging for SSE-APIer -->
</outbound>
<on-error>
<base />
</on-error>
</policies>
```
**Viktige APIM-hensyn for SSE:**
1. Deaktiver response buffering (`buffer-response="false"`)
2. Deaktiver `validate-content`-policy (buffrer respons)
3. Deaktiver request/response body-logging for Azure Monitor og Application Insights
4. Deaktiver response caching for streaming-endepunkter
5. Okt timeout (minimum 120 sekunder)
6. Hold forbindelser i live med TCP keepalive
### Application Gateway for SSE
```json
{
"properties": {
"responseBufferPolicy": {
"responseSendTimeoutInSeconds": 120,
"bufferResponseBody": false
},
"backendHttpSettings": {
"requestTimeout": 120,
"connectionDraining": {
"enabled": true,
"drainTimeoutInSec": 30
}
}
}
}
```
### Azure Front Door Route Policy
For SSE gjennom Azure Front Door:
```json
{
"routePolicy": {
"routeTimeout": "0s"
}
}
```
**Merk:** Idle timeout for Application Gateway for Containers er 5 minutter. Send keepalive-meldinger for a forhindre at forbindelsen lukkes:
```
: keep-alive\n\n
```
## Client-Side Stream Handling
### React/Next.js Frontend
```typescript
// React hook for SSE streaming fra Azure OpenAI
import { useState, useCallback } from "react";
interface StreamState {
content: string;
isStreaming: boolean;
error: string | null;
}
function useAIStream() {
const [state, setState] = useState<StreamState>({
content: "",
isStreaming: false,
error: null,
});
const startStream = useCallback(async (prompt: string) => {
setState({ content: "", isStreaming: true, error: null });
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ message: prompt }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error("No reader available");
let accumulated = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
accumulated += parsed.content;
setState((prev) => ({
...prev,
content: accumulated,
}));
}
} catch {
// Ignorer parsing-feil for ufullstendige chunks
}
}
}
}
setState((prev) => ({ ...prev, isStreaming: false }));
} catch (error) {
setState((prev) => ({
...prev,
isStreaming: false,
error: error instanceof Error ? error.message : "Ukjent feil",
}));
}
}, []);
return { ...state, startStream };
}
```
### Python SSE Client
```python
import httpx
import json
from typing import AsyncGenerator
async def consume_sse_stream(
url: str,
payload: dict,
api_key: str
) -> AsyncGenerator[str, None]:
"""Konsumer SSE-strom fra Azure OpenAI via HTTP."""
headers = {
"Content-Type": "application/json",
"api-key": api_key,
"Accept": "text/event-stream"
}
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
response.raise_for_status()
buffer = ""
async for chunk in response.aiter_text():
buffer += chunk
while "\n\n" in buffer:
event, buffer = buffer.split("\n\n", 1)
for line in event.split("\n"):
if line.startswith("data: "):
data = line[6:]
if data == "[DONE]":
return
try:
parsed = json.loads(data)
content = parsed["choices"][0]["delta"].get("content", "")
if content:
yield content
except (json.JSONDecodeError, KeyError, IndexError):
continue
```
## Error Recovery in Streams
### Haandtering av avbrutte strommer
Streaming-forbindelser kan avbrytes av flere arsaker:
| Feiltype | Arsak | Handtering |
|----------|-------|------------|
| Nettverksavbrudd | Ustabil forbindelse | Reconnect med checkpoint |
| Timeout | Idle > 4 min (Azure LB) | Keepalive-meldinger |
| 429 Rate Limit | Kapasitetsgrense | Retry med backoff |
| 500 Server Error | Midlertidig serverfeil | Retry etter pause |
| Content Filter | Innhold blokkert | Vis melding til bruker |
### Robust Streaming med Retry
```python
import asyncio
import time
from openai import AsyncAzureOpenAI, APIStatusError, APIConnectionError
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def resilient_stream(
messages: list,
max_retries: int = 3,
model: str = "gpt-4o"
) -> AsyncGenerator[str, None]:
"""Streaming med automatisk retry og feilhandtering."""
collected_tokens = []
attempt = 0
while attempt < max_retries:
try:
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
collected_tokens.append(token)
yield token
# Sjekk finish_reason
if chunk.choices and chunk.choices[0].finish_reason:
reason = chunk.choices[0].finish_reason
if reason == "content_filter":
yield "\n[Innhold filtrert av sikkerhetsfilter]"
return # Ferdig
return # Stromming fullfort
except APIStatusError as e:
attempt += 1
if e.status_code == 429:
retry_after = int(e.response.headers.get("retry-after", "5"))
await asyncio.sleep(retry_after)
elif e.status_code >= 500:
await asyncio.sleep(2 ** attempt) # Eksponentiell backoff
else:
raise # Ikke-gjenforsokbar feil
except APIConnectionError:
attempt += 1
await asyncio.sleep(2 ** attempt)
raise Exception(f"Streaming feilet etter {max_retries} forsok")
```
### Streaming med Partial Response Recovery
```python
async def stream_with_checkpoint(
messages: list,
on_token: callable,
on_complete: callable,
on_error: callable
):
"""Stream med checkpoint for delvis gjenoppretting."""
partial_response = []
last_chunk_time = time.time()
try:
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
current_time = time.time()
# Detekter unormalt lang pause mellom chunks
if current_time - last_chunk_time > 30:
# Mulig hengende forbindelse
break
last_chunk_time = current_time
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
partial_response.append(token)
await on_token(token)
if chunk.choices and chunk.choices[0].finish_reason == "stop":
await on_complete("".join(partial_response))
return
# Hvis vi nar hit uten "stop", har streamingen avbrultt
if partial_response:
await on_complete(
"".join(partial_response) +
"\n\n[Merk: Respons kan vaere ufullstendig]"
)
except Exception as e:
if partial_response:
await on_error(e, "".join(partial_response))
else:
await on_error(e, None)
```
## Nar bruke streaming vs. non-streaming
| Scenario | Anbefaling | Begrunnelse |
|----------|-----------|-------------|
| Chat-grensesnitt | Streaming | Bedre opplevd responstid |
| Innbyggerportal | Streaming | Visuell tilbakemelding under generering |
| Batch-klassifisering | Non-streaming | Kun sluttresultat er relevant |
| Dokumentanalyse | Non-streaming | Strukturert output, ingen inkrementell visning |
| Saksbehandlingsforslag | Streaming | Lang generering, bruker venter |
| API-integrasjon (maskin-til-maskin) | Non-streaming | Enklere feilhandtering |
| Sanntidsoversetning | Streaming | Lavest opplevd latens |
## Avanserte monstre
### Server-side Streaming Proxy med FastAPI
```python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncAzureOpenAI
app = FastAPI()
client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""Server-side proxy for Azure OpenAI streaming."""
async def generate():
try:
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": request.message}],
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
data = {"content": chunk.choices[0].delta.content}
yield f"data: {json.dumps(data)}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
error_data = {"error": str(e)}
yield f"data: {json.dumps(error_data)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Deaktiver nginx buffering
}
)
```
### Token-telling under streaming
```python
import tiktoken
async def stream_with_token_counting(messages: list, model: str = "gpt-4o"):
"""Stream med sanntids token-telling for kostnadsovervaking."""
encoding = tiktoken.encoding_for_model(model)
output_tokens = 0
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
stream_options={"include_usage": True} # Inkluder bruksdata
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
output_tokens += len(encoding.encode(content))
yield content
# Sjekk usage i siste chunk
if chunk.usage:
actual_tokens = chunk.usage.completion_tokens
cached_tokens = getattr(
chunk.usage.prompt_tokens_details, 'cached_tokens', 0
)
print(f"Faktisk token-bruk: {actual_tokens}")
print(f"Cache-treff: {cached_tokens}")
```
## Ytelsesmal for streaming
| Metrikk | Mal (P95) | Kritisk terskel |
|---------|-----------|-----------------|
| Time to First Token | < 500 ms | > 2000 ms |
| Inter-token latens | < 50 ms | > 200 ms |
| Reconnect-tid | < 2 s | > 10 s |
| Stream completion rate | > 99% | < 95% |
## For arkitekten
- **Streaming er obligatorisk** for alle brukerrettede AI-grensesnitt. Forskjellen i opplevd latens er dramatisk: 200 ms TTFT vs. 3-5 sekunders ventetid for komplett respons.
- **Infrastruktur-konfigurasjon er kritisk:** Hele kjeden (APIM, App Gateway, Front Door) ma ha response buffering deaktivert. En enkelt feilkonfigurert komponent blokkerer all streaming.
- **Feilhandtering i strommer krever eget design:** Implementer alltid reconnect-logikk, partial response recovery, og eksponentiell backoff for 429/5xx-feil.
- **Content filtering pavirker streaming:** `finish_reason: content_filter` kan oppsta midt i en strom. Klient-koden ma handtere dette gracefully med en brukermelding.
- **Token-telling under streaming:** Bruk `stream_options: {"include_usage": true}` for a fa eksakt token-bruk i siste chunk, viktig for kostnadsovervaking.