Practical outcome
You will build the central /assistant/stream endpoint: validate a question, retrieve authorised company evidence, stream answer text and finish with server-validated citations.
Compare buffered and streamed AI responses
Why stream an AI response?
An LLM may take several seconds to produce a complete answer. A normal JSON endpoint makes the client wait for the entire result. Streaming delivers small events as they become available, improving perceived responsiveness and allowing progress signals.
Streaming does not make generation cheaper or automatically faster. It changes how output travels to the client.
Choose an event contract
Do not stream anonymous text fragments without meaning. Define event types:
01meta → request accepted, request ID02token → incremental answer text03citation → validated source metadata04done → stream completed05error → safe failure information
Server-Sent Events (SSE) is a practical one-way server-to-client format for web applications.
01event: token02data: {"text":"Employees may carry"}0304event: citation05data: {"source_id":"hr-2026-4.2","title":"Employee Handbook","page":18}
Define the request
01from pydantic import BaseModel, Field0203class AssistantRequest(BaseModel):04question: str = Field(min_length=3, max_length=2000)05conversation_id: str | None = None
The user does not provide tenant, role or document filters. The server derives them from authenticated identity.
Build the event encoder
01import json0203def sse(event: str, payload: dict) -> str:04data = json.dumps(payload, ensure_ascii=False)05return f"event: {event}\ndata: {data}\n\n"
Never place raw untrusted newline content directly into the SSE protocol. Serialising JSON gives the client a predictable envelope.
Stream the grounded answer
01from collections.abc import AsyncIterator02from fastapi.responses import StreamingResponse0304async def answer_events(payload, user) -> AsyncIterator[str]:05request_id = create_request_id()06yield sse("meta", {"request_id": request_id})0708evidence = await retriever.search(09question=payload.question,10tenant_id=user.tenant_id,11access_groups=user.groups,12)1314if not evidence:15yield sse("error", {16"code": "insufficient_evidence",17"message": "No authorised source can answer this question.",18})19return2021allowed_ids = {item.source_id for item in evidence}2223async for text in model.stream(24question=payload.question,25evidence=evidence,26):27yield sse("token", {"text": text})2829for item in evidence:30if item.source_id in allowed_ids:31yield sse("citation", item.public_metadata())3233yield sse("done", {"request_id": request_id})
01@app.post("/assistant/stream")02async def stream_answer(payload: AssistantRequest, user: CurrentUser):03return StreamingResponse(04answer_events(payload, user),05media_type="text/event-stream",06headers={"Cache-Control": "no-cache"},07)
This example teaches the boundary. In production, handle disconnects, provider cancellation, trace completion and safe partial results.
Keep citations outside generated prose
The model may mention [S1], but application code should map S1 to a source actually retrieved for this request. Send citations as separate structured events so the interface can render safe titles and URLs.
01Model produces: answer text + source labels02Server verifies: label ∈ retrieved source IDs03Client receives: safe citation metadata owned by server
Stream errors deliberately
Once response headers are sent, changing the HTTP status may no longer be possible. Validate authentication, request shape and basic limits before creating the stream. For failures during streaming, send a defined error event, close cleanly and record the trace.
| Failure moment | Recommended signal |
|---|---|
| Before stream starts | Normal HTTP status and JSON error |
| During retrieval/provider call | SSE error event and clean close |
| Client disconnects | Cancel downstream work where supported |
| Partial answer fails validation | Do not send final citations or done success |
Real-time analogy: live sports commentary
Progress now, official record at the end
Commentary arrives as the match unfolds, but the final scorecard remains structured and authoritative. AI tokens are live commentary; validated citations and completion metadata are the scorecard. A dropped connection should not create a false final result.
Expose RAG and agents behind service interfaces
The route should not know which vector database or agent framework is used. It depends on capabilities:
01class EvidenceRetriever:02async def search(self, question: str, tenant_id: str, access_groups: list[str]): ...0304class AnswerGenerator:05async def stream(self, question: str, evidence: list): ...
This lets you replace LangChain, provider SDKs or storage without breaking the public endpoint.
Common mistakes
Common mistakes
- Streaming before authentication and request limits complete.
- Treating generated URLs as trustworthy citations.
- Ignoring client disconnects while the paid model call continues.
- Sending inconsistent chunks that the frontend cannot parse.
- Assuming streamed tokens are safe to pass directly into downstream tools.
Interview bit
How would you stream an LLM response safely?
I validate and authenticate before starting the stream, then use a versioned event envelope such as SSE with meta, token, citation, error and done events. Retrieval is scoped by verified identity. I bind citations to retrieved source IDs in server code, handle disconnect cancellation and trace partial failure because the HTTP status cannot reliably change after headers are sent.
Key takeaways
Key takeaways
- Streaming improves delivery experience, not model accuracy or cost.
- Define typed event semantics instead of sending unstructured token fragments.
- Complete authentication and basic validation before response headers are sent.
- Bind citations to server-owned retrieved evidence.
- Handle mid-stream failure and client disconnect as first-class states.
Frequently asked questions
Does streaming make the model generate faster? Not necessarily. It improves perceived responsiveness by returning useful events before the full answer is complete.
How should a streamed API report an error after output begins? Define explicit event types so the client can distinguish content, citation, completion and error events without parsing free-form text.
When should citations be sent? Send only citations backed by retrieved, authorised evidence and keep enough identifiers to trace the source.
Interactive Knowledge Check
Choose an answer, inspect the explanation and explain the idea in your own words.