通义千问实现AI问答

通义千问是阿里云自主研发的大语言模型,能够在用户自然语言输入的基础上,通过自然语言理解和语义分析,理解用户意图,在不同领域、任务内为用户提供服务和帮助。下面这篇文章是我的关于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;
}

实例结果:
在这里插入图片描述


好事定律:每件事最后都会是好事,如果不是好事,说明还没到最后。

### Java 通义语音合成功能实现 为了实现在Java环境中调用通义API完成语音合成的任务,可以借鉴CosyVoice项目的架构设计思路[^2]。具体而言,该过程涉及创建HTTP请求发送至服务器端点,并接收返回的音频数据流。 以下是简化版的Java代码片段用于展示如何集成通义API来进行基本的文字转语音(TTS)操作: ```java import java.io.*; import okhttp3.*; public class TextToSpeechExample { private static final String API_KEY = "your_api_key_here"; private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); public static void main(String[] args) throws Exception { OkHttpClient client = new OkHttpClient(); JSONObject jsonRequest = new JSONObject(); jsonRequest.put("text", "你好,世界!"); RequestBody body = RequestBody.create(jsonRequest.toString(), MEDIA_TYPE_JSON); Request request = new Request.Builder() .url("https://api.example.com/v1/tts") // 替换成实际接口地址 .post(body) .addHeader("Authorization", "Bearer " + API_KEY) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); // 将响应中的二进制音频保存到文件 Files.copy(response.body().byteStream(), Paths.get("./output.wav"), StandardCopyOption.REPLACE_EXISTING); System.out.println("Audio file has been saved successfully."); } } } ``` 这段程序展示了怎样通过POST方法提交待转换成语音的文字给远程TTS服务,并把得到的结果存储为本地WAV格式的声音文档。需要注意的是,这里使用的`okhttp3.OkHttpClient`库负责网络通信部分的工作;而JSON对象则用来封装传递的数据结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值