0


java接入AI大模型个人实践(一)

一、前言

大家好,你的月亮我的心,我是博主小阿金,欢迎各位工友。 近来工作比较清闲、当然这也得益于AI技术的日益成熟、由于一直使用的是发小公司的AI大模型产品、博主也没有跟上潮流去研究如何接入个人项目、本着知其然知其所以然,近几天就来浅浅的探究一下如何接入个人项目。

二、模型选择、demo测试

由于我个人比较懒,又比较抠,本着能省则省的原则,最终选择国产大模型通义千问作为本次接入的大模型之一、接入模型的方法千篇一律,一通百通,最终还是需要落脚在业的设计上。下面是java项目接入千问流程。

参考文章:通义千问模型服务

  1. 已开通服务并获得API-KEY:开通DashScope并创建API-KEY。
  2. 引入Java SDK:java sdk引入流程
  3. 选择适合的交互方式、单轮对话、多轮对话、流式输出、我选择的是市面上常见的多轮对话+流式输出模式,通过demo中的代码可以看出、要实现多轮对话需要将历史对话的上下文,传递给大模型,以实现对话的持续输出,观察流式输出样例中GenerationParam方法可知,我们只需要将连续的对话以Message集合的形式传入即可。
privatestatic GenerationParam buildGenerationParam(Message userMsg){return GenerationParam.builder().model("qwen-turbo").messages(Arrays.asList(userMsg)).resultFormat(GenerationParam.ResultFormat.MESSAGE).topP(0.8).incrementalOutput(true).build();}

进一步得到Message的属性,这便于我们进行相关表结构的设计

public class Message {
    String role;
    String content;
    @SerializedName("tool_calls")
    List<ToolCallBase> toolCalls;
    @SerializedName("tool_call_id")
    String toolCallId;
    @SerializedName("name")
    String name;

其中需要关注的暂时只有 role content,role对应的为身份标识,content是输出的内容,其中role有以下几种类型

USER("user"),ASSISTANT("assistant"),BOT("bot"),SYSTEM("system"),ATTACHMENT("attachment"),TOOL("tool");

常用的就是user和assistant一个是用户发送,一个是系统回复类型。

三、流程及表结构设计思考

设计思考

基于demo代码设计可知

  1. 为了实现连续对话、我们必须将对话历史存储在本地
  2. 需要一定的会话控制逻辑,一是确保连续对话的上下文token,二是对于持续会话时间的控制

基于以上两点,我在数据库建立了两张数据表一张为问题表,一张为回复表用以解决历史对话问题,新增了会话id即sessionId属性用来控制连续一段时间的对话。以下是代码实现、仅供参考,如有高见请私信或者评论指出,不胜感激。

表结构

  1. 表结构设计,两表以问题id关联,确保为一个对话,以user_id、与session_id进行区分不同用户的会话历史。
CREATETABLE`chat_answer_history`(`id`varchar(50)NOTNULLCOMMENT'主键id',`user_id` bigint NOTNULLCOMMENT'用户id',`session_id`varchar(30)NOTNULLCOMMENT'会话id',`question_id` bigint NOTNULLCOMMENT'问题id',`answer` longtext CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOTNULLCOMMENT'答案',`message_role`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'角色',`message_tool_calls`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULL,`message_tool_call_id`varchar(64)DEFAULT'',`message_name`varchar(64)DEFAULT'',`create_time` datetime DEFAULTNULLCOMMENT'创建时间',`create_by`varchar(255)DEFAULTNULLCOMMENT'创建者',PRIMARYKEY(`id`),KEY`session_id_index`(`session_id`)USINGBTREE,KEY`question_id_index`(`question_id`)USINGBTREE,KEY`user_id_index`(`user_id`)USINGBTREE)ENGINE=InnoDB DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='答案历史对话表';CREATETABLE`chat_question_history`(`id` bigint NOTNULLAUTO_INCREMENTCOMMENT'主键id',`user_id` bigint NOTNULLCOMMENT'用户id',`session_id`varchar(30)NOTNULLCOMMENT'会话id',`question` longtext CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOTNULLCOMMENT'问题',`question_file_name`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'文件名称',`question_file_url`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'文件地址',`model`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'模型类型',`assistant`varchar(30)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'定义模型方向',`create_time` datetime DEFAULTNULLCOMMENT'创建时间',`create_by`varchar(255)DEFAULTNULLCOMMENT'创建者',`message_role`varchar(10)CHARACTERSET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULTNULLCOMMENT'角色',PRIMARYKEY(`id`),KEY`session_id_index`(`session_id`)USINGBTREE,KEY`user_id_index`(`user_id`)USINGBTREE)ENGINE=InnoDB AUTO_INCREMENT=215DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='提问历史对话表';

四、多轮对话+流式输出代码实现

此处给出基本实现逻辑

1、请求接口

import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.kingoffice.common.core.web.controller.BaseController;
import com.kingoffice.common.core.web.page.TableDataInfo;
import com.kingoffice.common.security.utils.SecurityUtils;
import com.kingoffice.system.domain.vo.ChatHistoryVo;
import com.kingoffice.system.domain.vo.ChatQuestionVo;
import com.kingoffice.system.service.aigc.IChatQuestionHistoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Transient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Comparator;
import java.util.List;

@RestController
@RequestMapping("/chat")
public class ChatController extends BaseController{

    @Autowired
    private IChatQuestionHistoryService questionHistoryService;

        /**
         * 获取gpt返回
         *
         * @param chatQuestionVo
         * @return
         */
        @Transient
        @PostMapping(path = "/getChat")
        public Flux<GenerationResult> gptChat( @RequestBody ChatQuestionVo chatQuestionVo) {
            return questionHistoryService.gptChat(chatQuestionVo);
        }

        /**
         * 历史对话
         */
        @GetMapping("/historyList")
        public TableDataInfo historyList() {
            startPage();
            List<ChatHistoryVo> list = questionHistoryService.historyList(SecurityUtils.getUserId());
            list.sort(Comparator.comparing(ChatHistoryVo::getCreateTime));
            return getDataTable(list);
        }

}
//入参
@Data
public class ChatQuestionVo {

    /**
     * 问题
     */
    private String question;

    /**
     * 会话id
     */
    private String sessionId;

    /**
     * 调用模型
     */
    private String model;

    /**
     * 助手类型
     */
    private String assistant;

    private String questionFileName;

    private String questionFileUrl;

}

2、接口实现

import com.alibaba.dashscope.aigc.generation.Generation;import com.alibaba.dashscope.aigc.generation.GenerationParam;import com.alibaba.dashscope.aigc.generation.GenerationResult;import com.alibaba.dashscope.aigc.generation.models.QwenParam;import com.alibaba.dashscope.common.Message;import com.alibaba.dashscope.common.ResultCallback;import com.alibaba.dashscope.common.Role;import com.alibaba.dashscope.utils.Constants;import com.kingoffice.common.core.constant.ChatModelConstants;import com.kingoffice.common.core.utils.DateUtils;import com.kingoffice.common.security.utils.SecurityUtils;import com.kingoffice.system.domain.aigc.ChatAnswerHistory;import com.kingoffice.system.domain.aigc.ChatQuestionHistory;import com.kingoffice.system.domain.vo.ChatHistoryVo;import com.kingoffice.system.domain.vo.ChatQuestionVo;import com.kingoffice.system.mapper.aigc.ChatAnswerHistoryMapper;import com.kingoffice.system.mapper.aigc.ChatQuestionHistoryMapper;import com.kingoffice.system.service.aigc.IChatQuestionHistoryService;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import reactor.core.publisher.Flux;import java.util.ArrayList;import java.util.Date;import java.util.List;/**
 * 提问历史对话Service业务层处理
 *
 * @author kim
 * @date 2024-07-19
 */
@Service
publicclassChatQuestionHistoryServiceImplimplementsIChatQuestionHistoryService{
    @Autowired
    private ChatQuestionHistoryMapper chatQuestionHistoryMapper;

    @Autowired
    private ChatAnswerHistoryMapper chatAnswerHistoryMapper;

    @Value("${chat.accessKeyAli}")private String accessKeyAli;

    @Value("${chat.accessKeyMoon}")private String accessKeyMoon;

    @Override
    public Flux<GenerationResult>gptChat(ChatQuestionVo question){
        StringBuilder contentBuilder =newStringBuilder();
        Constants.apiKey = accessKeyAli;// 密钥
        Generation gen =newGeneration();// 创建流// 获取用户信息
        Long userId = SecurityUtils.getUserId();
        String username = SecurityUtils.getUsername();if(StringUtils.isEmpty(question.getSessionId())){//创建新的会话
            question.setSessionId(String.valueOf(System.currentTimeMillis()));}else{
            long sessionTime = Long.parseLong(question.getSessionId());if(System.currentTimeMillis()- sessionTime >5*60*1000){//会话超时  自动更新会话
                question.setSessionId(String.valueOf(System.currentTimeMillis()));}}// 加载历史message,并创建新的message
        List<Message> messages =conversationHistory(question.getSessionId());
        messages.add(Message.builder().role(Role.USER.getValue()).content(question.getQuestion()).build());// 保存用户问题历史记录
       GenerationParam param =buildGenerationParam(messages);// GenerationParam param = buildQwenParam(messages);return Flux.create(sink ->{try{
                gen.streamCall(param,newResultCallback<GenerationResult>(){
                    @Override
                    publicvoidonEvent(GenerationResult message){

                        String newContent = message.getOutput().getChoices().get(0).getMessage().getContent();//改造message属性 为此属性赋值sessionId
                        message.getOutput().getChoices().get(0).getMessage().setToolCallId(question.getSessionId());
                        contentBuilder.append(newContent);if("stop".equals(message.getOutput().getChoices().get(0).getFinishReason())){
                            ChatQuestionHistory history =newChatQuestionHistory();
                            BeanUtils.copyProperties(question, history);
                            history.setUserId(userId);
                            history.setCreateBy(username);
                            history.setMessageRole(Role.USER.getValue());
                            history.setCreateTime(newDate());
                            chatQuestionHistoryMapper.insertChatQuestionHistory(history);
                            ChatAnswerHistory answerHistory =newChatAnswerHistory();
                            answerHistory.setId(message.getRequestId());
                            answerHistory.setAnswer(contentBuilder.toString());
                            answerHistory.setUserId(userId);
                            answerHistory.setSessionId(question.getSessionId());
                            answerHistory.setQuestionId(history.getId());
                            answerHistory.setMessageRole(message.getOutput().getChoices().get(0).getMessage().getRole());
                            answerHistory.setCreateBy(username);
                            answerHistory.setCreateTime(newDate());
                            chatAnswerHistoryMapper.insertChatAnswerHistory(answerHistory);
                            contentBuilder.setLength(0);// 清空StringBuilder}

                        sink.next(message);}

                    @Override
                    publicvoidonError(Exception err){
                        sink.error(err);}

                    @Override
                    publicvoidonComplete(){
                        sink.complete();}});}catch(Exception e){
                sink.error(e);}});}

    @Override
    public List<ChatHistoryVo>historyList(Long userId){return chatQuestionHistoryMapper.historyList(userId);}private GenerationParam buildGenerationParam(List<Message> messages){return GenerationParam.builder().model(ChatModelConstants.Models.QWEN_MAX).messages(messages).resultFormat(GenerationParam.ResultFormat.MESSAGE).topP(0.8).incrementalOutput(true).build();}//接入开源模型 chatGLMprivate QwenParam buildQwenParam(List<Message> messages){return QwenParam.builder().model("chatglm3-6b").messages(messages).resultFormat(QwenParam.ResultFormat.MESSAGE).topP(0.8).incrementalOutput(true)// get streaming output incrementally.build();}private List<Message>conversationHistory(String sessionId){//根据用户id   会话id  查询10条记录
        List<Message> messages =newArrayList<>();if(StringUtils.isEmpty(sessionId)){return messages;}
        messages = chatAnswerHistoryMapper.conversationHistory(SecurityUtils.getUserId(), sessionId);return messages;}}

三、查询方法

此处给出sql

<select id="conversationHistory" resultType="com.alibaba.dashscope.common.Message">SELECT
        content,
        role
    FROM(SELECT
                question AS content,
                message_role AS role,
                create_time
            FROM
                kim_chat_question_history
            WHERE
                user_id = #{userId}AND session_id = #{sessionId}UNIONALLSELECT
                answer AS content,
                message_role AS role,
                create_time
            FROM
                kim_chat_answer_history
            WHERE
                user_id = #{userId}AND session_id = #{sessionId})AS combined_data
    ORDERBY
        create_time
        LIMIT10</select><select id="historyList" resultType="com.kingoffice.system.domain.vo.ChatHistoryVo">SELECT
        t1.question,
        t1.id AS questionId,
        t1.create_time AS createTime,
        t2.answer,
        t2.id AS answerId
    FROM
        kim_chat_question_history t1
            INNERJOIN kim_chat_answer_history t2 ON t1.id = t2.question_id
    WHERE
        t1.user_id = #{userId}ORDERBY
        t1.create_time DESC</select>

四、调用展示

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

五、总结

至此,后端已实现多轮对话以及流式输出,代码仍有待完善的地方,比如对于token的控制,参数校验,会话时长等等都是需要考虑的问题,后续在给出具体的优化建议,另外关注到Spring新出了SpringAI组件可以快速的实现众多大模型的调用,还有阿里也推出了同样的微服务组件,感兴趣的小伙伴可以去看下,我们此处不再赘述,下一篇我们进行前端调用后端流式接口的demo展示。

标签: 人工智能 java ai

本文转载自: https://blog.csdn.net/weixin_44700323/article/details/140669767
版权归原作者 kimloner 所有, 如有侵权,请联系我们删除。

“java接入AI大模型个人实践(一)”的评论:

还没有评论