Java基础、中级、高级、架构面试资料

基于Spring Boot的chatGPT问答助手

JAVA herman 6157浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

最近想在自己的一个项目里接入 chatGPT 实现 AI 对话助手,但是调研了一下 Open AI 提供的服务在国内不好用,需要搭代理,而且还有 feng 号的风险。于是在寻找别的方案时发现了微软也提供了相应的服务,并且国内可用,而且响应速度还是挺快的。

注册账号、申请服务以及部署模型

通过 Azure 官网https://azure.microsoft.com/en-us/操作即可,详细步骤站内已经有大佬出过博客了,大家自行搜索查看即可,本篇文章主要讲解怎么基于「SpringBoot」开发一个 AI 带有上下文的问答服务。

项目原理

整个 AI 问答助手的服务运行流程很简单,用户发起 HTTP 请求调用我们的 SpringBoot 的应用,再由这个应用去向微软的服务器发送 HTTP 请求,接收到微软传回的数据后,再抽离出回复的 content 返回给前端即可,因为原本返回的数据里还包含模型版本等无关信息,所以需要简单处理下。

具体实现

「主要讲讲整个功能的以下几个关键的实现点:」

加我微信:xmtxtt,赠送本文源码项目。

如何实现带有上下文的对话

其实并不复杂,我们每次请求发送的是一个消息列表,AI 通过读取整个消息列表,从而可以做出带有上下文的回复,同时需要对消息类型的归属做出区分,如这条消息是用户发送的还是 AI 回复的。因此我们每次发送给 AI 的消息主要包含两个参数:内容和角色,因此可以封装聊天消息类:

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";  
  }  
}

除了聊天消息之外,通过查看官方文档,得知还需要一些影响模型生成内容的参数,因此可以继续封装出这个完整的聊天请求类:

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 服务将会开始报错。」

如何实现对话能一直进行下去

上文我们提到了每次发送消息都是一个 List 集合同时 Azure 对不同模型的 totalTokens 又做了限制,所以如果我们不及时清理消息集合里的内容就会导致最终无法调用 Azure 的 AI 服务,因此我们需要一个接口实现清理消息列表的功能。但这样做又会导致一个新的问题:因为我们的项目是在页面上提供了一个聊天窗口供用户使用,如果由后端自动清除或在前端提供入口让用户手动清除都不是很友好(PS:但这个功能显然是由必要的)。

这里分享一下我的做法:我会将聊天内容存入 redis 中,既可以设置自动过期时间,又可以对 tokens 数进行判断从而实现过期较早的数据,最大程度上保留最近几次对话的内容,从而实现在单次使用里上下文对话不间断。

// 检索聊天历史  
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;  
}

如何给AI预置语料

在上文中提到了角色,里面除了 user 和 assistant,它们分别代码用户和 AI,还有一个 system,我们可以在发送的消息列表里添加以 system 作为角色的内容,这样就可以实现它的一些特定内容的回复,比如你可以把你的资料写进去,这样 AI 可以回复出关于你的内容 2333。

我为了更方便的对预置语料进行维护,声明了一个常量类来存储预置消息(如果你的语料很多,服务代码会变得很臃肿),这样初始化消息列表的时候,只需要将常量类里的内容 set 进去即可。

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 作为键实现对话的数据隔离等。整个项目的代码量不大,注释也很全面,有需要的朋友可以根据自己的需求使用或改造。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 基于Spring Boot的chatGPT问答助手