1.概述
訊飛星火大模型是科大訊飛最近開放的擁有跨領域的知識和語言理解能力的大模型,能夠完成問答對話和文學創(chuàng)作等。由于訊飛星火大模型最近可以免費試用,開發(fā)者都可以免費申請一個QPS不超過2的賬號,用來實現(xiàn)對平臺能力的驗證。本文將利用Springboot框架對星火大模型進行整合,使其能夠提供簡單的問答能力。
2.Springboot整合大模型
2.1 申請開發(fā)者賬號
訊飛星火認知大模型需要在訊飛星火官網(wǎng)進行申請(如下圖所示),點擊免費試用按鈕,填寫相關信息即可。
申請成功后可以在控制臺查看對應的賬號信息(如下圖所示),APPID、APPKey、APPSecret都是唯一的,不要輕易泄漏。
至此,賬號申請工作完成。由于本文主要展示的是利用JAVA語言來實現(xiàn)對大模型的調用,因此可以在API文檔中下載JAVA帶上下文的調用示例(如下圖所示),通過該文檔中的代碼可以快速進行一個簡單的小測試。
2.2 接口文檔參數(shù)分析
在訊飛星火認知大模型的對接文檔中,由于結果是流式返回的(不是一次性返回),因此案例中代碼通過WebSocket長連接方式與服務器建立連接并發(fā)送請求,實時接收返回結果。接口請求參數(shù)具體如下:
{
"header": {
"app_id": "12345",
"uid": "12345"
},
"parameter": {
"chat": {
"domain": "general",
"temperature": 0.5,
"max_tokens": 1024,
}
},
"payload": {
"message": {
# 如果想獲取結合上下文的回答,需要開發(fā)者每次將歷史問答信息一起傳給服務端,如下示例
# 注意:text里面的所有content內容加一起的tokens需要控制在8192以內,開發(fā)者如有較長對話需求,需要適當裁剪歷史信息
"text": [
{"role": "user", "content": "你是誰"} # 用戶的歷史問題
{"role": "assistant", "content": "....."} # AI的歷史回答結果
# ....... 省略的歷史對話
{"role": "user", "content": "你會做什么"} # 最新的一條問題,如無需上下文,可只傳最新一條問題
]
}
}
}
上述請求中對應的參數(shù)解釋如下:
在這里需要注意的是:app_id就是我們申請的APPID,uid可以區(qū)分不同用戶。如果想要大模型能夠根據(jù)結合上下文去進行問題解答,就要把歷史問題和歷史回答結果全部傳回服務端。
針對上述請求,大模型的接口響應結果如下:
# 接口為流式返回,此示例為最后一次返回結果,開發(fā)者需要將接口多次返回的結果進行拼接展示
{
"header":{
"code":0,
"message":"Success",
"sid":"cht000cb087@dx18793cd421fb894542",
"status":2
},
"payload":{
"choices":{
"status":2,
"seq":0,
"text":[
{
"content":"我可以幫助你的嗎?",
"role":"assistant",
"index":0
}
]
},
"usage":{
"text":{
"question_tokens":4,
"prompt_tokens":5,
"completion_tokens":9,
"total_tokens":14
}
}
}
}
返回字段的解釋如下:
需要注意的是:由于請求結果流式返回,因此需要根據(jù)header中的狀態(tài)值status來進行判斷(0代表首次返回結果,1代表中間結果,2代表最后一個結果),一次請求過程中可能會出現(xiàn)多個status為1的結果。
2.3 設計思路
本文設計思路如下圖所示:
客戶端通過webSocket的方式與整合大模型的Springboot進行連接建立,整合大模型的Springboot在接收到客戶端請求時,會去創(chuàng)建與訊飛大模型服務端的webSocket長連接(每次請求會創(chuàng)建一個長連接,當獲取到所有請求內容后,會斷開長連接)。由于本文使用的賬號為開發(fā)者賬號(非付費模式),因此并發(fā)能力有限,本文采用加鎖方式來控制請求訪問。
Springboot服務與客戶端的交互邏輯如下圖所示:
Springboot服務與訊飛認知大模型的交互邏輯如下圖所示:
2.3 項目結構
2.4 核心代碼
2.4.1 pom依賴
<properties>
<netty.verson>4.1.45.Final</netty.verson>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.verson}</version>
</dependency>
</dependencies>
2.4.2 application.properties配置文件
server.port=9903
xf.config.hostUrl=https://spark-api.xf-yun.com/v2.1/chat
xf.config.appId=
xf.config.apiSecret=
xf.config.apiKey=
#最大響應時間,單位:秒
xf.config.maxResponseTime=30
2.4.3 config配置文件
@Data
@Component
@ConfigurationProperties("xf.config")
public class XFConfig {
private String appId;
private String apiSecret;
private String apiKey;
private String hostUrl;
private Integer maxResponseTime;
}
2.4.4 listener文件
XFWebClient類主要用于發(fā)送請求至大模型服務端,內部有鑒權方法。
/**
* @Author: ChengLiang
* @CreateTime: 2023-10-19 11:04
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
@Component
public class XFWebClient {
@Autowired
private XFConfig xfConfig;
/**
* @description: 發(fā)送請求至大模型方法
* @author: ChengLiang
* @date: 2023/10/19 16:27
* @param: [用戶id, 請求內容, 返回結果監(jiān)聽器listener]
* @return: okhttp3.WebSocket
**/
public WebSocket sendMsg(String uid, List<RoleContent> questions, WebSocketListener listener) {
// 獲取鑒權url
String authUrl = null;
try {
authUrl = getAuthUrl(xfConfig.getHostUrl(), xfConfig.getApiKey(), xfConfig.getApiSecret());
} catch (Exception e) {
log.error("鑒權失敗:{}", e);
return null;
}
// 鑒權方法生成失敗,直接返回 null
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
// 將 https/http 連接替換為 ws/wss 連接
String url = authUrl.replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
// 建立 wss 連接
WebSocket webSocket = okHttpClient.newWebSocket(request, listener);
// 組裝請求參數(shù)
JSONObject requestDTO = createRequestParams(uid, questions);
// 發(fā)送請求
webSocket.send(JSONObject.toJSONString(requestDTO));
return webSocket;
}
/**
* @description: 鑒權方法
* @author: ChengLiang
* @date: 2023/10/19 16:25
* @param: [訊飛大模型請求地址, apiKey, apiSecret]
* @return: java.lang.String
**/
public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
URL url = new URL(hostUrl);
// 時間
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = format.format(new Date());
// 拼接
String preStr = "host: " + url.getHost() + "\n" +
"date: " + date + "\n" +
"GET " + url.getPath() + " HTTP/1.1";
// SHA256加密
Mac mac = Mac.getInstance("hmacsha256");
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256");
mac.init(spec);
byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8));
// Base64加密
String sha = Base64.getEncoder().encodeToString(hexDigits);
// 拼接
String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha);
// 拼接地址
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().//
addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).//
addQueryParameter("date", date).//
addQueryParameter("host", url.getHost()).//
build();
return httpUrl.toString();
}
/**
* @description: 請求參數(shù)組裝方法
* @author: ChengLiang
* @date: 2023/10/19 16:26
* @param: [用戶id, 請求內容]
* @return: com.alibaba.fastjson.JSONObject
**/
public JSONObject createRequestParams(String uid, List<RoleContent> questions) {
JSONObject requestJson = new JSONObject();
// header參數(shù)
JSONObject header = new JSONObject();
header.put("app_id", xfConfig.getAppId());
header.put("uid", uid);
// parameter參數(shù)
JSONObject parameter = new JSONObject();
JSONObject chat = new JSONObject();
chat.put("domain", "generalv2");
chat.put("temperature", 0.5);
chat.put("max_tokens", 4096);
parameter.put("chat", chat);
// payload參數(shù)
JSONObject payload = new JSONObject();
JSONObject message = new JSONObject();
JSONArray jsonArray = new JSONArray();
jsonArray.addAll(questions);
message.put("text", jsonArray);
payload.put("message", message);
requestJson.put("header", header);
requestJson.put("parameter", parameter);
requestJson.put("payload", payload);
return requestJson;
}
}
XFWebSocketListener 類主要功能是與星火認知大模型建立webSocket連接,核心代碼如下:
/**
* @Author: ChengLiang
* @CreateTime: 2023-10-18 10:17
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
public class XFWebSocketListener extends WebSocketListener {
//斷開websocket標志位
private boolean wsCloseFlag = false;
//語句組裝buffer,將大模型返回結果全部接收,在組裝成一句話返回
private StringBuilder answer = new StringBuilder();
public String getAnswer() {
return answer.toString();
}
public boolean isWsCloseFlag() {
return wsCloseFlag;
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
log.info("大模型服務器連接成功!");
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
JsonParse myJsonParse = JSON.parseObject(text, JsonParse.class);
log.info("myJsonParse:{}", JSON.toJSONString(myJsonParse));
if (myJsonParse.getHeader().getCode() != 0) {
log.error("發(fā)生錯誤,錯誤信息為:{}", JSON.toJSONString(myJsonParse.getHeader()));
this.answer.append("大模型響應異常,請聯(lián)系管理員");
// 關閉連接標識
wsCloseFlag = true;
return;
}
List<Text> textList = myJsonParse.getPayload().getChoices().getText();
for (Text temp : textList) {
log.info("返回結果信息為:【{}】", JSON.toJSONString(temp));
this.answer.append(temp.getContent());
}
log.info("result:{}", this.answer.toString());
if (myJsonParse.getHeader().getStatus() == 2) {
wsCloseFlag = true;
//todo 將問答信息入庫進行記錄,可自行實現(xiàn)
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
try {
if (null != response) {
int code = response.code();
log.error("onFailure body:{}", response.body().string());
if (101 != code) {
log.error("訊飛星火大模型連接異常");
}
}
} catch (IOException e) {
log.error("IO異常:{}", e);
}
}
}
2.4.5 netty文件
NettyServer主要是用來監(jiān)聽指定端口,接收客戶端的webSocket請求。
@Slf4j
@Component
public class NettyServer {
/**
* webSocket協(xié)議名
*/
private static final String WEBSOCKET_PROTOCOL = "WebSocket";
/**
* 端口號
*/
@Value("${webSocket.netty.port:62632}")
private int port;
/**
* webSocket路徑
*/
@Value("${webSocket.netty.path:/webSocket}")
private String webSocketPath;
@Autowired
private WebSocketHandler webSocketHandler;
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
/**
* 啟動
*
* @throws InterruptedException
*/
private void start() throws InterruptedException {
bossGroup = new NioEventLoopGroup();
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
// bossGroup輔助客戶端的tcp連接請求, workGroup負責與客戶端之前的讀寫操作
bootstrap.group(bossGroup, workGroup);
// 設置NIO類型的channel
bootstrap.channel(NioServerSocketChannel.class);
// 設置監(jiān)聽端口
bootstrap.localAddress(new InetSocketAddress(port));
// 連接到達時會創(chuàng)建一個通道
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 流水線管理通道中的處理程序(Handler),用來處理業(yè)務
// webSocket協(xié)議本身是基于http協(xié)議的,所以這邊也要使用http編解碼器
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ObjectEncoder());
// 以塊的方式來寫的處理器
ch.pipeline().addLast(new ChunkedWriteHandler());
/*
說明:
1、http數(shù)據(jù)在傳輸過程中是分段的,HttpObjectAggregator可以將多個段聚合
2、這就是為什么,當瀏覽器發(fā)送大量數(shù)據(jù)時,就會發(fā)送多次http請求
*/
ch.pipeline().addLast(new HttpObjectAggregator(8192));
/*
說明:
1、對應webSocket,它的數(shù)據(jù)是以幀(frame)的形式傳遞
2、瀏覽器請求時 ws://localhost:58080/xxx 表示請求的uri
3、核心功能是將http協(xié)議升級為ws協(xié)議,保持長連接
*/
ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
// 自定義的handler,處理業(yè)務邏輯
ch.pipeline().addLast(webSocketHandler);
}
});
// 配置完成,開始綁定server,通過調用sync同步方法阻塞直到綁定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
// 對關閉通道進行監(jiān)聽
channelFuture.channel().closeFuture().sync();
}
/**
* 釋放資源
*
* @throws InterruptedException
*/
@PreDestroy
public void destroy() throws InterruptedException {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
}
if (workGroup != null) {
workGroup.shutdownGracefully().sync();
}
}
@PostConstruct()
public void init() {
//需要開啟一個新的線程來執(zhí)行netty server 服務器
new Thread(() -> {
try {
start();
log.info("消息推送線程開啟!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
WebSocketHandler主要用于接收客戶端發(fā)送的消息,并返回消息。
/**
* @Author: ChengLiang
* @CreateTime: 2023-10-17 15:14
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Autowired
private PushService pushService;
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
log.info("handlerAdded被調用,{}", JSON.toJSONString(ctx));
//todo 添加校驗功能,校驗合法后添加到group中
// 添加到channelGroup 通道組
NettyGroup.getChannelGroup().add(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("服務器收到消息:{}", msg.text());
// 獲取用戶ID,關聯(lián)channel
JSONObject jsonObject = JSON.parseObject(msg.text());
String channelId = jsonObject.getString("uid");
// 將用戶ID作為自定義屬性加入到channel中,方便隨時channel中獲取用戶ID
AttributeKey<String> key = AttributeKey.valueOf("userId");
//String channelId = CharUtil.generateStr(uid);
NettyGroup.getUserChannelMap().put(channelId, ctx.channel());
boolean containsKey = NettyGroup.getUserChannelMap().containsKey(channelId);
//通道已存在,請求信息返回
if (containsKey) {
//接收消息格式{"uid":"123456","text":"中華人民共和國成立時間"}
String text = jsonObject.getString("text");
//請求大模型服務器,獲取結果
ResultBean resultBean = pushService.pushMessageToXFServer(channelId, text);
String data = (String) resultBean.getData();
//推送
pushService.pushToOne(channelId, JSON.toJSONString(data));
} else {
ctx.channel().attr(key).setIfAbsent(channelId);
log.info("連接通道id:{}", channelId);
// 回復消息
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(ResultBean.success(channelId))));
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
log.info("handlerRemoved被調用,{}", JSON.toJSONString(ctx));
// 刪除通道
NettyGroup.getChannelGroup().remove(ctx.channel());
removeUserId(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("通道異常:{}", cause.getMessage());
// 刪除通道
NettyGroup.getChannelGroup().remove(ctx.channel());
removeUserId(ctx);
ctx.close();
}
private void removeUserId(ChannelHandlerContext ctx) {
AttributeKey<String> key = AttributeKey.valueOf("userId");
String userId = ctx.channel().attr(key).get();
NettyGroup.getUserChannelMap().remove(userId);
}
}
2.4.6 service文件
PushServiceImpl 主要用于發(fā)送請求至訊飛大模型后臺獲取返回結果,以及根據(jù)指定通道發(fā)送信息至用戶。
/**
* @Author: ChengLiang
* @CreateTime: 2023-10-17 15:58
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
@Service
public class PushServiceImpl implements PushService {
@Autowired
private XFConfig xfConfig;
@Autowired
private XFWebClient xfWebClient;
@Override
public void pushToOne(String uid, String text) {
if (StringUtils.isEmpty(uid) || StringUtils.isEmpty(text)) {
log.error("uid或text均不能為空");
throw new RuntimeException("uid或text均不能為空");
}
ConcurrentHashMap<String, Channel> userChannelMap = NettyGroup.getUserChannelMap();
for (String channelId : userChannelMap.keySet()) {
if (channelId.equals(uid)) {
Channel channel = userChannelMap.get(channelId);
if (channel != null) {
ResultBean success = ResultBean.success(text);
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(success)));
log.info("信息發(fā)送成功:{}", JSON.toJSONString(success));
} else {
log.error("該id對于channelId不存在!");
}
return;
}
}
log.error("該用戶不存在!");
}
@Override
public void pushToAll(String text) {
String trim = text.trim();
ResultBean success = ResultBean.success(trim);
NettyGroup.getChannelGroup().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(success)));
log.info("信息推送成功:{}", JSON.toJSONString(success));
}
//測試賬號只有2個并發(fā),此處只使用一個,若是生產環(huán)境允許多個并發(fā),可以采用分布式鎖
@Override
public synchronized ResultBean pushMessageToXFServer(String uid, String text) {
RoleContent userRoleContent = RoleContent.createUserRoleContent(text);
ArrayList<RoleContent> questions = new ArrayList<>();
questions.add(userRoleContent);
XFWebSocketListener xfWebSocketListener = new XFWebSocketListener();
WebSocket webSocket = xfWebClient.sendMsg(uid, questions, xfWebSocketListener);
if (webSocket == null) {
log.error("webSocket連接異常");
ResultBean.fail("請求異常,請聯(lián)系管理員");
}
try {
int count = 0;
//參考代碼中休眠200ms,若配置了maxResponseTime,若指定時間內未返回,則返回請求失敗至前端
int maxCount = xfConfig.getMaxResponseTime() * 5;
while (count <= maxCount) {
Thread.sleep(200);
if (xfWebSocketListener.isWsCloseFlag()) {
break;
}
count++;
}
if (count > maxCount) {
return ResultBean.fail("響應超時,請聯(lián)系相關人員");
}
return ResultBean.success(xfWebSocketListener.getAnswer());
} catch (Exception e) {
log.error("請求異常:{}", e);
} finally {
webSocket.close(1000, "");
}
return ResultBean.success("");
}
}
所有代碼可參考附錄進行獲取。
2.5 測試結果
3.小結
1.本文代碼主要用于測試,若考慮并發(fā)及性能,需要在上述代碼上進行優(yōu)化;
2.訊飛星火認知大模型對于日常簡單問題的問答效率較高,對詩詞表達欠佳;
3.在本文代碼中,部分代碼仍可以優(yōu)化,后續(xù)可以將此模塊單獨抽象成一個springboot-starter,引入即可使用。
4.參考文獻
1.https://www.xfyun.cn/doc/spark/Web.html
2.https://console.xfyun.cn/services/bm2文章來源:http://www.zghlxwxcb.cn/news/detail-759663.html
5.附錄
https://gitee.com/Marinc/nacos/tree/master/xunfei-bigModel文章來源地址http://www.zghlxwxcb.cn/news/detail-759663.html
到了這里,關于springBoot整合訊飛星火認知大模型的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!