你好,我是独行。
上一节课我们详细讲解了基于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则让你的应用开发得更快、更简单、更安全。
接下来我们一步一步讲解。首先,安装所需要的依赖包。
安装依赖
1 2
| pip install fastapi pip install uvicorn
|
代码分层
简单来看,创建api.py,写入以下代码,就可以定义一个接口。
1 2 3 4 5 6 7 8 9 10 11 12
| 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)
|

实际开发过程中,接口输入可能是多个字段,和Java接口一样,需要定义一个Request实体类来承接HTTP请求参数,Python里使用Pydantic模型来定义数据结构,Pydantic是一个数据验证和设置管理的库,它利用Python类型提示来进行数据验证。类似Java里的Validation,下面这段代码你应该并不陌生。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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;
}
|
对应的Python实现就是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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工程化的时候,为了方便管理代码,也会进行分层,一个典型的代码结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| project_name/ │ ├── app/ │ ├── main.py │ └── controller/ │ └── chat.py │ └── common/ │ └── errors.py │ ├── services/ │ ├── chat_service.py │ ├── schemas/ │ ├── 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
1 2 3 4 5 6 7 8
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
1 2 3 4 5 6 7 8 9
| 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"}
|
参数类定义如下:
1 2 3 4 5 6 7 8 9 10 11 12
| 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里一样了。下面是一段简单的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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)
|
1 2
| {"message":"post message"} {"message":"get message"}
|
关于FastAPI的使用,你可以参考这个教程。工程化代码结构搞定,我们就可以封装大模型的接口了。
大模型接口封装
不同的大模型对应的对话接口不一样,下面的示例代码基于ChatGLM3-6B。我们在service层进行模型对话的封装。你可以看一下示例代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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类进行大模型的懒加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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变量来控制是否是流式输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if stream: async for token in callback.aiter(): 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时,接口输出是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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时,接口返回如下:
1
| data: {"text": "你好!我是人工智能助手,很高兴为您服务。请问有什么问题我可以帮您解答吗?", "message_id": "741a630ac3d64fd5b1832cc0bae6bb68"}
|
到这里,大模型的API基本就封装好了,接下来我们看下如何调用。
接口调用
在实际工程化过程中,我们一般会把AI相关的逻辑,包括大模型API的封装放在Python应用中,上层应用一般通过其他语言实现,比如Java、C#、Go等,这里我简单举一个Java版本的调用例子。非流式输出就是普通的HTTP请求,我们就不展示了,重点看下流式输出怎么进行调用,主要分两步,都是流式的。
- **Java调用Python接口:**主要用到了okhttp3框架,需要组装参数、发起流式请求,事件监听处理三步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @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方法,然后将数据写回前端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public void onEvent(EventSource eventSource, String id, String type, String data) { try { output.append(data); if ("finish".equals(type)) { } if ("error".equals(type)) { }
if (response != null) { response.getWriter().write(data); response.getWriter().flush(); } } catch (Exception e) { log.error("事件处理异常", e); } }
|
- **前端调用Java接口:**使用JS原生EventSource的API就可以。
1 2 3 4 5 6 7 8
| <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应用,你可以想一下为什么要这么设计,欢迎你把你的想法分享到评论区,我们一起讨论,如果你觉得这节课的内容对你有帮助的话,也欢迎你分享给其他朋友,我们下节课再见!