Java大模型应用开发day06-天机ai-学习笔记

--Day06--平台智能体,通用文本模型,语音

--今日任务

  • 实现通用文本模型
  • 实现语音与文字的互转

--1.通用文本模型

--1.1需求分析

学生在学习过程中,可以在社区中提问,如下:

在传统的系统功能中,一般这样的问题,都是其他的同学或者是老师进行回复,往往这样的回复的不及时的,为了解决这个问题,所以,需要进入**AI自动回复**,就是在学生问题发出后,AI进行自动回复。像这样:

除了AI自动回复功能外,还有其他的一些文本处理的功能,例如:

可以看到,这些功能基本都是文本的处理。而文本的处理,更适合用AI大模型来处理。

--1.2实现分析

此类文本处理的业务,直接交由大模型的智能体处理即可,但是,如果为每个功能都创建一个智能体的话,这样就会非常的繁琐,实际上,我们可以开发一个通用的文本处理类智能体来实现这些功能即可。

--1.3通用文本智能体

--1.3.1老三样

系统提示词

复制代码
角色
你是一名非常出色的IT行业的内容创作者,你的任务是负责内容的帮写、续写、润色和精简。你的目标是帮助学员完成内容的创作,确保内容的合理性。

技能
技能 1: 内容帮写
1. 基于用户提供的主题/关键词,智能生成完整的文案内容,帮助用户快速搭建内容框架。

技能 2: 内容续写
1. 在用户已有文本基础上,自动延续写作思路生成后续内容,保持上下文逻辑连贯性。

技能 3: 内容润色
1. 对现有文本进行语言优化,包括调整句式结构、替换精准词汇、统一行文风格等

技能 4: 内容精简
1. 通过语义分析智能提炼核心信息,删除冗余表达,将长文本压缩为简洁版本

限制:
- AI创作必须严格遵循法律法规和伦理准则,禁止生成危害国家安全、宣扬恐怖极端思想、传播虚假谣言、侵犯他人隐私及知识产权的内容,不得涉及暴力色情、种族宗教歧视、历史虚无主义等违背公序良俗的表述,同时要特别注意避免教唆犯罪、诱导危险行为、损害未成年人身心健康,并在医疗、金融、新闻等专业领域确保内容真实性和安全性,始终以社会主义核心价值观为框架,履行技术向善的社会责任。

读取配置--

改造aiproperties

application.yml中增加配置

在Config里加载配置,由于这个也不需要历史会话记录,那么我们用与路由智能体一样的chatclient即可

--1.3.2编写通用文本处理智能体
复制代码
package com.tianji.aigc.agent;

import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.enums.AgentTypeEnum;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class TextProcessAgent extends AbstractAgent{
    private final SystemPromptConfig systemPromptConfig;

    @Resource(name = "routeChatClient")
    private ChatClient routeChatClient;

    @Override
    protected ChatClient getChatClient() {
        return routeChatClient;
    }

    /**
     * 获取智能体类型标识
     *
     * @return 代理类型枚举值(如:ROUTE、RECOMMEND等)
     */
    @Override
    public AgentTypeEnum getAgentType() {
        return AgentTypeEnum.TEXT;
    }

    /**
     * 获取系统提示信息模板,默认为空字符串,子类可以覆盖重写该方法以返回自定义的系统提示信息。
     *
     * @return 系统提示的文本模板
     */
    @Override
    public String systemMessage() {
        return systemPromptConfig.getTextProcessAgentSystemMessage().get();
    }

    /**
     * 直接构建Prompt调用返回文本
     * @param text 文本
     * @return 处理结果
     */
    public String process(String text){
        return getChatClient()
                .prompt()
                .system(s->s.text(systemMessage()).params(systemMessageParams()))
                .advisors(a->a.advisors(advisors(text)))
                .tools(tools())
                .toolContext(Map.of())
                .user(text)
                .call()
                .content();
    }
}

跟知识讲解智能体一样一样的,复写了一个手动构建Prompt不走session的逻辑

由于路由的逻辑在路由智能体里的提示词已经写好,这里不用担心客服那边会误判智能体类型,我们在接口处直接调用该agent进行聊天就行了

--1.4文本聊天接口

--1.4.1接口分析

可以看到,这个接口的请求是通过body方式传递文本内容的。

--1.4.2代码编写

controller

复制代码
/**
     * 文本对话-通用文本处理
     * @param question 待处理文本
     * @return 处理结果
     */
    @PostMapping("/text")
    public String chatText(@RequestBody String question){
        return chatService.chatText(question);
    }

Service

复制代码
/**
     * 文本处理
     *
     * @param question 待处理文本
     * @return 处理结果
     */
    String chatText(String question);

Serviceimpl

复制代码
private final TextProcessAgent textProcessAgent;
/**
     * 文本处理
     *
     * @param question 待处理文本
     * @return 处理结果
     */
    @Override
    public String chatText(String question) {
        return textProcessAgent.process(question);
    }
--1.4.3测试

--1.5AI自动回复

--1.5.1功能接口分析

查看浏览器请求:

请求参数:

响应:

根据接口url确定到微服务在学习微服务tj-learning

再定位到学习微服务的中Controller:

所以,就需要再新增问题之后,进行调用AI服务进行自动回复。

--1.5.2增加feign接口

tj-learning微服务中,需要调用tj-aigc服务,需要通过Feign进行调用,所以需要先定义Feign接口。

java 复制代码
package com.tianji.api.client.aigc;

import com.tianji.api.client.aigc.fallback.AigcClientFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "aigc-service", contextId = "aigc", fallbackFactory = AigcClientFallback.class)
public interface AigcClient {

    @PostMapping("/chat/text")
    String chatText(@RequestBody String question);

}
java 复制代码
package com.tianji.api.client.aigc.fallback;

import com.tianji.api.client.aigc.AigcClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AigcClientFallback implements FallbackFactory<AigcClient> {

    @Override
    public AigcClient create(Throwable cause) {
        return new AigcClient() {
            @Override
            public String chatText(String question) {
                return "调用aigc服务出错!";
            }
        };
    }

}
--1.5.3剩余代码编写

tj-learning微服务中创建AIService,完成调用Feign接口,来访问tj-aigc微服务。

service

java 复制代码
package com.tianji.learning.service;

import com.tianji.learning.domain.po.InteractionQuestion;

public interface AIService {
    /**
     * AI 自动回复
     *
     * @param interactionQuestion 问题对象
     */
    void autoReply(InteractionQuestion interactionQuestion);
}

servicempl

java 复制代码
package com.tianji.learning.service.impl;

import cn.hutool.core.util.StrUtil;
import com.tianji.api.client.aigc.AigcClient;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.dto.ReplyDTO;
import com.tianji.learning.domain.po.InteractionQuestion;
import com.tianji.learning.service.AIService;
import com.tianji.learning.service.IInteractionReplyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class AIServiceImpl implements AIService {

    private final AigcClient aigcClient;
    private final IInteractionReplyService iInteractionReplyService;

    @Value("${tj.ai.user-id:9999}")
    private Long aiUserId;

    /**
     * 异步自动回复学生提出的互动问题
     *
     * @param interactionQuestion 互动问题对象,包含问题的标题、描述和唯一标识等信息
     * <p>
     * 实现说明:
     * 1. 使用格式化字符串构建包含问题标题和描述的查询内容
     * 2. 调用AI文本生成接口获取专业回复
     * 3. 构造系统回复DTO对象,设置系统用户ID(9999)和问题关联信息
     * 4. 通过服务层持久化回复数据
     */
    @Async
    @Override
    public void autoReply(InteractionQuestion interactionQuestion) {
        // 构建包含完整问题信息的查询模板(标题+描述)
        String question = StrUtil.format("""
                这是一个学生提出的问题,请以专业的角度进行回答,不要随意编造。
                标题:{} 。
                描述:{} 。""", interactionQuestion.getTitle(), interactionQuestion.getDescription());

        // 设置当前用户id,否在会出现401错误
        UserContext.setUser(interactionQuestion.getUserId());
        // 调用AI文本生成服务获取专业回答
        String reply = this.aigcClient.chatText(question);

        // 构建系统自动回复数据对象
        ReplyDTO replyDTO = ReplyDTO.builder()
                .userId(aiUserId)          // 固定系统用户ID
                .content(reply)         // AI生成的回复内容
                .anonymity(false)       // 明确显示系统回复身份
                .questionId(interactionQuestion.getId())  // 关联原始问题ID
                .isStudent(false)       // 标记为非学生回复
                .build();

        // 持久化存储生成的回复
        this.iInteractionReplyService.saveReply(replyDTO);
    }

}

在提交问题后,调用AIService进行自动回复

controller

java 复制代码
    private final AIService aiService;

    @Operation(summary = "新增互动问题")
    @PostMapping
    public void saveQuestion(@Valid @RequestBody QuestionFormDTO questionDTO) {
        InteractionQuestion interactionQuestion = questionService.saveQuestion(questionDTO);

        // 调用AI自动回复
        this.aiService.autoReply(interactionQuestion);
    }
--1.5.4功能测试

可以看到,有自动回复了。

--1.6其他功能

其他的文本处理功能,比如:AI续写、AI扩写等,其实也都是在调用文本聊天接口,前端已经开发完成,只需要给前端系统提示词即可。

--1.6.1接口分析

无请求参数,响应数据结构如下:

可以看到,会返回不同功能的提示词,其中$input就是用户输入的内容。

--1.6.2接口实现

定义templateVO类

java 复制代码
package com.tianji.aigc.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemplateVO {

    private String associationalWord = """
            用户输入关键词:$input|生成规则:生成3个,每个问题含【$input】不超过20字|输出要求:纯文本,问题间用|分隔
            """;
    private String helpedWrite = """
            基于用户提供的主题/关键词,智能生成完整的文案内容(如文章、邮件、报告等),帮助用户快速搭建内容框架
            用户输入:
            $input
            """;

    private String continuedWrite = """
            在用户已有文本基础上,自动延续写作思路生成后续内容,保持上下文逻辑连贯性
            用户输入:
            $input
            """;

    private String polish = """
            对现有文本进行语言优化,包括调整句式结构、替换精准词汇、统一行文风格等
            用户输入:
            $input
            """;

    private String streamline = """
            通过语义分析智能提炼核心信息,删除几余表达,将长文本压缩为简洁版本
            用户输入:
            $input
            """;
}

controller-chatcontroller

java 复制代码
    private static final TemplateVO TEMPLATE_VO = new TemplateVO();

    /**
     * 获取文本处理模板
     * @return 模板
     */
    @GetMapping("/templates")
    public TemplateVO getTemplates(){
        return TEMPLATE_VO;
    }
--1.6.3测试

功能测试

ai帮写:

太长了,去改一下上面前端给的提示词,加个限定让他少说一点就行

这个是生成提问问题的功能

--2.文字语音互转

对于语音和文字的互转,我们也是调用大模型来完成,需要有支持语音服务大模型

--2.1文字转语音--语音合成

--2.1.1功能需求
--2.1.2接口分析

可以看到,文字转语音的接口,提交参数是通过body方式提交的,是需要待转换的文字内容。

--2.1.3接口实现

官方文档:

https://bailian.console.aliyun.com/cn-beijing/?spm=5176.30260724.J_VtPmWTA0QjMbFsMNZMB1P.1.204f7de1Ag1Pbb&tab=doc#/doc/?type=model&url=2938790https://bailian.console.aliyun.com/cn-beijing/?spm=5176.30260724.J_VtPmWTA0QjMbFsMNZMB1P.1.204f7de1Ag1Pbb&tab=doc#/doc/?type=model&url=2938790增加配置

在nacos中的aigc-services配置的Dashscope层级下,增加如下内容

java 复制代码
audio:
  synthesis:
  options:
    model: cosyvoice-v3-flash
    voice: longanhuan
    response-format: mp3
    speed: 1.0

controller

java 复制代码
package com.tianji.aigc.controller;

import com.tianji.common.annotations.NoWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;

@RestController
@RequestMapping("/audio")
@RequiredArgsConstructor
public class AudioController {

    private final AudioService audioService;

    @NoWrapper
    @PostMapping(value = "/tts-stream",produces = "audio/mp3")
    public ResponseBodyEmitter ttsStream(@RequestBody String text){
        return audioService.ttsStream(text);
    }
}

service

java 复制代码
package com.tianji.aigc.service;

import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;

public interface AudioService {

    /**
     * 文字转语音(TTS)
     *
     * @param text 待合成的文本内容
     * @return 异步响应输出
     */
    ResponseBodyEmitter ttsStream(String text);

}

impl

java 复制代码
package com.tianji.aigc.service.impl;

import com.alibaba.cloud.ai.dashscope.audio.DashScopeSpeechSynthesisModel;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisPrompt;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisResponse;
import com.tianji.aigc.service.AudioService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.nio.ByteBuffer;

@Service
@RequiredArgsConstructor
@Slf4j
public class AudioServiceImpl implements AudioService {
    private final DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel;

    /**
     * 文字转语音(TTS)
     *
     * @param text 待合成的文本内容
     * @return 异步响应输出
     */
    @Override
    public ResponseBodyEmitter ttsStream(String text) {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();
        log.info("开始合成语音...,文本内容:{}",text);
        SpeechSynthesisPrompt speechPrompt = new SpeechSynthesisPrompt(text);
        Flux<SpeechSynthesisResponse> responseStream = dashScopeSpeechSynthesisModel.stream(speechPrompt);
        //订阅响应流并发送数据
        responseStream.subscribe(
                speechResponse ->{
                    try{
                        //获取响应输出的数据,并发送到响应体中
                        ByteBuffer audioBuffer = speechResponse.getResult().getOutput().getAudio();
                        byte[] audioBytes = new byte[audioBuffer.remaining()];
                        audioBuffer.duplicate().get(audioBytes);
                        emitter.send(audioBytes);
                    } catch (IOException e){
                        emitter.completeWithError(e);
                    }
                },
                emitter::completeWithError,
                emitter::complete
        );
        return emitter;
    }
}
--2.1.4 测试

能正确播放音频,我的是按照Dashscope平台风格写的

--2.2语音转文字--音频理解

--2.2.1功能需求

录音得到的音频文件,需要通过大模型转化为文字,并且填入到输入框中。

--2.2.2接口分析
--2.2.3功能实现

由于aliyun Dashscope平台的stt的api接口已经更新,导致此处调用的模型返回数据无法正常解析,且通过apikey调用的只能接受有公网url的录音文件,故此处采用Dashscope sdk方式实现

--引入依赖

XML 复制代码
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.22.4</version>
        </dependency>

controller

java 复制代码
 @PostMapping("/stt")
    public String stt(@RequestParam("audioFile") MultipartFile audioFile) throws IOException {
        return this.audioService.stt(audioFile);
    }

service

java 复制代码
/**
     * 语音转文字(STT)
     * @param audioFile 音频文件
     * @return 识别结果文本
     */
    String stt(MultipartFile audioFile) throws IOException;

serviceimpl

java 复制代码
@Value("${spring.ai.dashscope.api-key}")
    private String apiKey;
/**
     * 语音转文字(STT)
     *
     * @param multipartFile 音频文件
     * @return 识别结果文本
     */
    @Override
    public String stt(MultipartFile multipartFile) throws IOException {
        if (multipartFile == null || multipartFile.isEmpty()) {
            throw new IllegalArgumentException("AudioFile is Empty");
        }

        String originFilename = multipartFile.getOriginalFilename();
        String suffix = ".tmp";
        if (originFilename != null) {
            int dot = originFilename.lastIndexOf('.');
            if (dot >= 0 && dot < originFilename.length() - 1) {
                suffix = originFilename.substring(dot);
            }
        }

        Path tempFile = Files.createTempFile("stt-", suffix);
        try {
            // 保存上传的文件到临时文件
            multipartFile.transferTo(tempFile);

            // 构造file://协议的URL
            String localFilePath =tempFile.toUri().toString();

            // 创建MultiModalConversation实例
            MultiModalConversation conv = new MultiModalConversation();

            // 构建用户消息(包含音频文件)
            MultiModalMessage userMessage = MultiModalMessage.builder()
                    .role(Role.USER.getValue())
                    .content(List.of(
                            Collections.singletonMap("audio", localFilePath)))
                    .build();

            // 可选:系统消息(用于定制化识别Context)
            MultiModalMessage sysMessage = MultiModalMessage.builder()
                    .role(Role.SYSTEM.getValue())
                    .content(List.of(Collections.singletonMap("text", "")))
                    .build();

            // ASR配置选项
            Map<String, Object> asrOptions = new HashMap<>();
            asrOptions.put("enable_itn", true); // 启用逆文本归一化(例如:将"一万"转为"10000")
            asrOptions.put("language", "zh");   // 指定语种为中文

            // 构建请求参数
            MultiModalConversationParam param = MultiModalConversationParam.builder()
                    .apiKey(apiKey)
                    .model("qwen3-asr-flash") // 使用快速识别模型
                    .message(sysMessage)
                    .message(userMessage)
                    .parameter("asr_options", asrOptions)
                    .build();

            // 调用API
            MultiModalConversationResult result = conv.call(param);

            // 提取识别文本
            if (result != null && result.getOutput() != null
                    && result.getOutput().getChoices() != null
                    && !result.getOutput().getChoices().isEmpty()) {

                String text = result.getOutput().getChoices().get(0)
                        .getMessage().getContent().get(0).get("text").toString();

                log.info("语音识别成功,文本内容: {}", text);
                return text;
            }

            throw new RuntimeException("语音识别失败:无返回结果");

        } catch (NoApiKeyException | ApiException | UploadFileException e) {
            log.error("调用阿里云语音识别API失败", e);
            throw new RuntimeException("语音识别失败:" + e.getMessage(), e);
        } finally {
            // 清理临时文件
            try {
                Files.deleteIfExists(tempFile);
            } catch (IOException e) {
                log.warn("Failed to delete temp audio file: {}", tempFile, e);
            }
        }
    }

可以把配置常量拉出来放到nacos,我懒我就不做了

--2.2.4测试

关于无法开启麦克风的说明:

由于我们的项目在本地通过域名访问,这个域名是通过hosts文件映射的,浏览器会认为是不安全的,所以是无法打开麦克风权限的,需要进行特殊的设置。

以chrome为例,打开这个:chrome://flags/#unsafely-treat-insecure-origin-as-secure

输入,http://www.tianji.com 连接地址

这样,就可以开启麦克风权限了。

ok完事

相关推荐
JavaLearnerZGQ2 小时前
Vue3全部笔记
笔记
weixin_440730502 小时前
01测试前置知识笔记
笔记
小北方城市网2 小时前
SpringBoot 安全认证实战(Spring Security + JWT):打造无状态安全接口体系
数据库·spring boot·后端·安全·spring·mybatis·restful
大只鹅2 小时前
Stream使用
java·开发语言
Z_W_H_2 小时前
MyBatis-Plus 详细学习文档
学习·mybatis
青衫码上行2 小时前
maven依赖管理和生命周期
java·学习·maven
小六花s2 小时前
渗透测试前四天PHP文件包含笔记
android·学习·渗透测试
秋深枫叶红2 小时前
嵌入式第四十七篇——ARM汇编
汇编·arm开发·学习
散峰而望2 小时前
OJ 题目的做题模式和相关报错情况
java·c语言·数据结构·c++·vscode·算法·visual studio code