讯飞星火认知大模型-AI大语言模型-产品套餐
我们选择 “免费试用”。
点击 “确认下单”即可,注意这个免费套餐一个用户只能购买一次。
购买完毕,我们进入控制台界面查看应用信息:讯飞应用控制台
这个“服务接口认证信息”中的 APPID
、APISecret
、APIKey
就是我们需要在 Java 后端配置文件 application.yml 中配置中的信息。
Mr-Write/SpringbootDemo: 各种demo案例 (github.com)
我已经写了一个比较完整的 SpringBoot Demo 项目,为了方便理解对代码做了详细注释,已开源在 GitHub。
下载后只需要找到 xfxh-web-simple-demo 模块,在其 application.yml 文件配置你自己的 APPID
、APISecret
、APIKey
信息,再以 GET 方式访问接口:http://localhost:8080/test/sendQuestion?question=hello
该后端接口的大致实现逻辑:
如果你想了解更详细的与星火大模型之间的参数说明,请参考 星火认知大模型Web文档
该项目后端接口的实现功能:
如果想要使用支持上下文的接口,只需要找到 xfxh-web-support-context 模块,它在 xfxh-web-simple-demo 模块基础上实现了基于上下文的回答,该增强模块的后端接口说明:
代码还是易懂的,如果想了解如何实现的,建议先看完有完整注释的 xfxh-web-simple-demo 模块,再去看 xfxh-web-support-context 模块。xfxh-web-support-context 模块只是在 xfxh-web-simple-demo 模块进行了补充/增强。
SpringBoot Demo 项目的结构:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.0version>
<relativePath/>
parent>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.18version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.67version>
dependency>
<dependency>
<groupId>org.java-websocketgroupId>
<artifactId>Java-WebSocketartifactId>
<version>1.3.8version>
dependency>
<dependency>
<groupId>com.squareup.okhttp3groupId>
<artifactId>okhttpartifactId>
<version>4.10.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
xfxh:
# 服务引擎使用 讯飞星火认知大模型V2.0,如果使用 V1.5 需要将 hostUrl 修改为 https://spark-api.xf-yun.com/v1.1/chat
hostUrl: https://spark-api.xf-yun.com/v2.1/chat
# 发送请求时指定的访问领域,如果是 V1.5版本 设置为 general,如果是 V2版本 设置为 generalv2
domain: generalv2
# 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。取值 [0,1]
temperature: 0.5
# 模型回答的tokens的最大长度,V1.5取值为[1,4096],V2.0取值为[1,8192]。
maxTokens: 2048
# 大模型回复问题的最大响应时长,单位 s
maxResponseTime: 30
# 允许同时连接大模型的 websocket 数,如果是普通(免费)用户为 2,超过这个数连接响应会报错,具体参考官网。
QPS: 2
# 用于权限验证,从服务接口认证信息中获取
appId:
# 用于权限验证,从服务接口认证信息中获取
apiKey:
# 用于权限验证,从服务接口认证信息中获取
apiSecret:
/**
* @author 狐狸半面添
* @create 2023-09-15 0:46
*/
@Configuration
@ConfigurationProperties(prefix = "xfxh")
@Data
public class XfXhConfig {
/**
* 服务引擎使用 讯飞星火认知大模型V2.0,如果使用 V1.5 需要将 hostUrl 修改为 https://spark-api.xf-yun.com/v1.1/chat
*/
private String hostUrl;
/**
* 发送请求时指定的访问领域,如果是 V1.5版本 设置为 general,如果是 V2版本 设置为 generalv2
*/
private String domain;
/**
* 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。取值 [0,1]
*/
private Float temperature;
/**
* 模型回答的tokens的最大长度,V1.5取值为[1,4096],V2.0取值为[1,8192]。
*/
private Integer maxTokens;
/**
* 大模型回复问题的最大响应时长,单位 s
*/
private Integer maxResponseTime;
/**
* 用于权限验证,从服务接口认证信息中获取
*/
private String appId;
/**
* 用于权限验证,从服务接口认证信息中获取
*/
private String apiKey;
/**
* 用于权限验证,从服务接口认证信息中获取
*/
private String apiSecret;
}
/**
* 消息对象
*
* @author 狐狸半面添
* @create 2023-09-15 0:42
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MsgDTO {
/**
* 角色
*/
private String role;
/**
* 消息内容
*/
private String content;
/**
* 响应结果字段:结果序号,取值为[0,10]; 当前为保留字段,开发者可忽略
*/
private Integer index;
public static final String ROLE_USER = "user";
public static final String ROLE_ASSISTANT = "assistant";
public static MsgDTO createUserMsg(String content) {
return new MsgDTO(ROLE_USER, content, null);
}
public static MsgDTO createAssistantMsg(String content) {
return new MsgDTO(ROLE_ASSISTANT, content, null);
}
}
/**
* 请求参数
* 对应生成的 JSON 结构参考 resources/demo-json/request.json
*
* @author 狐狸半面添
* @create 2023-09-15 0:42
*/
@NoArgsConstructor
@Data
public class RequestDTO {
@JsonProperty("header")
private HeaderDTO header;
@JsonProperty("parameter")
private ParameterDTO parameter;
@JsonProperty("payload")
private PayloadDTO payload;
@NoArgsConstructor
@Data
@AllArgsConstructor
public static class HeaderDTO {
/**
* 应用appid,从开放平台控制台创建的应用中获取
*/
@JSONField(name = "app_id")
private String appId;
/**
* 每个用户的id,用于区分不同用户,最大长度32
*/
@JSONField(name = "uid")
private String uid;
}
@NoArgsConstructor
@Data
@AllArgsConstructor
public static class ParameterDTO {
private ChatDTO chat;
@NoArgsConstructor
@Data
@AllArgsConstructor
public static class ChatDTO {
/**
* 指定访问的领域,general指向V1.5版本 generalv2指向V2版本。注意:不同的取值对应的url也不一样!
*/
@JsonProperty("domain")
private String domain;
/**
* 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高
*/
@JsonProperty("temperature")
private Float temperature;
/**
* 模型回答的tokens的最大长度
*/
@JSONField(name = "max_tokens")
private Integer maxTokens;
}
}
@NoArgsConstructor
@Data
@AllArgsConstructor
public static class PayloadDTO {
@JsonProperty("message")
private MessageDTO message;
@NoArgsConstructor
@Data
@AllArgsConstructor
public static class MessageDTO {
@JsonProperty("text")
private List<MsgDTO> text;
}
}
}
/**
* 返回参数
* 对应生成的 JSON 结构参考 resources/demo-json/response.json
*
* @author 狐狸半面添
* @create 2023-09-15 0:42
*/
@NoArgsConstructor
@Data
public class ResponseDTO {
@JsonProperty("header")
private HeaderDTO header;
@JsonProperty("payload")
private PayloadDTO payload;
@NoArgsConstructor
@Data
public static class HeaderDTO {
/**
* 错误码,0表示正常,非0表示出错
*/
@JsonProperty("code")
private Integer code;
/**
* 会话是否成功的描述信息
*/
@JsonProperty("message")
private String message;
/**
* 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段
*/
@JsonProperty("sid")
private String sid;
/**
* 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果
*/
@JsonProperty("status")
private Integer status;
}
@NoArgsConstructor
@Data
public static class PayloadDTO {
@JsonProperty("choices")
private ChoicesDTO choices;
/**
* 在最后一次结果返回
*/
@JsonProperty("usage")
private UsageDTO usage;
@NoArgsConstructor
@Data
public static class ChoicesDTO {
/**
* 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果
*/
@JsonProperty("status")
private Integer status;
/**
* 返回的数据序号,取值为[0,9999999]
*/
@JsonProperty("seq")
private Integer seq;
/**
* 响应文本
*/
@JsonProperty("text")
private List<MsgDTO> text;
}
@NoArgsConstructor
@Data
public static class UsageDTO {
@JsonProperty("text")
private TextDTO text;
@NoArgsConstructor
@Data
public static class TextDTO {
/**
* 保留字段,可忽略
*/
@JsonProperty("question_tokens")
private Integer questionTokens;
/**
* 包含历史问题的总tokens大小
*/
@JsonProperty("prompt_tokens")
private Integer promptTokens;
/**
* 回答的tokens大小
*/
@JsonProperty("completion_tokens")
private Integer completionTokens;
/**
* prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小
*/
@JsonProperty("total_tokens")
private Integer totalTokens;
}
}
}
}
/**
* @author 狐狸半面添
* @create 2023-09-15 1:11
*/
@Slf4j
public class XfXhWebSocketListener extends WebSocketListener {
private StringBuilder answer = new StringBuilder();
private boolean wsCloseFlag = false;
public StringBuilder getAnswer() {
return answer;
}
public boolean isWsCloseFlag() {
return wsCloseFlag;
}
@Override
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
super.onOpen(webSocket, response);
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
super.onMessage(webSocket, text);
// 将大模型回复的 JSON 文本转为 ResponseDTO 对象
ResponseDTO responseData = JSONObject.parseObject(text, ResponseDTO.class);
// 如果响应数据中的 header 的 code 值不为 0,则表示响应错误
if (responseData.getHeader().getCode() != 0) {
// 日志记录
log.error("发生错误,错误码为:" + responseData.getHeader().getCode() + "; " + "信息:" + responseData.getHeader().getMessage());
// 设置回答
this.answer = new StringBuilder("大模型响应错误,请稍后再试");
// 关闭连接标识
wsCloseFlag = true;
return;
}
// 将回答进行拼接
for (MsgDTO msgDTO : responseData.getPayload().getChoices().getText()) {
this.answer.append(msgDTO.getContent());
}
// 对最后一个文本结果进行处理
if (2 == responseData.getHeader().getStatus()) {
wsCloseFlag = true;
}
}
@Override
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
super.onFailure(webSocket, t, response);
}
@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
super.onClosed(webSocket, code, reason);
}
}
/**
* @author 狐狸半面添
* @create 2023-09-15 1:10
*/
@Component
@Slf4j
public class XfXhStreamClient {
@Resource
private XfXhConfig xfXhConfig;
@Value("${xfxh.QPS}")
private int connectionTokenCount;
/**
* 获取令牌
*/
public static int GET_TOKEN_STATUS = 0;
/**
* 归还令牌
*/
public static int BACK_TOKEN_STATUS = 1;
/**
* 操作令牌
*
* @param status 0-获取令牌 1-归还令牌
* @return 是否操作成功
*/
public synchronized boolean operateToken(int status) {
if (status == GET_TOKEN_STATUS) {
// 获取令牌
if (connectionTokenCount != 0) {
// 说明还有令牌,将令牌数减一
connectionTokenCount -= 1;
return true;
} else {
return false;
}
} else {
// 放回令牌
connectionTokenCount += 1;
return true;
}
}
/**
* 发送消息
*
* @param uid 每个用户的id,用于区分不同用户
* @param msgList 发送给大模型的消息,可以包含上下文内容
* @return 获取websocket连接,以便于我们在获取完整大模型回复后手动关闭连接
*/
public WebSocket sendMsg(String uid, List<MsgDTO> msgList, WebSocketListener listener) {
// 获取鉴权url
String authUrl = this.getAuthUrl();
// 鉴权方法生成失败,直接返回 null
if (authUrl == null) {
return 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);
// 组装请求参数
RequestDTO requestDTO = getRequestParam(uid, msgList);
// 发送请求
webSocket.send(JSONObject.toJSONString(requestDTO));
return webSocket;
}
/**
* 生成鉴权方法,具体实现不用关心,这是讯飞官方定义的鉴权方式
*
* @return 鉴权访问大模型的路径
*/
public String getAuthUrl() {
try {
URL url = new URL(xfXhConfig.getHostUrl());
// 时间
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(xfXhConfig.getApiSecret().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 authorizationOrigin = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", xfXhConfig.getApiKey(), "hmac-sha256", "host date request-line", sha);
// 拼接地址
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().
addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(StandardCharsets.UTF_8))).
addQueryParameter("date", date).
addQueryParameter("host", url.getHost()).
build();
return httpUrl.toString();
} catch (Exception e) {
log.error("鉴权方法中发生错误:" + e.getMessage());
return null;
}
}
/**
* 获取请求参数
*
* @param uid 每个用户的id,用于区分不同用户
* @param msgList 发送给大模型的消息,可以包含上下文内容
* @return 请求DTO,该 DTO 转 json 字符串后生成的格式参考 resources/demo-json/request.json
*/
public RequestDTO getRequestParam(String uid, List<MsgDTO> msgList) {
RequestDTO requestDTO = new RequestDTO();
requestDTO.setHeader(new RequestDTO.HeaderDTO(xfXhConfig.getAppId(), uid));
requestDTO.setParameter(new RequestDTO.ParameterDTO(new RequestDTO.ParameterDTO.ChatDTO(xfXhConfig.getDomain(), xfXhConfig.getTemperature(), xfXhConfig.getMaxTokens())));
requestDTO.setPayload(new RequestDTO.PayloadDTO(new RequestDTO.PayloadDTO.MessageDTO(msgList)));
return requestDTO;
}
}
@SpringBootApplication
public class XfXhApplication {
public static void main(String[] args) {
SpringApplication.run(XfXhApplication.class, args);
}
}