最近想在自己的一个项目里接入chatGPT实现AI对话助手,但是调研了一下Open AI提供的服务在国内不好用,需要搭代理,而且还有封号的风险。于是在寻找别的方案时发现了微软也提供了相应的服务,并且国内可用,而且响应速度还是挺快的。
一、注册账号、申请服务以及部署模型
通过Azure官网操作即可,详细步骤站内已经有大佬出过博客了,大家自行搜索查看即可,本篇文章主要讲解怎么基于SpringBoot 开发一个AI带有上下文的问答服务。
二、项目原理
整个AI问答助手的服务运行流程很简单,用户发起HTTP请求调用我们的SpringBoot的应用,再由这个应用去向微软的服务器发送HTTP请求,接收到微软传回的数据后,再抽离出回复的content返回给前端即可,因为原本返回的数据里还包含模型版本等无关信息,所以需要简单处理下。
三、具体实现
主要讲讲整个功能的以下几个关键的实现点:
1.如何实现带有上下文的对话
其实并不复杂,我们每次请求发送的是一个消息列表,AI通过读取整个消息列表,从而可以做出带有上下文的回复,同时需要对消息类型的归属做出区分,如这条消息是用户发送的还是AI回复的。因此我们每次发送给AI的消息主要包含两个参数:内容和角色,因此可以封装聊天消息类:
java
import lombok.Data;
@Data
public class ChatMessage {
private String role;
private String content;
public ChatMessage(String role, String content) {
this.role = role;
this.content = content;
}
@SuppressWarnings("InterfaceIsType")
public interface Role {
String USER = "user";
String SYSTEM = "system";
String ASSISTANT = "assistant";
}
}
除了聊天消息之外,通过查看官方文档,得知还需要一些影响模型生成内容的参数,因此可以继续封装出这个完整的聊天请求类:
java
import com.alibaba.fastjson.annotation.JSONField;
import java.util.List;
import lombok.Data;
/**
* 请求参数类
*/
@Data
public class ChatRequest {
//消息列表
private List<ChatMessage> messages;
//生成文本的最大长度。
@JSONField(name = "max_tokens")
private int maxTokens = 2400;
//生成文本的随机性,取值从0到1,较高的"温度"值意味着模型将冒更多的风险。0表示随机性最低,创造性最差。
private double temperature = 0.75;
//使用词频惩罚。较高的频率惩罚将阻止模型重复。
@JSONField(name = "frequency_penalty")
private double frequencyPenalty = 0.1;
//使用存在惩罚。较高的存在惩罚将鼓励模型专注于输入提示本身。
@JSONField(name = "presence_penalty")
private double presencePenalty = 0.1;
//从模型预测中选择概率最高的标记,直到达到指定的总概率。默认为1。也就是说,一旦该分布超过top_p值,就会停止生成文本。例如,top_p为0.3表示仅考虑组成前30%概率质量的标记。
@JSONField(name = "top_p")
private double topP = 0.95;
//停止生成文本,当模型生成某些指定字符时,就停止不在生成。默认为空。
private String stop;
}
注意: 根据自己的需求可以调整maxTokens的值,可以通过官方文档查看自己注册部署的模型的maxTokens和totalTokens的限额,并不是无限大的。这里解释一下它们的含义。maxTokens指的是单次对话生成的文本最大长度,而totalTokens则指的是这轮对话所能接收的最大长度。
因此,maxTokens越大,单次对话生成的文本就有可能越长,进而导致对话的轮数变少,因为当你的消息集合里的文本长度超过你模型的totalTokens,你调用Azure服务将会开始报错。
2.如何实现对话能一直进行下去
上文我们提到了每次发送消息都是一个List集合同时Azure对不同模型的totalTokens又做了限制,所以如果我们不及时清理消息集合里的内容就会导致最终无法调用Azure的AI服务,因此我们需要一个接口实现清理消息列表的功能。但这样做又会导致一个新的问题:因为我们的项目是在页面上提供了一个聊天窗口供用户使用,如果由后端自动清除或在前端提供入口让用户手动清除都不是很友好(PS:但这个功能显然是由必要的)。
这里分享一下我的做法:我会将聊天内容存入redis中,既可以设置自动过期时间,又可以对tokens数进行判断从而实现过期较早的数据,最大程度上保留最近几次对话的内容,从而实现在单次使用里上下文对话不间断。
java
// 检索聊天历史
public List<ChatMessage> getHistory(String userId) {
//1.从Redis中检索聊天历史
List<ChatMessage> chatHistory = redisTemplate.opsForList().range(userId, 0, -1)
.stream()
.map(message -> JSON.parseObject(message, ChatMessage.class))
.collect(Collectors.toList());
//2.计算聊天历史的令牌数
int tokenCount = chatHistory.stream().mapToInt(message -> message.getContent().length()).sum();
//3.如果令牌数超过3800,就删除一些旧的消息
int index = 25; //保留前25条预置语料
while(tokenCount > 3800 && index < chatHistory.size()){
ChatMessage removedMessage = chatHistory.remove(index);
tokenCount -= removedMessage.getContent().length();
//从redis中删除相应的消息
redisTemplate.opsForList().remove(userId,0,JSON.toJSONString(removedMessage));
}
return chatHistory;
}
3.如何给AI预置语料
在上文中提到了角色,里面除了user和assistant,它们分别代码用户和AI,还有一个system,我们可以在发送的消息列表里添加以system作为角色的内容,这样就可以实现它的一些特定内容的回复,比如你可以把你的资料写进去,这样AI可以回复出关于你的内容2333。
我为了更方便的对预置语料进行维护,声明了一个常量类来存储预置消息(如果你的语料很多,服务代码会变得很臃肿),这样初始化消息列表的时候,只需要将常量类里的内容set进去即可。
java
import com.ita.data.ChatMessage;
import java.util.ArrayList;
import java.util.List;
/**
* AI身份信息常量类
*/
public class ChatConstant {
public static final String[] MESSAGES = {
"这里维护你的预置语料",
};
public static List<ChatMessage> generateSystemMessages() {
List<ChatMessage> messages = new ArrayList<>();
for (String content : MESSAGES) {
messages.add(new ChatMessage(ChatMessage.Role.SYSTEM, content));
}
return messages;
}
}
注意: 这里的内容不要写的太多,因为它是在消息列表里一起发送过去的,因此你预置的越多,自然可以单轮对话的次数相对就会减少,因为这也会占用你的totalTokens。
四、封装SDK
由于能找到的都是封装chatGPT那一套的,因此,我结合了我们的业务需求封装了一个基于Azure Open AI的Java版的sdk,比如redis里存储消息列表以userId作为键实现对话的数据隔离等。整个项目的代码量不大,注释也很全面,有需要的朋友可以根据自己的需求使用或改造。
如果本项目有帮到你,希望可以给项目点一个star!