上一篇我讲了SSE(Server-Sent Events)的实现方式,这一篇讲一下WebSocket的实现方式,首先来复习下使用的三种工具:
- Ollama:一个免费的开源框架,可以让大模型很容易的运行在本地电脑上
- FastAPI:是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用
Python 并基于标准的 Python 类型提示 - React:通过组件来构建用户界面的库
简单来说就类似于LLM(数据库)+FastAPI(服务端)+React(前端)
1、下载Ollama之后使用Ollama完成大模型的本地下载和的运行
ollama run llama3:8b
2、模型运行之后就可以调用了
curl http://localhost:11434/api/generate -d '{
"model": "llama3:8b",
"prompt": "Why is the sky blue?",
"stream": false
}'
3、服务端用的基于Python的FastAPI
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
import json
import requests
import asyncio
app = FastAPI(debug=True)
origins = [
"http://localhost", # 输入自己前端项目的地址
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
llm_list = [{'label': 'qwen:latest', "value": 'qwen:latest'},
{'label': 'llama3:8b', "value": 'llama3:8b'}, ]
@app.get("/llm/list")
def read_llm(model: str = 'qwen:latest'):
return {"data": llm_list}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await asyncio.create_task(stream_response(websocket, data))
except WebSocketDisconnect:
await websocket.close()
async def stream_response(websocket: WebSocket, query: str):
url = "http://localhost:11434/api/generate" # 本地调用ollama的地址
data = json.loads(query)
print('data', data, type(data))
payload = {"model": data['model_name'],
"prompt": data['prompt'],
"stream": True
}
print(payload)
response = requests.post(url, json=payload)
# 以512 字节的块大小逐块读取响应内容,并用换行符分隔出每行内容
buffer = ""
for chunk in response.iter_content(chunk_size=512):
if chunk:
buffer += chunk.decode('utf-8', errors='ignore')
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
try:
data = json.loads(line)
await websocket.send_text(json.dumps(data['response']))
except json.JSONDecodeError:
continue
if buffer:
try:
data = json.loads(buffer)
await websocket.send_text(json.dumps(data['response']))
except json.JSONDecodeError:
await websocket.send_text(buffer)
if __name__ == '__main__':
uvicorn.run(app="app", host="127.0.0.1", port=8000, reload=True)
4、前端使用React
import { Input, Dropdown, Select, Form, Button, Spin, Space } from 'antd';
import { useEffect, useState } from 'react';
import { getList } from './service';
import { useRequest } from '@umijs/max';
import { useWebSocket } from 'ahooks';
const { TextArea } = Input;
export default () => {
const { readyState, sendMessage, latestMessage, disconnect, connect } = useWebSocket(
'ws://localhost:8000/ws',
{
onOpen: () => {
console.log('connected');
},
onClose: () => {
console.log('disconnected');
},
onMessage: (message) => {
const { data } = message || {};
console.log(data);
if (data) {
const result = JSON.parse(data);
setValue((pre) => [...pre, result].join(''));
}
},
},
);
const [form] = Form.useForm();
const { data = [] } = useRequest(getList); // 用来获取/llm/list接口,返回可使用的大模型列表
const [value, setValue] = useState('');
const sharedProps = {
style: { width: '100%' },
autoSize: { minRows: 3, maxRows: 20 },
};
const onFinish = (values) => {
sendMessage(JSON.stringify(values));
};
return (
<div>
<Form onFinish={onFinish} form={form}>
<Form.Item name="model_name" label="模型">
<Select style={{ width: 200 }} options={[...data]} />
</Form.Item>
<Form.Item name="prompt" label="提问">
<Input />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
<TextArea value={value} {...sharedProps} />
</div>
);
};
👀下面我们来分析一下SSE实现和WebSocket实现的优劣以及为什么ChatGPT选择SSE
Server-Sent Events (SSE)
优点
- 简单实现:SSE 实现相对简单,使用标准的 HTTP 协议,便于客户端和服务器的实现和调试。
- 自动重连:SSE 内置自动重连机制,当连接断开时,客户端会自动尝试重新连接。
- 浏览器原生支持:大多数现代浏览器原生支持 SSE,无需额外的库或插件。
- 适合单向数据流:SSE 非常适合单向的数据流应用场景,例如实时通知、事件推送、日志更新等。
缺点
- 单向通信:SSE 只能从服务器向客户端推送数据,客户端不能主动向服务器发送消息。
- 仅支持文本数据:SSE 仅支持文本数据的传输,不支持二进制数据。
- 连接限制:部分浏览器对同一源的 SSE 连接数量有限制,这可能影响大规模应用。
WebSocket
优点
- 双向通信:WebSocket 支持双向通信,客户端和服务器都可以互相发送消息。
- 低延迟:WebSocket 保持一个持久连接,可以提供低延迟的实时通信,适合高实时性要求的应用。
- 支持二进制数据:WebSocket 支持发送二进制数据和文本数据,满足更多样化的数据传输需求。
缺点
- 实现复杂:WebSocket 相对实现较为复杂,需要处理连接的建立、维护和关闭等问题。
- 浏览器兼容性:虽然大多数现代浏览器支持 WebSocket,但在某些环境下可能需要额外的库或插件支持。
- 防火墙和代理问题:一些防火墙和代理服务器可能会阻止 WebSocket 连接,需要额外配置。
ChatGPT 选择 SSE 的原因
- 单向数据流需求:ChatGPT 流式输出的主要需求是服务器向客户端推送数据(模型生成的文本),这与 SSE 的单向数据流特性非常匹配。
- 简化实现:SSE 使用 HTTP/2,可以更简化地集成到现有的 Web 服务器和应用中,而不需要处理 WebSocket 的复杂连接管理。
- 自动重连机制:SSE 的自动重连机制有助于提高连接的稳定性和可靠性。
- 浏览器原生支持:SSE 受到现代浏览器的广泛支持,减少了客户端的兼容性问题。
总结
- SSE 适用于单向推送数据的场景,实现简单,具有自动重连机制,适合实时通知和事件推送。
- WebSocket 适用于双向实时通信的复杂应用,支持更高的实时性和多样化的数据传输需求,但实现较为复杂。
所以大家可以按照自己的业务需求来选择适合自己的那一种方式😊。