天机学堂AI助手智能体 - 平台智能体、通用文本模型与语音

参考视频:黑马程序员SpringAI+DeepSeek大模型应用开发实战视频教程

1. 在线平台智能体

在前面我们学习的智能体,是基于SpringAI代码的方式实现了智能体,实际上还可以通过在线平台创建智能体,通过代码的方式进行调用,这也是一种实现智能体的方式。像这样的平台有很多,比如阿里百炼扣子平台

下面,我们就以阿里百炼平台为例进行学习。

1.1. 创建智能体

1.1.1. 新增应用

img
img

1.1.2. 设置系统提示词

img
markdown
角色
你作为在线教育平台资深客服代表兼讲师。你的任务根据学员的需求,调用知识库中的课程信息,为学员推荐合适的课程,同时解答学员对课程内容和知识点的疑问。

技能 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. 测试

在右侧窗口,可以进行功能测试:

img

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

img

这是因为还没有给他设置知识库。

1.2. 添加知识库

1.2.1. 添加数据

添加知识库之前,需要先添加数据:

img

📎课程数据.xlsx

img

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

img

还可以预览数据:

img

点击确定,保存数据:

img

数据保存成功:

img

1.2.2. 添加知识库

img
img
img

选择数据:

img
img

数据导入成功:

img

1.2.3. 智能体添加知识库

需要返回到智能体应用中添加:

img

1.2.4. 功能测试

img

可以看到,添加的知识体已经生效了,大模型可以推荐课程了。

如果问他这个课程多钱,大模型是不知道的,因为在知识库中是没有这个数据的,需要添加插件,也就是我们程序中的Tools,根据Tools进行查询:

img

1.3. 添加插件

1.3.1. 内网穿透

我们的微服务是在本地运行的,而大模型则在阿里云上,直接访问不到本地的服务。要解决这个问题,可以使用内网穿透技术。 简单来说,内网穿透就是把本地的服务开放到互联网上,这样不管谁只要能上网就能访问你的服务了。

在这里推荐2个免费的内网穿透服务,分别是:

https://www.cpolar.com/

https://natapp.cn/

这里以【cpolar】为例,介绍使用方法:

第一步下载安装cpolar

参考文档进行安装:👉 文档 - cpolar 极点云官网

第二步注册账号并且登录

第三步设置token

请求 https://dashboard.cpolar.com/get-started 页面,查看命令cpolar authtoken xxxx后面的【xxxx】就是你自己的token,每个人是不一样的。token只需要设置一次。

第四步设置端口映射

例如:cpolar http 10010端口改成你自己的端口。

img

在线查看:

img

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

img
img

可以看到,已经能够调用通接口了,说明走外网已经可以访问本地服务了。

🚨 说明:由于我们使用的是免费账号,这个域名是临时域名,在重启cpolar服务后,域名将会被更换。

1.3.2. 创建插件

img
img

1.3.3. 查询课程工具

设置工具信息:

img

设置请求参数:

img

设置接口输出字段说明:

img

测试:

img

可以看到,工具已经测试成功了。

点击发布:

img

1.3.4. 智能体添加工具

img

设置token:

img

再次测试:

img

可以看到,大模型已经查询到数据了。

1.4. 程序调用

前面我们已经创建好了智能体,下面我们将通过代码的方式进行调用智能体。参考文档:阿里云帮助中心

第一步,导入依赖:

XML
<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>

第二步,编写测试用例:

JAVA
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在这里查找:

img

工具id在这里查找:

img

传递token的参数结构,参考文档:自定义参数传递

json
{
    "user_defined_tokens": {
        "tool_xxxx": { // 工具id
            "user_token": "xxxxxxxxx" // token值
        }
    }
}

运行结果:

bash

查询的课程信息
如下:

课程名称
: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中增加配置

yaml
tj:
  ai:
    dashscope:
      key: sk-xxxxxxxxxxx
      app-agent:
        id: xxxxxxxxx
        tools: 
          - "tool_xxxxx"
          - "tool_xxxxx"

第二步,编写配置类,读取上面的配置

java
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,用来集成平台的智能体

java
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);
    }
}

第四步,测试:

img

📚 说明:

虽然已经将平台智能体集成到项目中,但是功能并不完善,缺少历史记录功能,这个功能的开发,需要自己手动进行记录,和SpringAI就没有什么关系了,我们暂时不做实现。

1.6. 练习:工作流应用

阿里云百炼平台不只是提供智能体,还提供工作流的应用,和我们今天学习的路由工作流很类似,可以在平台上创建智能体工作流,在代码中进行调用,其调用方式与智能体类似。参考文档:工作流应用_大模型服务平台百炼(Model Studio)-阿里云帮助中心

类似下面这样的流程:

img

具体的实现,留给同学们在作业中完成。可以实现这样的工作流:

img

2. 通用文本模型

2.1. 需求分析

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

img

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

img

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

img
img

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

2.2. 实现分析

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

然而,这个文本处理类智能体,我们将对接OpenAI实现。(用阿里通义大模型也可以实现,只是为了在项目中构造对接两种不同平台的大模型场景而设定,这必然增加了复杂度,而我们学习的价值也更大)

2.3. 引入OpenAI

2.3.1. 导入依赖

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

2.3.2. 增加配置

nacosaigc-service.yaml配置项,增加如下内容:

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服务,但是,会发现报错:

img

这是因为,在SpringAI启动时,会需要注入ChatModel对象,但是,现在由于引入了阿里云和OpenAI,会存在2个ChatModel对象实例,所以导致了报错。

2.3.3. 分析问题

要想解决问题,首先我们需要找到启动时的自动配置类,在这里:

img

SpringAI就是由org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration类完成自动配置的。

可以看到,在这个类中的chatClientBuilder方法,中注入了ChatModel对象,由于现在的Spring容器中存在2个实例,所以就报错了。

img

怎么解决这个问题呢?

**解决方法:**在SpringAIConfig中不通过ChatClient.Builder构建ChatClient对象,而是通过注入ChatModel的方式,来创建不同的ChatClient对象。

2.3.4. 解决问题

SpringAIConfig中改造代码:

java
    @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. 接口说明

img

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

2.4.2. Controller

java
    @PostMapping("/text")
    public String chatText(@RequestBody String question) {
        return this.chatService.chatText(question);
    }

2.4.3. Service

java
    /**
     * 文本对话
     *
     * @param question 问题
     * @return 回答
     */
    String chatText(String question);

2.4.4. Impl

java
    @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配置

java
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; // 读取的超时时间,单位毫秒
        }
    }
}
java
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文件:

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

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

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

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

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

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

在application.yml中增加配置:

yaml
tj:
  ai:
    prompt:
      system:
        text:
          data-id: text-system-chat-message.txt

2.4.6. 测试

img

2.5. AI自动回复

2.5.1. 功能接口分析

img
img

查看浏览器请求:

img

请求参数:

img

响应:

img

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

网关中的配置

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

img

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

2.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服务出错!";
            }
        };
    }

}
img

2.5.3. Service

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

java
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

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) {
        // 构建包含完整问题信息的查询模板(标题+描述)
        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的配置项:

yaml
tj:
  ai:
    user-id: 9999  #默认id,数据库tj_user中的user表中,有对应的用户数据

2.5.5. Controller

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

java
    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. 功能测试

img

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

2.6. 练习:其他功能

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

2.6.1. 接口说明

img

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

img

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

2.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:

java
    private static final TemplateVO TEMPLATE_VO = new TemplateVO();

    @GetMapping("/templates")
    public TemplateVO getTemplates() {
        return TEMPLATE_VO;
    }

2.6.3. 测试

img

功能测试:

img
img
img

3. 语音文字互转

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

https://chatanywhere.apifox.cn/doc-2694962

3.1. 文字转语音

3.1.1. 功能需求

img

3.1.2. 接口说明

img

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

3.1.3. OpenAI接口

img

3.1.4. 功能实现

3.1.4.1. tts配置

nacos中的aigc-service.yaml增加OpenAItts配置:

yaml
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
java
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
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);

}
3.1.4.4. Impl
java
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. 测试

img

测试文字:

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

测试得到的音频:

此处为语雀音频卡片,点击链接查看:

3.2. 语音转文字

3.2.1. 功能需求

img

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

3.2.2. 接口说明

img

3.2.3. OpenAI接口

img

3.2.4. 功能实现

3.2.4.1. stt配置

nacos中的aigc-service.yaml增加OpenAIstt配置:

yaml
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
java
    @PostMapping("/stt")
    public String stt(@RequestParam("audioFile") MultipartFile audioFile) {
        return this.audioService.stt(audioFile);
    }
3.2.4.3. Service
java
    /**
     * 语音转文字(STT)
     * @param audioFile 音频文件
     * @return 识别结果文本
     */
    String stt(MultipartFile audioFile);
3.2.4.4. Impl
java
    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组件,所以需要导入其依赖:

xml
<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>opencc4j</artifactId>
    <version>1.8.0</version>
</dependency>
3.2.4.5. 测试

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

bash
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也会用到这个消息转化器,但是这个消息转化器是无法处理文件的,所以报消息转化出错:

img
3.2.4.6. 解决问题

为了解决上述问题,需要在发起请求时,添加一个标识来表明是SpringAI发起的请求,就不再进行包装处理了。

要想实现这样的效果,需要给RetryTemplate添加一个监听器,在发起请求前设置标识,请求结束后删除标识,这样就可以解决问题了。

具体代码如下:

java
    /**
     * 创建并配置自定义重试监听器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. 测试

img

可以看到,已经成功的将语音文件转化为文字了。

也可以基于前端页面进行测试:

img

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

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

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

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

img
img

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

3.3. 练习

语音与文字互转的功能,使用阿里云百炼平台的服务实现。参考《示例代码》


Q.E.D.
RabbitMQ 从入门到实战
天机学堂AI助手智能体 - 多智能体协同工作