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

React集成OpenAI流式响应时状态更新出现重复内容问题排查

问题

我做聊天界面流式展示OpenAI响应的时候,每个响应块本身内容是对的,但最终显示出来的内容有重复。下面是我的代码,还有界面和控制台的截图:

useEffect(() => {
  const doCompletion = async () => {
    if (!newMessage) return;
      
    try {
      await handleCompletion(messages);
      setNewMessage(false);
    } catch (err) {
      console.log("unable to communicate with model service: ", err)
    }
  }

  doCompletion();
}, [newMessage]);

const handleNewMessage = async (formData: FormData) => {
  const message = formData.get("message") as string;
  addMessage({id: uuidv4(), data: {role: "user", content: message}});
  setNewMessage(true);
}

const handleCompletion = async (messages: Message[]) => {
  const formattedMessages = messages.map((message: Message) => message.data)

  const stream = await openai.chat.completions.create({
    messages: formattedMessages,
    model: "deepseek-chat",
    stream: true
  });
    
  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content;
    console.log("content: ", content)
      
    setMessages((prev) => {
      let tmpMessages = [...prev];
      const lastMessageIndex = tmpMessages.length-1;
      if (tmpMessages[lastMessageIndex].data.role !== "assistant") {
        tmpMessages.push({id: chunk.id, data: {role: "assistant", content: ""}});
        return tmpMessages;
      }

      const prevContent = tmpMessages[lastMessageIndex].data.content;
      console.log("prevContent: ", prevContent)
      
      const newContent = prevContent! + content!;
      console.log("newContent: ", newContent)
        
      tmpMessages[lastMessageIndex].data.content = newContent;
      return tmpMessages;
    });

  }
}

聊天界面重复内容截图

问题原因

核心问题是闭包捕获了旧的状态快照:

  • useEffect触发时,把当前的messages传给了handleCompletion,这个messages是当时的状态快照。
  • 流式响应的循环里,每次调用setMessages虽然用了prev取最新状态,但循环本身是基于最初的messages快照在跑。如果组件因为状态更新重新渲染,可能会导致多个循环迭代同时操作不同的状态副本,最后叠加出重复内容。
  • 另外,有些chunk可能是空的content,直接拼接会导致无效重复操作。
解决办法

1. 不要给handleCompletion传messages参数

直接在handleCompletion内部通过setMessages的回调获取最新状态,避免依赖外部的旧快照。

2. 跳过空内容的chunk

防止空内容导致的无效拼接。

3. 加流式状态锁,避免重复触发

防止因为状态更新重复调用流式请求。

修改后的完整代码:

import { useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';

type Message = {
  id: string;
  data: {
    role: 'user' | 'assistant';
    content: string;
  };
};

const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState(false);
const [isStreaming, setIsStreaming] = useState(false); // 新增流式状态锁

useEffect(() => {
  const doCompletion = async () => {
    if (!newMessage || isStreaming) return; // 加锁判断
    setIsStreaming(true);
      
    try {
      await handleCompletion(); // 不再传messages
      setNewMessage(false);
    } catch (err) {
      console.log("unable to communicate with model service: ", err)
    } finally {
      setIsStreaming(false); // 解锁
    }
  }

  doCompletion();
}, [newMessage, isStreaming]);

const handleNewMessage = async (formData: FormData) => {
  const message = formData.get("message") as string;
  setMessages(prev => [...prev, {id: uuidv4(), data: {role: "user", content: message}}]);
  setNewMessage(true);
}

const handleCompletion = async () => {
  // 从最新状态获取格式化后的消息
  const formattedMessages = await new Promise<{role: string; content: string}[]>(resolve => {
    setMessages(prev => {
      resolve(prev.map(msg => msg.data));
      return prev;
    });
  });

  const stream = await openai.chat.completions.create({
    messages: formattedMessages,
    model: "deepseek-chat",
    stream: true
  });
    
  let assistantMessageId: string;
  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content;
    if (!content) continue; // 跳过空内容的chunk
      
    setMessages((prev) => {
      const tmpMessages = [...prev];
      const lastMessageIndex = tmpMessages.length - 1;
      
      // 检查是否需要创建新的assistant消息
      if (tmpMessages[lastMessageIndex].data.role !== "assistant") {
        assistantMessageId = chunk.id;
        tmpMessages.push({
          id: assistantMessageId,
          data: {role: "assistant", content: content}
        });
        return tmpMessages;
      }

      // 确保是当前正在处理的assistant消息,避免多请求冲突
      if (tmpMessages[lastMessageIndex].id !== assistantMessageId) {
        return prev;
      }

      // 拼接内容
      tmpMessages[lastMessageIndex].data.content += content;
      return tmpMessages;
    });
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 01:37:35