如何在Node 17及以下版本的Next.js中实现OpenAI流式响应传输
问题解决:Next.js API路由实现OpenAI流式响应(Node 17及以下版本)
问题背景
由于DigitalOcean App Platform暂不支持Node 18,只能使用Node 17及以下版本。尝试过openai-streams+nextjs-openai方案,但该方案仅兼容Node 18+,低版本无法运行。客户端直接调用OpenAI API虽能正常工作,但会暴露API密钥,因此需要在Next.js API路由中实现流式响应传输,但当前代码仅能获取第一块响应,无法实现ChatGPT式的流式输出。
问题分析与修复
1. API路由代码修复(/api/prompt.js)
原代码存在两个核心问题:
- 错误使用
Readable.from(string).pipe(res),无法正确逐块输出流式内容 - 未设置流式响应所需的HTTP头,且API密钥使用了
NEXT_PUBLIC_前缀(会暴露给客户端,存在安全风险)
修复后的代码:
import { Configuration, OpenAIApi } from "openai"; // 注意:不要用NEXT_PUBLIC_前缀,避免密钥暴露 const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(configuration); export default async function handler(req, res) { // 设置流式响应的HTTP头 res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Transfer-Encoding": "chunked", "Cache-Control": "no-cache", Connection: "keep-alive", }); try { const completion = await openai.createCompletion( { model: "text-davinci-003", prompt: "tell me a story", max_tokens: 500, stream: true, }, { responseType: "stream" } ); completion.data.on("data", (data) => { const lines = data.toString().split("\n").filter(line => line.trim() !== ""); for (const line of lines) { const message = line.replace(/^data: /, ""); if (message === "[DONE]") { res.end(); // 结束响应 return; } try { const parsed = JSON.parse(message); const string = parsed.choices[0].text; res.write(string); // 直接将文本块写入响应流 } catch (error) { console.error("解析流消息失败", message, error); } } }); // 处理流错误 completion.data.on("error", (err) => { console.error("流式请求错误", err); res.end(`Error: ${err.message}`); }); // 处理连接关闭 req.on("close", () => { completion.data.destroy(); res.end(); }); } catch (err) { console.error("API请求错误", err); res.status(500).end(`Server Error: ${err.message}`); } }
2. 前端组件代码修复(/components/Completion.js)
原代码使用response.text()会等待整个响应完成后才获取数据,无法实现流式更新。需要使用ReadableStream逐块读取响应内容:
import { useState } from "react"; export default function Completion() { const [text, setText] = useState(""); const [isLoading, setIsLoading] = useState(false); const generate = async () => { setIsLoading(true); setText(""); // 清空之前的内容 try { const response = await fetch("/api/prompt"); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); setText(prev => prev + chunk); // 逐块更新文本 } } catch (err) { setText(`生成失败:${err.message}`); } finally { setIsLoading(false); } }; return ( <div> <button onClick={generate} disabled={isLoading}> {isLoading ? "生成中..." : "生成故事"} </button> <div style={{ marginTop: "1rem", whiteSpace: "pre-wrap" }}>{text}</div> </div> ); }
关键说明
- API路由中必须设置
Transfer-Encoding: chunked和Cache-Control: no-cache,确保浏览器能正确处理流式响应 - 前端使用
getReader()和TextDecoder逐块读取响应流,实现实时更新文本内容 - 务必避免将OpenAI密钥暴露给客户端,不要使用
NEXT_PUBLIC_前缀命名密钥环境变量
内容的提问来源于stack exchange,提问作者samchuang
相关产品推荐
相关产品推荐

