使用nodejs接入ai服务并使用sse技术处理流式输出实现打字机效果

如何在项目中接入ai服务

简单搭建后端服务

这个服务是异步处理的 本质上是在ai服务供应商的服务器上处理请求 将请求信息转发到前端 也就是一个接口转发层

  1. node服务 主要的点是使用openai的api实现与ai服务器的通信 然后将接口转发到前端 openai.chat.completions.create
  2. openai.chat.completions.create函数包含一个对象参数 对象参数中需要两个属性
    • messages: [{ role: "user", content: message }], 包含用户输入的信息
    • model: "deepseek-chat" 使用的模型
js 复制代码
import express from 'express'
const app=express()
import OpenAI from "openai";
import cors from 'cors' 

app.use(cors())
app.use(express.json())

const openai = new OpenAI({
        baseURL: 'https://api.deepseek.com',
        apiKey: '你的api',
});
app.post('/api/chat',async (req,res)=>{
    try {
        const {message}=req.body
        console.log('收到消息:', message); // Debug log
        const completion = await openai.chat.completions.create({
            messages: [{ role: "user", content: message }],
            model: "deepseek-chat",
        });
        
        console.log('AI响应:', completion.choices[0].message.content);
        res.json({response:completion.choices[0].message.content})
    } catch (error) {
        console.error('服务器错误详细信息:', error); // 打印完整错误堆栈
        res.status(500).json({ 
            error: '服务器内部错误', 
            details: error.message 
        });
    }
})
app.listen(3000,()=>{
    console.log('server is running on port 3000')
})

completion用来接受ai服务器发送回来的响应内容

js 复制代码
const completion = await openai.chat.completions.create({
  messages: [{ role: "user", content: message }],
  model: "deepseek-chat",
});

搭建简答的前端服务

搭建相当简单 只需要对信息做收集 发送 接受 渲染即可

html 复制代码
<body>
    <input type="text" id="message-input" placeholder="请输入消息">
    <button id="send-button">发送</button>
    <div id="chat-container"></div>
</body>
<script>
    const messageInput=document.getElementById('message-input')
    const sendButton=document.getElementById('send-button')
    const chatContainer=document.getElementById('chat-container')
    async function sendMessage(message){
        const response=await fetch('http://localhost:3000/api/chat',{
            method:'POST',
            headers:{
                'Content-Type':'application/json'
            },
            body:JSON.stringify({message})
        })
        const data=await response.json()
        return data.response
    }
    sendButton.addEventListener('click',async ()=>{
        const message=messageInput.value
        if(!message) return
        messageInput.value=''
        const userMessage=document.createElement('div')
        userMessage.textContent=`你:${message}`
        chatContainer.appendChild(userMessage)
        const botMessage=document.createElement('div')
        botMessage.textContent=`助手:${await sendMessage(message)}`
        chatContainer.appendChild(botMessage)
    })
  </script>

改造node服务 请求流式响应 发送流式响应 前端渲染流式响应 实现打字机效果

1. 后端请求流式响应 并转发

开是要设置响应头: 这是必须要设置的 告诉前端这是一个流式响应 不能缓存 保持连接

js 复制代码
res.setHeader('Content-Type','text/event-stream')//流式输出
    res.setHeader('Cache-Control','no-cache')// 禁用缓存
    res.setHeader('Connection','keep-alive')// 保持连接

然后模拟ai服务 逐字返回 前端渲染打字机效果:

这里我们必须要注意 每次发送字符给前端 都必须要符合SSE协议格式 否则前端无法正确解析 也就是每次都发送data: ${JSON.stringify({ content: char })}\n\n 这是固定写法 而且必须使用res.write 发送 不能使用res.send 否则前端无法正确解析

在响应的最后必须发送data: [DONE]\n\n 这是固定写法 否则前端无法正确解析

js 复制代码
// 模拟ai逐字逐字返回
let index=0
const timer = setInterval(() => {
if (index < Answer.length) {
  //SSE 协议格式:data: 内容\n\n(必须严格遵守)
  const char = Answer[index];
  // 核心 使用res.write 发送字符给前端
  res.write(`data: ${JSON.stringify({ content: char })}\n\n`);
  index++;
} else {
  // 结束流式传输(发送结束标识)
  clearInterval(timer);
  res.write('data: [DONE]\n\n'); // 自定义结束标识
  res.end(); // 关闭连接
}
}, 100); // 每 100ms 返回一个字符,模拟打字机速度

因为我们在响应头设置了保持链接的响应头 所以必须主动去断开链接

js 复制代码
// 5. 处理前端断开连接(避免内存泄漏)
res.on('close', () => {
  clearInterval(timer);
  res.end();
});

完整代码

js 复制代码
sseApp.post('/api/chat',async (req,res)=>{
    // 设置响应头 发送sse 流
    res.setHeader('Content-Type','text/event-stream')//流式输出
    res.setHeader('Cache-Control','no-cache')// 禁用缓存
    res.setHeader('Connection','keep-alive')// 保持连接
    // 从前端获取消息
    const {message}=req.body
    // 发送消息给前端
    const Answer='这是你发送的消息:'+message+'这段消息用来处理学习sse数据的实现一个极简的 Demo:前端输入文字 → 后端模拟 AI 逐字返回(打字机效果)→ 前端实时展示,不涉及 AI 真实接口,只聚焦 SSE 流式处理本身'
    // 模拟ai逐字逐字返回
    let index=0
    const timer = setInterval(() => {
    if (index < Answer.length) {
      //SSE 协议格式:data: 内容\n\n(必须严格遵守)
      const char = Answer[index];
      // 核心 使用res.write 发送字符给前端
      res.write(`data: ${JSON.stringify({ content: char })}\n\n`);
      index++;
    } else {
      // 结束流式传输(发送结束标识)
      clearInterval(timer);
      res.write('data: [DONE]\n\n'); // 自定义结束标识
      res.end(); // 关闭连接
    }
  }, 100); // 每 100ms 返回一个字符,模拟打字机速度

  // 5. 处理前端断开连接(避免内存泄漏)
  res.on('close', () => {
    clearInterval(timer);
    res.end();
  });
})

2. 前端渲染流式响应 实现打字机效果

  1. 发送请求
    在发送请求是必须带上响应头: 否则前端不能解析后端传来的数据
js 复制代码
'Content-Type':'application/json'
  1. 处理后端数据 实现打字机效果
  2. 首先使用response.body.getReader() 方法获取到一个ReadableStreamDefaultReader 对象 这个对象可以用来读取流式响应的数据 就是创建一个读取器 这个读取器允许我们手动、逐块读取流式响应中的数据
js 复制代码
 const reader = response.body.getReader();
  1. 创建一个解码器 用来将获取的二进制字符转换为字符数据 这里我们使用TextDecoder 这个类 来创建一个解码器 这个解码器可以将二进制数据转换为字符串 注意因为流式处理每次只能传递一个字符 我们需要使用循环来读取所有字符
js 复制代码
 const decoder = new TextDecoder(); 
  while (true) {
   //....
}
  1. 使用读取器中的read方法 将获取的数据逐块读取 并使用解码器将二进制数据转换为字符串 value是二进制数据表示当前读取的字符 done表示是否读取完成
js 复制代码
 const { done, value } = await reader.read();
 console.log('done:',done);   // 布尔值
 console.log('value:',value); //二进制数据
 if (done) break;
  1. 使用解码器将获取的value数据转化为字符串 chunk:" data: {"content":"身"}\n\n '空' " 这是我们转化得到的数据 使用\n\n来分割分割后的数据 lines:['data: {"content":"身"}', '']是一个数组
js 复制代码
 const chunk = decoder.decode(value); // 解析后是一个字符串 " data: {"content":"身"}\n\n '空' "
 console.log('chunk:',chunk);
 // SSE 数据可能一次包含多个消息,按 \n\n 分割
 const lines = chunk.split('\n\n');
 console.log('lines:',lines);// 分割后是一个数组 ['data: {"content":"身"}\n\n', '']
 ```
5. 遍历lines数组 对每个元素进行处理 去掉前缀'data: '和分割后多余的空字符串'' 得到最终的数据 {"content":"身"}  注意w3c规范流式输出结束后需要传递[data: [DONE]] 我们需要判断是否是结束标识 如果是结束标识 则直接跳出循环 否则继续处理
6. 将得到的数据转化为json数据  const json = JSON.parse(dataStr);  aiAnswer.textContent += json.content;最后渲染到前端
```js
for (const line of lines) {
       if (line.startsWith('data: ')) {
           const dataStr = line.replace('data: ', '').trim(); // 去掉前缀 'data: '和分割后多余的空字符串''
           console.log('dataStr:',dataStr); // {"content":"身"} 得到的数据
           if (dataStr === '[DONE]') { //根据协议标准 到响应的最后阶段会传递结束符 chunk: data: [DONE]
               console.log('流传输结束');
               break;
           }
           try {
               // 必带:解析 JSON 字符串 原本 {"content":"身"}--> {content:'身'}
               const json = JSON.parse(dataStr); 
               console.log('json:',json);
               // 实时追加内容
               aiAnswer.textContent += json.content;
           } catch (e) {
               console.error('解析 JSON 失败:', e, dataStr);
           }
       }
   }

完整代码

js 复制代码
sendBtn.addEventListener('click',async ()=>{
  const message=inputMsg.value
  if(!message) return
  inputMsg.value=''
  aiAnswer.textContent='' // 清空之前的回答        
  const response=await fetch('http://localhost:3000/api/chat',{
      method:'POST',
      headers:{
          // 必带:告诉后端请求体是 JSON 格式(否则后端 req.body 为 undefined)
          'Content-Type':'application/json'
      },
      body:JSON.stringify({message})
  })

  // 核心:处理流式响应
  const reader = response.body.getReader();
  console.log('reader:',reader);
  // 必带:创建文本解码器,将二进制数据转换为字符串
  const decoder = new TextDecoder(); 
  while (true) {
      console.log('读取数据');
      // done: 表示流是否结束 value: 表示读取到的二进制数据
      const { done, value } = await reader.read();
      console.log('done:',done);   // 布尔值
      console.log('value:',value); //二进制数据
      if (done) break;
      // 解码二进制数据
      const chunk = decoder.decode(value); // 解析后是一个字符串 " data: {"content":"身"}\n\n '空' "
      console.log('chunk:',chunk);
      // SSE 数据可能一次包含多个消息,按 \n\n 分割
      const lines = chunk.split('\n\n');
      console.log('lines:',lines);// 分割后是一个数组 ['data: {"content":"身"}\n\n', '']
      
      for (const line of lines) {
          if (line.startsWith('data: ')) {
              const dataStr = line.replace('data: ', '').trim(); // 去掉前缀 'data: '和分割后多余的空字符串''
              console.log('dataStr:',dataStr); // {"content":"身"} 得到的数据
              if (dataStr === '[DONE]') { //根据协议标准 到响应的最后阶段会传递结束符 chunk: data: [DONE]
                  console.log('流传输结束');
                  break;
              }
              try {
                  // 必带:解析 JSON 字符串 原本 {"content":"身"}--> {content:'身'}
                  const json = JSON.parse(dataStr); 
                  console.log('json:',json);
                  // 实时追加内容
                  aiAnswer.textContent += json.content;
              } catch (e) {
                  console.error('解析 JSON 失败:', e, dataStr);
              }
          }
      }
  }
})

实现效果

相关推荐
模型时代2 小时前
F5推出AI安全防护平台扩展新产品
人工智能
币之互联万物2 小时前
消费品营销战略咨询公司怎么选?哪家靠谱?
大数据·人工智能
极客小云2 小时前
【React + TypeScript 实现高性能多列多选组件】
前端·react.js·typescript
lm down2 小时前
一键部署 HeartMuLa,支持 Mac 和 Windows
人工智能·音视频
码农三叔2 小时前
(4-2)机械传动系统与关节设计: 减速器与传动机构
人工智能·架构·机器人·人形机器人
whaosoft-1432 小时前
51c视觉~OCR~合集2
人工智能
bin91532 小时前
(文后附完整代码)html+css+javascript 弹球射击游戏项目分析
前端·javascript·css·游戏·html·前端开发
qq_459558692 小时前
使用DrissionPage打开Edge
前端·edge
许泽宇的技术分享2 小时前
AI开发者的福音:MCP Feedback Enhanced 让你的AI交互像“开挂”一样丝滑!
人工智能·交互·mcp