如何在项目中接入ai服务
简单搭建后端服务
这个服务是异步处理的 本质上是在ai服务供应商的服务器上处理请求 将请求信息转发到前端 也就是一个接口转发层
- node服务 主要的点是使用openai的api实现与ai服务器的通信 然后将接口转发到前端 openai.chat.completions.create
- 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. 前端渲染流式响应 实现打字机效果
- 发送请求
在发送请求是必须带上响应头: 否则前端不能解析后端传来的数据
js
'Content-Type':'application/json'
- 处理后端数据 实现打字机效果
- 首先使用
response.body.getReader()方法获取到一个ReadableStreamDefaultReader对象 这个对象可以用来读取流式响应的数据 就是创建一个读取器 这个读取器允许我们手动、逐块读取流式响应中的数据
js
const reader = response.body.getReader();
- 创建一个解码器 用来将获取的二进制字符转换为字符数据 这里我们使用
TextDecoder这个类 来创建一个解码器 这个解码器可以将二进制数据转换为字符串 注意因为流式处理每次只能传递一个字符 我们需要使用循环来读取所有字符
js
const decoder = new TextDecoder();
while (true) {
//....
}
- 使用读取器中的read方法 将获取的数据逐块读取 并使用解码器将二进制数据转换为字符串 value是二进制数据表示当前读取的字符 done表示是否读取完成
js
const { done, value } = await reader.read();
console.log('done:',done); // 布尔值
console.log('value:',value); //二进制数据
if (done) break;
- 使用解码器将获取的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);
}
}
}
}
})
实现效果
