GTP网站
近年来各种人工智能对话网站如雨后春笋,遍地都是。这些网站大多数都是调用市面上现有的大模型接口,比如豆包,质谱清言、文心一言。好处是这些平台都提供了友好的开发者指南,所以直接调用接口会很方便。
于是我自己也试着做了一个网站,前端用vite+vue3,后端用node.js+express,十分常规的组合。
页面不会设计,主要是模仿的ChatGTP,足够简洁和优美。
后端
后端简单的很,用了一个不是很主流的人工智能平台,科大讯飞。主要是因为有很多免费额度。
看了官方接口文档,接口主要有两种调用方法,一种是WebSocket,一种是HTTP。WebSocket看起来复杂一点,所以用了HTTP。这是请求帧的数据格式。
curl -i -k -X POST 'https://spark-api-open.xf-yun.com/v1/chat/completions' \
--header 'Authorization: Bearer 123456' \
--header 'Content-Type: application/json' \
--data '{
"model":"generalv3.5",
"messages": [
{
"role": "user",
"content": "来一个只有程序员能听懂的笑话"
}
],
"stream": true
}'
重要的是,接口返回的数据是一个词一个词蹦出来的,页面也是一段一段的显示。起初我以为是故意这样的设计,实际上这和大语言模型的工作原理有关。网络方面,可以用HTTP协议自带的EventSource接口来完成这种流式传输效果。
一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件,此连接会一直保持开启直到通过调用 EventSource.close() 关闭。
这和WebSocket有点类似,但这是单向的数据流通。后端只做一个中转处理的功能,提供接口和调用接口都用EventSource接口的方式。
接受到数据要先设置响应头
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
请求参数按照接口文档来就好。下面是请求配置,也要带上请求头。
const requestConfig = {
headers: {
Authorization: `Bearer ${API_PASSWORD}`,
Accept: "text/event-stream",
"Content-Type": "application/json",
},
responseType: "stream",
timeout: TIMEOUT,
};
然后就可以发送请求了
const response = await axios.post(URL, params, requestConfig);
response.data.on("data", (chunk) => {
let data = chunk.toString("utf8")
if(data.includes('[DONE]')) return
data = data.startsWith('data: ') ? data.substring(6) : data;
data = JSON.parse(data);
if(data.code !== 0) {
console.error("消息异常", data.message);
res.write(`data:${data.message}\n\n`);
res.end();
};
const content = data.choices?.[0]?.delta?.content || "";
console.log("内容=>", content);
res.write(`data:${JSON.stringify({ content })}\n\n`);
});
response.data.on("end", () => {
console.error("发送结束");
res.write("data:[DONE]\n\n");
res.end();
});
response.data.on("error", (error) => {
console.error("接受异常", error.message);
res.write(`data:{"error":"${error.message}"}\n\n`);
res.end();
});
EventSource采用订阅的方式返回数据,返回的一节数据的原始数据好像是二进制,先转为字符串。一次返回数据的格式是data: value\n,解析中间value的值返回给前端就好了。
前端
然后主要看一下前端的调用的用法。
const eventSource = new EventSource(`/api/stream?message=${content.value}`)
content.value = ''
eventSource.onmessage = (event) => {
let response = event.data
console.log(response)
if (response == '[DONE]') {
close()
return
}
const parsed = JSON.parse(response)
if(parsed.error){
const errorString = `!!! error 连接异常,请稍后再试\n错误原因:${parsed.error}`
const content = messageList.value[messageList.value.length - 1].content
messageList.value[messageList.value.length - 1].content = [...content, '\n', ...errorString]
close()
return
}
messageList.value[messageList.value.length - 1].content.push(parsed.content)
window.localStorage.setItem('messageList', JSON.stringify(messageList.value))
scrollToBottom()
}
eventSource.onerror = (e) => {
close()
ElMessage({
type: 'error',
message: '连接失败',
})
const errorString = `!!! error 连接异常,请稍后再试\n错误原因:${e.message}`
messageList.value[messageList.value.length - 1].content = [...errorString]
scrollToBottom()
}
const close = () => {
eventSource.close()
messageList.value[messageList.value.length - 1].loading = false
isLoading.value = false
}
返回的数据用一个数组存放,展示的地方拼接成字符串即可。此外由于人工智能回复的消息很多是有格式的,所以最好配置一个富文本编辑器,随便找一个库就能用。还能直接显示错误的alert,不用写额外的样式了。还可以用localhost保存消息记录,这样数据刷新后也不会丢失了。
前后端的开发本身没有什么难度,重点在于看懂文档,正确调用接口和第三方库。这得多调试才能明白。
服务器
然后我想部署到服务器,这样就能直接访问了。我的服务器用的是宝塔面板,可以添加各种项目。
第一次,我把构建好的前端文件放到html项目中启动,然后我用node项目启动后端项目,并使用相同的端口,这样就能避免跨域问题,管理起来也方便。
可是这样做失败了,当我启动node项目时,错误显示端口已占用。原来HTML项目用的是nginx代理,后来我也也没明白为什么他们不能共同同一个端口。
所以我换了一个做法,把前端的资源用express来管理。先把dist文件夹放到后端文件夹下,然后在express加两句代码。express本身就支持解析静态资源。
app.use(express.static(path.join(__dirname, 'dist')));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
这样服务器就应该能访问到前端页面了,而且前端也可以直接调用服务器接口。
可是我启动后死活访问不到页面,日志也没也任何异常。
后来仔细检查后发现了两个原因:
一是express应该监听0.0.0.0上的端口,而不是localhost或172.0.0.1上的端口。这样服务器才能被外部访问。
app.listen(1024, '0.0.0.0', () => {
console.log('Server running at http://0.0.0.0:1024');
});
二是应该放行安全组和防火墙。
只在阿里云控制台的安全组中放行端口还不够,还要在宝塔面板上放行防火墙的端口。
以上就是本次开发的全部内容了,总的来说还算完整。网站后续功能有待改进。