如何从Firebase云函数流式传输OpenAI ChatCompletion响应到ReactJS
解决React + Firebase云函数实现OpenAI流式响应的问题
当前错误原因
你使用的Firebase Callable云函数无法支持流式响应,且直接返回fetch的Response对象会导致前端无法解析——Callable函数要求返回可序列化的JSON数据,这就是你遇到Cannot read properties of undefined (reading '0')错误的核心原因:前端拿到的result.data是undefined,后续处理时试图访问未定义的属性。
要实现流式响应,必须改用HTTP云函数(onRequest),它可以直接处理并转发流式数据。
解决方案:后端(HTTP云函数)实现
替换原Callable函数为HTTP云函数,负责转发OpenAI的流式响应:
const functions = require("firebase-functions"); const fetch = require("node-fetch"); // Node.js 18+可改用内置fetch const SYSTEM_MESSAGE = "你的系统提示词内容"; const GPT_MODEL = "gpt-3.5-turbo"; exports.getChatCompletionStream = functions.https.onRequest(async (req, res) => { // 设置流式响应头 res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); try { const { messages } = req.body; const fullMessages = [ { role: "system", content: SYSTEM_MESSAGE }, ...messages ]; const openAIResponse = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ model: GPT_MODEL, messages: fullMessages, temperature: 0, stream: true }), }); // 流式转发OpenAI响应 const reader = openAIResponse.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 处理OpenAI的SSE格式数据 const lines = chunk.split("\n").filter(line => line.trim() !== "" && line.startsWith("data:")); for (const line of lines) { const data = line.replace(/^data: /, ""); if (data === "[DONE]") { res.write("data: [DONE]\n\n"); res.end(); return; } try { const parsed = JSON.parse(data); const delta = parsed.choices[0].delta?.content || ""; // 以SSE格式发送数据给前端 res.write(`data: ${JSON.stringify({ delta })}\n\n`); } catch (e) { functions.logger.error("解析OpenAI响应出错", e); } } } res.end(); } catch (error) { functions.logger.error("云函数执行出错", error); res.status(500).send(JSON.stringify({ error: error.message })); } });
后端注意事项
- Node.js版本18+可直接使用内置
fetch,旧版本需安装node-fetch依赖 - 部署云函数后,记录下函数的完整URL(格式:
https://<region>-<project-id>.cloudfunctions.net/getChatCompletionStream)
解决方案:前端(React)实现
修改React代码,通过fetch调用HTTP云函数,处理流式响应并实时更新UI:
const Chat = () => { const [inputValue, setInputValue] = useState(''); const [messages, setMessages] = useState([]); const handleKeyDown = async (event) => { if (event.key === 'Enter') { event.preventDefault(); if (!inputValue.trim()) return; const userMessage = { role: 'user', content: inputValue }; const updatedMessages = [...messages, userMessage]; setMessages(updatedMessages); setInputValue(''); // 创建带唯一ID的助手消息初始状态(用于后续实时更新) const assistantMessageId = Date.now(); setMessages(prev => [...prev, { role: 'assistant', content: '', id: assistantMessageId }]); try { const response = await fetch('https://<你的云函数URL>', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ messages: updatedMessages }), }); if (!response.ok) throw new Error('请求失败'); 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); const lines = chunk.split("\n").filter(line => line.trim() !== "" && line.startsWith("data:")); for (const line of lines) { const data = line.replace(/^data: /, ""); if (data === "[DONE]") return; const parsed = JSON.parse(data); const delta = parsed.delta; if (delta) { // 实时更新助手消息内容 setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: msg.content + delta } : msg )); } } } } catch (error) { console.error('流式请求出错:', error); // 更新助手消息为错误提示 setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: '抱歉,请求出错了,请重试。' } : msg )); } } }; const handleChange = (event) => { setInputValue(event.target.value); }; return ( <div className='chat'> <div className='chat-log-container'> {messages.map((m) => ( <ChatMessage key={m.id || Date.now() + Math.random()} role={m.role} content={m.content} /> ))} </div> <textarea className="chat-input-textarea" maxRows={3} placeholder='Ask a question' value={inputValue} onChange={handleChange} onKeyDown={handleKeyDown} /> </div> ); };
前端注意事项
- 替换
<你的云函数URL>为实际部署的HTTP云函数地址 - 给助手消息添加唯一ID,避免用数组index作为key导致的渲染异常
- 通过
ReadableStream逐段解析SSE数据,实现响应内容的实时更新
内容的提问来源于stack exchange,提问作者Koh
相关产品推荐
相关产品推荐

