--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接口实现
官方文档:
在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完事

