跳转至

07 大模型API封装:自建大模型如何对外服务?

你好,我是独行。

上一节课我们详细讲解了基于ChatGLM3-6B + LangChain + 向量数据库的企业内部知识系统,在这个演示项目中,其实已经用到了API的封装,我们从WebUI界面提问,通过接口将数据传到后端服务,从而获得响应。大模型是没有Web API的,所以需要我们进行一次封装,将大模型的核心接口封装成Web API来为用户提供服务,这是企业自建大模型的必经之路。

在这里我们需要引入一个类似于SpringBoot的框架,用来做接口服务化,在Python技术体系里,有一个框架叫 FastAPI,可以很方便地实现接口注册,所以我们这节课会基于FastAPI对大模型的接口进行封装。实际上光写一个Demo不算难,但是如果要完整地用于工程化项目,还是有不少事情要注意,所以这节课我会把各种各样和API相关的细节梳理出来,学完这节课的内容,再结合前面学习的大模型部署,你本地搭建的大模型基本可以对外提供服务了。

接口封装

提供Web API服务需要两个技术组件:Uvicorn和FastAPI。

Uvicorn作为Web服务器,类似Tomcat,但是比Tomcat轻很多。允许异步处理 HTTP 请求,所以非常适合处理并发请求。基于uvloop和httptools,所以具备非常高的性能,适合高并发请求的现代Web应用。

FastAPI作为API框架,和SpringBoot差不多,同样比SpringBoot轻很多,只是形式上类似于SpringBoot的角色。结合使用Uvicorn和FastAPI,你可以构建一个高性能、易于扩展的异步Web应用程序或API。Uvicorn作为服务器运行你的FastAPI应用,可以提供优异的并发处理能力,而FastAPI则让你的应用开发得更快、更简单、更安全。

接下来我们一步一步讲解。首先,安装所需要的依赖包。

安装依赖

pip install fastapi
pip install uvicorn

代码分层

简单来看,创建api.py,写入以下代码,就可以定义一个接口。

import uvicorn
from fastapi import FastAPI

# 创建API应用
app = FastAPI()

@app.get("/")
async def root():
  return {"message": "Hello World"}

if __name__ == '__main__':
  uvicorn.run(app, host='0.0.0.0', port=6006, log_level="info", workers=1)
python api.py

图片

实际开发过程中,接口输入可能是多个字段,和Java接口一样,需要定义一个Request实体类来承接HTTP请求参数,Python里使用Pydantic模型来定义数据结构,Pydantic是一个数据验证和设置管理的库,它利用Python类型提示来进行数据验证。类似Java里的Validation,下面这段代码你应该并不陌生。

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Product {

    @NotNull
    @Size(min = 2, max = 30)
    private String name;

    @NotNull
    @Min(0)
    private Float price;

    // 构造器、getter 和 setter 省略
}

对应的Python实现就是这样的:

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional, List

app = FastAPI()

class Message(BaseModel):
    role: str
    content: str

class ChatMessage(BaseModel):
    history: List[Message]
    prompt: str
    max_tokens: int
    temperature: float
    top_p: float = Field(default=1.0)

@app.post("/v1/chat/completions")
async def create_chat_response(message: ChatMessage):
    return {"message": "Hello World"}

if __name__ == '__main__':
  uvicorn.run(app, host='0.0.0.0', port=6006, log_level="info", workers=1)

这里引入了一个BaseModel类,类似于Java里的Object类,但是又不完全是Object,Object是所有Java类的基类,Java中所有类会默认集成Object类的公共方法,比如toString()、equals()、hashcode()等,而BaseModel 是为了数据验证和管理而设计的。当你创建一个继承自BaseModel的类时,比如上面的ChatSession和Message类,将自动获得数据验证、序列化和反序列化的功能。

另外,我们实际开发过程中,也不可能把所有API的定义和Pydantic类放在最外层,按照Java工程化的最佳实践,Web应用我们一般会进行分层,比如controller、service、model、tool等,Python工程化的时候,为了方便管理代码,也会进行分层,一个典型的代码结构如下:

project_name/

├── app/                         # 主应用目录
   ├── main.py                  # FastAPI 应用入口
   └── controller/              # API 特定逻辑
       └── chat.py
   └── common/                  # 通用API组件
       └── errors.py            # 错误处理和自定义异常

├── services/                    # 服务层目录
   ├── chat_service.py          # 聊天服务相关逻辑

├── schemas/                     # Pydantic 模型(请求和响应模式)
   ├── chat_schema.py           # 聊天数据模式

├── database/                    # 数据库连接和会话管理
   ├── session.py               # 数据库会话配置
   └── engine.py                # 数据库引擎配置

├── tools/                       # 工具和实用程序目录
   ├── data_migration.py        # 数据迁移工具

├── tests/                       # 测试目录
   ├── conftest.py              # 测试配置和夹具
   ├── test_services/           # 服务层测试
      ├── test_chat_service.py
   └── test_controller/                
       ├── test_chat_controller.py

├── requirements.txt             # 项目依赖文件
└── setup.py                     # 安装、打包、分发配置文件

FastAPI的include_router方法就是用来将不同的路由集成到主应用中的,有助于组织和分离代码,特别是在构建大型工程化应用时,非常好用。你可以看一下修改后的代码。

应用入口main.py

import uvicorn as uvicorn
from fastapi import FastAPI
from controller.chat_controller import chat_router as chat_router
app = FastAPI()
app.include_router(chat_router, prefix="/chat", tags=["chat"])
if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=6006, log_level="info", workers=1)

chat_controller.py

from fastapi import APIRouter
from service.chat_service import ChatService
from schema.chat_schema import ChatMessage, MessageDisplay
chat_router = APIRouter()
chat_service = ChatService()

@chat_router.post("/new/message/")
def post_message(message: ChatMessage):
    return chat_service.post_message(message)

@chat_router.get("/get/messages/")
def get_messages():
    return chat_service.get_messages()

chat_service.py

from schema.chat_schema import ChatMessage

class ChatService:
    def post_message(self, message: ChatMessage) :
        print(message.prompt)
        return {"message": "post message"}
    def get_messages(self):
        return {"message": "get message"}

参数类定义如下:

from pydantic import BaseModel, Field

class Message(BaseModel):
    role: str
    content: str

class ChatMessage(BaseModel):
    prompt: str
    max_tokens: int
    temperature: float = Field(default=1.0)
    top_p: float = Field(default=1.0)

我们可以在chat_service里进行详细地业务逻辑处理,到这里基本就和Java里一样了。下面是一段简单的测试代码:

import json
import requests

url = 'http://localhost:6006/chat/new/message/'
data = {
    'prompt': 'hello',
    'max_tokens': 1000
}

response = requests.post(url, data=json.dumps(data))
print(response.text)

url2 = 'http://localhost:6006/chat/get/messages/'
response = requests.get(url2)
print(response.text)
{"message":"post message"}
{"message":"get message"}

关于FastAPI的使用,你可以参考这个教程。工程化代码结构搞定,我们就可以封装大模型的接口了。

大模型接口封装

不同的大模型对应的对话接口不一样,下面的示例代码基于ChatGLM3-6B。我们在service层进行模型对话的封装。你可以看一下示例代码。

from datetime import datetime
import model_manager
from schema.chat_schema import ChatMessage

class ChatService:
    def post_message(self, message: ChatMessage):
        print(message.prompt)
        model = model_manager.ModelManager.get_model()
        tokenizer = model_manager.ModelManager.get_tokenizer()
        response, history = model.chat(
            tokenizer,
            message.prompt,
            history=message.histroy,
            max_length=message.max_tokens,
            top_p=message.top_p,
            temperature=message.temperature
        )
        now = datetime.datetime.now()  # 获取当前时间
        time = now.strftime("%Y-%m-%d %H:%M:%S")  # 格式化时间为字符串
        answer = {
            "response": response,
            "history": history,
            "status": 200,
            "time": time
        }
        log = "[" + time + "] " + '", prompt:"' + message.prompt + '", response:"' + repr(response) + '"'
        print(log)
        return answer
    def get_messages(self):
        return {"message": "get message"}

定义一个ModelManager类进行大模型的懒加载。

from transformers import AutoTokenizer, AutoModelForCausalLM

class ModelManager:
    _model = None
    _tokenizer = None

    @classmethod
    def get_model(cls):
        if cls._model is None:
            _model = AutoModelForCausalLM.from_pretrained("chatglm3-6b", trust_remote_code=True).half().cuda().eval()
        return _model

        @classmethod
    def get_tokenizer(cls):
        if cls._tokenizer is None:
            _tokenizer = AutoTokenizer.from_pretrained("chatglm3-6b", trust_remote_code=True)
        return _tokenizer

model.chat()是6B暴露的对话接口,通过对model.chat()的封装就可以实现基本的对话接口了,这个接口一次性输出大模型返回的内容,而我们在使用大模型产品的时候,比如ChatGPT或者文心一言,会发现大模型是一个字一个字返回的,那是什么原因呢?那种模式叫流式输出

流式输出

流式输出使用另一个接口:model.stream_chat,有几种模式,像一个字一个字输出,比如:

或者每次输出当前已经输出的全部,比如:

我是
我是中
我是中国
我是中国人

当然也有每次吐出2个字的,实际生产过程中可以根据产品交互设计自行修改逻辑。我们看一个简单的代码片段,通过stream变量来控制是否是流式输出。

if stream:
    async for token in callback.aiter():
        # Use server-sent-events to stream the response
        yield json.dumps(
            {"text": token, "message_id": message_id},
            ensure_ascii=False)
else:
    answer = ""
    async for token in callback.aiter():
        answer += token
    yield json.dumps(
        {"text": answer, "message_id": message_id},
        ensure_ascii=False)
await task

我们输入“你好”,当stream=true时,接口输出是这样的:

data: {"text": "你", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "好", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "👋", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "!", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "我是", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "人工智能", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "助手", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": " Chat", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "GL", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "M", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "3", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "-", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "6", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "B", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": ",", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "很高兴", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "见到", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "你", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": ",", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "欢迎", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "问我", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "任何", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "问题", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}
data: {"text": "。", "message_id": "80b2af55c5b7440eaca6b9d510677a75"}

当stream=false时,接口返回如下:

data: {"text": "你好!我是人工智能助手,很高兴为您服务。请问有什么问题我可以帮您解答吗?", "message_id": "741a630ac3d64fd5b1832cc0bae6bb68"}

到这里,大模型的API基本就封装好了,接下来我们看下如何调用。

接口调用

在实际工程化过程中,我们一般会把AI相关的逻辑,包括大模型API的封装放在Python应用中,上层应用一般通过其他语言实现,比如Java、C#、Go等,这里我简单举一个Java版本的调用例子。非流式输出就是普通的HTTP请求,我们就不展示了,重点看下流式输出怎么进行调用,主要分两步,都是流式的。

  1. Java调用Python接口:主要用到了okhttp3框架,需要组装参数、发起流式请求,事件监听处理三步。
@ApiOperation(value = "流式发送对话消息")
@PostMapping(value = "sendMessage")
public void sendMessage(@RequestBody ChatRequest request, HttpServletResponse response) {
    try {
        JSONObject body = new JSONObject();
        body.put("model", request.getModel());
        body.put("stream", true);
        JSONArray messages = new JSONArray();
        JSONObject query = new JSONObject();
        query.put("role", "user");
        query.put("content", request.getQuery());
        messages.add(query);
        body.put("messages", messages);
        EsListener eventSourceListener = new EsListener(request, response);



        RequestBody formBody = RequestBody.create(body, MediaType.parse("application/json"));
        Request.Builder requestBuilder = new Request.Builder();

        Request request2 = requestBuilder.url(URL).post(formBody).build();
        EventSource.Factory factory = EventSources.createFactory(OkHttpUtil.getInstance());

        factory.newEventSource(request2, eventSourceListener);
        eventSourceListener.getCountDownLatch().await();
    } catch (Exception e) {
        log.error("流式调用异常", e);
    }
}

EsListener继承自EventSourceListener,在Request请求的过程中不断触发EsListener的onEvent方法,然后将数据写回前端。

@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
    try {
        output.append(data);
        if ("finish".equals(type)) {
        }
        if ("error".equals(type)) {
        }

        // 开始处理data,此处只展示基本操作
        // 开发过程中具体逻辑可自行扩展
        if (response != null) {
            response.getWriter().write(data);
            response.getWriter().flush();
        }
    } catch (Exception e) {
        log.error("事件处理异常", e);
    }
}
  1. 前端调用Java接口:使用JS原生EventSource的API就可以。
<script>
    let eventData = '';
    const eventSource = new EventSource('http://localhost:8888/sendMessage');
    eventSource.onmessage = function(event) {
        // 累加接收到的事件数据
        eventData += event.data;
    };
</script>

到这一步,大模型API从封装到调用就基本完成了,你可以把整个链路都串起来跑一跑,体验下效果。实际工程化的过程中,还会遇到其他问题,比如API的鉴权(指Java->Python)、跨域问题、API限流问题(大模型的吞吐量有限),我们会在后面的课程中讲解。

小结

我们这节课学的内容是自建大模型服务不可缺少的一步,整体来说不算难,唯一可能难一点的就是要使用Python语言,因为在使用FastAPI的过程中,会有大量的异步操作,和Java的处理方式有点差异,需要注意下。

这节课学完,我们基本上把企业内部构建大模型的过程全部讲完了,你自己构建的大模型基本可以对外提供服务了。如果在生产环境使用,一定要注意做好降级准备,因为有很多不确定性,比如模型的吞吐量(TPS)评估是否准确,模型会不会出现意想不到的输出等等,一旦出现问题随时降级。

思考题

前面我们提到,大模型相关的API封装在Python应用中,对用户提供服务的时候,会再套一层Java应用,你可以想一下为什么要这么设计,欢迎你把你的想法分享到评论区,我们一起讨论,如果你觉得这节课的内容对你有帮助的话,也欢迎你分享给其他朋友,我们下节课再见!

精选留言(15)
  • 张申傲 👍(13) 💬(3)

    第7讲打卡~ 思考题:这里应该主要考虑的是不同语言的优势和适用场景。使用Python实现大模型的核心API,应该是因为Python是机器学习领域最主流的语言,包括了像pytorch、TensorFlow等主流的框架,而且一些主流的大模型像ChatGPT也提供了完善的Python SDK,使用起来比较方便。而在外面又套了一层Java应用,应该是考虑这么多年来Java在Web服务端领域积累下来的完善的生态,针对用户端的应用,可以快速构建起服务鉴权、路由、熔断、降级、限流、可观测等等能力。

    2024-06-17

  • Lee 👍(3) 💬(1)

    老师 我等不及了 上瘾了 催更催更

    2024-06-17

  • 狒狒 👍(1) 💬(1)

    python基础不是太好,有可用的完整代码吗,文中的代码似乎不能直接使用,比如结构版本与实际案例不一致,在controller中引入serveice时无法直接引入

    2024-12-02

  • Geek_4d9162 👍(0) 💬(1)

    老师 紧急求助,按课程我用开源大模型封装成问答服务后,如何让大模型回答“你是谁”这类自我认知问题时,按我的要求回答?比如回答 我是xx人开发的大模型

    2025-01-10

  • 神经蛙 👍(0) 💬(1)

    大模型的输出降级方案有哪些考虑方案?比如如果被引导输出不当言论,如何快速封掉这个漏洞?机器负载达到上限后又该如何处理?

    2024-11-05

  • cricket1981 👍(0) 💬(1)

    请问国内如何使用huggingface? 很多国外大模型网站被墙了,有什么替代方案或work around方法吗?

    2024-08-15

  • 大宽 👍(0) 💬(1)

    老师 ,示例示例中的大模型推理框架用的是哪个呢,可否引入 VLLM

    2024-07-13

  • Geek_frank 👍(0) 💬(1)

    打卡第六课:python封装大模型借口提供AI服务有天然的优势,基本上大模型都是用python开发的,开源的也都有python的lib。使用python进行API封装或者模型微调都特别方便。 在AI服务之上的一些政增值服务可能java来提供更好一些,因为java适合业务的封装。但也不是非java不可,python也可以实现不做的业务服务。我现在做的一个运维一体化平台很多业务实现都是python来做的。比如认证鉴权,配置仓库,在线作业管理等等

    2024-07-10

  • season 👍(0) 💬(3)

    课程里面的代码,有代码仓库吗?

    2024-06-22

  • 风轻扬 👍(0) 💬(1)

    如果大家在跑demo的时候遇到这个错误。TypeError: ChatGLMForConditionalGeneration.chat() missing 1 required positional argument: 'query',可以尝试将chat_service.py文件中,model.chat方法的第二个入参去掉"prompt=",只保留message.prompt即可

    2024-06-19

  • 0.0 👍(0) 💬(2)

    dify 是否更优雅

    2024-06-19

  • 风轻扬 👍(0) 💬(1)

    老师,ModelManager类中,两个from_pretrained方法的第一个入参都是模型的路径吧?我按照文中的代码实现了一下,在一张3090 gpu上跑,python版本3.11,一直报:TypeError: ChatGLMForConditionalGeneration.chat() missing 1 required positional argument: 'query',查了查ChatGLMForConditionalGeneration,并没有chat方法,不知道咋排查了。。。。

    2024-06-19

  • 风轻扬 👍(0) 💬(1)

    思考题,是为了解耦?工程类的代码修改,不太应该让模型跟着一起发布。

    2024-06-18

  • Eleven 👍(0) 💬(2)

    我微调完成后再去composite_demo启动,对话好像没有效果呀

    2024-06-17

  • 徐石头 👍(0) 💬(1)

    为什么这么设计? 高内聚,低耦合。把大模型 API 封装在 python 中作为一个微服务从整个架构中独立出来,它只处理和大模型相关的,不和其他业务接口放一起。它就可以单独部署,扩容,隔离,而且由单独的人维护,也是微服务的优势。 如果我来做,我还会增加 rpc接口,注册到注册中心。

    2024-06-17