Ollama+FastAPI+React手把手构建自己的本地大模型,支持WebSocket

上一篇我讲了SSE(Server-Sent Events)的实现方式,这一篇讲一下WebSocket的实现方式,首先来复习下使用的三种工具

  • Ollama:一个免费的开源框架,可以让大模型很容易的运行在本地电脑上
  • FastAPI:是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用
    Python 并基于标准的 Python 类型提示
  • React:通过组件来构建用户界面的库

简单来说就类似于LLM(数据库)+FastAPI(服务端)+React(前端)

image.png

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)

优点

  1. 简单实现:SSE 实现相对简单,使用标准的 HTTP 协议,便于客户端和服务器的实现和调试。
  2. 自动重连:SSE 内置自动重连机制,当连接断开时,客户端会自动尝试重新连接。
  3. 浏览器原生支持:大多数现代浏览器原生支持 SSE,无需额外的库或插件。
  4. 适合单向数据流:SSE 非常适合单向的数据流应用场景,例如实时通知、事件推送、日志更新等。

缺点

  1. 单向通信:SSE 只能从服务器向客户端推送数据,客户端不能主动向服务器发送消息。
  2. 仅支持文本数据:SSE 仅支持文本数据的传输,不支持二进制数据。
  3. 连接限制:部分浏览器对同一源的 SSE 连接数量有限制,这可能影响大规模应用。

WebSocket

优点

  1. 双向通信:WebSocket 支持双向通信,客户端和服务器都可以互相发送消息。
  2. 低延迟:WebSocket 保持一个持久连接,可以提供低延迟的实时通信,适合高实时性要求的应用。
  3. 支持二进制数据:WebSocket 支持发送二进制数据和文本数据,满足更多样化的数据传输需求。

缺点

  1. 实现复杂:WebSocket 相对实现较为复杂,需要处理连接的建立、维护和关闭等问题。
  2. 浏览器兼容性:虽然大多数现代浏览器支持 WebSocket,但在某些环境下可能需要额外的库或插件支持。
  3. 防火墙和代理问题:一些防火墙和代理服务器可能会阻止 WebSocket 连接,需要额外配置。

ChatGPT 选择 SSE 的原因

  1. 单向数据流需求:ChatGPT 流式输出的主要需求是服务器向客户端推送数据(模型生成的文本),这与 SSE 的单向数据流特性非常匹配。
  2. 简化实现:SSE 使用 HTTP/2,可以更简化地集成到现有的 Web 服务器和应用中,而不需要处理 WebSocket 的复杂连接管理。
  3. 自动重连机制:SSE 的自动重连机制有助于提高连接的稳定性和可靠性。
  4. 浏览器原生支持:SSE 受到现代浏览器的广泛支持,减少了客户端的兼容性问题。

总结

  • SSE 适用于单向推送数据的场景,实现简单,具有自动重连机制,适合实时通知和事件推送。
  • WebSocket 适用于双向实时通信的复杂应用,支持更高的实时性和多样化的数据传输需求,但实现较为复杂。

所以大家可以按照自己的业务需求来选择适合自己的那一种方式😊。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容