aurora博客接入讯飞星火大模型
接口申请
aurora博客是一个web项目,所以申请免费的web接口https://xinghuo.xfyun.cn/sparkapi?scr=price,这里选择免费的接口即可。
按照网站上的提示需要先在控制台创建应用,将该套餐包绑定在这个应用上即可。
接口使用(官方示例代码)
使用Java请求接口使用的是websocket,不是httprequest,这里简单说一下两者的区别HTTP1.1中,Connection默认为Keep-alive参数,Keep-alive的确可以实现长连接,但是这个长连接是有问题的,本质上依然是客户端主动发起-服务端应答的模式,是没法做到服务端主动发送通知给客户端的。也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是一个request只能有一个response。而且这个response也是被动的,不能主动发起。开启了Keep-alive,可以看出依然是一问一答的模式,只是省略了每次的关闭和打开操作。WebSocket是可以互相主动发起的。相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在具体的实现中是继承WebSocketListener类,进行websocket的建立,发送信息,接受信息的。具体的步骤大概可以分为构造鉴权url、建立连接、发送问题、接受答案、关闭连接 这五步。
1 构造鉴权Url
在自己控制台创建的应用中得到appid,APIKey, APISecret。鉴权的参数如下:
参数 | 类型 | 必须 | 说明 | 示例 |
---|---|---|---|---|
host | string | 是 | 请求的主机 | aichat.xf-yun.com(使用时需替换为实际使用的接口地址) |
date | string | 是 | 当前时间戳,采用RFC1123格式,时间偏差需控制在300s内 | Fri, 05 May 2023 10:43:39 GMT |
authorization | string | 是 | base64编码的签名信息 | 参考下方生成方式 |
把具体生成鉴权Url的Java代码贴出
// 鉴权方法
public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
// 传入的参数为 hostUrl请求的主机地址(就是要访问的模型地址),apiKey,apiSecret
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());
// 拼接 将时间和url参数拼接起来
String preStr = "host: " + url.getHost() + "\n" +
"date: " + date + "\n" +
"GET " + url.getPath() + " HTTP/1.1";
// System.err.println(preStr);
// SHA256加密 用hmac-sha256算法结合APISecret对上一步的tmp签名,获得签名后的摘要tmp_sha。
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加密 进一步加密得到签名signature
String sha = Base64.getEncoder().encodeToString(hexDigits);
// System.err.println(sha);
// 拼接 将签名signature和特定的字符串拼接得到authorization
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();
// System.err.println(httpUrl.toString());
return httpUrl.toString();
}
2 建立连接
使用OkHttpClient来建立websocket连接,首先在maven中添加依赖
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
然后利用生成的鉴权Url创建websocket连接
// 构建鉴权url
String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret);
OkHttpClient client = new OkHttpClient.Builder().build();
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
// 这里的BigModelNew类是一个继承了WebSocketListener的子类,实现了相应的onmessage onopen onfailure接口,能够箭头websocket的连接,对连接过程做出 // 相应的处理
WebSocket webSocket = client.newWebSocket(request, new BigModelNew(i + "",false));
3. 发送问题
官方示例中给出的示例是通过控制台的输入得到用户输入的问题
Scanner scanner=new Scanner(System.in);
System.out.print("我:");
totalFlag=false;
NewQuestion=scanner.nextLine(); // 得到问题
发送问题的时机在于和大模型建立了Websocket连接之后,onOpen监听到websocket已经建立好就会新开一个线程发送问题.
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
System.out.print("大模型:");
MyThread myThread = new MyThread(webSocket);
myThread.start();
}
MyThread继承了Thread,其run函数的主要作用是按照接口文档中的要求构造请求参数然后发送
public void run() {
try {
JSONObject requestJson=new JSONObject();
JSONObject header=new JSONObject(); // header参数
header.put("app_id",appid);
header.put("uid",UUID.randomUUID().toString().substring(0, 10));
JSONObject parameter=new JSONObject(); // parameter参数
// 省略
JSONObject payload=new JSONObject(); // payload参数
// 省略
// 最新问题
RoleContent roleContent=new RoleContent();
roleContent.role="user";
roleContent.content=NewQuestion;
text.add(JSON.toJSON(roleContent))
message.put("text",text);
payload.put("message",message);
requestJson.put("header",header);
requestJson.put("parameter",parameter);
requestJson.put("payload",payload);
// System.err.println(requestJson); // 可以打印看每次的传参明细
webSocket.send(requestJson.toString());
// 等待服务端返回完毕后关闭
while (true) {
// System.err.println(wsCloseFlag + "---");
Thread.sleep(200);
if (wsCloseFlag) { // 当大模型传回来所有的答案之后 wsCloseFlag会被置为true
break;
}
}
webSocket.close(1000, "");
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到当构造好请求参数之后利用webSocket.send(requestJson.toString());发送给大模型,然后进入循环中等待,当大模型传回来所有答案之后关闭websocket连接。
4. 接受答案 & 关闭连接
值得一提的是星火模型通过websocket返回答案是流式返回的,流式返回是一种将数据以流的形式传输到客户端的机制,与传统的一次性请求-响应模式不同。流式返回使我们能够在模型生成文本的同时逐步将结果发送给客户端,实现实时的交互体验。在客户端再一次的问题请求当中能够收到多个返回,需要将多次返回的结果进行拼接显示。一次返回的结果格式如下
# 接口为流式返回,此示例为最后一次返回结果,开发者需要将接口多次返回的结果进行拼接展示
{
"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
}
}
}
}
按照这种格式可以得到回答的状态信息和当前会话的位置。
onMessage函数也是websocketListener的一个监听函数,会单独开一个线程运行onMessage,当监听到星火模型返回信息给客户端时就会运行。onMessage的主要作用是接收到返回信息后按照格式截取到答案,并拼接在一起。
public void onMessage(WebSocket webSocket, String text) {
// System.out.println(userId + "用来区分那个用户的结果" + text);
// 将返回的字符串解析成目标类
JsonParse myJsonParse = gson.fromJson(text, JsonParse.class);
// 判断返回状态
if (myJsonParse.header.code != 0) {
System.out.println("发生错误,错误码为:" + myJsonParse.header.code);
System.out.println("本次请求的sid为:" + myJsonParse.header.sid);
webSocket.close(1000, "");
}
// 将此次返回的信息加入到答案中
List<Text> textList = myJsonParse.payload.choices.text;
for (Text temp : textList) {
System.out.print(temp.content);
totalAnswer=totalAnswer+temp.content;
}
// 如果是最后一个答案了就把wsCloseFlag设置为true,然后MyThread中会检测到wsCloseFlag为true,把连接关闭
if (myJsonParse.header.status == 2) {
// 可以关闭连接,释放资源
System.out.println();
System.out.println("*************************************************************************************");
if(canAddHistory()){
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
}else{
historyList.remove(0);
RoleContent roleContent=new RoleContent();
roleContent.setRole("assistant");
roleContent.setContent(totalAnswer);
historyList.add(roleContent);
}
wsCloseFlag = true;
totalFlag=true;
}
}
项目实战
1. 添加依赖
按照上门所说的加入websocket、okhttp和gson相关依赖。
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
2. 添加Model
按照星火模型返回数据的格式设置相应的模型来接受数据
@Data
public class JsonParseDTO { //存放总的Json数据
HeaderDTO header;
PayloadDTO payload;
}
@Data
public class HeaderDTO { // 存放返回的header部分
int code;
int status;
String sid;
}
@Data
public class PayloadDTO { // 存放负载部分
ChoicesDTO choices;
}
@Data
public class ChoicesDTO {
List<TextDTO> text;
}
@Data
public class TextDTO {
String role;
String content;
}
3.添加配置和工具类
将需要到的appId hostUrl等固定参数放入到常数类中,方便以后使用
public interface BigModelProperties {
String HOSTURL = "https://spark-api.xf-yun.com/v2.1/chat";
String APPID = "68baf566";
String APISECRET = "NDk4ZjhiZjU2MGYxNDgyZjE3ZGMyMDkw";
String APIKEY = "4724db19b6af069b6211a48a3ff95984";
}
然后将生成鉴权url函数和构造请求头函数提取出来放入到gptutil工具类中
public class GptUtil {
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";
// System.err.println(preStr);
// 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);
// System.err.println(sha);
// 拼接
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();
// System.err.println(httpUrl.toString());
return httpUrl.toString();
}
public static String processQuestion(String question){
JSONObject requestJson=new JSONObject();
JSONObject header=new JSONObject(); // header参数
header.put("app_id",APPID);
header.put("uid",UUID.randomUUID().toString().substring(0, 10));
JSONObject parameter=new JSONObject(); // parameter参数
JSONObject chat=new JSONObject();
chat.put("domain","generalv2");
chat.put("temperature",0.5);
chat.put("max_tokens",4096);
parameter.put("chat",chat);
JSONObject payload=new JSONObject(); // payload参数
JSONObject message=new JSONObject();
JSONArray text=new JSONArray();
RoleContentDTO roleContentDTO = new RoleContentDTO();
roleContentDTO.setRole("user");
roleContentDTO.setContent(question);
text.add(JSON.toJSON(roleContentDTO));
message.put("text",text);
payload.put("message",message);
requestJson.put("header",header);
requestJson.put("parameter",parameter);
requestJson.put("payload",payload);
return requestJson.toString();
}
}
编写继承了WebSocketListener的gpt监听类,监听websocket连接,当建立连接之后发送问题,收到答案后拼接答案并且当检测到这是最后一个答案时关闭连接。
@Override
@SneakyThrows
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
super.onOpen(webSocket, response);
CompletableFuture.supplyAsync(()->webSocket.send(question));
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
super.onMessage(webSocket, text);
JsonParseDTO jsonParseDTO = gson.fromJson(text, JsonParseDTO.class);
if(jsonParseDTO.getHeader().getCode() != 0){
System.out.println("发生错误,错误码为:" + jsonParseDTO.getHeader().getCode());
System.out.println("本次请求的sid为:" + jsonParseDTO.getHeader().getSid());
webSocket.close(1000, "");
}
List<TextDTO> textList = jsonParseDTO.getPayload().getChoices().getText();
answer += textList.stream().map(TextDTO::getContent).collect(Collectors.joining(""));
if(jsonParseDTO.getHeader().getStatus() == 2){
System.out.println(answer);
webSocket.close(1000, "");
wsClosed = true;
}
}
@Override
@SneakyThrows
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
wsClosed = true;
super.onFailure(webSocket, t, response);
if(null != response){
int code = response.code();
System.out.println("onFailure code:" + code);
System.out.println("onFailure body:" + response.body().string());
if (101 != code) {
System.out.println("connection failed");
}
}
}
4. 添加controller接口
添加访问接口,并设置请求限制,防止短时间内多次请求gpt回答
@AccessLimit(seconds = 600, maxCount = 1)
@OptLog(optType = SAVE)
@ApiOperation("添加GPT问题")
@PostMapping("/commentsGPT/save")
public ResultVO<?> saveCommentGPT(@Valid @RequestBody CommentVO commentVO) {
commentService.saveCommentGPT(commentVO);
return ResultVO.ok();
}
5. 添加service处理
在说service处理逻辑之前先简单说明要实现一下前端询问gpt以及gpt回答的效果,效果如下。用户在评论区编写问题,然后点击ask gpt按钮,后端收到请求之后提取出问题,先将该问题作为一层评论存放到数据库中,再与星火模型建立连接发送信息,得到答案之后将该答案作为这个问题回复存放到数据库中,然后返回前端成功标志,前端重新要求后端返回评论区,即可达到效果。
根据上面的逻辑可以实现service的接口为
public void saveCommentGPT(CommentVO commentVO){
// Integer parentId = saveComment(commentVO);
// 新开一个线程存放问题作为评论,提出问题的人的信息就是当前用户的信息,但是默认情况下Spring Security相关的认证信息是绑定到某个线程上的,
// 也就是说在此线程以外的其它线程上我们无法获取当前登录用户的信息。比如在我们使用@Async来启用一个新的线程的情况下。所以这里提前得到该用户的信息传递给异步任务
Integer id = UserUtil.getUserDetailsDTO().getUserInfoId();
String fromNickname = UserUtil.getUserDetailsDTO().getNickname();
// 异步保存用户的提问作为评论,并返回生成的id
CompletableFuture<Integer> asyncParentId = CompletableFuture.supplyAsync(() -> saveComment(commentVO, id, fromNickname));
WebsiteConfigDTO websiteConfig = auroraInfoService.getWebsiteConfig();
Integer isCommentReview = websiteConfig.getIsCommentReview();
String question = commentVO.getCommentContent();
// 生成鉴权url
String authUrl = GptUtil.getAuthUrl(HOSTURL, APIKEY, APISECRET);
OkHttpClient client = new OkHttpClient.Builder().build();
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
gpt.setWsClosed(false);
// 构造请求头传递给gpt连接监听类
gpt.setQuestion(GptUtil.processQuestion(question));
gpt.setAnswer("");
// 向星火模型构造websocket连接
WebSocket webSocket = client.newWebSocket(request, gpt);
// 循环等待直到得到所有的答案,连接已经关闭
while (true){
if(gpt.getWsClosed()){
break;
}
}
// 将该答案作为评论的回复存放到数据库之中
Comment comment = Comment.builder()
.userId(1) //先把GPT的身份定位自己的身份1
.replyUserId(UserUtil.getUserDetailsDTO().getUserInfoId()) //GPT回复的对象就是当前发出提问的对象
.topicId(commentVO.getTopicId())
.commentContent(gpt.getAnswer())
.parentId(asyncParentId.get())
.type(commentVO.getType())
.isReview(isCommentReview == TRUE ? FALSE : TRUE)
.build();
commentMapper.insert(comment);
if (websiteConfig.getIsEmailNotice().equals(TRUE)) {
CompletableFuture.runAsync(() -> notice(comment, fromNickname));
}
}
6. 前端处理
在添加评论的组件CommentForm中设置请求按钮和接口
添加请求按钮
<button
@click="saveComment(0)"
id="submit-button"
:disabled="isButtonDisabled"
class="mt-5 w-32 text-white p-2 rounded-lg shadow-lg transition transform hover:scale-105 flex float-right margin-left">
<span class="text-center flex-grow commit">Add Comment</span>
</button>
<button
@click="saveComment(1)"
id="submit-button"
:disabled="isButtonDisabled"
class="mt-5 w-32 text-white p-2 rounded-lg shadow-lg transition transform hover:scale-105 flex float-right margin-right">
<span class="text-center flex-grow commit">Ask GPT</span>
</button>
判断当前按钮是哪一个,如果是gpt就执行以下逻辑,先让按钮禁用防止多次请求,然后发送请求给后端,得到信息之后再把按钮恢复。
reactiveData.isButtonDisabled = true
api.saveCommentGPT(params).then(({ data }) => {
if (data.flag) {
fetchComments()
let isCommentReview = appStore.websiteConfig.isCommentReview
if (isCommentReview) {
proxy.$notify({
title: 'Warning',
message: '评论成功,正在审核中',
type: 'warning'
})
} else {
proxy.$notify({
title: 'Success',
message: '评论成功',
type: 'success'
})
}
reactiveData.commentContent = ''
} else {
proxy.$notify({
title: 'Error',
message: data.message,
type: 'error'
})
}
reactiveData.isButtonDisabled = false
})