You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于NestJS与Angular实现OpenAI流式响应的问题排查

解决NestJS+Angular流式聊天机器人的两个问题

问题1:流式响应结束后自动重复调用接口

EventSource默认会在连接断开(包括正常结束)时自动重试,这是重复调用的核心原因,需要前后端配合处理:

后端(NestJS)处理

确保流式响应结束时发送明确的结束标记,并主动关闭响应流:

@Post('chat-stream')
async chatStream(@Body() body: ChatRequest, @Res() res: Response) {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const stream = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: body.messages,
    stream: true,
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    if (content) {
      res.write(`data: ${JSON.stringify({ content })}\n\n`);
      await new Promise(resolve => setTimeout(resolve, 10));
    }
  }

  // 发送结束标记
  res.write('data: [DONE]\n\n');
  res.end(); // 主动关闭响应连接
}

前端(Angular)处理

监听消息事件,收到结束标记后主动关闭EventSource实例:

private eventSource: EventSource | null = null;

startChat(messages: ChatMessage[]) {
  // 先关闭已有连接
  if (this.eventSource) {
    this.eventSource.close();
  }

  const url = `/api/chat-stream?messages=${encodeURIComponent(JSON.stringify(messages))}`;
  this.eventSource = new EventSource(url);

  this.eventSource.onmessage = (event) => {
    if (event.data === '[DONE]') {
      this.eventSource?.close(); // 收到结束信号后主动终止连接
      return;
    }
    const data = JSON.parse(event.data);
    // 处理流式返回的内容
    console.log(data.content);
  };

  this.eventSource.onerror = () => {
    this.eventSource?.close(); // 出错时也主动关闭
  };
}

问题2:无法通过EventSource发送POST数据

EventSource仅支持GET请求,无法直接携带POST请求体,推荐两种解决方案:

方式1:用Fetch API模拟SSE(优先推荐)

通过Fetch发送POST请求,手动处理流式响应,完全兼容POST传参,效果和SSE一致:

async startChat(messages: ChatMessage[]) {
  const response = await fetch('/api/chat-stream', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ messages }),
  });

  if (!response.body) return;

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    // 分割SSE格式的消息
    const messageChunks = chunk.split('\n\n').filter(msg => msg.startsWith('data:'));
    for (const msg of messageChunks) {
      const data = msg.slice(5); // 移除"data: "前缀
      if (data === '[DONE]') {
        return;
      }
      const content = JSON.parse(data).content;
      // 处理流式内容
      console.log(content);
    }
  }
}

后端无需修改,保持原有流式输出逻辑即可。

方式2:将数据编码到GET参数(仅适合小体量数据)

如果对话历史、上下文数据量较小,可以将JSON数据编码后放到URL参数中,但受限于URL长度限制,不推荐用于大量数据场景。


内容的提问来源于stack exchange,提问作者Robouste

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 15:29:49