Java 篇-项目实战-AI 天机学堂(从 0 到 1)-day6
java 篇: 1.基础地基 2.设计原理 3.项目实战
通用文本模型与语音-通用文本模型-引入 OpenAI:
导入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
增加配置:
在 `nacos` 中 `aigc-service.yaml` 配置项,增加如下内容:
tj:
ai:
openai:
key: ${OPENAI_API_KEY}
spring:
ai:
openai:
base-url: https://api.chatanywhere.tech
api-key: ${tj.ai.openai.key}
chat:
options:
model: gpt-3.5-turbo
enabled: true
这里走的是代理
重启 aigc 服务,但是,会发现报错:

这是因为,在 `SpringAI` 启动时,会需要注入 `ChatModel` 对象,但是,现在由于引入了阿里云和 `OpenAI`,会存在 2 个 `ChatModel` 对象实例,所以导致了报错。
分析问题:
要想解决问题,首先我们需要找到启动时的自动配置类,在这里:

SpringAI 就是由 `org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration` 类完成自动配置的。
可以看到,在这个类中的 `chatClientBuilder` 方法,中注入了 `ChatModel` 对象,由于现在的 Spring 容器中存在 2 个实例,所以就报错了。

怎么解决这个问题呢?
**解决方法:**在 `SpringAIConfig` 中不通过 `ChatClient.Builder` 构建 `ChatClient` 对象,而是通过注入 `ChatModel` 的方式,来创建不同的 `ChatClient` 对象。
**解决问题:**
在 `SpringAIConfig` 中改造代码:
@Bean
public ChatClient chatClient(@Qualifier("dashscopeChatModel") ChatModel dashScopeChatModel, //注意这里@Qualifier的指定的名称,不要写错了
Advisor loggerAdvisor, // 日志记录器
Advisor messageChatMemoryAdvisor,
Advisor recordOptimizationAdvisor // 记录优化
// CourseTools courseTools, // 课程工具
// OrderTools orderTools // 预下单工具
) {
return ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor, recordOptimizationAdvisor) //添加 Advisor 功能增强
// .defaultTools(courseTools, orderTools)
.build();
}
@Bean
public ChatClient openAiChatClient(@Qualifier("openAiChatModel") ChatModel openAiChatModel,
Advisor loggerAdvisor // 日志记录器
) {
return ChatClient.builder(openAiChatModel)
.defaultAdvisors(loggerAdvisor)
.build();
}
通用文本模型与语音-通用文本模型-文本聊天接口:
接口说明:

可以看到,这个接口的请求是通过 body 方式传递文本内容的。
Controller:
@PostMapping("/text")
public String chatText(@RequestBody String question) {
return this.chatService.chatText(question);
}
Service:
/**
* 文本对话
*
* @param question 问题
* @return 回答
*/
String chatText(String question);
Impl:

@Override
public String chatText(String question) {
return this.openAiChatClient.prompt()
.system(promptSystem -> promptSystem.text(this.systemPromptConfig.getTextSystemMessage().get()))
.user(question)
.call()
.content();
}
prompt 配置:
package com.tianji.aigc.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.prompt")
public class AIProperties {
private System system; // 系统提示语,用于课程推荐、购买业务
@Data
public static class System {
private Chat chat; // 系统提示语,用于课程推荐、购买业务
private Chat routeAgent; // 路由智能体系统提示词
private Chat recommendAgent; // 推荐智能体系统提示词
private Chat buyAgent; // 购买智能体系统提示词
private Chat consultAgent; // 咨询智能体系统提示词
private Chat knowledgeAgent; // 知识讲解智能体系统提示词
private Chat text; // 文本提示语,用于问答回复、润色等文本类型的业务
@Data
public static class Chat {
private String dataId;
private String group = "DEFAULT_GROUP";
private long timeoutMs = 20000L; // 读取的超时时间,单位毫秒
}
}
}
package com.tianji.aigc.config;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
@Getter
@Configuration
@RequiredArgsConstructor
public class SystemPromptConfig {
private final NacosConfigManager nacosConfigManager;
private final AIProperties aiProperties;
// 使用原子引用,保证线程安全
private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();
private final AtomicReference<String> routeAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> recommendAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> buyAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> consultAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> knowledgeAgentSystemMessage = new AtomicReference<>();
private final AtomicReference<String> textSystemMessage = new AtomicReference<>();
@PostConstruct // 初始化时加载配置
public void init() {
// 读取配置文件
loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
loadConfig(aiProperties.getSystem().getRouteAgent(), routeAgentSystemMessage);
loadConfig(aiProperties.getSystem().getRecommendAgent(), recommendAgentSystemMessage);
loadConfig(aiProperties.getSystem().getBuyAgent(), buyAgentSystemMessage);
loadConfig(aiProperties.getSystem().getConsultAgent(), consultAgentSystemMessage);
loadConfig(aiProperties.getSystem().getKnowledgeAgent(), knowledgeAgentSystemMessage);
loadConfig(aiProperties.getSystem().getText(), textSystemMessage);
}
private void loadConfig(AIProperties.System.Chat chatConfig, AtomicReference<String> target) {
try {
String dataId = chatConfig.getDataId();
String group = chatConfig.getGroup();
long timeoutMs = chatConfig.getTimeoutMs();
String config = nacosConfigManager.getConfigService().getConfig(dataId, group, timeoutMs);
target.set(config);
log.info("读取{}成功,内容为:{}", target, config);
nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String info) {
target.set(info);
log.info("更新{}成功,内容为:{}", target, info);
}
});
} catch (Exception e) {
log.error("加载配置失败", e);
}
}
}
在 nacos 中增加 `text-system-chat-message.txt` 文件:
角色
你是一名非常出色的IT行业的内容创作者,你的任务是负责内容的帮写、续写、润色和精简。你的目标是帮助学员完成内容的创作,确保内容的合理性。
技能
技能 1: 内容帮写
1. 基于用户提供的主题/关键词,智能生成完整的文案内容,帮助用户快速搭建内容框架。
技能 2: 内容续写
1. 在用户已有文本基础上,自动延续写作思路生成后续内容,保持上下文逻辑连贯性。
技能 3: 内容润色
1. 对现有文本进行语言优化,包括调整句式结构、替换精准词汇、统一行文风格等
技能 4: 内容精简
1. 通过语义分析智能提炼核心信息,删除冗余表达,将长文本压缩为简洁版本
限制:
- AI创作必须严格遵循法律法规和伦理准则,禁止生成危害国家安全、宣扬恐怖极端思想、传播虚假谣言、侵犯他人隐私及知识产权的内容,不得涉及暴力色情、种族宗教歧视、历史虚无主义等违背公序良俗的表述,同时要特别注意避免教唆犯罪、诱导危险行为、损害未成年人身心健康,并在医疗、金融、新闻等专业领域确保内容真实性和安全性,始终以社会主义核心价值观为框架,履行技术向善的社会责任。
在 application.yml 中增加配置:
tj:
ai:
prompt:
system:
text:
data-id: text-system-chat-message.txt
测试:

通用文本模型与语音-通用文本模型-定义 Feign 接口:
功能接口分析:


查看浏览器请求:

请求参数:

响应:

根据接口 url,可以确定到微服务是在学习微服务(tj-learning):

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

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

增加 Feign 接口:
在 `tj-learning` 微服务中,需要调用 `tj-aigc` 服务,需要通过 `Feign` 进行调用,所以需要先定义 Feign 接口。
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);
}
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服务出错!";
}
};
}
}

Service:
在 `tj-learning` 微服务中创建 `AIService`,完成调用 `Feign` 接口,来访问 `tj-aigc` 微服务。
package com.tianji.learning.service;
import com.tianji.learning.domain.po.InteractionQuestion;
public interface AIService {
/**
* AI 自动回复
*
* @param interactionQuestion 问题对象
*/
void autoReply(InteractionQuestion interactionQuestion);
}
Impl:
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) {
// 构建包含完整问题信息的查询模板(标题+描述)
var question = StrUtil.format("""
这是一个学生提出的问题,请以专业的角度进行回答,不要随意编造。
标题:{} 。
描述:{} 。""", interactionQuestion.getTitle(), interactionQuestion.getDescription());
// 设置当前用户id,否在会出现401错误
UserContext.setUser(interactionQuestion.getUserId());
// 调用AI文本生成服务获取专业回答
var reply = this.aigcClient.chatText(question);
// 构建系统自动回复数据对象
var replyDTO = ReplyDTO.builder()
.userId(aiUserId) // 固定系统用户ID
.content(reply) // AI生成的回复内容
.anonymity(false) // 明确显示系统回复身份
.questionId(interactionQuestion.getId()) // 关联原始问题ID
.isStudent(false) // 标记为非学生回复
.build();
// 持久化存储生成的回复
this.iInteractionReplyService.saveReply(replyDTO);
}
}
在 `tj-learning` 微服务中,增加 AI 用户 id 的配置项:
tj:
ai:
user-id: 9999 #默认id,数据库tj_user中的user表中,有对应的用户数据

Controller:
在提交问题后,调用 AIService 进行自动回复:
private final AIService aiService;
@Operation(summary = "新增互动问题")
@PostMapping
public void saveQuestion(@Valid @RequestBody QuestionFormDTO questionDTO) {
var interactionQuestion = questionService.saveQuestion(questionDTO);
// 调用AI自动回复
this.aiService.autoReply(interactionQuestion);
}
功能测试:

可以看到,有自动回复了。
通用文本模型与语音-通用文本模型-其他功能:
其他的文本处理功能,比如:AI 续写、AI 扩写等,其实也都是在调用文本聊天接口,前端已经开发完成,只需要给前端系统提示词即可。
接口说明:

这个接口没有请求参数,响应数据结构如下:

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


定义 TemplateVO 类:
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:
private static final TemplateVO TEMPLATE_VO = new TemplateVO();
@GetMapping("/templates")
public TemplateVO getTemplates() {
return TEMPLATE_VO;
}
测试:

功能测试:


当中的 templates 刚进去就会发起请求了


通用文本模型与语音-文字转语音-实现分析:
对于语音和文字的互转,我们也是调用大模型来完成,需要有支持语音服务大模型,这里我们采用 OpenAI 的接口实现。

功能需求:

接口说明:

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


功能实现:
tts 配置:
在 `nacos` 中的 `aigc-service.yaml` 增加 `OpenAI` 的 `tts` 配置:
spring:
ai:
openai:
audio:
speech:
base-url: https://api.chatanywhere.tech/
api-key: ${tj.ai.openai.key}
options:
model: tts-1 #可用的 TTS 模型之一:tts-1 或 tts-1-hd
voice: alloy #生成音频时使用的语音。支持的语音有:alloy、echo、fable、onyx、nova 和 shimmer。
response-format: mp3 #默认为 mp3 音频的格式。支持的格式有:mp3、opus、aac 和 flac。
speed: 1.0 #默认为 1 生成的音频速度。选择0.25到4.0之间的值。1.0是默认值。

Controller:
package com.tianji.aigc.controller;
import com.tianji.aigc.service.AudioService;
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 this.audioService.ttsStream(text);
}
}
Service:
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:
package com.tianji.aigc.service.impl;
import com.tianji.aigc.service.AudioService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.audio.speech.SpeechPrompt;
import org.springframework.ai.openai.audio.speech.SpeechResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;
import java.io.IOException;
@Slf4j
@Service
@RequiredArgsConstructor
public class OpenAIAudioServiceImpl implements AudioService {
private final OpenAiAudioSpeechModel openAiAudioSpeechModel;
@Override
public ResponseBodyEmitter ttsStream(String text) {
var emitter = new ResponseBodyEmitter();
log.info("开始语音合成, 文本内容:{}", text);
var speechPrompt = new SpeechPrompt(text);
var responseStream = openAiAudioSpeechModel.stream(speechPrompt);
// 订阅响应流并发送数据
responseStream.subscribe(
speechResponse -> {
try {
// 获取响应输出的数据,并发送到响应体中
byte[] audioBytes = speechResponse.getResult().getOutput();
emitter.send(audioBytes);
} catch (IOException e) {
emitter.completeWithError(e);
}
},
emitter::completeWithError,
emitter::complete
);
return emitter;
}
}
测试:

测试文字:
装饰器是一种特殊的函数,它可以修改或增强另一个函数的功能。简单来说,装饰器可以在不改变原有代码的情况下,增加新的功能或者修改原有的功能。例如,在Python中,我们可以使用@装饰器来定义一个函数的装饰器,从而在运行时自动添加一些额外的功能。
对于这个问题,由于提供的信息是关于课程的信息,而不是IT领域的知识,所以我是无法回答的。如果你有任何问题,可以继续提问哦!
测试得到的音频:

通用文本模型与语音-语音转文字-实现分析:
功能需求:

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

OpenAI 接口:

功能实现:
stt 配置:
在 `nacos` 中的 `aigc-service.yaml` 增加 `OpenAI` 的 `stt` 配置:
spring:
ai:
openai:
audio:
transcription:
base-url: https://api.chatanywhere.tech/
api-key: ${tj.ai.openai.key}
options:
model: whisper-1 #要使用的模型 ID。目前只有 whisper-1 是可用的。
response-format: text #转录输出的格式,可选择:json、text、srt、verbose_json 或 vtt。
temperature: 0 #默认为 0,采样温度,between 0 和 1。更高的值像 0.8 会使输出更随机,而更低的值像 0.2 会使其更集中和确定性。如果设置为 0,模型将使用对数概率自动增加温度直到达到特定阈值。
language: zh #输入音频的语言。以 ISO-639-1 格式提供输入语言可以提高准确性和延迟。
Controller:
@PostMapping("/stt")
public String stt(@RequestParam("audioFile") MultipartFile audioFile) {
return this.audioService.stt(audioFile);
}
Service:
/**
* 语音转文字(STT)
* @param audioFile 音频文件
* @return 识别结果文本
*/
String stt(MultipartFile audioFile);
在输出的结果中,可能会存在繁体中文,所以需要将繁体转化为简体中文,用到了 opencc4j 组件,所以需要导入其依赖:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>opencc4j</artifactId>
<version>1.8.0</version>
</dependency>
测试:
apifox 测试:
先文字转语音,获得音频:


音频转文字:

重新启动服务,进行测试,会发现控制台报错:


通过网关去调用会携带请求头,根据请求头来判断是不是网关来的,如果是的话,就会进行消息转换器。
这是因为在原有的代码中,自定义了 `WrapperResponseMessageConverter` 消息转化器,他的作用是对输出的内容进行包装,而 `SpringAI` 底层用的发起 `http` 请求的组件是 `RetryTemplate`,而 `RetryTemplate` 也会用到这个消息转化器,但是这个消息转化器是无法处理文件的,所以报消息转化出错:

解决问题:
为了解决上述问题,需要在发起请求时,添加一个标识来表明是 SpringAI 发起的请求,就不再进行包装处理了。
要想实现这样的效果,需要给 `RetryTemplate` 添加一个监听器,在发起请求前设置标识,请求结束后删除标识,这样就可以解决问题了。
具体代码如下:
/**
* 创建并配置自定义重试监听器Bean
* <p>
* 实现说明:
* 1. 创建匿名RetryListener实现,在重试操作期间管理Web属性
* 2. 将监听器注册到提供的RetryTemplate实例
*
* @param retryTemplate Spring Retry模板对象,用于注册重试监听器
* @return RetryListener 已注册到模板的重试监听器实例,将由Spring容器管理
*/
@Bean
public RetryListener customizeRetryTemplate(RetryTemplate retryTemplate) {
// 创建自定义重试监听器,实现以下核心功能:
// - 重试开始时设置上下文标识
// - 重试结束后清理上下文标识
RetryListener retryListener = new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
WebUtils.setAttribute(Constant.SPRING_AI_ATTR, Constant.SPRING_AI_FLAG);
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
WebUtils.removeAttribute(Constant.SPRING_AI_ATTR);
}
};
// 将监听器注册到重试模板
retryTemplate.registerListener(retryListener);
return retryListener;
}
测试:

可以看到,已经成功的将语音文件转化为文字了。
也可以基于前端页面进行测试:

如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)