作者:小傅哥
博客:https://bugstack.cn
大家好,我是技術UP主小傅哥。
鑒于智譜AI發(fā)布了最新一代 GLM3.0
、GLM4.0
基座大模型,我又要對自己開發(fā)的這款開源 chatglm-sdk-java 進行改造了!因為需要做新老接口的模型調用中數(shù)據(jù)格式兼容,這將是一場編碼設計
與復雜場景
的對抗挑戰(zhàn)。 請看小傅哥如何操刀改造!
為什么改造SDK會比較復雜?
通常來說,我們開發(fā)好一款SDK后,就會投入到項目中使用,而使用的方式會根據(jù)SDK的出入?yún)藴蔬M行對接。比如一個接口的入?yún)⒃居?個參數(shù),A String
、B String
類型,但現(xiàn)在因為有額外的功能添加,從2個參數(shù)調整為3個參數(shù),同時需要對原本的 B 參數(shù) String 類型,擴展為 Object 類型,添加更多的屬性信息。同時出參也有對應響應結構變化。
那么對于這種線上正在使用又需要改造代碼的情況,我們不可能把原有的代碼都鏟了不要,所以需要做一些優(yōu)雅的兼容的開發(fā)處理。讓工程更加好維護、好迭代。
在設計原則和設計模式的錘煉下,寫出高質量的代碼。
那么,接下來小傅哥就帶著大家講講這段關于GLM新增模型后 SDK 的重構操作。
文末有整個 SDK 的源碼,直接免費獲取,拿過去就是嘎嘎學習!
一、需求場景
智譜AI文檔:https://open.bigmodel.cn/overview
本次文檔中新增加了 GLM-3-Turbo、GLM-4、GLM-4v、cogview,這樣四個新模型,與原來的舊版 chatGLM_6b_SSE、chatglm_lite、chatglm_lite_32k、chatglm_std、chatglm_pro,在接口調用上做了不小的修改。因為新版的模型增加了如插件「聯(lián)網(wǎng)、知識庫、函數(shù)庫」、畫圖、圖片識別這樣的能力,所以出入?yún)⒁灿邢鄳淖兓?/p>
1. curl 舊版
curl?-X?POST?
????????-H?"Authorization:?Bearer?BearerTokenUtils.getToken(獲取)"?
????????-H?"Content-Type:?application/json"?
????????-H?"Accept:?text/event-stream"?
????????-d?'{
??????????????"top_p":?0.7,
??????????????"sseFormat":?"data",
??????????????"temperature":?0.9,
??????????????"incremental":?true,
??????????????"request_id":?"xfg-1696992276607",
??????????????"prompt":?[
??????????????????{
??????????????????"role":?"user",
??????????????????"content":?"寫個java冒泡排序"
??????????????????}
??????????????]
????????????}'?
??http://open.bigmodel.cn/api/paas/v3/model-api/chatglm_lite/sse-invoke
-
- 注意:舊版的調用方式是把模型放到接口請求中,如;
chatglm_lite
- 就是放到請求地址中。
2. curl 3&4
curl?-X?POST?
????????-H?"Authorization:?Bearer?BearerTokenUtils.getToken(獲取)"?
????????-H?"Content-Type:?application/json"?
????????-d?'{
??????????"model":"glm-3-turbo",
??????????"stream":?"true",
??????????"messages":?[
??????????????{
??????????????????"role":?"user",
??????????????????"content":?"寫個java冒泡排序"
??????????????}
??????????]
????????}'?
??https://open.bigmodel.cn/api/paas/v4/chat/completions
-
- 注意:新版的模型為入?yún)⒎绞秸{用,接口是統(tǒng)一的接口。此外入?yún)⒏袷降牧魇娇梢酝ㄟ^參數(shù)
"stream": "true"
- 控制。
3. curl 4v
curl?-X?POST?
????????-H?"Authorization:?Bearer?BearerTokenUtils.getToken(獲取)"?
????????-H?"Content-Type:?application/json"?
????????-d?'{
????????????????"messages":?[
????????????????????{
????????????????????????"role":?"user",
????????????????????????"content":?[
??????????????????????????{
????????????????????????????"type":?"text",
????????????????????????????"text":?"這是什么圖片"
??????????????????????????},
??????????????????????????{
????????????????????????????"type":?"image_url",
????????????????????????????"image_url":?{
??????????????????????????????"url"?:?"支持base64和圖片地址;https://bugstack.cn/images/article/project/chatgpt/chatgpt-extra-231011-01.png"
????????????????????????????}
??????????????????????????}
????????????????????????]
????????????????????}
????????????????],
????????????????"model":?"glm-4v",
????????????????"stream":?"true"
????????????}'?
??https://open.bigmodel.cn/api/paas/v4/chat/completions
- 注意:多模態(tài)4v模型,content 字符串升級為對象。這部分與 chatgpt 的參數(shù)結構是一致的。所以我們在開發(fā)這部分功能的時候,也需要做兼容處理。因為本身它既可以支持對象也可以支持 conten 為字符串。
4. curl cogview
curl?-X?POST?
????????-H?"Authorization:?Bearer?BearerTokenUtils.getToken(獲取)"?
????????-H?"Content-Type:?application/json"?
????????-d?'{
??????????"model":"cogview-3",
??????????"prompt":"畫一個小狗狗"
????????}'?
??https://open.bigmodel.cn/api/paas/v4/images/generations
- 注意:圖片的文生圖是一個新的功能,舊版沒有這個接口。所以開發(fā)的時候按照獨立的接口開發(fā)即可。
綜上,這些接口開發(fā)還需要注意;
- 首先,小傅哥根據(jù)官網(wǎng)文檔編寫了對應的 curl 請求格式。方便開發(fā)時可以參考和驗證。之后,curl 舊版是文本類處理,curl 3&4 是新版的文檔處理。兩個可以對比看出,舊版的入?yún)⑹?prompt,新版是 messages另外,本次API文檔新增加了文生圖,和4v(vision)多模態(tài)的圖片識別。
接下來,我們就要來設計怎么在舊版的SDK中兼容這些功能實現(xiàn)。
二、功能實現(xiàn)
1. 調用流程
如圖,是整個本次 SDK 的實現(xiàn)的核心流程,其中執(zhí)行器部分是本次重點開發(fā)的內容。在舊版的 SDK 中是直接從會話請求
進入模型的調用,沒有執(zhí)行器的添加。而執(zhí)行器的引入則是為了解耦調用過程,依照于不同的請求模型(chatglm_std、chatglm_pro、GLM_4...),可以調用到不同的執(zhí)行器上去。
2. 執(zhí)行器「解耦」
2.1 接口
public?interface?Executor?{
????/**
?????*?問答模式,流式反饋
?????*
?????*?@param?chatCompletionRequest?請求信息
?????*?@param?eventSourceListener???實現(xiàn)監(jiān)聽;通過監(jiān)聽的?onEvent?方法接收數(shù)據(jù)
?????*?@return?應答結果
?????*?@throws?Exception?異常
?????*/
????EventSource?completions(ChatCompletionRequest?chatCompletionRequest,?EventSourceListener?eventSourceListener)?throws?Exception;
????
????//?...?省略部分接口
}????
2.2 舊版
public?class?GLMOldExecutor?implements?Executor?{
????/**
?????*?OpenAi?接口
?????*/
????private?final?Configuration?configuration;
????/**
?????*?工廠事件
?????*/
????private?final?EventSource.Factory?factory;
????public?GLMOldExecutor(Configuration?configuration)?{
????????this.configuration?=?configuration;
????????this.factory?=?configuration.createRequestFactory();
????}
????@Override
????public?EventSource?completions(ChatCompletionRequest?chatCompletionRequest,?EventSourceListener?eventSourceListener)?throws?Exception?{
????????//?構建請求信息
????????Request?request?=?new?Request.Builder()
????????????????.url(configuration.getApiHost().concat(IOpenAiApi.v3_completions).replace("{model}",?chatCompletionRequest.getModel().getCode()))
????????????????.post(RequestBody.create(MediaType.parse("application/json"),?chatCompletionRequest.toString()))
????????????????.build();
????????//?返回事件結果
????????return?factory.newEventSource(request,?eventSourceListener);
????}
????
????//?...?省略部分接口
????
}????
2.3 新版
public?class?GLMExecutor?implements?Executor,?ResultHandler?{
????/**
?????*?OpenAi?接口
?????*/
????private?final?Configuration?configuration;
????/**
?????*?工廠事件
?????*/
????private?final?EventSource.Factory?factory;
????/**
?????*?統(tǒng)一接口
?????*/
????private?IOpenAiApi?openAiApi;
????private?OkHttpClient?okHttpClient;
????public?GLMExecutor(Configuration?configuration)?{
????????this.configuration?=?configuration;
????????this.factory?=?configuration.createRequestFactory();
????????this.openAiApi?=?configuration.getOpenAiApi();
????????this.okHttpClient?=?configuration.getOkHttpClient();
????}
????@Override
????public?EventSource?completions(ChatCompletionRequest?chatCompletionRequest,?EventSourceListener?eventSourceListener)?throws?Exception?{
????????//?構建請求信息
????????Request?request?=?new?Request.Builder()
????????????????.url(configuration.getApiHost().concat(IOpenAiApi.v4))
????????????????.post(RequestBody.create(MediaType.parse(Configuration.JSON_CONTENT_TYPE),?chatCompletionRequest.toString()))
????????????????.build();
????????//?返回事件結果
????????return?factory.newEventSource(request,?chatCompletionRequest.getIsCompatible()???eventSourceListener(eventSourceListener)?:?eventSourceListener);
????}
????
????//?...?省略部分接口
????
}????
在新版的執(zhí)行實現(xiàn)中,除了 IOpenAiApi.v4 接口的變動,還有一塊是對外結果的封裝處理。這是因為在舊版的接口對接中使用的是 EventType「add、finish、error、interrupted」枚舉來判斷。但在新版中則只是判斷 stop 標記符。所以為了讓之前的SDK使用用戶更少的改動代碼,這里做了結果的封裝。
3. 注入配置
源碼:cn.bugstack.chatglm.session.Configuration
public?HashMap<Model,?Executor>?newExecutorGroup()?{
????this.executorGroup?=?new?HashMap<>();
????//?舊版模型,兼容
????Executor?glmOldExecutor?=?new?GLMOldExecutor(this);
????this.executorGroup.put(Model.CHATGLM_6B_SSE,?glmOldExecutor);
????this.executorGroup.put(Model.CHATGLM_LITE,?glmOldExecutor);
????this.executorGroup.put(Model.CHATGLM_LITE_32K,?glmOldExecutor);
????this.executorGroup.put(Model.CHATGLM_STD,?glmOldExecutor);
????this.executorGroup.put(Model.CHATGLM_PRO,?glmOldExecutor);
????this.executorGroup.put(Model.CHATGLM_TURBO,?glmOldExecutor);
????//?新版模型,配置
????Executor?glmExecutor?=?new?GLMExecutor(this);
????this.executorGroup.put(Model.GLM_3_5_TURBO,?glmExecutor);
????this.executorGroup.put(Model.GLM_4,?glmExecutor);
????this.executorGroup.put(Model.GLM_4V,?glmExecutor);
????this.executorGroup.put(Model.COGVIEW_3,?glmExecutor);
????return?this.executorGroup;
}
- 對于不同的模型,走哪個執(zhí)行器,在 Configuration 配置文件中寫了這樣的配置信息。這樣當你調用 CHATGLM_TURBO 就會走到 glmOldExecutor 模型,調用 GLM_4V 就會走到 glmExecutor 模型。
4. 參數(shù)兼容
ChatCompletionRequest 作為一個重要的應答參數(shù)對象,在本次的接口變化中也是調整了不少字段。但好在小傅哥之前就提供了一個 toString 對象的方法。在這里我們可以做不同類型參數(shù)的處理。
public?String?toString()?{
????try?{
????????//?24年1月發(fā)布新模型后調整
????????if?(Model.GLM_3_5_TURBO.equals(this.model)?||?Model.GLM_4.equals(this.model)?||?Model.GLM_4V.equals(this.model))?{
????????????Map<String,?Object>?paramsMap?=?new?HashMap<>();
????????????paramsMap.put("model",?this.model.getCode());
????????????if?(null?==?this.messages?&&?null?==?this.prompt)?{
????????????????throw?new?RuntimeException("One?of?messages?or?prompt?must?not?be?empty!");
????????????}
????????????paramsMap.put("messages",?this.messages?!=?null???this.messages?:?this.prompt);
????????????if?(null?!=?this.requestId)?{
????????????????paramsMap.put("request_id",?this.requestId);
????????????}
????????????if?(null?!=?this.doSample)?{
????????????????paramsMap.put("do_sample",?this.doSample);
????????????}
????????????paramsMap.put("stream",?this.stream);
????????????paramsMap.put("temperature",?this.temperature);
????????????paramsMap.put("top_p",?this.topP);
????????????paramsMap.put("max_tokens",?this.maxTokens);
????????????if?(null?!=?this.stop?&&?this.stop.size()?>?0)?{
????????????????paramsMap.put("stop",?this.stop);
????????????}
????????????if?(null?!=?this.tools?&&?this.tools.size()?>?0)?{
????????????????paramsMap.put("tools",?this.tools);
????????????????paramsMap.put("tool_choice",?this.toolChoice);
????????????}
????????????return?new?ObjectMapper().writeValueAsString(paramsMap);
????????}
????????
????????//?默認
????????Map<String,?Object>?paramsMap?=?new?HashMap<>();
????????paramsMap.put("request_id",?requestId);
????????paramsMap.put("prompt",?prompt);
????????paramsMap.put("incremental",?incremental);
????????paramsMap.put("temperature",?temperature);
????????paramsMap.put("top_p",?topP);
????????paramsMap.put("sseFormat",?sseFormat);
????????return?new?ObjectMapper().writeValueAsString(paramsMap);
????}?catch?(JsonProcessingException?e)?{
????????throw?new?RuntimeException(e);
????}
}
-
- 如果為本次調整的新增模型,則走新的方式裝配參數(shù)信息。通過這樣的方式可以很輕松的把以前叫做 prompt 的字段調整為 messages 名稱。類似的操作可以看具體的代碼。
關于字段的出入?yún)⑻幚?,但比較同類,就不一一列舉了
三、功能驗證
注意:測試前需要申請ApiKey https://open.bigmodel.cn/overview 有非常多的免費額度。
@Before
public?void?test_OpenAiSessionFactory()?{
????//?1.?配置文件
????Configuration?configuration?=?new?Configuration();
????configuration.setApiHost("https://open.bigmodel.cn/");
????configuration.setApiSecretKey("62ddec38b1d0b9a7b0fddaf271e6ed90.HpD0SUBUlvqd05ey");
????configuration.setLevel(HttpLoggingInterceptor.Level.BODY);
????//?2.?會話工廠
????OpenAiSessionFactory?factory?=?new?DefaultOpenAiSessionFactory(configuration);
????//?3.?開啟會話
????this.openAiSession?=?factory.openSession();
}
- 申請后把你的 ApiKey 替換 setApiSecretKey 就可以使用了。
1. 文生文「支持聯(lián)網(wǎng)」
@Test
public?void?test_completions()?throws?Exception?{
????CountDownLatch?countDownLatch?=?new?CountDownLatch(1);
????//?入?yún)ⅲ荒P?、請求信?
????ChatCompletionRequest?request?=?new?ChatCompletionRequest();
????request.setModel(Model.GLM_3_5_TURBO);?//?chatGLM_6b_SSE、chatglm_lite、chatglm_lite_32k、chatglm_std、chatglm_pro
????request.setIncremental(false);
????request.setIsCompatible(true);?//?是否對返回結果數(shù)據(jù)做兼容,24年1月發(fā)布的?GLM_3_5_TURBO、GLM_4?模型,與之前的模型在返回結果上有差異。開啟?true?可以做兼容。
????//?24年1月發(fā)布的?glm-3-turbo、glm-4?支持函數(shù)、知識庫、聯(lián)網(wǎng)功能
????request.setTools(new?ArrayList<ChatCompletionRequest.Tool>()?{
????????private?static?final?long?serialVersionUID?=?-7988151926241837899L;
????????{
????????????add(ChatCompletionRequest.Tool.builder()
????????????????????.type(ChatCompletionRequest.Tool.Type.web_search)
????????????????????.webSearch(ChatCompletionRequest.Tool.WebSearch.builder().enable(true).searchQuery("小傅哥").build())
????????????????????.build());
????????}
????});
????request.setPrompt(new?ArrayList<ChatCompletionRequest.Prompt>()?{
????????private?static?final?long?serialVersionUID?=?-7988151926241837899L;
????????{
????????????add(ChatCompletionRequest.Prompt.builder()
????????????????????.role(Role.user.getCode())
????????????????????.content("小傅哥的是誰")
????????????????????.build());
????????}
????});
????//?請求
????openAiSession.completions(request,?new?EventSourceListener()?{
????????@Override
????????public?void?onEvent(EventSource?eventSource,?@Nullable?String?id,?@Nullable?String?type,?String?data)?{
????????????ChatCompletionResponse?response?=?JSON.parseObject(data,?ChatCompletionResponse.class);
????????????log.info("測試結果?onEvent:{}",?response.getData());
????????????//?type?消息類型,add?增量,finish?結束,error?錯誤,interrupted?中斷
????????????if?(EventType.finish.getCode().equals(type))?{
????????????????ChatCompletionResponse.Meta?meta?=?JSON.parseObject(response.getMeta(),?ChatCompletionResponse.Meta.class);
????????????????log.info("[輸出結束]?Tokens?{}",?JSON.toJSONString(meta));
????????????}
????????}
????????@Override
????????public?void?onClosed(EventSource?eventSource)?{
????????????log.info("對話完成");
????????????countDownLatch.countDown();
????????}
????????@Override
????????public?void?onFailure(EventSource?eventSource,?@Nullable?Throwable?t,?@Nullable?Response?response)?{
????????????log.info("對話異常");
????????????countDownLatch.countDown();
????????}
????});
????//?等待
????countDownLatch.await();
}
2. 文生圖
@Test
public?void?test_genImages()?throws?Exception?{
????ImageCompletionRequest?request?=?new?ImageCompletionRequest();
????request.setModel(Model.COGVIEW_3);
????request.setPrompt("畫個小狗");
????ImageCompletionResponse?response?=?openAiSession.genImages(request);
????log.info("測試結果:{}",?JSON.toJSONString(response));
}
3. 多模態(tài)
public?void?test_completions_v4()?throws?Exception?{
????CountDownLatch?countDownLatch?=?new?CountDownLatch(1);
????//?入?yún)ⅲ荒P?、請求信?
????ChatCompletionRequest?request?=?new?ChatCompletionRequest();
????request.setModel(Model.GLM_4V);?//?GLM_3_5_TURBO、GLM_4
????request.setStream(true);
????request.setMessages(new?ArrayList<ChatCompletionRequest.Prompt>()?{
????????private?static?final?long?serialVersionUID?=?-7988151926241837899L;
????????{
????????????//?content?字符串格式
????????????add(ChatCompletionRequest.Prompt.builder()
????????????????????.role(Role.user.getCode())
????????????????????.content("這個圖片寫了什么")
????????????????????.build());
????????????//?content?對象格式
????????????add(ChatCompletionRequest.Prompt.builder()
????????????????????.role(Role.user.getCode())
????????????????????.content(ChatCompletionRequest.Prompt.Content.builder()
????????????????????????????.type(ChatCompletionRequest.Prompt.Content.Type.text.getCode())
????????????????????????????.text("這是什么圖片")
????????????????????????????.build())
????????????????????.build());
????????????//?content?對象格式,上傳圖片;圖片支持url、basde64
????????????add(ChatCompletionRequest.Prompt.builder()
????????????????????.role(Role.user.getCode())
????????????????????.content(ChatCompletionRequest.Prompt.Content.builder()
????????????????????????????.type(ChatCompletionRequest.Prompt.Content.Type.image_url.getCode())
????????????????????????????.imageUrl(ChatCompletionRequest.Prompt.Content.ImageUrl.builder().url("https://bugstack.cn/images/article/project/chatgpt/chatgpt-extra-231011-01.png").buil
????????????????????????????.build())
????????????????????.build());
????????}
????});
????openAiSession.completions(request,?new?EventSourceListener()?{
????????@Override
????????public?void?onEvent(EventSource?eventSource,?@Nullable?String?id,?@Nullable?String?type,?String?data)?{
????????????if?("[DONE]".equals(data))?{
????????????????log.info("[輸出結束]?Tokens?{}",?JSON.toJSONString(data));
????????????????return;
????????????}
????????????ChatCompletionResponse?response?=?JSON.parseObject(data,?ChatCompletionResponse.class);
????????????log.info("測試結果:{}",?JSON.toJSONString(response));
????????}
????????@Override
????????public?void?onClosed(EventSource?eventSource)?{
????????????log.info("對話完成");
????????????countDownLatch.countDown();
????????}
????????@Override
????????public?void?onFailure(EventSource?eventSource,?@Nullable?Throwable?t,?@Nullable?Response?response)?{
????????????log.error("對話失敗",?t);
????????????countDownLatch.countDown();
????????}
????});
????//?等待
????countDownLatch.await();
}
四、工程源碼
-
- 申請ApiKey:https://open.bigmodel.cn/usercenter/apikeys - 注冊申請開通,即可獲得 ApiKey運行環(huán)境:JDK 1.8+支持模型:chatGLM_6b_SSE、chatglm_lite、chatglm_lite_32k、chatglm_std、chatglm_pro、chatglm_turbo、glm-3-turbo、glm-4、glm-4v、cogview-3maven pom -
已發(fā)布到Maven倉庫
<dependency>
<groupId>cn.bugstack</groupId>
<artifactId>chatglm-sdk-java</artifactId>
<version>2.0</version>
</dependency>
- 源碼(Github):https://github.com/fuzhengwei/chatglm-sdk-java源碼(Gitee):https://gitee.com/fustack/chatglm-sdk-java源碼(Gitcode):https://gitcode.net/KnowledgePlanet/road-map/chatglm-sdk-java
五、實戰(zhàn)項目
小傅哥耗時了4個多月前后端 + Dev-Ops
,全棧式編程。手把手&漸進式,逐個章節(jié)錄制視頻 + 編寫小冊開發(fā)的 —— OpenAI 項目完結了。使用了 ChatGPT、ChatGLM 模型的對接使用。
這個項目運用了全新的 DDD 領域驅動設計、使用設計模式處理多模型、交易、登錄鑒權等各個場景,完整對接支付「解耦、補償」、可上線部署和配置監(jiān)控。體驗地址:https://gaga.plus
注意,本項目也只是【星球:碼農會鎖】眾多項目中的1個,其他的項目還包括:大營銷平臺、API網(wǎng)關、Lottery抽獎、IM通信、SpringBoot Starter 組件開發(fā)、IDEA Plugin 插件開發(fā)等,并還有開源項目學習。
這樣整套項目,放在一些平臺售賣,至少都是上千元。但小傅哥的星球,只需要100多,就可以獲得全部的學習項目!