部署文心4.5開源模型給Android設備調用

前言

在上一篇文章《文心4.5開源大模型的使用和部署》已經介紹瞭如何使用fastdeploy部署文心4.5開源大模型的,並且簡單調用了接口,本篇文章來介紹Android如何調用這個部署的接口,並實現對話。

部署

  1. 首先還是需要下載要部署的文心模型,之前爲了演示我使用了一個比較小的模型,現在部署上我使用更大的模型ERNIE-4.5-21B-A3B-Paddle
aistudio download --model PaddlePaddle/ERNIE-4.5-21B-A3B-Paddle --local_dir ./models/ERNIE-4.5-21B-A3B-Paddle/
  1. 啓動fastdeploy服務,要留意這個8180端口號,下面會使用到,注意爲了節省顯存,這次量化我使用了是wint4方法。
python -m fastdeploy.entrypoints.openai.api_server \
 --model ./models/ERNIE-4.5-21B-A3B-Paddle/ \
 --port 8180 \
 --quantization wint4 \
 --max-model-len 32768 \
 --max-num-seqs 32
  1. 寫個Python腳本作爲中轉和記錄對話歷史數據,首先寫一個LLM類作爲整體的工具調用。
class LLM:
 def __init__(self, host, port):
 self.client = openai.Client(base_url=f"http://{host}:{port}/v1", api_key="null")
 self.system_prompt = {"role": "system", "content": "You are a helpful assistant."}
 self.histories: Dict[str, list] = {}
 self.base_prompt = {"role": "user", "content": "請扮演一個AI助手角色,你的名字文心4.5。"}
 self.base_prompt_res = {"role": "assistant", "content": "好的,我已經記住了。您有什麼問題想要問我嗎?"}

 # 流式回覆
 def generate_stream(self, prompt, max_length=8192, top_p=0.8, temperature=0.95, session_id=None):
 # 如果session_id存在之前的歷史,則獲取history
 if session_id and session_id in self.histories.keys():
 history = self.histories[session_id]
 else:
 # 否則創建新的session_id
 session_id = str(uuid.uuid4()).replace('-', '')
 history = [self.system_prompt, self.base_prompt, self.base_prompt_res]
 history.append({"role": "user", "content": prompt})
 print(f"歷史紀錄:{history}")
 print("=" * 70)
 print(f"【用戶提問】:{prompt}")
 all_output = ""
 response = self.client.chat.completions.create(model="null",
 messages=history,
 max_tokens=max_length,
 temperature=temperature,
 top_p=top_p,
 stream=True)
 for chunk in response:
 if chunk.choices[0].delta:
 output = chunk.choices[0].delta.content
 if output == "": continue
 ret = {"response": output, "code": 0, "session_id": session_id}
 all_output += output
 # 更新history
 history[-1] = {"role": "assistant", "content": all_output}
 self.histories[session_id] = history
 # 返回json格式的字節
 yield json.dumps(ret).encode() + b"\0"
  1. 啓動自己的服務接口,注意這幾個參數,hostport參數值本身服務暴露給Android調用的,fastdeploy_hostfastdeploy_port是fastdeploy部署的接口,也就是第二步設置的端口號及其部署所在的服務器IP。執行這個腳本就可以啓動服務了,接下來就等Android調用了。
app = FastAPI()

@app.post("/llm")
async def api_llm(request: Request):
 params = await request.json()

 generator = model.generate_stream(**params)
 background_tasks = BackgroundTasks()
 return StreamingResponse(generator, background=background_tasks)


if __name__ == "__main__":
 parser = argparse.ArgumentParser()
 parser.add_argument("--host", type=str, default="0.0.0.0")
 parser.add_argument("--port", type=int, default=8000)
 parser.add_argument("--fastdeploy_host", type=str, default="127.0.0.1")
 parser.add_argument("--fastdeploy_port", type=int, default=8180)
 args = parser.parse_args()
 model = LLM(host=args.fastdeploy_host, port=args.fastdeploy_port)
 # 啓動服務
 uvicorn.run(app, host=args.host, port=args.port)

Android調用

在Android中,核心代碼如下,其中CHAT_HOST的值爲http://192.168.1.100:8000,其中IP是開發者部署上面服務的服務器IP,端口是port指定的端口號。下面的代碼。主要通過即時從服務器接收返回的數據並解析,同時顯示在頁面上,實現了打字的效果。

// 發送文本結果到大語言模型接口
private void sendChat(String text) {
 if (text.isEmpty()) {
 return;
 }
 runOnUiThread(() -> sendBtn.setEnabled(false));
 // 請求的參數
 Map<String, String> map = new HashMap<>();
 map.put("prompt", text);
 if (session_id != null) {
 map.put("session_id", session_id);
 }
 JSONObject jsonObject = new JSONObject(map);
 try {
 jsonObject.put("top_p", 0.8);
 jsonObject.put("temperature", 0.95);
 } catch (JSONException e) {
 throw new RuntimeException(e);
 }
 RequestBody requestBodyJson = RequestBody.create(jsonObject.toString(),
 MediaType.parse("application/json; charset=utf-8"));
 Request request = new Request.Builder()
 .url(CHAT_HOST + "/llm")
 .post(requestBodyJson)
 .build();
 OkHttpClient client = new OkHttpClient.Builder()
 .connectTimeout(30, TimeUnit.SECONDS)//設置連接超時時間
 .readTimeout(30, TimeUnit.SECONDS)//設置讀取超時時間
 .build();
 try {
 Response response = client.newCall(request).execute();
 ResponseBody responseBody = response.body();
 // 接收流式結果
 InputStream inputStream = responseBody.byteStream();
 byte[] buffer = new byte[2048];
 int len;
 StringBuilder all_response = new StringBuilder();
 StringBuilder sb = new StringBuilder();
 while ((len = inputStream.read(buffer)) != -1) {
 try {
 // 處理讀取到的數據
 String data = new String(buffer, 0, len - 1, StandardCharsets.UTF_8);
 sb.append(data);
 byte lastBuffer = buffer[len - 2];
 buffer = new byte[2048];
 if (lastBuffer != 0x7d) {
 continue;
 }
 data = sb.toString();
 sb = new StringBuilder();
 Log.d(TAG, data);
 JSONObject resultJson = new JSONObject(data);
 int code = resultJson.getInt("code");
 String resp = resultJson.getString("response");
 all_response.append(resp);
 session_id = resultJson.getString("session_id");
 runOnUiThread(() -> {
 Msg lastMsg = mMsgList.get(mMsgList.size() - 1);
 if (lastMsg.getType() == Msg.TYPE_RECEIVED) {
 mMsgList.get(mMsgList.size() - 1).setContent(all_response.toString());
 // 有新消息時,刷新RecyclerView中的顯示
 mAdapter.notifyItemChanged(mMsgList.size() - 1);
 } else {
 mMsgList.add(new Msg(resp, Msg.TYPE_RECEIVED));
 // 有新消息時,刷新RecyclerView中的顯示
 mAdapter.notifyItemInserted(mMsgList.size() - 1);
 }
 // 將RecyclerView定位到最後一行
 mRecyclerView.scrollToPosition(mMsgList.size() - 1);
 });
 } catch (JSONException e) {
 e.printStackTrace();
 }
 }
 inputStream.close();
 response.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 runOnUiThread(() -> sendBtn.setEnabled(true));
}

效果圖如下:

e6f32c41fc1a4b6ba04ea5603afb0bc1.gif

獲取源碼

在公衆號中回覆【部署文心4.5開源模型給Android設備調用】即可獲取源碼。

小夜