算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
streaming-protocols【设计与创意】Use when designing or debugging streaming in AI systems — SSE, NDJSON, HTTP Streaming, WebSocket, MCP transport, or A2UI transport. Covers the three-layer mental model, bidirectionality, real-world wire formats (OpenAI, Anthropic, MCP), production pitfalls (proxy buffering, compression, mobile reconnection), and the decision guide for which protocol …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Pick one option from each of the three layers (Transport / Framing / Message). The "which protocol?" question answers itself.
Most streaming confusion comes from treating options at different layers as alternatives. They compose — they are not competitors.
┌─────────────────────────────────────────────────────────┐
│ MESSAGE LAYER — what each message means │
│ JSON-RPC · A2UI envelope · OpenAI delta · plain JSON │
├─────────────────────────────────────────────────────────┤
│ FRAMING LAYER — how messages are delimited │
│ SSE events · NDJSON lines · HTTP chunks · WS frames │
├─────────────────────────────────────────────────────────┤
│ TRANSPORT LAYER — the connection itself │
│ HTTP · HTTP Streaming · WebSocket · stdio │
└─────────────────────────────────────────────────────────┘
Transport opens the pipe.
Framing chops the pipe into messages.
Message format gives each message meaning.
The most common real-world stack:
HTTP Streaming (transport)
└── SSE (framing)
└── JSON-RPC or provider-specific JSON (message)
For tool/agent IPC:
stdio (transport)
└── newline-delimited JSON (framing)
└── JSON-RPC 2.0 (message)
SSE is not a competitor to HTTP Streaming. SSE is one kind of HTTP Streaming.
HTTP Streaming (the behavior: server sends response gradually)
│
├── SSE ← text/event-stream, blank-line framed
├── NDJSON ← one JSON object per line
├── Chunked HTML ← progressive page rendering
├── Raw binary chunks ← video/audio, file downloads
└── Custom formats ← gRPC-Web, protobuf streams
Asking "SSE vs HTTP Streaming" is like asking "JSON vs HTTP body" — JSON is one of the things you can put in a body.
Why people conflate them:
EventSource is the only ergonomic way to consume HTTP streams. Everything else requires fetch() + ReadableStream + manual byte parsing.When to pick which framing:
| Scenario | Best framing | Why |
|---|---|---|
| Browser receives live updates | SSE | Native EventSource, auto-reconnect, simple to parse |
| AI agent token streaming to a UI | SSE | Browser-friendly, typed event names for tool use |
| Server-to-server streaming | NDJSON | No event-name overhead, easy to log/replay |
| Batch processing pipelines | NDJSON | Line-addressable, replayable, strong tooling |
| stdio IPC (MCP local) | NDJSON | OS-pipe-friendly newline framing |
| Binary data (video, files) | Raw chunks | No text encoding overhead |
| gRPC internal ML platforms | gRPC frames | Strong types, multiplexing, codegen |
| HTTP/1.1 Streaming | HTTP/2 Streaming | WebSocket | |
|---|---|---|---|
| Direction per stream | Half-duplex | Half-duplex within a stream | Full-duplex |
| Bidirectional in practice? | Only by opening 2+ requests | Yes, via separate streams | Yes, natively |
| Auto-reconnect? | Yes (SSE Last-Event-ID) | Per protocol | No — implement yourself |
| Works through corporate proxies? | Almost always | Almost always | Often blocked or buggy |
| Good for AI token streaming? | Yes (standard choice) | Yes | Overkill |
| Good for voice / collab editing? | No | Marginal | Yes |
| Server can initiate? | No (only respond) | Server push (limited) | Yes, anytime |
The crisp distinction:
HTTP Streaming = one-direction-at-a-time per request (walkie-talkie)
WebSocket = both-directions-simultaneously on one connection (phone call)
Why ChatGPT's "stop generating" button closes the connection: once the POST is sent, the client is in receive-only mode. There's no upstream channel to send a "stop" message on. The only option is to drop the connection.
Pick HTTP Streaming (SSE) when:
Pick WebSocket when:
Practical reality for AI apps: 95% of the time, SSE is the right choice. WebSocket earns its weight for voice and collaborative editing — not for token streaming.
MCP needs bidirectional communication (clients call tools; servers call back via sampling/createMessage, roots/list, elicitation). It achieves this over half-duplex HTTP using two simultaneous requests:
CLIENT MCP SERVER
│ │
│── POST /mcp (tools/call) ──────────►│
│◄── HTTP 200, SSE response ──────────│ ← Stream A: responds to client POST
│◄── data: {progress notification} │
│◄── data: {final result} │
│ │
│── GET /mcp (Accept: SSE) ─────────►│
│◄── HTTP 200, SSE response ──────────│ ← Stream B: long-lived GET for
│◄── data: {server→client request} │ server-initiated messages
│ │
│── POST /mcp (response to above) ───►│ ← Client replies via new POST
│◄── HTTP 200 ────────────────────────│
"Bidirectional messaging" in MCP = messages flow in both directions across two streams, not both directions on a single TCP stream.
Two headers that carry the operational glue:
| Header | Purpose |
|---|---|
Mcp-Session-Id | Set by server at init. Must be echoed on every subsequent request. Missing = silent state bugs. |
Last-Event-ID | Standard SSE header. Client sends last received ID on reconnect; server replays missed events. |
SSE deprecation note: SSE was deprecated as a standalone MCP transport (March 2025, replaced by Streamable HTTP). SSE itself is alive and well for LLM streaming, dashboards, and all browser-facing live updates.
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"The"},"index":0}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":" answer"},"index":0}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop","index":0}]}
data: [DONE]
[DONE] sentinel is OpenAI's convention, not part of the SSE specchoices[0].delta.content and appends to rendered messageevent: message_start
data: {"type":"message_start","message":{"id":"msg_01...","role":"assistant"}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The answer"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_stop
data: {"type":"message_stop"}
event: field carries semantic type — clients dispatch on type rather than inspecting payload shapeevent: error with HTTP status still 200event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":30}}
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"done"}]}}
event: a2ui
data: {"version":"v0.9","createSurface":{"surfaceId":"ticket_1","catalogId":"..."}}
event: a2ui
data: {"version":"v0.9","updateDataModel":{"surfaceId":"ticket_1","path":"/status","value":"Open"}}
{"version":"v0.9","createSurface":{"surfaceId":"ticket_1","catalogId":"..."}}
{"version":"v0.9","updateDataModel":{"surfaceId":"ticket_1","path":"/status","value":"Open"}}
{"version":"v0.9","updateComponents":{"surfaceId":"ticket_1","components":[...]}}
Nginx, CloudFront, and most CDNs buffer responses by default. Your "streaming" tokens arrive in one batch.
Fix:
location /stream {
proxy_buffering off;
add_header X-Accel-Buffering no;
}
Also set X-Accel-Buffering: no as a response header from your app server.
Compression middleware accumulates bytes before compressing — turns smooth token flow into chunky bursts.
Fix: Disable compression for text/event-stream responses. The bandwidth saving is not worth the latency cost.
// Express example
app.use(compression({
filter: (req, res) => {
if (res.getHeader('Content-Type') === 'text/event-stream') return false;
return compression.filter(req, res);
},
}));
Long agent runs exceed default proxy timeouts (often 60s). Connection drops mid-stream.
Fix: Send periodic keepalive comments:
: keepalive
(A colon-prefixed line is an SSE comment — ignored by clients, but prevents timeout.)
Last-Event-IDiOS kills HTTP connections when the app backgrounds. Without Last-Event-ID, the stream restarts from scratch on reconnect.
Fix:
id: field on every SSE eventLast-Event-ID header; server replays from that point// Client: use exponential backoff — default EventSource reconnect is too aggressive
const es = new EventSource('/stream');
let retryDelay = 1000;
es.onerror = () => {
setTimeout(() => reconnect(), retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000);
};
traceparent PropagationA single user action may generate 30+ network calls. Without a correlation ID, cross-service debugging is impossible.
Fix: Generate traceparent (W3C Trace Context) at the edge; forward it on every outbound call.
User action
└─► Backend generates traceparent: 00-{trace_id}-{span_id}-01
├─► POST to LLM API — Header: traceparent
├─► POST to MCP server — Header: traceparent, Mcp-Session-Id
└─► SSE response to client — echo traceparent back
Every modern observability tool (Langfuse, Datadog, Honeycomb, Grafana Tempo) understands traceparent. Include it in NDJSON logs so you can join logs across services after the fact.
id and SSE idThey are unrelated and live at different layers.
| Field | Layer | Purpose |
|---|---|---|
JSON-RPC id | Message | Correlates request to response within a JSON-RPC session |
SSE id | Framing | Enables reconnection via Last-Event-ID header |
Streamable HTTP (MCP) is a transport pattern. SSE is one of the response formats Streamable HTTP can return. They compose.
If data flow is server→client only (which most LLM streaming is), WebSocket adds operational complexity without benefit. SSE is simpler, proxy-friendly, and auto-reconnecting. Reach for WebSocket only when you genuinely need bidirectional realtime.
| Your need | Transport | Framing | Message |
|---|---|---|---|
| Chat UI with token streaming | HTTP Streaming | SSE | Provider JSON (OpenAI/Anthropic) |
| Local AI app calling tools | stdio | newline-delimited | JSON-RPC (MCP) |
| Remote AI app calling tools | HTTP Streaming | JSON or SSE | JSON-RPC (MCP) |
| Agent pushing UI updates (A2UI) | HTTP Streaming | SSE | A2UI envelope |
| Recording agent runs (eval/replay) | File / HTTP POST | NDJSON | App-specific trace |
| Bulk LLM inference | HTTP | NDJSON | Provider JSON (Batch API) |
| Voice / collaborative editing | WebSocket | WS frames | App-specific |
| High-perf server-to-server AI | HTTP/2 | gRPC frames | Protobuf |
| Simple one-shot answer | HTTP | — | Plain JSON |
NDJSON rarely appears in the live request path (SSE wins there). It dominates in three places:
SSE → live, server→client, browser-facing
NDJSON → batch, logs, replay, stdio, server-to-server
WebSocket → genuinely bidirectional realtime (voice, collab editing)
Plain JSON → one-shot response, no streaming needed
Three rules that catch 90% of bugs:
Mcp-Session-Id on every MCP request after init, or session state silently breaks.traceparent across every layer, or you'll never debug a multi-hop agent failure.The four real-world stacks:
| Stack | Transport | Framing | Message |
|---|---|---|---|
| Chat token streaming | HTTP Streaming | SSE | Provider JSON |
| MCP (local) | stdio | newline-delimited | JSON-RPC |
| MCP (remote) | HTTP Streaming | JSON or SSE | JSON-RPC |
| Generative UI (A2UI) | HTTP Streaming | SSE | A2UI envelope |