• vue+java实现简易AI问答组件(基于百度文心大模型)


    一、需求

    公司想要在页面中加入AI智能对话功能,故查找免费gpt接口,最终决定百度千帆大模型(进入官网官方文档中心);

    二、主要功能列举

    • AI智能对话;
    • 记录上下文回答环境;
    • 折叠/展开窗口;
    • 可提前中止回答;
    • 回答内容逐字展示并语音播报;

    三、效果图

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

    四、技术选型

    1、前端环境

    • node(14.21.3)
    • VueCli 2
    • element-ui(^2.15.14)
    • axios
    • node-sass(^4.14.1)
    • sass-loader(^7.3.1)
    • js-md5(^0.8.3)

    2、后端环境

    • JDK8
    • springboot

    五、声明

    • 本文章以及源码纯粹自己写着玩,等于是个demo,有许多需要完善和优化的地方,仅供大家参考,有错误的地方欢迎大家批评指正~~
    • 由于作者其实是java,所以前端代码中如果看到神奇的地方,希望大家包涵,哈哈哈哈

    四、百度千帆大模型应用创建

    1、访问官网,注册账号并登录;

    2、选择“应用接入”-“创建应用”

    在这里插入图片描述
    进去填一个应用名及描述即可,服务默认全勾选上;

    3、保存后返回应用列表,获取api key和secret key

    在这里插入图片描述

    PS:

    百度提供的大模型服务有好多种,我此处是白嫖的其中一个免费的,如下图:
    其中ERNIE开头的是百度自己的,文心一言用的就是这种,其他有些是三方大模型;
    在这里插入图片描述

    具体不同服务之间有什么区别可以看官方介绍,个人觉得免费的几个主要在于轻量等级、响应速度、回答内容复杂程度、可保存的上下文大小等几个方面;

    五、部分后台代码

    1、官网下载java sdk或者引入百度千帆pom

    官放文档地址:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/7ltgucw50
    java SDK地址:https://github.com/baidubce/bce-qianfan-sdk/tree/main/java
    maven仓库地址:https://mvnrepository.com/artifact/com.baidubce/qianfan

    POM:

    
    
        com.baidubce
        qianfan
        0.0.4
    
    

    2、创建springboot项目,并导入sdk或引入千帆pom

    此处是把sdk导入到工程中;

    3、项目代码结构如下

    在这里插入图片描述

    pom.xml :

    
    	4.0.0
    	com.baidu
    	aichat
    	0.0.1-SNAPSHOT
    	war
    	aichat
    
    	
    		org.springframework.boot
    		spring-boot-starter-parent
    		2.5.9
    	
    	
    		
    		
    			org.springframework.boot
    			spring-boot-dependencies
    			2.2.13.RELEASE
    			pom
    			import
    		
    		
    			org.springframework.boot
    			spring-boot-starter-web
    		
    
    		
    			junit
    			junit
    			3.8.1
    			test
    		
    
    		
    		    org.apache.commons
    		    commons-lang3
    		
    		
    		
    			com.google.code.gson
    			gson
    			2.10.1
    		
    
    		
    			org.apache.httpcomponents.client5
    			httpclient5
    			5.3.1
    		
    
    
    	
    
    	
    		
    			
    				org.springframework.boot
    				spring-boot-maven-plugin
    				2.1.1.RELEASE
    				
    					true 
    				
    				
    					
    						
    							repackage
    						
    					
    				
    			
    			
    				org.apache.maven.plugins
    				maven-war-plugin
    				3.1.0
    				
    					false
    					${project.artifactId}
    				
    			
    		
    		${project.artifactId}
    	
    
    
    

    AiChatController.java:

    package com.baidubce.controller;
    
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.baidubce.qianfan.Qianfan;
    import com.baidubce.qianfan.core.builder.ChatBuilder;
    import com.baidubce.qianfan.model.chat.ChatResponse;
    import com.baidubce.qianfan.model.chat.Message;
    import com.baidubce.utils.JsonUtils;
    import com.baidubce.utils.SecUtils;
    
    @RestController
    public class AiChatController {
    
    	private static final String accessKey = "你创建的应用的API Key";
    	private static final String secretKey = "你创建的应用的Secret Key";
    
    	private static Qianfan qianfan = new Qianfan("OAuth", accessKey, secretKey);
    
    	
    	/**
    	* 	参数:
    	* 		messages: 对话记录,role:user是用户,assistant是AI,如:[{"role":"user","content":"1"},{"role":"assistant","content":"“1”是一个数字。"},{"role":"user","content":"你是"},{"role":"assistant","content":""}]
    	* 		timestamp:请求毫秒值,1717742825695
    	* 		signature: 签名,4bc9c3b8dbe4de5bc924b6fa0506c606
    	* @author x轩
    	* @version 2024年6月7日 下午2:46:32
    	*/
    	@PostMapping("/sendMsg")
    	public String sendMsg(@RequestBody Map<String, Object> params) {
    		// 验签,我自己加的,防止恶意调用,作用不大,提高门槛而已
    		if(!checkSign(params)) {
    			return "签名不正确!";
    		}
    		String result = null;
    		try {
    			result = chat(String.valueOf(params.get("messages")));
    		} catch (Exception e) {
    			e.printStackTrace();
    			return "接口繁忙,请稍后再试!";
    		}
    		return result;
    	}
    
    	/**
    	 * 	参数:
    	 * 		messages: 业务参数
    	* 		timestamp:请求毫秒值
    	* 		signature: 签名
    	* 
    	*	加签规则:
    	*		要求1:timestamp和当前系统时间不能超过5秒钟
    	*		要求2:MYCHAT|timestamp|messages拼接后MD53次加密
    	*
    	* @author x轩
    	* @version 2024年6月6日 下午4:13:03
    	*/
    	private boolean checkSign(Map<String, Object> params) {
    		String timestamp = String.valueOf(params.get("timestamp"));
    		String messages = String.valueOf(params.get("messages"));
    		String signature = String.valueOf(params.get("signature"));
    		if(StringUtils.isAnyBlank(timestamp, messages, signature)) {
    			return false;
    		}
    
    		// 1.判断时间
    		if((System.currentTimeMillis()- Long.valueOf(timestamp))>5000) {
    			// 过期
    			return false;
    		}
    		// 2.验签
    		String p = "MYCHAT|"+timestamp+"|"+messages;
    		String md5of3 = SecUtils.encoderByMd5With32Bit(SecUtils.encoderByMd5With32Bit(SecUtils.encoderByMd5With32Bit(p)));
    		if(!signature.equalsIgnoreCase(md5of3)) {
    			return false;
    		}
    		return true;
    	}
    
    	public static void main(String[] args) {
    //        chat("对于调休你怎么看");
    		chatStream("介绍一下自己");
    	}
    
    	private static String chat(String messages) {
    		
    		ChatBuilder bulder = qianfan.chatCompletion()
    //        		.model("ERNIE-Speed-128K")
    //        		.model("ERNIE-Speed-8K")
    				.model("ERNIE-Tiny-8K");
    		List<Message> messageList = JsonUtils.readValues(messages, Message.class);
    		// 过滤一下,去除空内容对象
    		messageList = messageList.stream().filter(m->{
    			return StringUtils.isNotBlank(m.getContent());
    		}).collect(Collectors.toList());
    		for(Message m : messageList) {
    			bulder.addMessage(m);
    		}
    		ChatResponse response = bulder.execute();
    		return response.getResult();
    	}
    
    	private static void chatStream(String message) {
    		Iterator<ChatResponse> stream = qianfan.chatCompletion()
    //		.model("ERNIE-Speed-128K")
    //		.model("ERNIE-Speed-8K")
    				.model("ERNIE-Tiny-8K").addMessage("user", message).executeStream();
    		while(stream.hasNext()) {
    			System.out.println(stream.next().getResult());
    		}
    	}
    }
    

    六、 Vue部分代码

    1、vue.config.js

    const port = process.env.port || process.env.npm_config_port || 80 // 端口
    
    module.exports = {
        
        lintOnSave: false,
        publicPath: "/aichat-front",
        assetsDir: 'static',
        // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
        productionSourceMap: false,
        devServer: {
            host: '0.0.0.0',
            port: port,
            open: true,
            proxy: {
                ['/aichat']: {
                    target: `http://127.0.0.1:8080/aichat`,
                    changeOrigin: true,
                    pathRewrite: {
                        ['^/aichat']: ''
                    }
                },
            },
        },
    }
    

    2、App.vue

    <template>
      <div id="app">
        <qian-fan-chat/>
      </div>
    </template>
    
    <script>
    import QianFanChat from './components/QianFanChat'
    
    export default {
      name: 'App',
      components: {
        QianFanChat
      }
    }
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
    

    3、@/components/QianFanChat组件

    <template>
        <div class="chat-div">
            <div v-show="showChatBox" class="chat-main">
                <div id="messagediv" class="messagediv">
                    <div class="item">
                        <img class="avatar" :src="aiAvatar" >
                        <div class="answerDiv">
                            <p>我是AI智能小助手,有问题请咨询我吧!</p>
                        </div>
                    </div>
                    <div v-for="(item, index) in messageList" class="item">
                        <img v-if="item.role=='assistant'" class="avatar" :src="aiAvatar" >
                        <img v-if="item.role=='user'" class="avatar" :src="userAvatar" >
                        <div class="answerDiv">
                            <p v-if="!item.loading" v-html="item.content"></p>
                            <p v-else>思考中 <i class="el-icon-loading"></i></p>
                            <a v-if="index==messageList.length-1 &&item.role=='assistant' && (loading || speaking)" href="#" @click="stopAnswer">停止回答</a>
                        </div>
                    </div>
                </div>
                <div class="sendDiv">
                    <el-input @keyup.enter.native="getAnswer" v-model="question" placeholder="输入中医药相关内容搜一搜"></el-input>
                    <el-button @click="getAnswer">
                        <i class="el-icon-s-promotion"></i>
                    </el-button>
                </div>
            </div>
            <div v-show="showChatBox" class="close-btn" @click="showChatBox=false">
                <i class="el-icon-close"></i>
            </div>
            <div v-show="!showChatBox" class="small-window" @click="showChatBox=true">
                AI问答
            </div>
        </div>
    </template>
    <script>
    const msg = new SpeechSynthesisUtterance();
    
    import { sendMsg } from '@/api/aichat';
    import { sign } from '@/utils/securityUtil'
    import aiAvatar from '@/assets/images/ai-avatar.jpg';
    import userAvatar from '@/assets/images/user-avatar.jpg';
    
    export default {
        name: 'QianFanChat',
        data(){
            return {
                showChatBox: false,
                loading: false,
                speaking: false,
                aiAvatar,userAvatar,
                question:'',
                messageList:[
                ],
                // 逐字输出 START
                timer: null,
                length: 0,
                index: 0,
                // 逐字输出 END
            }
        },
        mounted(){
    
        },
        methods: {
            getAnswer(){
                if(this.loading){
                    this.$message({type: 'warning', message:'正在回答,请耐心等待!'});
                    return;
                }
                if(!this.question.trim()){
                    this.$message({type: 'warning', message:'请输入内容!'});
                    return;
                }
                this.loading = true;
                let tmpQustion = this.question;
                this.question = '';
                
                this.messageList.push({ role:'user', content: tmpQustion, loading: false });
                this.messageList.push({ role:'assistant', content: '' , loading: true});
    
                this.$nextTick(()=>{
                    this.scroll();
                })
    
                let params = {
                    messages: JSON.stringify(this.messageList)
                };
                
                params.timestamp = new Date().getTime();
                
                params.signature = sign(params);
                sendMsg(params).then(res=>{
                    // 判断loading是否被中断(停止回答可中断)
                    if(!this.loading){
                        // 点击了“停止回答”
                        return;
                    }
                    // 接口请求完毕,替换最后一条内容
                    this.messageList[this.messageList.length-1].loading = false;
    
                    this.index = 0;
                    this.length = res.length;
                    this.handleSpeak(res);
                    // 一个字一个字给我蹦
                    this.timer = setInterval(()=>{
                        if(this.index<=this.length-1){
                            let word = res.charAt(this.index);
                            if(word=='\n'){
                                word = '
    '
    } this.messageList[this.messageList.length-1].content += word; this.index++; }else{ // 结束 this.loading = false; clearInterval(this.timer); } this.scroll(); }, 30); }) }, scroll(){ messagediv.scrollTo({ top: messagediv.scrollHeight, }) }, stopAnswer(){ this.handleStop(); clearInterval(this.timer); this.length = 0; this.index = 0; this.loading = false; // 判断最后一条内容是不是空,是则给上默认输出 let lastMsg = this.messageList[this.messageList.length-1]; if(!lastMsg.content){ lastMsg.loading = false; lastMsg.content = '请继续向我提问吧!'; } }, // 语音播报的函数 handleSpeak(text) { this.handleStop(); this.speaking = true; // 处理多音字 msg.text = text; // 朗读内容 msg.lang = "zh-CN"; // 使用的语言:中文 msg.volume = 0.5; // 声音音量:1 设置将在其中发言的音量。区间范围是0到1,默认是1 msg.rate = 1.6; // 语速:1 设置说话的速度。默认值是1,范围是0.1到10,表示语速的倍数,例如2表示正常语速的两倍 msg.pitch = 1.5; // 音高:2 设置说话的音调(音高)。范围从0(最小)到2(最大)。默认值为1 // msg.voiceURI = 'Google 普通话(中国大陆)'; msg.onstart = (e)=>{ }; msg.onend = (e)=>{ this.speaking = false; }; msg.onboundary = (e) => { } speechSynthesis.speak(msg); // 播放 }, // 语音停止 handleStop(e) { this.speaking = false; msg.text = e; msg.lang = "zh-CN"; speechSynthesis.cancel(msg); }, } } </script> <style lang="scss" scoped> ::v-deep { .el-input { width: 270px; input { background-color: rgba(0,0,0,.5); border: none; color: white; } } .el-button { background-color: rgba(0,0,0,.5); border: none; margin-left: 10px; padding: 0 20px; i { font-size: 20px; color: white; } &:focus { background-color: rgba(0,0,0,.5); } &:hover { background-color: white; i { color: black; } } } } .chat-div { position: absolute; top: 0; left: 0; display: flex; z-index: 99; cursor: pointer; .close-btn { margin-top: 18px; color: white; background-color: rgba(0, 0, 0, .6); height: 30px; width: 30px; text-align: center; line-height: 30px; border-radius: 50%; } .small-window { color: white; margin-top: 18px; padding: 10px; background-color: rgba(16, 168, 129, .8); width: 26px; font-size: 20px; border-top-right-radius: 10px; border-bottom-right-radius: 10px; } } .chat-main { border-radius: 4px; margin: 10px 0; padding: .1rem .1rem; width: 374px; .messagediv { overflow: auto; max-height: 60vh; .item { display: flex; margin-bottom: .1rem; .avatar { width: 40px; height: 40px; } .answerDiv { p { color: white; background-color: rgba(0, 0, 0, .6); padding: 10px; margin: 0 10px; font-size: 16px; border-radius: 6px; text-align: left; } a { cursor: pointer; font-size: 15px; color: red; text-decoration: underline; margin-left: 10px; font-weight: bold; } } } &::-webkit-scrollbar { width: 0; } } .sendDiv { display: flex; justify-content: end; margin: 10px 10px 0 0; } } </style>

    4、@/utils/request.js

    import axios from 'axios'
    import { Notification, MessageBox, Message } from 'element-ui'
    
    axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
    // 创建axios实例
    const service = axios.create({
      // axios中请求配置有baseURL选项,表示请求URL公共部分
      baseURL: process.env.VUE_APP_BASE_API,
      // 超时
      timeout: 50000
    })
    // request拦截器
    service.interceptors.request.use(config => {
      // get请求映射params参数
      if (config.method === 'get' && config.params) {
        let url = config.url + '?';
        for (const propName of Object.keys(config.params)) {
          const value = config.params[propName];
          var part = encodeURIComponent(propName) + "=";
          if (value !== null && typeof (value) !== "undefined") {
            if (typeof value === 'object') {
              for (const key of Object.keys(value)) {
                if (value[key] !== null && typeof (value[key]) !== 'undefined') {
                  let params = propName + '[' + key + ']';
                  let subPart = encodeURIComponent(params) + '=';
                  url += subPart + encodeURIComponent(value[key]) + '&';
                }
              }
            } else {
              url += part + encodeURIComponent(value) + "&";
            }
          }
        }
        url = url.slice(0, -1);
        config.params = {};
        config.url = url;
      }
      return config
    }, error => {
      console.log(error)
      Promise.reject(error)
    })
    
    // 响应拦截器
    service.interceptors.response.use(res => {
      return res.data;
    
    },
      error => {
        console.log('err' + error)
        let { message } = error;
        if (message == "Network Error") {
          message = "后端接口连接异常";
        }
        else if (message.includes("timeout")) {
          message = "系统接口请求超时";
        }
        else if (message.includes("Request failed with status code")) {
          message = "系统接口" + message.substr(message.length - 3) + "异常";
        }
        Message({
          message: message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(error)
      }
    )
    
    export default service
    
    

    5、@/utils/securityUtil.js

    PS: 由于这个项目不需要登录,我又怕接口泄露导致别人恶意调用,所以给接口加了个签名,具体策略大家可以自定义(讲真的,没什么用,前台加签别人打开调试模式照样可以看到加签策略。。。为了应对这个情况,我把前台加签JS给做了个混淆,算是增加一下门槛吧;还可以禁止用户点击F12和右键事件【具体代码见此篇文章】)

    // 加签方法,方法接收两个参数: timestamp和messages(消息JSON字符串),返回签名;已混淆,以下代码具体签名策略如下:固定字符串"MYCHAT"、时间戳、消息体用|拼接后进行3次MD5加密:如MYCHAT|1718181053994|[{"role":"user","content":"1"},{"role":"assistant","content":"“1”是一个数字。"}]
    const _0x4e66=['MYCHAT','timestamp'];const _0x3524=function(_0x4e6602,_0x35247b){_0x4e6602=_0x4e6602-0x0;let _0x2ee47d=_0x4e66[_0x4e6602];return _0x2ee47d;};import _0x50d0de from'js-md5';export function sign(_0x5c789a){let _0x1bf721='|'+_0x5c789a[_0x3524('0x1')];let _0x202f97='|'+_0x5c789a['messages'];let _0x74f806=_0x3524('0x0')+_0x1bf721+_0x202f97;_0x74f806=_0x50d0de(_0x74f806);_0x74f806=_0x50d0de(_0x74f806);_0x74f806=_0x50d0de(_0x74f806);return _0x74f806;}
    

    6、@/api/aichat.js

    import request from '@/utils/request'
    
    export function sendMsg(data) {
        return request({
          url: '/sendMsg',
          method: 'post',
          data: data
        })
      }
    

    有问题或者需要完整项目源码的话私聊吧,关机下班底薪到手~

  • 相关阅读:
    SSM框架的科普有毒蘑菇网站系统源码
    面试问题-理解数字后仿,其次针对性理解数字后仿中的sdf文件(约束文件)的作用
    ISCSLP 2022 | NPU-ASLP实验室8篇论文被录用
    【MyAndroid】viewpage+cardView卡片海报效果展示--100个经典UI设计模板(99/100)
    Bresenham 算法
    MyCat 管理及监控
    网络协议--ICMP:Internet控制报文协议
    pygame播放视频并实现音视频同步
    MongoDB备份与恢复以及导入导出
    windows通过nginx反向代理配置https安装SSL证书
  • 原文地址:https://blog.csdn.net/lx_nhs/article/details/139627599