任务-:
- 启动一个http服务
- 提供一个SSE接口,把模型输出一块块推送给前端
- 实现一个简单的html页面,实现打字机效果
SEE接口实现
// 用node:http实现
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
const { method, url, headers } = req;
if (method === 'OPTIONS') {
res.writeHead(200)
return res.end()
}
if (url === '/stream' && method === 'GET') {
res.writeHead(200, {
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // 禁用 Nginx 缓冲
})
let stream;
try {
stream = await client.chat.completions.create({
model: MODEL,
messages,
stream: true,
stream_options: { include_usage: true },
})
if (!stream) return res.end('模型未响应')
// 接口返回chunk数据,需要特殊处理
for await (const chunk of stream) { //完整接收大模型返回数据
if (chunk.choices && chunk.choices.length > 0) {
const content = chunk.choices?.[0]?.delta.content || ''
if (content) {
res.write(`data: ${JSON.stringify({content})}\n\n`)
}
} else if (chunk.usage) {
console.log('\n--- 请求用量 ---');
console.log(`输入 Tokens${chunk.usage.prompt_tokens}`);
console.log(`输出 Tokens ${chunk.usage.completion_tokens}`);
console.log(`总量 Tokens${chunk.usage.total_tokens}`);
}
}
} catch (error) {
res.end(JSON.stringify(error))
} finally {
res.end();
}
// 传输完成
res.write(`data: ${JSON.stringify({ done: true })}\n\n`)
res.end();
} else {
res.writeHead(404, {"content-type": 'text/plain'})
res.end('404 Not Found')
}
})
前端实现
// html部分
<button id="startBtn">开始</button>
<button id="stopBtn">结束</button>
<div id="output"></div>
// js部分
let eventSource = null;
// 开始连接
document.getElementById('startBtn').addEventListener('click', () => {
if (eventSource) {
eventSource.close();
}
const output = document.getElementById('output');
output.textContent = ''; // 清空
// 创建 EventSource 连接
eventSource = new EventSource('http://localhost:3333/stream');
// 接收消息
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.content) {
output.textContent += data.content;
}
// 对话完成
if (data.done) {
eventSource.close();
}
};
// 错误处理
eventSource.onerror = (error) => {
console.error('SSE 错误:', error);
eventSource.close();
};
});
// 手动停止
document.getElementById('stopBtn').addEventListener('click', () => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
});
总结
- 主要是在原始的api接口上增加一个
stream参数,那么接口就会以流式的数据返回 - api响应的数据类型是
chunk类型,常规的console打印根本无法展示出来,debug比较难 - 前端通过
EventSource建立长链接,然后把内容拼接到容器的文本中渲染