3.FastAPI:让大模型飞上云端

在上篇文章中,小编详细阐述了如何让大模型在我们本地,或者是服务器上跑起来,让我们可以实现正常的、在终端的对话需求。

但是,对于业务而言,光有这一点是远远不够,我们还想让大模型飞上“云端”,以让我们的各种服务可以远程调用,比如说Langchain服务,那就还需要借助另外一个Web框架,那就是FastAPI。

所以,在这篇文章中,小编将会对现在市面上的三大主流Web框架做分析,并且选择用FastAPI做咱们以后的开发框架,并且还会规划咱们以后的项目架构,以为服务扩展做准备。

Python的三大主流Web框架

所谓Web框架,就是为了让我们提升效率,节省时间,避免处理那些低级细节的,如果能达到这个目标,就是一个合适的框架。

特别是在Python领域内的Web框架,现在主要就是有3个,分别是: Django,Flask 和 FastAPI。它们都非常优秀,但有各自的特点。

Django

Django是一个高级Python Web框架,它鼓励快速开发和干净、实用的设计。它提供了一个全包式的解决方案,包括一个对象关系映射器(ORM)、站点地图、用户认证和许多其他功能,使得开发复杂的数据库驱动的网站变得简单。

它的核心特性简单直接:

  1. 2003年被创建,历史悠久,网上的解决方案也十分的多。
  2. 生态丰富,社区活跃,甚至说,不管你在Django中遇到什么困难,都有前人帮你踩过了!
  3. 全包的解决方案,不用在一直选择用什么框架做什么事情了,Django说:我直接给你全包了,你也别找了!
  4. 自带ORM,允许开发者使用Python代码来定义数据库模型,它会自动转换成相应的数据库操作。
  5. Django内置了许多安全功能,如用户认证、防跨站请求伪造等。

但是实际上,小编认为它“太重”了。就小编个人的使用的感觉来看,我不喜欢太重的框架,会让小编感觉浑身被束缚住…

但是小编也是认为,它是一个极其优秀的框架,要不然也不能一直火到现在,连工作面试中,也会面试很多这个框架相关的内容,我觉得就算不用,但是也非常值得一学!

Flask

Flask是一个微框架,它的设计哲学是“微核心”加上灵活的扩展,使得它非常适合于小到中型项目以及在一个微服务架构中作为单一服务的开发。它提供了必要的工具和技术来构建一个Web应用,但同时保持了极度的简洁和灵活性。

它的核心特性我非常喜欢:

  1. 轻量级: Flask本身非常简单,但它允许通过扩展来添加额外的功能,如数据库操作、表单验证、上传管理等。
  2. 扩展性: 社区提供了大量的扩展来支持各种常见的web开发需求。
  3. 简易上手: Flask的学习曲线相对平缓,新手可以快速上手进行web开发。

轻,这个框架很轻。严格来说,Flask就包含了两个组件,分别是Jinja模板引擎,和Werkzeug WSGI工具集。换种说法说,Flask就包含了一个轻量级的Web服务器,用于URL映射。

但是…

如果我用了Flask,我为什么不用FastAPI呢?毕竟FastAPI更快…但是小编仍然承认,Flask拥有着庞大且活跃的社区支持,这也让Flask的第三方库越发庞大,这也是Flask屹立不倒的原因。

FastAPI

FastAPI是一个现代、快速(高性能)的web框架,用于构建APIs与异步web应用。它基于标准Python类型提示特性,旨在创建快速、简单而又健壮的APIs。FastAPI结合了Python 3.6+类型提示的优势和异步编程的能力,提供了非常高的性能,同时保持代码简洁且易于理解。

小编认为,FastAPI参考了Flask的“微核心”的概念,然后更加的现代化。

  1. 异步支持: FastAPI允许你以异步的方式编写代码,这可以显著提高性能,尤其是在I/O密集型应用中。
  2. 类型检查: 利用Python的类型提示来验证数据,这不仅帮助开发者减少bug,还可以自动生成文档和前端代码。
  3. 快速:可与 NodeJS 和 Go 并肩的极高性能(归功于 Starlette 和 Pydantic)。
  4. 简单:设计的易于使用和学习,阅读文档的时间更短。

总之,小编对FastAPI赞不绝口,真的是一个非常健壮和现代化的Web框架,文档非常简单,基本有基础,看一遍就会了。就算是没基础,python有pip这个包管理器,随便写两下,也能弄个差不多!

快速开始

如果想要入手一个框架,并且让我们能够快速的开始让FastAPI提供大模型服务,那就当然是把代码逻辑都写在一个文件里面啊!

首先,我们先要做的就是使用conda安装依赖!(如果你不知道conda该如何使用,请查看此专栏的第一篇文章!)

在这里,我为了照顾所有读者的感受,我就用阿里最新发布的Qwen2.5大模型,因为它很多尺寸的都有,适用于所有的人在自己本地跑。

# 这是执行创建虚拟环境的,如果你已经有了,请忽略
conda create -n 虚拟环境名称 python=3.11
# 切换虚拟环境
conda activate 虚拟环境名称
# 下载pytorch套件
conda install pytorch=2.2.1 torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
# 下载cudnn
conda install cudnn
# 下载qwen2.5适配的transformers
conda install conda-forge::transformers==4.44.2
# 下载其它辅助包
pip install optimum
pip install auto-gptq==0.7.1

一定要按照我上面的包来安装,因为auto-gptq包和pytorch包要对应,要不然就会出现不适配的情况,导致在推推理时无法使用gpu,推理极其缓慢。

小编自己用的大模型是Qwen2.5-14B-int4的模型,占用内存12G,以下是流式输出代码。

from threading import Thread
from transformers import AutoTokenizer, TextIteratorStreamer, AutoModelForCausalLM

model_path = "你的模型地址"

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 加载正常模型
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, device_map="auto").eval()

history = []
message = ""

# 循环提问
while True:
    question = input("请输入问题(输入exit退出):")
    if question == "exit":
        break
    current_length = 0
    print("")
    # 添加用户输入
    history.append({"role": "user", "content": question})
    # 构造模型输入
    model_inputs = tokenizer.apply_chat_template(history,
                                                 add_generation_prompt=True,
                                                 tokenize=True,
                                                 return_tensors="pt").to("cuda")
    # 添加文本stream流
    streamer = TextIteratorStreamer(
        tokenizer=tokenizer,
        timeout=60,
        skip_prompt=True,
        skip_special_tokens=True
    )
    # 设置参数
    generate_kwargs = {
        "input_ids": model_inputs,
        "streamer": streamer,
        "max_length": 204800
    }
    # 启动线程
    t = Thread(target=model.generate, kwargs=generate_kwargs)
    t.start()
    # 构造迭代器
    for new_token in streamer:
        if new_token:
            print(new_token, end="", flush=True)
            message = message + new_token
    history.append({"role": "assistant", "content": message})
    message = ""
    print("\n")

这样,我们就可以把Qwen2.5模型在自己本地跑通了!

接下里,就让我们踏入FastAPI的世界吧,构造一个简单的大模型Web,向外提供服务吧!

文件规划

在进行开发之前,我们首先要做的就是文件夹的规划,这样我们以后在扩展和写代码的时候,就不会显得杂乱无章了。

llm # 项目名称
--app # 存放项目路由和实体类的地方
--frame # 存放项目架构的地方,以后我们的大模型会在这里加载
--resources # 存放配置文件的文件夹
--main.py # 项目的主入口
--config.py # 项目的配置类
--README.md # 项目的readme文档
--conda_env_windows.yaml # 项目的conda导出的yaml环境文件(windows),以便于后续迁移
--conda_env_linux.yaml # 项目的conda导出的yaml环境文件(linux),以便于后续迁移
--run.sh # 项目启动的sh文件,用于在linux环境下快速部署
--stop.sh # 项目停止的sh文件,用于在linux环境下快速停止
--requirements.txt # 如果不使用conda环境安装,需要的包文件
--.gitignore # 项目的git上传文件

实际上,这是一个生产环境下的文件规划,但是我们就算是开发,也要把这些东西提前规划好,以后我们扩展的时候,也会变得简单的多。

启程:main

首先,我们需要先安装下FastAPI,过程非常简单,在你激活的conda环境之下,直接执行以下命令就可以:

pip install fastapi
pip install "uvicorn[standard]"

其中,fastapi是FastAPI的包本体,启动包括负责Web部分的Starlette和负责数据部分的Pydantic。

而unicorn,则是一个ASGI服务器(你可以类比成tomcat),包裹着FastAPI程序运行。

那么我们现在可以根据我们的文件规划,设计一个简易的大模型文件,下面是一个示例。

接着,我们来编写main.py中的方法,让它能够提供一个简单的FastAPI服务。

Hello World

以下是完整代码:

# 导入FastAPI包
from fastapi import FastAPI

# 初始化FastAPI
app = FastAPI()


# 配置路由
@app.get("/hello")
def hello_world():
    return "Hello World"


# 程序主入口
if __name__ == "__main__":
    # 导入unicorn服务器的包
    import uvicorn
    # 运行服务器
    # 监听地址:0.0.0.0,端口:6732,单进程,日志输出级别是error
    uvicorn.run(app, host="0.0.0.0", port=6732, workers=1, log_level="error")

在启动之后,通过Apifox来调用端口,我们可以很轻松的访问到我们的FastAPI应用。

这样,我们的FastAPI应用就算入门了。

上面代码十分的短,可以十分清晰的看到如何启动一个FastAPI应用,但是实际上,我们不仅仅需要它只输出“hello world”,我们需要的是它作为服务器,流式输出token,以满足我们的对话需求,而不是等全部输出完,才给我们返回。

那样叫对话吗!那样不叫对话!

流式输出

以下是完整代码:

# 导入FastAPI包
from threading import Thread
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Literal
from starlette.responses import StreamingResponse
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer

# 初始化FastAPI
app = FastAPI()

# 设置模型路径
model_path = "你的模型的地址"
# 加载模型
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, device_map="cuda").eval()
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)


class ChatMessage(BaseModel):
    """
    此类是一个聊天消息类,主要用于描述一个聊天消息,包括消息的角色、内容、名称。

    role:此参数仅仅只接收三种值(字符串),分别是"user"、"assistant"、"system",用于限制传入角色类型,以免错误地传入其他角色信息。
    content:此参数接收一个字符串类型,用于描述对应角色的内容
    """
    role: Literal["user", "assistant", "system"]
    content: str = ""


class ChatRequest(BaseModel):
    """
    接收前端传来的消息请求

    question:需要询问的消息
    history:历史消息,如果没有历史消息,则返回空列表,表示是新一轮的会话
    """
    question: str = ""
    history: List[ChatMessage] = []


@app.post("/chat/stream")
def chat_stream(request: ChatRequest):
    """
    通用流式传输接口
    """
    # 组装历史消息
    history = []
    for message in request.history:
        history.append({"role": message.role, "content": message.content})
    history.append({"role": "user", "content": request.question})
    # 构造模型输入
    model_inputs = tokenizer.apply_chat_template(history,
                                                 add_generation_prompt=True,
                                                 tokenize=True,
                                                 return_tensors="pt").to("cuda")
    # 添加文本stream流
    streamer = TextIteratorStreamer(
        tokenizer=tokenizer,
        timeout=60,
        skip_prompt=True,
        skip_special_tokens=True
    )
    # 设置参数
    generate_kwargs = {
        "input_ids": model_inputs,
        "streamer": streamer,
        "max_length": 204800
    }
    # 设置参数
    generate_kwargs = {
        "input_ids": model_inputs,
        "streamer": streamer,
        "max_length": 204800
    }
    # 启动线程
    t = Thread(target=model.generate, kwargs=generate_kwargs)
    t.start()
    
    # 设置迭代器
    def gen():
        for new_token in streamer:
            if new_token:
                yield new_token+"\n"
        yield "[Done]"
    # 返回流式输出
    return StreamingResponse(gen(), media_type="text/event-stream")


# 程序主入口
if __name__ == "__main__":
    # 导入unicorn服务器的包
    import uvicorn
    # 运行服务器
    # 监听地址:0.0.0.0,端口:6732,单进程,日志输出级别是error
    uvicorn.run(app, host="0.0.0.0", port=6732, workers=1, log_level="error")

现在main函数中,我们首先初始化FastAPI应用,紧接着,我们开始加载模型喝分词器,用于保证在整个应用中,只有一个模型和分词器存在,避免每次请求都要重复加载。

# 初始化FastAPI
app = FastAPI()

# 设置模型路径
model_path = "你的模型的地址"
# 加载模型
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, device_map="cuda").eval()
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

紧接着,我们使用pydantic库,来创造两个基本model,用来接收前端传来的请求,这里主要是接收问题和前端传来的历史消息。

实际上,在很多时候,我们的大模型并不直接存储历史消息,而是交给它的调用端,这样,就可以保证大模型服务整体稳定,而不会因为存储了很多对话,而占用太多内存。

class ChatMessage(BaseModel):
    """
    此类是一个聊天消息类,主要用于描述一个聊天消息,包括消息的角色、内容、名称。

    role:此参数仅仅只接收三种值(字符串),分别是"user"、"assistant"、"system",用于限制传入角色类型,以免错误地传入其他角色信息。
    content:此参数接收一个字符串类型,用于描述对应角色的内容
    """
    role: Literal["user", "assistant", "system"]
    content: str = ""


class ChatRequest(BaseModel):
    """
    接收前端传来的消息请求

    question:需要询问的消息
    history:历史消息,如果没有历史消息,则返回空列表,表示是新一轮的会话
    """
    question: str = ""
    history: List[ChatMessage] = []

写一个post接口,用来接收调用端传来的信息,并且设置一个迭代器,来流式返回数据。

实际上,大家其实可以看到,这个和cli中的流式输出基本一模一样,除了用到了yield关键字之外,基本没有改变。

yield关键字主要就是做一个迭代器的作用,用于将大模型吐出的新token流式返回给调用端。

@app.post("/chat/stream")
def chat_stream(request: ChatRequest):
    """
    通用流式传输接口
    """
    # 组装历史消息
    history = []
    for message in request.history:
        history.append({"role": message.role, "content": message.content})
    history.append({"role": "user", "content": request.question})
    # 构造模型输入
    model_inputs = tokenizer.apply_chat_template(history,
                                                 add_generation_prompt=True,
                                                 tokenize=True,
                                                 return_tensors="pt").to("cuda")
    # 添加文本stream流
    streamer = TextIteratorStreamer(
        tokenizer=tokenizer,
        timeout=60,
        skip_prompt=True,
        skip_special_tokens=True
    )
    # 设置参数
    generate_kwargs = {
        "input_ids": model_inputs,
        "streamer": streamer,
        "max_length": 204800
    }
    # 设置参数
    generate_kwargs = {
        "input_ids": model_inputs,
        "streamer": streamer,
        "max_length": 204800
    }
    # 启动线程
    t = Thread(target=model.generate, kwargs=generate_kwargs)
    t.start()
    
    # 设置迭代器
    def gen():
        for new_token in streamer:
            if new_token:
                yield new_token+"\n"
        yield "[Done]"
    # 返回流式输出
    return StreamingResponse(gen(), media_type="text/event-stream")

程序主入口,用来启动服务。

# 程序主入口
if __name__ == "__main__":
    # 导入unicorn服务器的包
    import uvicorn
    # 运行服务器
    # 监听地址:0.0.0.0,端口:6732,单进程,日志输出级别是error
    uvicorn.run(app, host="0.0.0.0", port=6732, workers=1, log_level="error")

到此,我们在一个main.py文件中,完成了让大模型飞上云端,以及流式调用!

测试

import requests
import json
import time

# api请求地址
url = "http://127.0.0.1:6732/chat/stream"

# 设置头部信息
headers = {
   'Content-Type': 'application/json',
}

# 设置历史记录和整段对话
history = []
message = ""

# 循环提问
while True:
    # 用户输入
    user_input = input("Human (or 'exit' to quit): ")
    if user_input.lower() == "exit":
        break
    # 设置请求信息
    payload = json.dumps({
        "question": user_input,
        "history": history
    })
    message = ""
    start_time = time.time()
    # 发送stream请求
    response = requests.request("POST", url, headers=headers, data=payload, stream=True)
    # 逐行读取响应
    for line in response.iter_lines():
        if line:
            line_data = line.decode('utf-8')
            if line_data == '[Done]':
                break
            print(line_data, end='', flush=True)
            message += line_data
    end_time = time.time()
    elapsed_time = end_time - start_time
    # 添加历史记录
    history.append({"role":"user", "content": user_input})
    history.append({"role":"assistant", "content": message})
    print(f"\nTotal time taken: {elapsed_time:.2f} seconds")

上面是一个测试函数,主要的作用就是连接到大模型服务,试一下流式输出是否正常!

总结

  1. 在这一节课中,我们分析了Python中的Web三大框架,并且选择了FastAPI作为我们的主框架使用。
  2. 使用FastAPI简单的搭建了一个"Hello World",初步熟悉一下FastAPI的接口开发。
  3. 在main.py函数中,我们成功的搭建了一个大模型的流式服务,并且提供了测试代码。
  4. 初步熟悉了unicorn,FastAPI和pydantic库,为以后更方便的开发打下基础。

实际上,我们在真正开发的时候,不会将所有的东西,都写在main.py文件之中,而是分散在其它文件夹中,要不然就会造成代码混乱,难以维护。

在下一节课中,我将着重介绍大模型服务中的其它基础文件的搭建,包括日志记录器,通用返回类,还有各种设计模式的使用。

如果大家看的爽,请关注一下我的专栏!!!!小编在这里谢谢啦!你的支持就是我更新的最大动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值