看老夫,如何带你操刀重构ChatGLM-SDK!



By
jonson
22 1 月 24
0
comment

作者:小傅哥
博客:https://bugstack.cn

大家好,我是技术UP主小傅哥。

鉴于智谱AI发布了最新一代 GLM3.0GLM4.0 基座大模型,我又要对自己开发的这款开源 chatglm-sdk-java 进行改造了!因为需要做新老接口的模型调用中数据格式兼容,这将是一场编码设计复杂场景的对抗挑战。 请看小傅哥如何操刀改造!

为什么改造SDK会比较复杂?

通常来说,我们开发好一款SDK后,就会投入到项目中使用,而使用的方式会根据SDK的出入参标准进行对接。比如一个接口的入参原本有2个参数,A StringB String 类型,但现在因为有额外的功能添加,从2个参数调整为3个参数,同时需要对原本的 B 参数 String 类型,扩展为 Object 类型,添加更多的属性信息。同时出参也有对应响应结构变化。

那么对于这种线上正在使用又需要改造代码的情况,我们不可能把原有的代码都铲了不要,所以需要做一些优雅的兼容的开发处理。让工程更加好维护、好迭代。

在设计原则和设计模式的锤炼下,写出高质量的代码。

那么,接下来小傅哥就带着大家讲讲这段关于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,在接口调用上做了不小的修改。因为新版的模型增加了如插件「联网、知识库、函数库」、画图、图片识别这样的能力,所以出入参也有相应的变化。

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
    • 注意:新版的模型为入参方式调用,接口是统一的接口。此外入参格式的流式可以通过参数

"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
    注意:多模态4v模型,content 字符串升级为对象。这部分与 chatgpt 的参数结构是一致的。所以我们在开发这部分功能的时候,也需要做兼容处理。因为本身它既可以支持对象也可以支持 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
    注意:图片的文生图是一个新的功能,旧版没有这个接口。所以开发的时候按照独立的接口开发即可。

综上,这些接口开发还需要注意;

    首先,小傅哥根据官网文档编写了对应的 curl 请求格式。方便开发时可以参考和验证。之后,curl 旧版是文本类处理,curl 3&4 是新版的文档处理。两个可以对比看出,旧版的入参是 prompt,新版是 messages另外,本次API文档新增加了文生图,和4v(vision)多模态的图片识别。

接下来,我们就要来设计怎么在旧版的SDK中兼容这些功能实现。

二、功能实现

1. 调用流程

如图,是整个本次 SDK 的实现的核心流程,其中执行器部分是本次重点开发的内容。在旧版的 SDK 中是直接从会话请求进入模型的调用,没有执行器的添加。而执行器的引入则是为了解耦调用过程,依照于不同的请求模型(chatglm_std、chatglm_pro、GLM_4…),可以调用到不同的执行器上去。

2. 执行器「解耦」

2.1 接口
public interface Executor {

    /**
     * 问答模式,流式反馈
     *
     * @param chatCompletionRequest 请求信息
     * @param eventSourceListener   实现监听;通过监听的 onEvent 方法接收数据
     * @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;
    /**
     * 统一接口
     */
    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);
    }

    
    // ... 省略部分接口
    
}    

在新版的执行实现中,除了 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;
}
    对于不同的模型,走哪个执行器,在 Configuration 配置文件中写了这样的配置信息。这样当你调用 CHATGLM_TURBO 就会走到 glmOldExecutor 模型,调用 GLM_4V 就会走到 glmExecutor 模型。

4. 参数兼容

ChatCompletionRequest 作为一个重要的应答参数对象,在本次的接口变化中也是调整了不少字段。但好在小傅哥之前就提供了一个 toString 对象的方法。在这里我们可以做不同类型参数的处理。

public String toString() {
    try {
        // 24年1月发布新模型后调整
        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);
    }
}
    • 如果为本次调整的新增模型,则走新的方式装配参数信息。通过这样的方式可以很轻松的把以前叫做 prompt 的字段调整为 messages 名称。类似的操作可以看具体的代码。

关于字段的出入参处理,但比较同类,就不一一列举了

三、功能验证

注意:测试前需要申请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. 文生文「支持联网」

@Test
public void test_completions() throws Exception {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    // 入参;模型、请求信息
    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); // 是否对返回结果数据做兼容,24年1月发布的 GLM_3_5_TURBO、GLM_4 模型,与之前的模型在返回结果上有差异。开启 true 可以做兼容。
    // 24年1月发布的 glm-3-turbo、glm-4 支持函数、知识库、联网功能
    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. 多模态

public void test_completions_v4() throws Exception {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    // 入参;模型、请求信息
    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运行环境: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 –

已发布到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

五、实战项目

小傅哥耗时了4个多月前后端 + Dev-Ops,全栈式编程。手把手&渐进式,逐个章节录制视频 + 编写小册开发的 —— OpenAI 项目完结了。使用了 ChatGPT、ChatGLM 模型的对接使用。

这个项目运用了全新的 DDD 领域驱动设计、使用设计模式处理多模型、交易、登录鉴权等各个场景,完整对接支付「解耦、补偿」、可上线部署和配置监控。体验地址:https://gaga.plus

注意,本项目也只是【星球:码农会锁】众多项目中的1个,其他的项目还包括:大营销平台、API网关、Lottery抽奖、IM通信、SpringBoot Starter 组件开发、IDEA Plugin 插件开发等,并还有开源项目学习。

这样整套项目,放在一些平台售卖,至少都是上千元。但小傅哥的星球,只需要100多,就可以获得全部的学习项目!

发表回复