参考视频:黑马程序员SpringAI+DeepSeek大模型应用开发实战视频教程
1. 在线平台智能体
在前面我们学习的智能体,是基于SpringAI代码的方式实现了智能体,实际上还可以通过在线平台创建智能体,通过代码的方式进行调用,这也是一种实现智能体的方式。像这样的平台有很多,比如阿里百炼、扣子平台。
下面,我们就以阿里百炼平台为例进行学习。
1.1. 创建智能体
1.1.1. 新增应用


1.1.2. 设置系统提示词

角色
你作为在线教育平台资深客服代表兼讲师。你的任务根据学员的需求,调用知识库中的课程信息,为学员推荐合适的课程,同时解答学员对课程内容和知识点的疑问。
技能 1: 课程推荐
1. 当学员提出课程推荐需求时,需判断是否提供必要信息。必要信息包含年龄、学历、是否有编程基础。
2. 若缺少必要信息,需礼貌追问。
3. 若学员未提供感兴趣的方向,需追问。若没有明确方向,优先推荐学习人数多的课程。
4. 若信息充足,根据必要信息和感兴趣的课程方向,去知识库匹配合适的课程,获取课程id,调用queryCourseById,根据课程id查询课程详细信息,为学员推荐课程,可推荐单门/多门课程。
5. 若知识库未包含学员感兴趣方向,需明确告知学员未提供该方向课程,并推荐其他课程。
6. 若必要信息未匹配合适课程,需提示学员您的情况与现有课程要求并不完全匹配,说明详细原因后,再推荐其他课程。
7. 推荐课程,必须要通过queryCourseById查询后,才能返回数据。
技能 2: 课程购买
1. 当学员提出购买课程时,需判断此次会话中,学员是否明确提出购买xx课/系统已为学员推荐课程。
2. 若已推荐/明确课程名称,需调用prePlaceOrder,根据此次上文已推荐/学员明确的课程,直接进入预下单流程。
3. 若未推荐课程,需引导学员进入到课程推荐流程。
4. 若学员未明确提出购买某门课程时,需询问用户购买哪门课程。
5. 支持购买一门/多门课程。
技能 3: 课程咨询
1. 当学员咨询课程内容时,需去知识库匹配合适的课程,获取课程id,根据课程id查询课程详细信息。回复的内容要全面,要引导学员报名购买。
2. 若未查询到,需礼貌告知学员未检索到相关的内容,请联系人工客服010-12345678。
3. 若咨询课程有效期,需将当前时间{now} 与 课程有效期相加,回复学员准确日期。课程有效期999天,代表永久有效。
技能 4: 知识讲解
1. 当学员咨询与IT相关的知识点内容时,需详细讲解知识点并提供示例。
限制:
- 推荐的课程只能从知识库中选择,坚决不能凭空编造
- 回答的内容要逻辑清晰、内容全面、不要有遗漏。
- 只能回答与课程和IT知识点相关的内容,若学员咨询与课程无关的内容,你需告知学员不能回答与课程和IT知识点无关的问题,并引导学员咨询与课程/IT知识点相关的问题。
- 若学员询问课程ID,则告知学员无法提供课程ID,引导学员咨询其他的问题。1.1.3. 测试
在右侧窗口,可以进行功能测试:

但是,他回答的内容却不是我们想要的:

这是因为还没有给他设置知识库。
1.2. 添加知识库
1.2.1. 添加数据
添加知识库之前,需要先添加数据:


在下面可以查看数据表结构:

还可以预览数据:

点击确定,保存数据:

数据保存成功:

1.2.2. 添加知识库



选择数据:


数据导入成功:

1.2.3. 智能体添加知识库
需要返回到智能体应用中添加:

1.2.4. 功能测试

可以看到,添加的知识体已经生效了,大模型可以推荐课程了。
如果问他这个课程多钱,大模型是不知道的,因为在知识库中是没有这个数据的,需要添加插件,也就是我们程序中的Tools,根据Tools进行查询:

1.3. 添加插件
1.3.1. 内网穿透
我们的微服务是在本地运行的,而大模型则在阿里云上,直接访问不到本地的服务。要解决这个问题,可以使用内网穿透技术。 简单来说,内网穿透就是把本地的服务开放到互联网上,这样不管谁只要能上网就能访问你的服务了。
在这里推荐2个免费的内网穿透服务,分别是:
这里以【cpolar】为例,介绍使用方法:
第一步,下载安装cpolar:
参考文档进行安装:👉 文档 - cpolar 极点云官网
第二步,注册账号并且登录。
第三步,设置token:
请求 https://dashboard.cpolar.com/get-started 页面,查看命令cpolar authtoken xxxx后面的【xxxx】就是你自己的token,每个人是不一样的。token只需要设置一次。
第四步,设置端口映射:
例如:cpolar http 10010端口改成你自己的端口。

在线查看:

我们在Apifox中,添加内网穿透地址,进行功能测试:


可以看到,已经能够调用通接口了,说明走外网已经可以访问本地服务了。
🚨 说明:由于我们使用的是免费账号,这个域名是临时域名,在重启cpolar服务后,域名将会被更换。
1.3.2. 创建插件


1.3.3. 查询课程工具
设置工具信息:

设置请求参数:

设置接口输出字段说明:

测试:

可以看到,工具已经测试成功了。
点击发布:

1.3.4. 智能体添加工具

设置token:

再次测试:

可以看到,大模型已经查询到数据了。
1.4. 程序调用
前面我们已经创建好了智能体,下面我们将通过代码的方式进行调用智能体。参考文档:阿里云帮助中心
第一步,导入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.19.0</version>
<exclusions>
<exclusion>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
</exclusion>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-simple</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>第二步,编写测试用例:
package com.tianji.aigc.agent;
import cn.hutool.core.map.MapUtil;
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.utils.JsonUtils;
import io.reactivex.Flowable;
import org.junit.jupiter.api.Test;
import java.util.Map;
public class AppTest {
@Test
public void testAppCall() throws Exception {
// 构造业务参数
String token = "xxxxxxxxxxx";
Map<String, Object> bizParams = MapUtil.<String, Object>builder()
.put("user_defined_tokens", MapUtil.of("tool_xxxxx", // 工具id
MapUtil.of("user_token", token)))
.build();
// bizParams.add("user_defined_tokens", JsonObject);
ApplicationParam param = ApplicationParam.builder()
// 若没有配置环境变量,可用百炼API Key将下行替换为:.apiKey("sk-xxx")。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
.apiKey("sk-xxxxxx")
.appId("xxxxxxxxxx") // 智能体id
.prompt("查询课程,id为:1880533253575225346")
.incrementalOutput(true) // 开启增量输出
.bizParams(JsonUtils.toJsonObject(bizParams))
.build();
Application application = new Application();
Flowable<ApplicationResult> result = application.streamCall(param);
// 阻塞式的打印内容
result.blockingForEach(data -> {
System.out.printf("%s\n",data.getOutput().getText());
});
}
}应用id在这里查找:

工具id在这里查找:

传递token的参数结构,参考文档:自定义参数传递
{
"user_defined_tokens": {
"tool_xxxx": { // 工具id
"user_token": "xxxxxxxxx" // token值
}
}
}运行结果:
您
查询的课程信息
如下:
课程名称
:Java开发零
基础入门
分类
:IT·互联网
/Java开发/
Java零基础
适用人群:本
课程适合18
至28岁
之间的学员,要求
具备大专及以上学历
,无需任何编程
基础。学员应
具有对软件开发
的浓厚兴趣,
希望在Java开发
领域有所发展。
课程介绍:
这门课程专注于
Java开发的基础知识
,特别适合零
基础学员。学员
将学习如何编写
基本的Java代码
,理解面向对象
编程的核心概念,并
掌握常用的Java开发
环境(如E
clipse、Intelli
J IDEA)。课程
内容包括Java语法
基础、变量与
数据类型、控制
结构、类与
对象、异常处理
等。我们还将
深入探讨如何进行
项目管理和团队协作
,帮助学员全面提升
Java开发的能力。
通过大量的实战案例
和项目练习,
学员可以快速掌握
Java开发的核心方法
,并在实际工作中
灵活运用。完成
本课程后,
学员将具备独立
编写简单Java应用程序
的能力。
课程价格
:1元
有效期限:永久
有效(99
99天)
购买截止时间
:202
9年1月
26日
23:5
9:59
如果对这
门课程感兴趣,
可以考虑报名参加
哦!1.5. 集成到项目
第一步,在nacos中增加配置
tj:
ai:
dashscope:
key: sk-xxxxxxxxxxx
app-agent:
id: xxxxxxxxx
tools:
- "tool_xxxxx"
- "tool_xxxxx"第二步,编写配置类,读取上面的配置
package com.tianji.aigc.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.dashscope")
public class DashScopeProperties {
private String key;
private AppAgent appAgent;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AppAgent {
private String id;
private List<String> tools;
}
}第三步,编写AppAgentChatService,用来集成平台的智能体
package com.tianji.aigc.service.impl;
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.utils.JsonUtils;
import com.tianji.aigc.config.DashScopeProperties;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.TokenContext;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "tj.ai", name = "chat-type", havingValue = "APP")
public class AppAgentChatService implements ChatService {
private final DashScopeProperties dashScopeProperties;
// 存储大模型的生成状态,这里采用ConcurrentHashMap是确保线程安全
// 目前的版本暂时用Map实现,如果考虑分布式环境的话,可以考虑用redis来实现
private static final Map<String, Boolean> GENERATE_STATUS = new ConcurrentHashMap<>();
// 输出结束的标记
private static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();
@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {
// 获取对话id
var conversationId = ChatService.getConversationId(sessionId);
var token = TokenContext.getToken();
var toolsMap = new HashMap<String, Object>();
for (var tool : dashScopeProperties.getAppAgent().getTools()) {
toolsMap.put(tool, Map.of("user_token", token));
}
var bizParams = Map.of("user_defined_tokens", toolsMap);
var param = ApplicationParam.builder()
.apiKey(dashScopeProperties.getKey())
.appId(dashScopeProperties.getAppAgent().getId()) // 智能体id
.prompt(question)
.incrementalOutput(true) // 开启增量输出
.bizParams(JsonUtils.toJsonObject(bizParams))
.sessionId(conversationId) // 设置会话ID
.build();
var application = new Application();
try {
var result = application.streamCall(param);
// 将Flowable 转化为 Flux 进行处理输出
return Flux.from(result)
.doFirst(() -> { //输出开始,标记正在输出
GENERATE_STATUS.put(sessionId, true);
})
.doOnComplete(() -> { //输出结束,清除标记
GENERATE_STATUS.remove(sessionId);
})
.doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 错误时清除标记
// 输出过程中,判断是否正在输出,如果正在输出,则继续输出,否则结束输出
.takeWhile(s -> GENERATE_STATUS.getOrDefault(sessionId, false))
.map(applicationResult -> {
// 获取大模型的输出的内容
var text = applicationResult.getOutput().getText();
// 封装响应对象
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(STOP_EVENT));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void stop(String sessionId) {
// 移除标记
GENERATE_STATUS.remove(sessionId);
}
}第四步,测试:

📚 说明:
虽然已经将平台智能体集成到项目中,但是功能并不完善,缺少历史记录功能,这个功能的开发,需要自己手动进行记录,和SpringAI就没有什么关系了,我们暂时不做实现。
1.6. 练习:工作流应用
阿里云百炼平台不只是提供智能体,还提供工作流的应用,和我们今天学习的路由工作流很类似,可以在平台上创建智能体工作流,在代码中进行调用,其调用方式与智能体类似。参考文档:工作流应用_大模型服务平台百炼(Model Studio)-阿里云帮助中心
类似下面这样的流程:
具体的实现,留给同学们在作业中完成。可以实现这样的工作流:

2. 通用文本模型
2.1. 需求分析
学生在学习过程中,可以在社区中提问,如下:

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

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


可以看到,这些功能基本都是文本的处理。而文本的处理,更适合用AI大模型来处理。
2.2. 实现分析
此类文本处理的业务,直接交由大模型的智能体处理即可,但是,如果为每个功能都创建一个智能体的话,这样就会非常的繁琐,实际上,我们可以开发一个通用的文本处理类智能体来实现这些功能即可。
然而,这个文本处理类智能体,我们将对接OpenAI实现。(用阿里通义大模型也可以实现,只是为了在项目中构造对接两种不同平台的大模型场景而设定,这必然增加了复杂度,而我们学习的价值也更大)
2.3. 引入OpenAI
2.3.1. 导入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>2.3.2. 增加配置
在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对象实例,所以导致了报错。
2.3.3. 分析问题
要想解决问题,首先我们需要找到启动时的自动配置类,在这里:

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

怎么解决这个问题呢?
**解决方法:**在SpringAIConfig中不通过ChatClient.Builder构建ChatClient对象,而是通过注入ChatModel的方式,来创建不同的ChatClient对象。
2.3.4. 解决问题
在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();
}重启测试即可。
2.4. 文本聊天接口
2.4.1. 接口说明

可以看到,这个接口的请求是通过body方式传递文本内容的。
2.4.2. Controller
@PostMapping("/text")
public String chatText(@RequestBody String question) {
return this.chatService.chatText(question);
}2.4.3. Service
/**
* 文本对话
*
* @param question 问题
* @return 回答
*/
String chatText(String question);2.4.4. Impl
@Override
public String chatText(String question) {
return this.openAiChatClient.prompt()
.system(promptSystem -> promptSystem.text(this.systemPromptConfig.getTextSystemMessage().get()))
.user(question)
.call()
.content();
}2.4.5. 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.txt2.4.6. 测试

2.5. AI自动回复
2.5.1. 功能接口分析


查看浏览器请求:

请求参数:

响应:

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

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

所以,就需要再新增问题之后,进行调用AI服务进行自动回复。
2.5.2. 增加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服务出错!";
}
};
}
}
2.5.3. 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);
}2.5.4. 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表中,有对应的用户数据2.5.5. 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);
}2.5.6. 功能测试

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

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

可以看到,会返回不同功能的提示词,其中$input就是用户输入的内容。
2.6.2. 接口实现
定义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;
}2.6.3. 测试

功能测试:



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

3.1. 文字转语音
3.1.1. 功能需求

3.1.2. 接口说明

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

3.1.4. 功能实现
3.1.4.1. 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是默认值。3.1.4.2. 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);
}
}3.1.4.3. 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);
}3.1.4.4. 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;
}
}3.1.5. 测试

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

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

3.2.3. OpenAI接口

3.2.4. 功能实现
3.2.4.1. 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 格式提供输入语言可以提高准确性和延迟。3.2.4.2. Controller
@PostMapping("/stt")
public String stt(@RequestParam("audioFile") MultipartFile audioFile) {
return this.audioService.stt(audioFile);
}3.2.4.3. Service
/**
* 语音转文字(STT)
* @param audioFile 音频文件
* @return 识别结果文本
*/
String stt(MultipartFile audioFile);3.2.4.4. Impl
private final OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel;
@Override
public String stt(MultipartFile multipartFile) {
// 将MultipartFile转换为Resource
Resource audioResource = multipartFile.getResource();
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioResource);
// 调用OpenAiAudioTranscriptionModel进行语音识别
AudioTranscriptionResponse response = openAiAudioTranscriptionModel.call(transcriptionRequest);
// 获取识别结果
String output = response.getResult().getOutput();
// 将繁体转换为简体
return ZhConverterUtil.toSimple(output);
}在输出的结果中,可能会存在繁体中文,所以需要将繁体转化为简体中文,用到了opencc4j组件,所以需要导入其依赖:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>opencc4j</artifactId>
<version>1.8.0</version>
</dependency>3.2.4.5. 测试
重新启动服务,进行测试,会发现控制台报错:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class java.io.ByteArrayInputStream]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:489) ~[spring-web-6.1.14.jar:6.1.14]
at org.springframework.http.converter.AbstractGenericHttpMessageConverter.writeInternal(AbstractGenericHttpMessageConverter.java:123) ~[spring-web-6.1.14.jar:6.1.14]
at org.springframework.http.converter.AbstractHttpMessageConverter$1.writeTo(AbstractHttpMessageConverter.java:216) ~[spring-web-6.1.14.jar:6.1.14]
at org.springframework.http.client.OkHttp3ClientHttpRequest$BodyRequestBody.writeTo(OkHttp3ClientHttpRequest.java:130) ~[spring-web-6.1.14.jar:6.1.14]
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:62) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) ~[okhttp-4.12.0.jar:na]
at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) ~[okhttp-4.12.0.jar:na]这是因为在原有的代码中,自定义了WrapperResponseMessageConverter消息转化器,他的作用是对输出的内容进行包装,而SpringAI底层用的发起http请求的组件是RetryTemplate,而RetryTemplate也会用到这个消息转化器,但是这个消息转化器是无法处理文件的,所以报消息转化出错:

3.2.4.6. 解决问题
为了解决上述问题,需要在发起请求时,添加一个标识来表明是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;
}3.2.5. 测试

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

🚨 关于无法开启麦克风的说明:
由于我们的项目在本地通过域名访问,这个域名是通过hosts文件映射的,浏览器会认为是不安全的,所以是无法打开麦克风权限的,需要进行特殊的设置。
以chrome为例,打开这个:chrome://flags/#unsafely-treat-insecure-origin-as-secure
输入,http://www.tianji.com 连接地址


这样,就可以开启麦克风权限了。
3.3. 练习
语音与文字互转的功能,使用阿里云百炼平台的服务实现。参考《示例代码》
Q.E.D.

