網(wǎng)絡(luò)營(yíng)銷的特征優(yōu)化大師使用方法
1.概述
訊飛星火大模型是科大訊飛最近開(kāi)放的擁有跨領(lǐng)域的知識(shí)和語(yǔ)言理解能力的大模型,能夠完成問(wèn)答對(duì)話和文學(xué)創(chuàng)作等。由于訊飛星火大模型最近可以免費(fèi)試用,開(kāi)發(fā)者都可以免費(fèi)申請(qǐng)一個(gè)QPS不超過(guò)2的賬號(hào),用來(lái)實(shí)現(xiàn)對(duì)平臺(tái)能力的驗(yàn)證。本文將利用Springboot框架對(duì)星火大模型進(jìn)行整合,使其能夠提供簡(jiǎn)單的問(wèn)答能力。
2.Springboot整合大模型
2.1 申請(qǐng)開(kāi)發(fā)者賬號(hào)
訊飛星火認(rèn)知大模型需要在訊飛星火官網(wǎng)進(jìn)行申請(qǐng)(如下圖所示),點(diǎn)擊免費(fèi)試用按鈕,填寫相關(guān)信息即可。
申請(qǐng)成功后可以在控制臺(tái)查看對(duì)應(yīng)的賬號(hào)信息(如下圖所示),APPID、APPKey、APPSecret都是唯一的,不要輕易泄漏。
至此,賬號(hào)申請(qǐng)工作完成。由于本文主要展示的是利用JAVA語(yǔ)言來(lái)實(shí)現(xiàn)對(duì)大模型的調(diào)用,因此可以在API文檔中下載JAVA帶上下文的調(diào)用示例(如下圖所示),通過(guò)該文檔中的代碼可以快速進(jìn)行一個(gè)簡(jiǎn)單的小測(cè)試。
2.2 接口文檔參數(shù)分析
在訊飛星火認(rèn)知大模型的對(duì)接文檔中,由于結(jié)果是流式返回的(不是一次性返回),因此案例中代碼通過(guò)WebSocket長(zhǎng)連接方式與服務(wù)器建立連接并發(fā)送請(qǐng)求,實(shí)時(shí)接收返回結(jié)果。接口請(qǐng)求參數(shù)具體如下:
{"header": {"app_id": "12345","uid": "12345"},"parameter": {"chat": {"domain": "general","temperature": 0.5,"max_tokens": 1024, }},"payload": {"message": {# 如果想獲取結(jié)合上下文的回答,需要開(kāi)發(fā)者每次將歷史問(wèn)答信息一起傳給服務(wù)端,如下示例# 注意:text里面的所有content內(nèi)容加一起的tokens需要控制在8192以內(nèi),開(kāi)發(fā)者如有較長(zhǎng)對(duì)話需求,需要適當(dāng)裁剪歷史信息"text": [{"role": "user", "content": "你是誰(shuí)"} # 用戶的歷史問(wèn)題{"role": "assistant", "content": "....."} # AI的歷史回答結(jié)果# ....... 省略的歷史對(duì)話{"role": "user", "content": "你會(huì)做什么"} # 最新的一條問(wèn)題,如無(wú)需上下文,可只傳最新一條問(wèn)題]}}
}
上述請(qǐng)求中對(duì)應(yīng)的參數(shù)解釋如下:
在這里需要注意的是:app_id就是我們申請(qǐng)的APPID,uid可以區(qū)分不同用戶。如果想要大模型能夠根據(jù)結(jié)合上下文去進(jìn)行問(wèn)題解答,就要把歷史問(wèn)題和歷史回答結(jié)果全部傳回服務(wù)端。
針對(duì)上述請(qǐng)求,大模型的接口響應(yīng)結(jié)果如下:
# 接口為流式返回,此示例為最后一次返回結(jié)果,開(kāi)發(fā)者需要將接口多次返回的結(jié)果進(jìn)行拼接展示
{"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}}}
}
返回字段的解釋如下:
需要注意的是:由于請(qǐng)求結(jié)果流式返回,因此需要根據(jù)header中的狀態(tài)值status來(lái)進(jìn)行判斷(0代表首次返回結(jié)果,1代表中間結(jié)果,2代表最后一個(gè)結(jié)果),一次請(qǐng)求過(guò)程中可能會(huì)出現(xiàn)多個(gè)status為1的結(jié)果。
2.3 設(shè)計(jì)思路
本文設(shè)計(jì)思路如下圖所示:
客戶端通過(guò)webSocket的方式與整合大模型的Springboot進(jìn)行連接建立,整合大模型的Springboot在接收到客戶端請(qǐng)求時(shí),會(huì)去創(chuàng)建與訊飛大模型服務(wù)端的webSocket長(zhǎng)連接(每次請(qǐng)求會(huì)創(chuàng)建一個(gè)長(zhǎng)連接,當(dāng)獲取到所有請(qǐng)求內(nèi)容后,會(huì)斷開(kāi)長(zhǎng)連接)。由于本文使用的賬號(hào)為開(kāi)發(fā)者賬號(hào)(非付費(fèi)模式),因此并發(fā)能力有限,本文采用加鎖方式來(lái)控制請(qǐng)求訪問(wèn)。
Springboot服務(wù)與客戶端的交互邏輯如下圖所示:
Springboot服務(wù)與訊飛認(rèn)知大模型的交互邏輯如下圖所示:
2.3 項(xiàng)目結(jié)構(gòu)
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=efc8c037
xf.config.apiSecret=NDdkNWFiZjdlODM0YzEzNzhkZWRjYTU1
xf.config.apiKey=2733d38dd4717855c7de2f2450c028c2
#最大響應(yīng)時(shí)間,單位:秒
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ā)送請(qǐng)求至大模型服務(wù)端,內(nèi)部有鑒權(quán)方法。
/*** @Author: ChengLiang* @CreateTime: 2023-10-19 11:04* @Description: TODO* @Version: 1.0*/
@Slf4j
@Component
public class XFWebClient {@Autowiredprivate XFConfig xfConfig;/*** @description: 發(fā)送請(qǐng)求至大模型方法* @author: ChengLiang* @date: 2023/10/19 16:27* @param: [用戶id, 請(qǐng)求內(nèi)容, 返回結(jié)果監(jiān)聽(tīng)器listener]* @return: okhttp3.WebSocket**/public WebSocket sendMsg(String uid, List<RoleContent> questions, WebSocketListener listener) {// 獲取鑒權(quán)urlString authUrl = null;try {authUrl = getAuthUrl(xfConfig.getHostUrl(), xfConfig.getApiKey(), xfConfig.getApiSecret());} catch (Exception e) {log.error("鑒權(quán)失敗:{}", e);return null;}// 鑒權(quán)方法生成失敗,直接返回 nullOkHttpClient 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);// 組裝請(qǐng)求參數(shù)JSONObject requestDTO = createRequestParams(uid, questions);// 發(fā)送請(qǐng)求webSocket.send(JSONObject.toJSONString(requestDTO));return webSocket;}/*** @description: 鑒權(quán)方法* @author: ChengLiang* @date: 2023/10/19 16:25* @param: [訊飛大模型請(qǐng)求地址, apiKey, apiSecret]* @return: java.lang.String**/public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {URL url = new URL(hostUrl);// 時(shí)間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: 請(qǐng)求參數(shù)組裝方法* @author: ChengLiang* @date: 2023/10/19 16:26* @param: [用戶id, 請(qǐng)求內(nèi)容]* @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 類主要功能是與星火認(rèn)知大模型建立webSocket連接,核心代碼如下:
/*** @Author: ChengLiang* @CreateTime: 2023-10-18 10:17* @Description: TODO* @Version: 1.0*/
@Slf4j
public class XFWebSocketListener extends WebSocketListener {//斷開(kāi)websocket標(biāo)志位private boolean wsCloseFlag = false;//語(yǔ)句組裝buffer,將大模型返回結(jié)果全部接收,在組裝成一句話返回private StringBuilder answer = new StringBuilder();public String getAnswer() {return answer.toString();}public boolean isWsCloseFlag() {return wsCloseFlag;}@Overridepublic void onOpen(WebSocket webSocket, Response response) {super.onOpen(webSocket, response);log.info("大模型服務(wù)器連接成功!");}@Overridepublic 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ā)生錯(cuò)誤,錯(cuò)誤信息為:{}", JSON.toJSONString(myJsonParse.getHeader()));this.answer.append("大模型響應(yīng)異常,請(qǐng)聯(lián)系管理員");// 關(guān)閉連接標(biāo)識(shí)wsCloseFlag = true;return;}List<Text> textList = myJsonParse.getPayload().getChoices().getText();for (Text temp : textList) {log.info("返回結(jié)果信息為:【{}】", JSON.toJSONString(temp));this.answer.append(temp.getContent());}log.info("result:{}", this.answer.toString());if (myJsonParse.getHeader().getStatus() == 2) {wsCloseFlag = true;//todo 將問(wèn)答信息入庫(kù)進(jìn)行記錄,可自行實(shí)現(xiàn)}}@Overridepublic 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主要是用來(lái)監(jiān)聽(tīng)指定端口,接收客戶端的webSocket請(qǐng)求。
@Slf4j
@Component
public class NettyServer {/*** webSocket協(xié)議名*/private static final String WEBSOCKET_PROTOCOL = "WebSocket";/*** 端口號(hào)*/@Value("${webSocket.netty.port:62632}")private int port;/*** webSocket路徑*/@Value("${webSocket.netty.path:/webSocket}")private String webSocketPath;@Autowiredprivate WebSocketHandler webSocketHandler;private EventLoopGroup bossGroup;private EventLoopGroup workGroup;/*** 啟動(dòng)** @throws InterruptedException*/private void start() throws InterruptedException {bossGroup = new NioEventLoopGroup();workGroup = new NioEventLoopGroup();ServerBootstrap bootstrap = new ServerBootstrap();// bossGroup輔助客戶端的tcp連接請(qǐng)求, workGroup負(fù)責(zé)與客戶端之前的讀寫操作bootstrap.group(bossGroup, workGroup);// 設(shè)置NIO類型的channelbootstrap.channel(NioServerSocketChannel.class);// 設(shè)置監(jiān)聽(tīng)端口bootstrap.localAddress(new InetSocketAddress(port));// 連接到達(dá)時(shí)會(huì)創(chuàng)建一個(gè)通道bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {// 流水線管理通道中的處理程序(Handler),用來(lái)處理業(yè)務(wù)// webSocket協(xié)議本身是基于http協(xié)議的,所以這邊也要使用http編解碼器ch.pipeline().addLast(new HttpServerCodec());ch.pipeline().addLast(new ObjectEncoder());// 以塊的方式來(lái)寫的處理器ch.pipeline().addLast(new ChunkedWriteHandler());/*說(shuō)明:1、http數(shù)據(jù)在傳輸過(guò)程中是分段的,HttpObjectAggregator可以將多個(gè)段聚合2、這就是為什么,當(dāng)瀏覽器發(fā)送大量數(shù)據(jù)時(shí),就會(huì)發(fā)送多次http請(qǐng)求*/ch.pipeline().addLast(new HttpObjectAggregator(8192));/*說(shuō)明:1、對(duì)應(yīng)webSocket,它的數(shù)據(jù)是以幀(frame)的形式傳遞2、瀏覽器請(qǐng)求時(shí) ws://localhost:58080/xxx 表示請(qǐng)求的uri3、核心功能是將http協(xié)議升級(jí)為ws協(xié)議,保持長(zhǎng)連接*/ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));// 自定義的handler,處理業(yè)務(wù)邏輯ch.pipeline().addLast(webSocketHandler);}});// 配置完成,開(kāi)始綁定server,通過(guò)調(diào)用sync同步方法阻塞直到綁定成功ChannelFuture channelFuture = bootstrap.bind().sync();log.info("Server started and listen on:{}", channelFuture.channel().localAddress());// 對(duì)關(guān)閉通道進(jìn)行監(jiān)聽(tīng)channelFuture.channel().closeFuture().sync();}/*** 釋放資源** @throws InterruptedException*/@PreDestroypublic void destroy() throws InterruptedException {if (bossGroup != null) {bossGroup.shutdownGracefully().sync();}if (workGroup != null) {workGroup.shutdownGracefully().sync();}}@PostConstruct()public void init() {//需要開(kāi)啟一個(gè)新的線程來(lái)執(zhí)行netty server 服務(wù)器new Thread(() -> {try {start();log.info("消息推送線程開(kāi)啟!");} 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> {@Autowiredprivate PushService pushService;@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {log.info("handlerAdded被調(diào)用,{}", JSON.toJSONString(ctx));//todo 添加校驗(yàn)功能,校驗(yàn)合法后添加到group中// 添加到channelGroup 通道組NettyGroup.getChannelGroup().add(ctx.channel());}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {log.info("服務(wù)器收到消息:{}", msg.text());// 獲取用戶ID,關(guān)聯(lián)channelJSONObject jsonObject = JSON.parseObject(msg.text());String channelId = jsonObject.getString("uid");// 將用戶ID作為自定義屬性加入到channel中,方便隨時(shí)channel中獲取用戶IDAttributeKey<String> key = AttributeKey.valueOf("userId");//String channelId = CharUtil.generateStr(uid);NettyGroup.getUserChannelMap().put(channelId, ctx.channel());boolean containsKey = NettyGroup.getUserChannelMap().containsKey(channelId);//通道已存在,請(qǐng)求信息返回if (containsKey) {//接收消息格式{"uid":"123456","text":"中華人民共和國(guó)成立時(shí)間"}String text = jsonObject.getString("text");//請(qǐng)求大模型服務(wù)器,獲取結(jié)果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);// 回復(fù)消息ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(ResultBean.success(channelId))));}}@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception {log.info("handlerRemoved被調(diào)用,{}", JSON.toJSONString(ctx));// 刪除通道NettyGroup.getChannelGroup().remove(ctx.channel());removeUserId(ctx);}@Overridepublic 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ā)送請(qǐng)求至訊飛大模型后臺(tái)獲取返回結(jié)果,以及根據(jù)指定通道發(fā)送信息至用戶。
/*** @Author: ChengLiang* @CreateTime: 2023-10-17 15:58* @Description: TODO* @Version: 1.0*/
@Slf4j
@Service
public class PushServiceImpl implements PushService {@Autowiredprivate XFConfig xfConfig;@Autowiredprivate XFWebClient xfWebClient;@Overridepublic 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對(duì)于channelId不存在!");}return;}}log.error("該用戶不存在!");}@Overridepublic 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));}//測(cè)試賬號(hào)只有2個(gè)并發(fā),此處只使用一個(gè),若是生產(chǎn)環(huán)境允許多個(gè)并發(fā),可以采用分布式鎖@Overridepublic 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("請(qǐng)求異常,請(qǐng)聯(lián)系管理員");}try {int count = 0;//參考代碼中休眠200ms,若配置了maxResponseTime,若指定時(shí)間內(nèi)未返回,則返回請(qǐng)求失敗至前端int maxCount = xfConfig.getMaxResponseTime() * 5;while (count <= maxCount) {Thread.sleep(200);if (xfWebSocketListener.isWsCloseFlag()) {break;}count++;}if (count > maxCount) {return ResultBean.fail("響應(yīng)超時(shí),請(qǐng)聯(lián)系相關(guān)人員");}return ResultBean.success(xfWebSocketListener.getAnswer());} catch (Exception e) {log.error("請(qǐng)求異常:{}", e);} finally {webSocket.close(1000, "");}return ResultBean.success("");}
}
所有代碼可參考附錄進(jìn)行獲取。
2.5 測(cè)試結(jié)果
3.小結(jié)
1.本文代碼主要用于測(cè)試,若考慮并發(fā)及性能,需要在上述代碼上進(jìn)行優(yōu)化;
2.訊飛星火認(rèn)知大模型對(duì)于日常簡(jiǎn)單問(wèn)題的問(wèn)答效率較高,對(duì)詩(shī)詞表達(dá)欠佳;
3.在本文代碼中,部分代碼仍可以優(yōu)化,后續(xù)可以將此模塊單獨(dú)抽象成一個(gè)springboot-starter,引入即可使用。
4.參考文獻(xiàn)
1.https://www.xfyun.cn/doc/spark/Web.html
2.https://console.xfyun.cn/services/bm2
5.附錄
https://gitee.com/Marinc/nacos/tree/master/xunfei-bigModel