跳转到正文

通过 SSE 流式传输响应

本导读跟踪一条聊天响应从 ASP.NET Core API 到 Blazor WebAssembly 浏览器客户端的完整过程。你将了解 SSE 的确切传输格式、刷新操作的重要性,以及客户端如何解析流式数据块。

API 入口点

ChatController.StreamChat 处理 POST /api/chat/stream。它接收 ChatRequest,选择请求中指定的模型;若未指定,则使用默认模型 claude-haiku-4.5 ,并将响应配置为服务器发送事件(Server-Sent Events):

Response.ContentType = "text/event-stream";
Response.Headers.CacheControl = "no-cache";
Response.Headers.Connection = "keep-alive";

这些响应头会告知中间组件和浏览器:这是一个长生命周期流,而不是应缓冲到完成后再处理的普通 JSON 响应。

传输格式

对于来自 CopilotChatService.ChatStreamAsync的每个数据块,控制器都会序列化一个小型 JSON 对象并写入一条 SSE 消息:

data: {"content":"..."}

当流正常结束时,端点会写入一个哨兵值:

data: [DONE]

如果流启动后引发异常,控制器会在同一个 SSE 数据通道中写入错误事件:

data: {"error":"..."}

此时已无法可靠地切换为 HTTP 错误状态。状态码和响应头已经发送,因此报告后期故障的唯一有效方式是在流负载中携带错误信息。

刷新每个数据块

每写入一个内容数据块后, ChatController.StreamChat 都会刷新响应正文:

await Response.WriteAsync($"data: {data}\n\n", cancellationToken);
await Response.Body.FlushAsync(cancellationToken);

如果没有 FlushAsync,服务器、主机、代理或浏览器可能会缓冲数据。即使 SDK 正确生成了增量,用户也要等到缓冲区填满或请求结束后才能看到内容,从而让流式传输看起来像是失效了。

取消操作

StreamChat 接收由 ASP.NET Core 提供的请求 CancellationToken 。控制器将其传入 CopilotChatService.ChatStreamAsync,在循环期间检查 IsCancellationRequested ,并将其同时传给 WriteAsyncFlushAsync。如果浏览器标签页被关闭或请求被放弃,API 就能停止写入并终止流式处理。

浏览器客户端

Blazor 客户端代码位于 ChatService.StreamChatAsync。它向 /api/chat/stream 发起 POST 请求,并使用 HttpCompletionOption.ResponseHeadersRead

using var response = await _http.SendAsync(
    httpRequest,
    HttpCompletionOption.ResponseHeadersRead);

ResponseHeadersRead 非常重要,因为响应头一到达它就会返回。随后,客户端可以逐行读取正文流,而不必等待完整响应。

解析器会忽略空行,查找 data: 前缀,在遇到 [DONE]时停止,并解析 JSON 数据事件。 content 值会传递给 UI; error 值则会转换为异常。

零售分析系统提示

ChatService.StreamChatAsync 会在调用方未提供系统消息时发送默认系统消息。该提示将助手设定为服务于杂货零售商的零售分析助手,为其提供演示上下文,并要求使用 Markdown 表格和项目符号给出数据驱动的业务洞察。提示还要求助手不要修改代码或建议代码更改。

Home.razor 将用户提示和所选模型传给 ChatService.StreamChatAsync。因此,在请求到达 API 之前,客户端服务就已应用默认系统提示。

使用 curl 试用

请将请求发送到本地 API 使用的端口。例如,如果 API 正在 Web 客户端的默认 API 基址上监听,请使用 curl -N 发送流式请求,以防 curl 缓冲响应:

curl -N -X POST http://localhost:5050/api/chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Summarise customer C003 and recommend a segment.",
    "model": "claude-haiku-4.5",
    "systemMessage": "You are a retail analytics assistant."
  }'

你应当看到多条 data: {"content":"..."} 消息,最后是 data: [DONE]