上一篇我講了SSE(Server-Sent Events)的實(shí)現(xiàn)方式于购,這一篇講一下WebSocket的實(shí)現(xiàn)方式袍睡,首先來(lái)復(fù)習(xí)下使用的三種工具:
- Ollama:一個(gè)免費(fèi)的開(kāi)源框架,可以讓大模型很容易的運(yùn)行在本地電腦上
- FastAPI:是一個(gè)用于構(gòu)建 API 的現(xiàn)代肋僧、快速(高性能)的 web 框架斑胜,使用
Python 并基于標(biāo)準(zhǔn)的 Python 類(lèi)型提示 - React:通過(guò)組件來(lái)構(gòu)建用戶界面的庫(kù)
簡(jiǎn)單來(lái)說(shuō)就類(lèi)似于LLM(數(shù)據(jù)庫(kù))+FastAPI(服務(wù)端)+React(前端)
1、下載Ollama之后使用Ollama完成大模型的本地下載和的運(yùn)行
ollama run llama3:8b
2嫌吠、模型運(yùn)行之后就可以調(diào)用了
curl http://localhost:11434/api/generate -d '{
"model": "llama3:8b",
"prompt": "Why is the sky blue?",
"stream": false
}'
3止潘、服務(wù)端用的基于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", # 輸入自己前端項(xiàng)目的地址
]
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" # 本地調(diào)用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 字節(jié)的塊大小逐塊讀取響應(yīng)內(nèi)容,并用換行符分隔出每行內(nèi)容
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); // 用來(lái)獲取/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="提問(wèn)">
<Input />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
<TextArea value={value} {...sharedProps} />
</div>
);
};
??下面我們來(lái)分析一下SSE實(shí)現(xiàn)和WebSocket實(shí)現(xiàn)的優(yōu)劣以及為什么ChatGPT選擇SSE
Server-Sent Events (SSE)
優(yōu)點(diǎn)
- 簡(jiǎn)單實(shí)現(xiàn):SSE 實(shí)現(xiàn)相對(duì)簡(jiǎn)單,使用標(biāo)準(zhǔn)的 HTTP 協(xié)議泥栖,便于客戶端和服務(wù)器的實(shí)現(xiàn)和調(diào)試簇宽。
- 自動(dòng)重連:SSE 內(nèi)置自動(dòng)重連機(jī)制,當(dāng)連接斷開(kāi)時(shí)吧享,客戶端會(huì)自動(dòng)嘗試重新連接魏割。
- 瀏覽器原生支持:大多數(shù)現(xiàn)代瀏覽器原生支持 SSE,無(wú)需額外的庫(kù)或插件钢颂。
- 適合單向數(shù)據(jù)流:SSE 非常適合單向的數(shù)據(jù)流應(yīng)用場(chǎng)景钞它,例如實(shí)時(shí)通知、事件推送殊鞭、日志更新等遭垛。
缺點(diǎn)
- 單向通信:SSE 只能從服務(wù)器向客戶端推送數(shù)據(jù),客戶端不能主動(dòng)向服務(wù)器發(fā)送消息操灿。
- 僅支持文本數(shù)據(jù):SSE 僅支持文本數(shù)據(jù)的傳輸锯仪,不支持二進(jìn)制數(shù)據(jù)。
- 連接限制:部分瀏覽器對(duì)同一源的 SSE 連接數(shù)量有限制趾盐,這可能影響大規(guī)模應(yīng)用庶喜。
WebSocket
優(yōu)點(diǎn)
- 雙向通信:WebSocket 支持雙向通信小腊,客戶端和服務(wù)器都可以互相發(fā)送消息。
- 低延遲:WebSocket 保持一個(gè)持久連接久窟,可以提供低延遲的實(shí)時(shí)通信秩冈,適合高實(shí)時(shí)性要求的應(yīng)用。
- 支持二進(jìn)制數(shù)據(jù):WebSocket 支持發(fā)送二進(jìn)制數(shù)據(jù)和文本數(shù)據(jù)斥扛,滿足更多樣化的數(shù)據(jù)傳輸需求入问。
缺點(diǎn)
- 實(shí)現(xiàn)復(fù)雜:WebSocket 相對(duì)實(shí)現(xiàn)較為復(fù)雜,需要處理連接的建立稀颁、維護(hù)和關(guān)閉等問(wèn)題芬失。
- 瀏覽器兼容性:雖然大多數(shù)現(xiàn)代瀏覽器支持 WebSocket,但在某些環(huán)境下可能需要額外的庫(kù)或插件支持峻村。
- 防火墻和代理問(wèn)題:一些防火墻和代理服務(wù)器可能會(huì)阻止 WebSocket 連接,需要額外配置锡凝。
ChatGPT 選擇 SSE 的原因
- 單向數(shù)據(jù)流需求:ChatGPT 流式輸出的主要需求是服務(wù)器向客戶端推送數(shù)據(jù)(模型生成的文本)粘昨,這與 SSE 的單向數(shù)據(jù)流特性非常匹配。
- 簡(jiǎn)化實(shí)現(xiàn):SSE 使用 HTTP/2窜锯,可以更簡(jiǎn)化地集成到現(xiàn)有的 Web 服務(wù)器和應(yīng)用中张肾,而不需要處理 WebSocket 的復(fù)雜連接管理。
- 自動(dòng)重連機(jī)制:SSE 的自動(dòng)重連機(jī)制有助于提高連接的穩(wěn)定性和可靠性锚扎。
- 瀏覽器原生支持:SSE 受到現(xiàn)代瀏覽器的廣泛支持吞瞪,減少了客戶端的兼容性問(wèn)題。
總結(jié)
- SSE 適用于單向推送數(shù)據(jù)的場(chǎng)景驾孔,實(shí)現(xiàn)簡(jiǎn)單芍秆,具有自動(dòng)重連機(jī)制,適合實(shí)時(shí)通知和事件推送翠勉。
- WebSocket 適用于雙向?qū)崟r(shí)通信的復(fù)雜應(yīng)用妖啥,支持更高的實(shí)時(shí)性和多樣化的數(shù)據(jù)傳輸需求,但實(shí)現(xiàn)較為復(fù)雜对碌。
所以大家可以按照自己的業(yè)務(wù)需求來(lái)選擇適合自己的那一種方式??荆虱。