通义千问是阿里云自主研发的大语言模型,能够在用户自然语言输入的基础上,通过自然语言理解和语义分析,理解用户意图,在不同领域、任务内为用户提供服务和帮助。下面这篇文章是我的关于java调用通义千问API的详细完整步骤,需要的朋友可以参考下
一、申请通义千问 API
首先,找到通义千问在哪里,直接在阿里云官网主页:
然后点击搜索通义千问:
点击申请通义千问API:
我这里是第一次申请,然后系统提示需要开通模型服务灵积,点击去开通:
点击已阅读并同意:
然后好像是点击确认,系统就会提示开通成功:
回到下图的这个界面,点击我已开通,刷新页面(这步也可以在上一图直接点击前往控制台,然后在模型广场的通义千问下点击申请体验):
此时需要填一个问卷,模型名称就是通义千问,其他信息如实填写即可:
然后就是等待审核通过了:
此时,原本申请体验就变成了体验申请审核中,请耐心等待的状态了:
在等待审核中,让我们看看让人关心的计费情况:-3:
下图是文心千帆大模型的计费情况(大模型训练里的服务,都比预制服务的价格贵),对比起来,都差不多,openai的其实价格换算一下,其实差不多,所以我选通义千问(因为我是白嫖怪,而且我也不知道怎么给上线的服务器加魔法,如果是分布式的话,是每个服务器都要给魔法?):
然后就可以看看快速开始了:
审核通过后,会发短信到手机上:
二、前置工作
获取API-KEY
复制并保存创建的 API-KEY:
安装 DashScope SDK
<!-- 通义千问 DashScope sdk -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.12.0</version>
</dependency>
三、Java 代码
有些请求字段大家可以参考通义千问 API 里面有更详细的解释,这里就不过多赘述了。
多轮调用
/**
* 通义千问AI--多轮调用,一次性输出结果
* role: user(用户发给模型)、system(指定模型目标或角色)、assistant(模型回复消息)
* content:内容
* model: "qwen-turbo"-模型名称
* messages:由历史对话组成的消息列表
* resultFormat: 指定返回内容的格式
* stream:是否流式输出回复,默认为false
* incrementalOutput: 是否在流式输出模式下是否开启增量输出,默认为false。增量输出,即后续输出内容不包含已输出的内容。需要实时地逐个读取这些片段以获得完整的结果
* topP: 核采样的概率阈值,top_p越高,生成的文本更多样。反之,生成的文本更确定
* enableSearch: 控制模型在生成文本时是否使用互联网搜索结果进行参考
* text: 用户发出的信息
*/
@Test
void callWithMessages() throws NoApiKeyException, ApiException, InputRequiredException {
Constants.apiKey = "你的 api-key";
Generation gen = new Generation();
Message systemMsg = Message.builder()
.role(Role.SYSTEM.getValue())
.content("You are a helpful assistant.")
.build();
Message userMsg = Message.builder()
.role(Role.USER.getValue())
.content("在斗破苍穹小说中,萧炎是个怎样的人?")
.build();
List<Message> messages = new ArrayList<>();
messages.add(systemMsg);
messages.add(userMsg);
GenerationParam param = GenerationParam.builder()
.model("qwen-turbo")
.messages(messages)
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8)
.enableSearch(true)
.build();
GenerationResult result = gen.call(param);
System.out.println("第一次回答:\n" + result.getOutput().getChoices().get(0).getMessage().getContent());
// 添加assistant返回到messages列表,user/assistant消息必须交替出现
messages.add(result.getOutput().getChoices().get(0).getMessage());
// new message
userMsg = Message.builder().role(Role.USER.getValue()).content("如果你是书中的人物,你愿意追随他吗?").build();
messages.add(userMsg);
result = gen.call(param);
System.out.println("第二次回答:\n" + result.getOutput().getChoices().get(0).getMessage().getContent());
}
结果如下:
流式输出
/**
* 通义千问AI--单轮调用,流式输出
* stream:是否流式输出回复,默认为false
* incrementalOutput: 是否在流式输出模式下是否开启增量输出,默认为false。增量输出,即后续输出内容不包含已输出的内容。需要实时地逐个读取这些片段以获得完整的结果
*/
@Test
void streamCallWithMessage() throws NoApiKeyException, ApiException, InputRequiredException, IOException {
Constants.apiKey = "你的 api-key";
Generation gen = new Generation();
Message userMsg = Message.builder()
.role(Role.USER.getValue())
.content("静夜思全文是什么?")
.build();
GenerationParam param = GenerationParam.builder()
.model("qwen-turbo")
.messages(Arrays.asList(userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8)
.enableSearch(true)
.incrementalOutput(true)
.build();
Flowable<GenerationResult> result = gen.streamCall(param);
StringBuilder fullContent = new StringBuilder();
result.blockingForEach(message -> {
String content = message.getOutput().getChoices().get(0).getMessage().getContent();
System.out.println(content);
fullContent.append(content);
});
System.out.println("Full content: \n" + fullContent.toString());
}
四、实例演示
创建实体类:
public class RecordAskReq {
@NotNull(message = "话题id不能为空")
@Schema(description = "话题id",requiredMode = Schema.RequiredMode.REQUIRED)
private Long subjectId;
@Schema(description = "系统指令")
private String systemPrompt;
@Schema(description = "内容")
private String content;
@Schema(description = "文件id")
private Long sysFileId;
}
实例方法:
@Override
@Transactional(rollbackFor = Exception.class)
public SseEmitter qwen_ai(RecordAskReq req, Long createUserId, HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
Long subjectId = req.getSubjectId();
// 查询历史对话记录,以便于多轮调用
List<RecordListResp> recordList = subject_record_list(subjectId);
// 业务代码,需要返回保存的记录id给前端
Long askId = this.record_add(subjectId, RecordTypeEnum.A1.getCode(), req.getSystemPrompt(), req.getContent(), req.getSysFileId(), createUserId);
Long answerId = this.record_add(subjectId, RecordTypeEnum.A2.getCode(), null, null, null, null);
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
StringBuilder fullContent = new StringBuilder();
String text = req.getContent() + "\n" + analysisWord(getUrl(req.getSysFileId()));
// 建立一个线程,用于发送消息
new Thread(() -> {
try {
//
Flowable<GenerationResult> result = callWithMessage(recordList, text, req.getSystemPrompt());
emitter.send(SseEmitter.event().name("askId").data(askId));
emitter.send(SseEmitter.event().name("answerId").data(answerId));
result.blockingForEach(message -> {
String c = message.getOutput().getChoices().get(0).getMessage().getContent();
fullContent.append(c);
// 这里由于某些原因,前端无法获取换行提示,需要手动提示换行。不然下面一行代码就能解决了
//emitter.send(SseEmitter.event().name("message").data(c));
if (c.contains("\n")) {
String[] split = c.split("\n");
for (int i = 0; i < split.length; i++) {
if (ObjectUtil.isNotEmpty(split[i])){
emitter.send(SseEmitter.event().name("message").data(split[i]));
if (i!=split.length-1 || c.endsWith("\n")) emitter.send(SseEmitter.event().name("newline").data("换行"));
}else {
emitter.send(SseEmitter.event().name("newline").data("换行"));
}
}
}else {
emitter.send(SseEmitter.event().name("message").data(c));
}
// 每秒发送一条消息
Thread.sleep(100);
});
}catch (Exception e){
e.printStackTrace();
emitter.completeWithError(e);
}finally {
// 发送完毕后关闭连接
emitter.complete();
// TODO: 2024年11月13日, 0013 讲回答内容保存
}
}).start();
return emitter;
}
这里使用了 SseEmitter 类,用于实现 SSE。通过 HTTP 响应流(ResponseBody)来持续发送消息。
参考博文:SSE (Server-Sent Events) 服务器实时推送详解
根据对话记录,多轮调用AI接口
public Flowable<GenerationResult> callWithMessage(List<RecordListResp> recordList, String text, String systemPrompt) throws NoApiKeyException, InputRequiredException {
Constants.apiKey="**************************";
Generation gen = new Generation();
List<Message> messages = new ArrayList<>();
String sp = "你是一个热心的智能AI助手";
if (ObjectUtil.isNotNull(systemPrompt) && ObjectUtil.isNotEmpty(systemPrompt)) sp = systemPrompt;
messages.add(Message.builder().role(Role.SYSTEM.getValue()).content(sp).build());
for (RecordListResp r:recordList){
if (ObjectUtil.isNotNull(r.getContent()) && ObjectUtil.isNotEmpty(r.getContent())){
Message msg = Message.builder().content(r.getContent()).build();
switch (r.getRecordType()){
case 101: //提问
msg.setRole(Role.USER.getValue());
if (ObjectUtil.isNotNull(r.getSysFileId())){
String s = r.getContent() + "\n" + analysisWord(r.getFileList().get(0).getUrl());
msg.setContent(s);
}
break;
case 102: //回答
msg.setRole(Role.ASSISTANT.getValue());
break;
case 201:
case 202:
case 203:
break;
}
messages.add(msg);
}
}
messages.add(Message.builder().role(Role.USER.getValue()).content(text).build());
GenerationParam param = GenerationParam.builder()
.model("qwen-turbo")
.messages(messages)
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8).enableSearch(true)
.incrementalOutput(true)
.build();
return gen.streamCall(param);
}
注意,这里会遇到一个问题:当我们在一个线程里跨服务调用接口时,会发生下面的报错
排查到的位置是这里的的 attributes.getRequest() 方法出了问题,但是目前实力有限,还不会改。只能像下面这样把调用文件的方法分开:
/**
* 根据id获取文件url
*/
public String getUrl(Long fileId) {
String result = "";
if (ObjectUtil.isNotNull(fileId)){
// 跨服务调用-获取文件详情接口
List<FileResp> fileList = remoteSysFile.getFileList(List.of(fileId));
if (fileList.size()>0){
FileResp fileResp = fileList.get(0);
if (fileResp.getExt().equals("docx")){
result = fileResp.getUrl();
}
}
}
return result;
}
/**
* 解析word文档成字符串
*/
public String analysisWord(String fileUrl) {
String result = "";
if (ObjectUtil.isNotEmpty(fileUrl)){
StringBuilder wordText = new StringBuilder();
try {
URL url = new URL(fileUrl);
InputStream fis = url.openStream();
XWPFDocument document = new XWPFDocument(fis);
// 读取段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph para : paragraphs) {
wordText.append(para.getText());
wordText.append("\n");
}
fis.close();
result = result + "\n" + wordText;
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
实例结果:
好事定律:每件事最后都会是好事,如果不是好事,说明还没到最后。