使用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);
              }
          }
      }
  }
})

实现效果

相关推荐
数据皮皮侠AI2 分钟前
中国城市可再生能源数据集(2005-2021)|顶刊 Sci Data 11 种能源面板
大数据·人工智能·笔记·能源·1024程序员节
G31135422736 分钟前
如何用 QClaw 龙虾做一个规律作息健康助理 Agent
大数据·人工智能·ai·云计算
幂律智能7 分钟前
零售行业合同管理数智化转型解决方案
大数据·人工智能·零售
旺财矿工8 分钟前
零基础搭建 OpenClaw 2.6.6 Win11 本地化运行环境
人工智能·openclaw·小龙虾·龙虾·openclaw安装包
九成宫10 分钟前
动手学深度学习PyTorch版初步安装过程
人工智能·pytorch·深度学习
Traving Yu10 分钟前
Prompt提示词工程
人工智能·prompt
NOCSAH11 分钟前
统好AI CRM功能解析:智能录入与跟进
人工智能
He少年12 分钟前
【AI 辅助编程做设备数据采集:一个真实项目的迭代复盘(OpenSpec 驱动)】
人工智能
华万通信king16 分钟前
WorkBuddy知识库企业级搭建实战:从零到生产级别的完整路径
大数据·人工智能
测试员周周23 分钟前
【AI测试系统】第3篇:AI生成的测试用例太“水”?14年老兵:规则引擎+AI才是王炸组合
人工智能·python·测试