SpringBoot3+微信小程序实现AI智能体调用及对话1

369 阅读7分钟

背景

我们的IT系统中要调用AI模型现在变得超级简单,本文基于最新的SpringBoot3.4.4 + 微信多端应用框架实现基本的与AI对话的功能,模型使用阿里云的百炼平台。

效果

图片

图片

前提

    1)需要开通阿里云账户,并开通阿里云的百炼大模型平台,开通是免费的。
2)有基础的java开发能力,会使用springboot。

1、创建模型应用

    在阿里云的百炼大模型平台创建智能体应用:

图片

选择模型的时候,可以根据自己的应用场景选择模型,如果没有什么特别的,可以随便选一个:

图片

创建完应用后,在应用管理的列表页面,有一个应用ID,需要记录下来,等下调用需要用到:

图片

要通过接口调用该智能体应用,需要创建一个API-KEY,如图:

图片

并且把该API key要记录下来。

2、搭建后端代码结构

    JDK用17 ,工具用IDEA Community 版本 。

    使用IDEA创建一个maven项目:

图片

创建好对应的目录:

图片

最核心的就是controller、service目录、启动类GutApplication,配置文件application.yml 、pom.xml

编写pom.xml文件,并在IDEA右侧刷新,IDEA会自动下载依赖包:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/>
    </parent>

    <groupId>com.yunei</groupId>
    <artifactId>gut</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gut</name>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-neo4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.26</version>
        </dependency>
        <!-- Spring Boot Redis Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <!-- SpringDoc OpenAPI (Swagger 3) -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.4.0</version>
        </dependency>
        <!-- Jakarta Validation API for Spring Boot 3 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.huaweicloud/esdk-obs-java-bundle -->
        <dependency>
            <groupId>com.huaweicloud</groupId>
            <artifactId>esdk-obs-java-bundle</artifactId>
            <version>3.24.12</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.19.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <release>17</release>
                </configuration>
        </plugin>
        </plugins>
    </build>
</project>

其中最核心的是这个依赖,封装了对阿里云百炼大模型的调用:

解释

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <version>2.19.2</version>
</dependency>

3、后台调用智能体应用代码

public  String callPsycNoLogin(String userInMsg)
        throws ApiException, NoApiKeyException, InputRequiredException {
    if(StringUtils.isBlank(userInMsg)){
        return "请输入您的问题";
    }
    ApplicationParam param = ApplicationParam.builder()
            .apiKey(AIConstant.ali_api_key)
            .appId(AIConstant.ali_psyc_app_id)
            .prompt(userInMsg)
            .build();
    Application application = new Application();
    ApplicationResult result = application.call(param);
    return result.getOutput().getText();
}

输入参数:userInMsg,代表用户的输入。

ApplicationParam 就是调用的参数,其中要设置apiKey,AIConstant.ali_api_key就是我定义的一个常量,需替换成你们的智能体应用的api key。

AIConstant.ali_psyc_app_id 要替换成你们的应用ID。

这个方法应该放在service层,controller层写一个调用的类供前端调用:

@Operation(summary = "心理咨询师咨询", description = "心理咨询师咨询")
@PostMapping("/nologin/aiChatNoLogin")
public PageResult<String> aiChatNoLogin(
        @Parameter(description = "创建咨询聊天请求") @RequestBody @Valid CreateChatReq chatReq,
        HttpServletRequest request) {

    try {
        String msg = psyCService.callPsycNoLogin(chatReq.getUserInMsg());
        return PageResult.success(msg);
    }catch (Exception e){
        log.error(e.getMessage(),e);
        return PageResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR);
    }

}

CreateChatReq 是定义的前端传入的用户提问。

4、前端代码

前端使用微信小程序原生多端框架 + TDesign UI组件实现。一套代码可以生成安卓、IOS、微信小程序多端应用。微信官方文档还比较完备。可以使用微信开发者工具创建多端应用,也可以根据模板创建,如图:

图片

最核心的就5个文件:

图片

页面代码index.wxml实现了页面的基础框架,如下:

<wxs src="./index.wxs" module="utils" />

<t-navbar class="nav-bar" title="{{ name }}"  left-arrow/>

<view class="chat-container">
  <scroll-view class="content" scroll-y scroll-into-view="{{ anchor }}">
    <view class="messages">
      
      <block wx:for="{{ messages }}" wx:key="index">
        <view wx:if="{{ index === 0 || item.time - messages[index - 1].time > 120000 }}" class="time" >
          {{ utils.formatTime(item.time) }}
        </view>
      <!-- AI的消息 -->
        <view wx:if="{{ item.from === 0 }}" class="message-area">
          <view class="message self">
          <text space="nbsp">{{ item.content }}</text>
          <!-- <towxml nodes="{{item.content}}"/> -->
          <t-loading
            wx:if="{{ item.messageId === null }}"
            t-class="loading"
            theme="spinner"
            size="32rpx"
            class="wrapper"
          />
          </view>
          <t-avatar image="{{ myAvatar }}" size="small" />
        </view>
        <!-- 登录消息 -->
        <view wx:elif="{{ item.from === 3 }}" class="message-area">
          <t-avatar image="{{ avatar }}" size="small" />
          <view class="message other">
            请先登录,我会为您提供更专业的服务!
          <t-link theme="primary" content="点击登录" bind:tap="go2Login" hover/>
          </view>
        </view>
        <!-- 测评消息 -->
        <view wx:elif="{{ item.from === 4 }}" class="message-area">
          <t-avatar image="{{ avatar }}" size="small" />
          <view class="message other">
            您也可直接完成如下测评!
          <t-link theme="primary" size="small" content="1、MBTI职业性格测试" data-key="mbti" bind:tap="go2Test" hover/>
          <t-link theme="primary" size="small" content="2、DISC性格测试" bind:tap="go2Login" hover/>
          <t-link theme="primary" size="small" content="3、霍兰德职业兴趣测试" bind:tap="go2Login" hover/>
          <t-link theme="primary" size="small" content="4、九型人格测试" bind:tap="go2Login" hover/>
          <t-link theme="primary" size="small" content="5、五元性格测试" bind:tap="go2Login" hover/>
          </view>
        </view>
        <!-- AI的消息 -->
        <view wx:else class="message-area">
          <t-avatar image="{{ avatar }}" size="small" />
          <view class="message other">
            <towxml wx:if="{{item.textType=='markdown'}}" nodes="{{item.content}}"/>
          <text wx:else space="nbsp">{{ item.content }}</text>
          
          </view>
        </view>
      </block>
      <view id="bottom" />
    </view>
  </scroll-view>
</view>

<view class="block" style="margin-bottom: {{ keyboardHeight }}px" />
<view class="bottom" style="margin-bottom: {{ keyboardHeight }}px">
  <view class="input">
    <input
      value="{{ input }}"
      type="text"
      confirm-type="send"
      placeholder="请输入"
      placeholder-style="color: #00000066"
      adjust-position="{{ false }}"
      hold-keyboard
      confirm-hold
      bindkeyboardheightchange="handleKeyboardHeightChange"
      bindblur="handleBlur"
      bindinput="handleInput"
      bindconfirm="sendMessageFlux"
    />
  </view>
  <t-button class="send" theme="primary" shape="round" disabled="{{ !input }}" bind:tap="sendMessageFlux">发送</t-button>
</view>

index.less 代码定义了页面UI的样式

/* pages/chat/index.wxss */
.chat-container {
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  height: 100vh;
  font-size: 32rpx;
  background-color: rgb(255, 255, 255);
}

.nav-bar {
  border-bottom: 1rpx solid #e7e7e7;
}

.content {
  height: 0;
  flex-grow: 1;
}

.messages {
  display: flex;
  flex-direction: column;
  gap: 32rpx;
  padding: 32rpx 24rpx;
}

.time {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  height: 56rpx;
  color: #00000066;
  font-size: 24rpx;
  line-height: 40rpx;
}

.message-area {
  display: flex;
  align-items: flex-start;
  gap: 16rpx;
}

.message {
  position: relative;
  box-sizing: border-box;
  max-width: 510rpx;
  padding: 24rpx;
  font-size: 26rpx;
  line-height: 44rpx;
}

.message.self {
  border-radius: 24rpx 0 24rpx 24rpx;
  margin-left: auto;
  // background-color: #d9e1ff;
   // 添加 1px 浅灰色实线边框
   border: 1px solid #e5e5e5;
   // 添加卡片阴影(兼容性写法)
   box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
}

.message.other {
  border-radius: 0 24rpx 24rpx 24rpx;
  // background-color: #f3f3f3;
     // 添加 1px 浅灰色实线边框
     border: 1px solid #e5e5e5;
     // 添加卡片阴影(兼容性写法)
     box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
}

.loading {
  position: absolute;
  right: calc(100% + 16rpx);
  top: 50%;
  transform: translateY(-50%);
}

.block {
  height: calc(env(safe-area-inset-bottom) + 129rpx);
}

.bottom {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24rpx;
  padding: 24rpx 24rpx calc(env(safe-area-inset-bottom) + 24rpx);
  border-top: 1rpx solid #e7e7e7;
  background-color: #ffffff;
}

.input {
  box-sizing: border-box;
  flex-grow: 1;
  height: 80rpx;
  padding: 16rpx 32rpx;
  border-radius: 40rpx;
  border: 1rpx solid #dcdcdc;
  background: #f3f3f3;
  line-height: 48rpx;
}

.input > input {
  width: 100%;
  height: 48rpx;
  line-height: 48rpx;
  margin-bottom: 40rpx;
}

.send {
  width: 128rpx !important;
  margin: 0;
  font-weight: normal !important;
}

index.json定义了使用tdesign哪些组件

{
  "usingComponents": {
    "t-avatar": "tdesign-miniprogram/avatar/avatar",
    "t-loading": "tdesign-miniprogram/loading/loading",
    "t-button": "tdesign-miniprogram/button/button",
    "t-link": "tdesign-miniprogram/link/link",
    "towxml": "/towxml/towxml"
  },
  "navigationStyle": "custom"
}

页面逻辑index.js ,定义了页面的所有逻辑及数据

import {
  psycChatAPI,
  aiChatNoLoginAPI
} from '~/api/chat/aiAPI'
const app = getApp();
const {
  socket
} = app.globalData; // 获取已连接的socketTask
const psycMessage = {
  messageId: Date.now() + 4,
  from: 4,
  content: '',
  time: Date.now(),
  read: true
};

const loginMessage = {
  messageId: Date.now() + 3,
  from: 3, // 3代表提示登录的消息
  content: '',
  time: Date.now(),
  read: true
};
const tempLoad = {
  messageId: Date.now(),
  from: 1,
  content: '请稍等。。。\n',
  time: Date.now(),
  read: true
};
Page({
  /** 页面的初始数据 */
  data: {
    myAvatar: '/static/chat/avatar.png', // 自己的头像
    userId: null, // 对方userId
    avatar: '/static/chat/avatar-psyc.png', // 对方头像
    name: 'AI咨询师', // 对方昵称
    messages: [{
      messageId: 1,
      from: 'PYSC',
      content: '您好,有什么可以帮您?',
      time: Date.now(),
      read: true
    }], // 消息列表 { messageId, from, content, time, read }
    input: '', // 输入框内容
    anchor: '', // 消息列表滚动到 id 与之相同的元素的位置
    keyboardHeight: 0, // 键盘当前高度(px)
    isLogin: false,
    loginUser: null
  },

  /** 生命周期函数--监听页面加载 */
  onLoad(options) {
    // this.getOpenerEventChannel().on('update', this.update);
    var loginUser = wx.getStorageSync('userInfo');
    console.log("loginUser=" + JSON.stringify(loginUser))
    if (loginUser != "") {
      const {
        messages,
      } = this.data;
      messages.push(psycMessage)
      this.setData({
        isLogin: true,
        loginUser,
        messages
      })
    } else {
      //添加登录提醒
      const {
        messages,
      } = this.data;
      messages.push(loginMessage)

      this.setData({
        messages
      });
    }
  },
  go2Test(e) {
    var key = e.currentTarget.dataset.key;
    console.log(key)
    wx.navigateTo({
      url: '/pages/home/psyc/mbti/index',
    })
  },

  /** 发送消息 */
  async sendMessage() {
    const {
      userId,
      messages,
      input: content
    } = this.data;
    if (!content) return;
    var param = {
      userInMsg: content
    };
    var res = null;
    if (this.data.isLogin) {

      const message = {
        messageId: Date.now(),
        from: 0,
        content,
        time: Date.now(),
        read: true
      };

      messages.push(message);
      messages.push(tempLoad);
      this.setData({
        input: '',
        messages
      });

      //
      res = await psycChatAPI(param);
      console.log(JSON.stringify(res));
      if (res.data.code == 0) {
        var aiRep = res.data.data.aiOutMsg;
        //添加AI的回复
        const message2 = {
          messageId: Date.now() + 1,
          from: 1,
          content: aiRep,
          time: Date.now(),
          read: true
        };
        messages.push(message2)

      }
    } else { //未登录用户
      const message = {
        messageId: Date.now(),
        from: 0,
        content,
        time: Date.now(),
        read: true
      };

      messages.push(message);
      messages.push(tempLoad)
      this.setData({
        input: '',
        messages
      });
      console.log("send message =" + content)
      res = await aiChatNoLoginAPI(param)
      console.log("res ==>" + JSON.stringify(res))
      if (res?.data.code == 0) {
        var aiRep = res.data.data;
        //添加AI的回复
        const message2 = {
          messageId: Date.now() + 1,
          from: 1,
          content: aiRep,
          time: Date.now(),
          read: true
        };
        messages.push(message2)
        //添加登录提醒
        messages.push(loginMessage)
      }

    }
    this.setData({
      input: '',
      messages
    });
    // socket.send(JSON.stringify({ type: 'message', data: { userId, content } }));
    wx.nextTick(this.scrollToBottom);
  },
  async sendMessageFlux() {
    const {
      messages,
      input: content
    } = this.data;
    if (!content) return;

    console.log("content="+content)
    //ws传递到后台的参数
    var param = {
      content,
      type: 'psyc'
    };
    if (this.data.isLogin) {
      param['userId'] = this.data.loginUser.userId;
    }
    //用户的消息
    const message = {
      messageId: Date.now(),
      from: 0,
      content,
      time: Date.now(),
      read: true
    };
    messages.push(message);
    messages.push(tempLoad)
    this.setData({
      input: '',
      messages
    });
    // console.log("send messages =" + JSON.stringify(messages))
    //发送消息到后端
    socket.send({
      data: JSON.stringify(param)
    });


    var that = this;
    wx.onSocketMessage((result) => {
      console.log(JSON.stringify(result));
      //添加AI的回复
      var messages = this.data.messages;
      var message1 = messages.pop();
      var message2 = {
        messageId: Date.now() + 1,
        from: 1,
        content: '',
        time: Date.now(),
        read: true
      };
      if(result.data == 'FINISH'){
        var fullContent = message1['content'];
        let markdownContent= app.towxml(fullContent,"markdown")
        message2['content'] = markdownContent;
        message2['textType']='markdown'
      }else{
        message2['content'] = message1['content'] + result?.data;
      }
      
      messages.push(message2)
      that.setData({
        input: '',
        messages,
        anchor: 'bottom'
      });
    
    })
  },

  go2Login() {
    wx.navigateTo({
      url: '/pages/login/login',
    })
  },
  /** 生命周期函数--监听页面初次渲染完成 */
  onReady() {},

  /** 生命周期函数--监听页面显示 */
  onShow() {},

  /** 生命周期函数--监听页面隐藏 */
  onHide() {},

  /** 生命周期函数--监听页面卸载 */
  onUnload() {
    app.eventBus.off('update', this.update);
  },

  /** 页面相关事件处理函数--监听用户下拉动作 */
  onPullDownRefresh() {},

  /** 页面上拉触底事件的处理函数 */
  onReachBottom() {},

  /** 用户点击右上角分享 */
  onShareAppMessage() {},

  /** 更新数据 */
  update({
    userId,
    avatar,
    name,
    messages
  }) {
    this.setData({
      userId,
      avatar,
      name,
      messages: [...messages]
    });
    wx.nextTick(this.scrollToBottom);
  },

  /** 处理唤起键盘事件 */
  handleKeyboardHeightChange(event) {
    const {
      height
    } = event.detail;
    if (!height) return;
    this.setData({
      keyboardHeight: height
    });
    wx.nextTick(this.scrollToBottom);
  },

  /** 处理收起键盘事件 */
  handleBlur() {
    this.setData({
      keyboardHeight: 0
    });
  },

  /** 处理输入事件 */
  handleInput(event) {
    this.setData({
      input: event.detail.value
    });
  },


  /** 消息列表滚动到底部 */
  scrollToBottom() {
    this.setData({
      anchor: 'bottom'
    });
  },
});

微信小程序原生框架把显示wxml和逻辑.js文件分开,虽然文件多了,但是这样逻辑更清晰,比vue页面和逻辑放在一个文件中要好一些。

整个系统还在开发中,完整代码请进群获取
图片