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
相关产品推荐
相关产品推荐

