背景
我们的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页面和逻辑放在一个文件中要好一些。
整个系统还在开发中,完整代码请进群获取