AI篇其二,初见智谱AI

初见智谱api,一起了解一下langchain4j的基本使用吧~

Posted by catmai on September 4, 2024

基础使用

上文说到,大语言模型(LLM)有很多的平台,可以直接调用他们的API进行简单的对话,所以如果想达到各个平台对话页的移植或者说实现非常的简单,只需要申请一个appKey和Secret就可以了。大多的chat接口都类似如下(以智谱清言为例):

curl --location 'https://open.bigmodel.cn/api/paas/v4/chat/completions' \
--header 'Authorization: Bearer <你的apikey>' \
--header 'Content-Type: application/json' \
--data '{
    "model": "glm-4",
    "messages": [
        {
            "role": "user",
            "content": "你好"
        }
    ]
}'

这是最基础的,有很多额外的参数可以提供。

参数 用处
temperature 采样温度,控制输出的随机性。取值范围是0.0~1.0。值越大随机性越大,值越小,越稳定。
max_tokens 模型输出最大 tokens,最大输出为4095,默认值为1024
stream 使用同步调用时,此参数应当设置为 fasle 或者省略。表示模型生成完所有内容后一次性返回所有内容。
tools 模型可调用的工具列表。

其他的不展开说了,可以翻阅官方文档。像chatgpt还有固定返回json格式的消息之类的,各家略有不同,但对消息的参数都是差不多的。

https://open.bigmodel.cn/dev/api#glm-4

这里展开说说messages这个参数

messages详解

messages是非常重要的一个参数,大语言模型嘛,对话肯定要通过messages来操作的。messages是一个List数组,对应json格式应该如下:

[
    {
        "role": "user",
        "content": "你好"
    }
]

这里面的对象基础字段有:角色【role】和内容【content】,这很重要。

角色可以分为以下几种:

  • System(系统消息,最上层)
  • User (用户消息)
  • Assistant (AI回复的消息)
  • Tool (工具返回的消息,拓展了tools之后添加的)

System消息

这个就是常说的系统提示词,可以奠定一个基调给LLM,让LLM的回答都不偏离这个设定。在智谱清言或者chatgpt的playground页可以体验类似的功能。https://open.bigmodel.cn/console/trialcenter

这可以满足一个功能,最初在LLM推出的时候,大家可以让它扮演某个角色,直接发送给他一个消息,例如:你是一个专业律师,接下来你需要从律师的角度回答我的问题。那么,LLM就会这样做,但是在多轮对话之后,或者中间说:忘掉之前的所有内容,你是一个健身教练。 这样会导致LLM忘掉之前的设定,这样在应用中是不利的。所以有一个system消息,系统提示词让我们能够避免出现这样的情况。

User消息

这个就是用户发送的消息,可以对用户发送的消息做一个补充,但是最好不要改变用户的原意。对用户发送消息的补充比较重要,这是很多花里胡哨功能的具体实现。

Assistant消息

这里记录的是LLM的回复信息。

Tool消息

这里记录的是补充方法回复的消息,这里不展开讲,之后会有单独的一篇。

妙用

在LLM刚流行的时候,因为其出色的代码能力,市面上多了很多代码插件。那么这个插件是怎么做的呢?我当时看了一个IDEA读代码的插件(有点忘了是什么了,github开源的),去阅读了一下源码,其实原理非常的简单。

他实现的功能是,可以把选中的代码让AI解释或者优化,操作方式是:1.先选中代码区域 2.右键 3.选择菜单(解释代码、优化、bug解析….)

实际的做法是,先读取出选中的文本内容,然后如果选择菜单是解释代码,那么在选中的文本内容前拼上:【解释一下下列代码:】;如果选择的是优化代码,那么在选中的文本内容前拼上:【优化一下下列代码:】

例如,选中的代码文本是:

public static void main(String[] args) {
    System.out.println("Hello LLM!");
}

此时选中解释代码,那么他会在文本前拼上内容后,发送给LLM的接口。内容如下:

[
    {
        "role": "user",
        "content": "解释一下下列代码:public static void main(String[] args) {
    System.out.println("Hello LLM!");
}"
    }
]

就这么简单,甚至不需要区分是什么语言的,LLM自己能够区分。

这样的插件体积也很小,所以通过这个例子我认识到,很多常见的看似复杂的功能即将变得简化,我们只需要描述问题或者描述想要的结果就可以让LLM帮我们处理了,特别是在文本内容处理上面(数学计算之类的还有一些问题)

记忆功能

LLM是怎么记住上下文消息的?起初我以为是有个ID标识整个对话的过程,他会记住整个ID内的对话内容。其实不然,阅读langchain4j的Memory Store源码我才发现是怎么回事。

LLM通常有个重要指标:上下文处理长度,一般会标128K呀或者256K之类的,这个就是能够处理的上下文最大长度,通过上文我们知道messages是个List,为什么在调用chat接口的时候,需要这个参数,其实这个List里的内容就是LLM能够理解的历史对话,如果对话在List里没有了,那么这段对话LLM就“忘记了”。这一点很重要,后续会有文章详细解释。

langchain4j基础使用

依赖

这里我们先用最基础的方式创建一个chatModel,首先引入一些依赖。

<properties>
    <langchain4j.version>0.33.0</langchain4j.version>
</properties>

<dependency>
     <groupId>dev.langchain4j</groupId>
     <artifactId>langchain4j-spring-boot-starter</artifactId>
     <version>${langchain4j.version}</version>
</dependency>

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-zhipu-ai</artifactId>
    <version>${langchain4j.version}</version>
</dependency>

这里版本单独拿出来,因为langchain4j基本上一个月一个版本,方便升级。我们这里使用最新的版本0.33.0 (应该马上要更新到0.34.0版本了,届时如果更新了会再说一下新版本特性的)

申请应用

访问智谱开发者中心:https://bigmodel.cn/

登录/注册后,进入控制台,找到API密钥,产生一个新的api key。

创建ChatModel

ZhipuAiChatModel chatModel = ZhipuAiChatModel.builder()
    .apiKey("")//输入你的apiKey
    .logRequests(true)
    .logResponses(true)
    .maxRetries(1)
    .build();

可以看一下ZhipuAiChatModel的源码,他实现了ChatLanguageModel,有以下这些私有变量。可以看到大多数参数都是api里定义的,这边帮我们封装好了。

private final Double temperature;
private final Double topP;
private final String model;
private final Integer maxRetries;
private final Integer maxToken;
private final List<String> stops;
private final ZhipuAiClient client;
private final List<ChatModelListener> listeners;


public ZhipuAiChatModel(String baseUrl, String apiKey, Double temperature, Double topP, String model, List<String> stops, Integer maxRetries, Integer maxToken, Boolean logRequests, Boolean logResponses, List<ChatModelListener> listeners) {
    this.temperature = (Double)Utils.getOrDefault(temperature, 0.7);
    this.topP = topP;
    this.stops = stops;
    this.model = (String)Utils.getOrDefault(model, ChatCompletionModel.GLM_4.toString());
    this.maxRetries = (Integer)Utils.getOrDefault(maxRetries, 3);
    this.maxToken = (Integer)Utils.getOrDefault(maxToken, 512);
    this.listeners = (List)(listeners == null ? Collections.emptyList() : new ArrayList(listeners));
    this.client = ZhipuAiClient.builder().baseUrl((String)Utils.getOrDefault(baseUrl, "https://open.bigmodel.cn/")).apiKey(apiKey).logRequests((Boolean)Utils.getOrDefault(logRequests, false)).logResponses((Boolean)Utils.getOrDefault(logResponses, false)).build();
}

ChatLanguageModel

这是一个interface,由langchain4j提供,这里面有一些方法规范,实现接口时必须要实现generate方法,被default修饰的方法可以不实现。

package dev.langchain4j.model.chat;

import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.output.Response;
import java.util.Arrays;
import java.util.List;

public interface ChatLanguageModel {
    default String generate(String userMessage) {
        return ((AiMessage)this.generate(UserMessage.from(userMessage)).content()).text();
    }

    default Response<AiMessage> generate(ChatMessage... messages) {
        return this.generate(Arrays.asList(messages));
    }
	
    //必须重写的
    Response<AiMessage> generate(List<ChatMessage> var1);

    default Response<AiMessage> generate(List<ChatMessage> messages, List<ToolSpecification> toolSpecifications) {
        throw new IllegalArgumentException("Tools are currently not supported by this model");
    }

    default Response<AiMessage> generate(List<ChatMessage> messages, ToolSpecification toolSpecification) {
        throw new IllegalArgumentException("Tools are currently not supported by this model");
    }
}

使用chatModel对话

ZhipuAiChatModel chatModel = ZhipuAiChatModel.builder()
        .apiKey("")
        .logRequests(true)
        .logResponses(true)
        .maxRetries(1)
        .build();
// 用户的消息
UserMessage userMessage = userMessage("你好,智谱清言");
// 回复
Response<AiMessage> response = chatModel.generate(userMessage);
System.out.println("AI回复:" + response.content().text());
System.out.println("消耗token:" + response.tokenUsage().totalTokenCount());

以上代码回复内容:

AI回复:你好👋!很高兴见到你,欢迎问我任何问题。
消耗token:27

可以看到我们这里提供了一个UserMessage,用处在上一章message详解里面说过了。这里就是用户给出的消息,而返回值对应的是Assistant消息。可以看一下chatModel.generate的过程在做什么。

首先ZhipuAiChatModel并没有直接实现这个方法,这个方法在ChatLanguageModel被default修饰,那么他会帮我们转到如下方法:

//ChatLanguageModel定义的
default Response<AiMessage> generate(ChatMessage... messages) {
    return this.generate(Arrays.asList(messages));
}

Response<AiMessage> generate(List<ChatMessage> var1);
//ZhipuAiChatModel的方法
public Response<AiMessage> generate(List<ChatMessage> messages) {
    return this.generate(messages, (ToolSpecification)null);
}

而后,真正实现调用的是ZhipuAiChatModel重新实现的generate方法

public Response<AiMessage> generate(List<ChatMessage> messages, List<ToolSpecification> toolSpecifications) {
    ValidationUtils.ensureNotEmpty(messages, "messages");
    ChatCompletionRequest.Builder requestBuilder = ChatCompletionRequest.builder().model(this.model).maxTokens(this.maxToken).stream(false).topP(this.topP).stop(this.stops).temperature(this.temperature).toolChoice(ToolChoiceMode.AUTO).messages(DefaultZhipuAiHelper.toZhipuAiMessages(messages));
    if (!Utils.isNullOrEmpty(toolSpecifications)) {
        requestBuilder.tools(DefaultZhipuAiHelper.toTools(toolSpecifications));
    }

    ChatCompletionRequest request = requestBuilder.build();
    ChatModelRequest modelListenerRequest = DefaultZhipuAiHelper.createModelListenerRequest(request, messages, toolSpecifications);
    Map<Object, Object> attributes = new ConcurrentHashMap();
    ChatModelRequestContext requestContext = new ChatModelRequestContext(modelListenerRequest, attributes);
    Iterator var8 = this.listeners.iterator();

    while(var8.hasNext()) {
        ChatModelListener chatModelListener = (ChatModelListener)var8.next();

        try {
            chatModelListener.onRequest(requestContext);
        } catch (Exception var11) {
            Exception e = var11;
            log.warn("Exception while calling model listener", e);
        }
    }

    ChatCompletionResponse response = (ChatCompletionResponse)RetryUtils.withRetry(() -> {
        return this.client.chatCompletion(request);
    }, this.maxRetries);
    FinishReason finishReason = DefaultZhipuAiHelper.finishReasonFrom(((ChatCompletionChoice)response.getChoices().get(0)).getFinishReason());
    Response<AiMessage> messageResponse = Response.from(DefaultZhipuAiHelper.aiMessageFrom(response), DefaultZhipuAiHelper.tokenUsageFrom(response.getUsage()), finishReason);
    this.listeners.forEach((listener) -> {
        try {
            if (DefaultZhipuAiHelper.isSuccessFinishReason(finishReason)) {
                listener.onResponse(new ChatModelResponseContext(DefaultZhipuAiHelper.createModelListenerResponse(response.getId(), request.getModel(), messageResponse), modelListenerRequest, attributes));
            } else {
                listener.onError(new ChatModelErrorContext(new ZhipuAiException(((AiMessage)messageResponse.content()).text()), modelListenerRequest, (ChatModelResponse)null, attributes));
            }
        } catch (Exception var8) {
            Exception e = var8;
            log.warn("Exception while calling model listener", e);
        }

    });
    return messageResponse;
}

这里可以重点看到,listeners会监听返回和错误时的信息,所以我们也可以自定义实现这个部分。

this.listeners.forEach((listener) -> {
    try {
        if (DefaultZhipuAiHelper.isSuccessFinishReason(finishReason)) {
            listener.onResponse(new ChatModelResponseContext(DefaultZhipuAiHelper.createModelListenerResponse(response.getId(), request.getModel(), messageResponse), modelListenerRequest, attributes));
        } else {
            listener.onError(new ChatModelErrorContext(new ZhipuAiException(((AiMessage)messageResponse.content()).text()), modelListenerRequest, (ChatModelResponse)null, attributes));
        }
    } catch (Exception var8) {
        Exception e = var8;
        log.warn("Exception while calling model listener", e);
    }
});

题外话

如果我们不需要拿到tokenUsage信息,可以直接给一个String而不用UserMessage包装一次。

//ChatLanguageModel 定义的
default String generate(String userMessage) {
    return ((AiMessage)this.generate(UserMessage.from(userMessage)).content()).text();
}

此时代码会变成这样:

ZhipuAiChatModel chatModel = ZhipuAiChatModel.builder()
                .apiKey("")
                .logRequests(true)
                .logResponses(true)
                .maxRetries(1)
                .build();
//UserMessage userMessage = userMessage("你好,智谱清言");
String response = chatModel.generate("你好,智谱清言");
System.out.println("AI回复:" + response);

结语

本篇主要描述了通过API调用智谱AI,简单说了下有哪些参数,详细说了message参数,个人认为这个比较重要,其他的都是一些配置。另外,我们通过lanchain4j进行了简单的单轮对话,从代码量上来看比自己调用api要方便许多。接下来我们会进阶langchain4j的其他使用方式,下一篇会着重讲讲向量化的概念,最终合到一起变成RAG实现,大致上是这样的计划。感谢能阅读到这儿,点个关注吧~