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

测试:

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

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


如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐