SpringAI的基础学习

Spring-AI

langchain4j vs springAI

维度 Spring AI LangChain4j
技术栈绑定 强依赖 Spring 生态 无框架依赖,可独立使用
适用场景 SpringBoot应用快速接入单模型 多模型(动态模型)平台

官网:

https://spring.io/projects/spring-ai#learn

前置准备

  1. 现在建议用阿里百炼:https://bailian.console.aliyun.com/
  2. jdk17
  3. springboot3

实现

首先我们先创建一个SpringBoot项目,需要添加Spring Web依赖和OpenAI依赖

创建完后会发现加入了依赖:

xml 复制代码
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>


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

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>

  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>

</dependencies>

接着 我们需要在application.properties中添加如下配置,我所使用的是千问的开源模型qwen3

properties 复制代码
#api密钥
spring.ai.openai.api-key=${ALI_API_KEY}
#中转接口地址
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode
#文本模型名称
spring.ai.openai.chat.options.model=qwen3-next-80b-a3b-instruct

然后我们在项目包下创建一个子包controller,创建一个MyController.class文件,开启我们和AI的第一段对话

java 复制代码
package com.arguan.ai.springaiarguan.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/ai")
public class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    String generation(@RequestParam(value = "message", defaultValue = "高冠林是谁") String message) {
        // prompt: 提示词
        return this.chatClient.prompt()
                // user: 用户输入
                .user(message)
                // call: 调用模型
                .call()
                // content: 模型返回结果
                .content();
    }
}

角色预设

那么,当我们运用SpringAI去开发项目的时候,是有针对性的开发的,也就是说,在一个项目中AI充当的是一个领域的角色,比如医疗助手,法务专家等,这时候我们需要去对大模型去进行一个角色的预设,让他接下来的回答都具有针对性和专业性,在官方文档中给我们提供了角色预设的办法

java 复制代码
@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
                .build();
    }

}

我们将他应用到我的项目中

java 复制代码
package com.arguan.ai.springaiarguan.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你现在不是千问模型了,你是一名资深的医疗专家,你所在的医院有一名非常著名的医生叫阿冠")
                .build();
    }
}

此时,对话客户端就变成了一个bean,就不需要在controller中通过构造器的形式创建了,只需要自动注入即可

java 复制代码
package com.arguan.ai.springaiarguan.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/ai")
public class MyController {

    @Autowired
    private ChatClient chatClient;


    @GetMapping("/chat")
    String generation(@RequestParam(value = "message", defaultValue = "高冠林是谁") String message) {
        // prompt: 提示词
        return this.chatClient.prompt()
                // user: 用户输入
                .user(message)
                // call: 调用模型
                .call()
                // content: 模型返回结果
                .content();
    }
}

这样,如果你的需求是为AI赋予上特定的角色,就可以使用这个办法来进行角色预设

流式响应

上文所创建的对话,在网页中是全部显示出来的,也就是说是大模型回答完整后才将内容显示到网页上,但是我们平常所使用的国内的大模型再回答问题时,是一个token一个token进行显示出来的,这就用到了流式响应,官方同样给我们提供了相应的解决办法

java 复制代码
Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();

应用到项目中

java 复制代码
 @GetMapping("/stream")
    Flux<String> stream(@RequestParam(value = "message", defaultValue = "阿冠是谁") String message) {
        Flux<String> output = chatClient.prompt()
                .user(message)
                .stream()
                .content();
        return output;
    }

如果我们这时运行一下看看结果如何,会发现网页中会出现乱码情况,这是因为我们并没有在请求头中设置流式响应的格式,具体更改如下:

java 复制代码
// 在注解中设置produces属性 
 @GetMapping(value = "/stream", produces = "text/html;charset=UTF-8")
    Flux<String> stream(@RequestParam(value = "message", defaultValue = "阿冠是谁") String message) {
        Flux<String> output = chatClient.prompt()
                .user(message)
                .stream()
                .content();
        return output;
    }

ChatModel对话组件

与ChatClient的区别

ChatClient ChatModel
通用语言大模型的对话组件,不能实现特定模型的特定功能 可以实现特定模型的特定功能
java 复制代码
    @Autowired
    private ChatModel chatModel;

    @GetMapping("/chat/model")
    String chatModel(@RequestParam(value = "message", defaultValue = "阿冠是谁") String message) {
        ChatResponse response = chatModel.call(
                new Prompt(
                        new UserMessage(message),// = message
                        OpenAiChatOptions.builder()
                                .temperature(0.4)
                                .build()
                ));
        return response.getResult().getOutput().getText();
    }

实际上,通过对源码的分析,我们知道ChatClient的一些方法是将ChatModel中的多个方法进行了封装,使创建对话变成一个链式结构,提升了便利性

文生图

官方给出的原码

java 复制代码
ImageResponse response = openaiImageModel.call(
        new ImagePrompt("A light cream colored mini golden doodle",
        OpenAiImageOptions.builder()
                .quality("hd")
                .N(4)
                .height(1024)
                .width(1024).build())

);

应用到项目中

java 复制代码
    @Autowired
    private OpenAiImageModel openaiImageModel;

    @GetMapping("/img")
    String generateImg(@RequestParam(value = "message", defaultValue = "画个猫") String message) {
        ImageResponse response = openaiImageModel.call(
                new ImagePrompt(
                        message,
                        OpenAiImageOptions.builder()
                                // 调整图片质量
                                .quality("hd")
                                // 调整图片数量
                                .N(4)
                                // 调整图片大小
                                .height(1024)
                                // 调整图片宽度
                                .width(1024).build())

        );
        return response.getResult().getOutput().getUrl();
    }

由于我所使用的是阿里百炼的模型,openai中的接口地址不支持图片生成,所以没有实现

文生语音

官方原码

java 复制代码
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
    .model("gpt-4o-mini-tts")
    .voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
    .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
    .speed(1.0)
    .build();

TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

项目应用

java 复制代码
    @Autowired
    private OpenAiAudioSpeechModel openAiAudioSpeechModel;

    @GetMapping("/audio")
    String generateAudio() {
        // 文生语音配置项
        OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
                .model("gpt-4o-mini-tts")
                .voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
                .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
                .speed(1.0)
                .build();

        TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
        TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

        byte[] audioData = response.getResult().getOutput();

        writeByteArrayToMP3(audioData, System.getProperty("user.dir"));

        return "ok";
    }

    public static void writeByteArrayToMP3(byte[] byteArray, String filePath) {
        try (FileOutputStream fos = new FileOutputStream(filePath + "arguan.mp3")) {
            fos.write(byteArray);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

由于我使用的仍然是阿里百炼模型,中转接口的地址不支持文生语音,所以没有实现

语音翻译

官方原码

java 复制代码
var openAiAudioApi = new OpenAiAudioApi(System.getenv("OPENAI_API_KEY"));

var openAiAudioTranscriptionModel = new OpenAiAudioTranscriptionModel(this.openAiAudioApi);

var transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
    .responseFormat(TranscriptResponseFormat.TEXT)
    .temperature(0f)
    .build();

var audioFile = new FileSystemResource("/path/to/your/resource/speech/jfk.flac");

AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(this.audioFile, this.transcriptionOptions);
AudioTranscriptionResponse response = openAiTranscriptionModel.call(this.transcriptionRequest);

项目应用

java 复制代码
    @Autowired
    private OpenAiAudioTranscriptionModel openAiTranscriptionModel;


    @GetMapping("/audio2")
    String generateAudio2() {

        // 声明语音翻译的可选配置
        var transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
                // 响应类型
                .responseFormat(OpenAiAudioApi.TranscriptResponseFormat.TEXT)
                // 与语音一致,不需要创造力
                .temperature(0f)
                .build();

        var audioFile = new ClassPathResource("audio.mp3");

        AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
        AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptionRequest);

        return response.getResult().getOutput();
    }

多模态

项目应用

java 复制代码
@GetMapping("mutil")
    public String mutilModel(@RequestParam(value = "message", defaultValue = "你从这个图片中看到了什么") String message) throws IOException {

        // 图片的二进制流
        byte[] imageData = new ClassPathResource("/test.png").getContentAsByteArray();

        // 用户信息
        var userMessage = new UserMessage(
                message, // content
                List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));// media

        ChatResponse response = chatModel.call(new Prompt(userMessage,
                OpenAiChatOptions.builder()
                        .model(OpenAiApi.ChatModel.GPT_5_CHAT_LATEST.getValue())
                        .build()));

        return response.getResult().getOutput().getText();
    }

Function Call(Tool Calling)

官方给出的原码

java 复制代码
import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

}

项目应用

Tool Calling工具类

java 复制代码
package com.arguan.ai.springaiarguan.config;

import org.springframework.ai.tool.annotation.Tool;

public class LocationNameTools {

    @Tool(description = "某个地方有多少个叫什么名字的人")
    public String getName() {
        return "十个";
    }
}

控制器类

java 复制代码
package com.arguan.ai.springaiarguan.controller;

import com.arguan.ai.springaiarguan.config.LocationNameTools;
import com.arguan.ai.springaiarguan.config.MyConfig;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tool")
public class FunctionCallController {

    @Autowired
    ChatModel chatModel;



    @GetMapping("/call")
    public String call(@RequestParam(value = "message", defaultValue = "哈尔滨有多少个叫阿冠的人") String message) {
        String response = ChatClient.create(chatModel)
                .prompt(message)
                .tools(new LocationNameTools())
                .call()
                .content();
        return response;
    }
}

SpringAI-1.0

首先我们创建一个父工程名为spring-new-ai-arguan,接着在父工程中创建一个模块名为quick-start,并修改两个pom文件

父工程pom

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.arguan.ai</groupId>
    <artifactId>string-new-ai-arguan</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<!--    这里要添加packaging标签,否则maven会报错-->
    <packaging>pom</packaging>
    <name>string-new-ai-arguan</name>
    <description>string-new-ai-arguan</description>

    <modules>
        <module>quick-start</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.4</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
<!--            springai依赖-->
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

子模块pom

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
<!--        此处的父依赖变为spring-new-ai-arguan-->
        <groupId>com.arguan.ai</groupId>
        <artifactId>string-new-ai-arguan</artifactId>
        <version>0.0.1-SNAPSHOT</version>
<!--        这里的relativePath标签改为了../pom.xml,告诉maven去父依赖的pom.xml中寻找依赖-->
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.arguan.ai</groupId>
    <artifactId>quick-start</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>quick-start</name>
    <description>quick-start</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
<!--        deepseek的模型依赖-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-deepseek</artifactId>
        </dependency>
<!--        由于我们的父类中包含springai和web的依赖,所以这里不需要再添加-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

接着我们去子模块的配置文件去设置deepseek相关的配置信息

properties 复制代码
spring.application.name=quick-start

#DeepSeek API Key
spring.ai.deepseek.api-key=${DEEP_SEEK_API_KEY}
#deepseek对话模型
spring.ai.openai.chat.options.model=deepseek-chathat

配置完成后,我们去test文件夹下新建一个测试类TestDeepSeek,去测试deepseek的模型

java 复制代码
package com.arguan.ai.quickstart;

import org.junit.jupiter.api.Test;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestDeepSeek {

    @Test
    public void testDeepSeek(@Autowired DeepSeekChatModel deepSeekChatModel) {
        // 调用deepseek对话模型
        String content = deepSeekChatModel.call("你好,你是谁?");
        System.out.println(content);
    }
}

结果如下

流式响应

java 复制代码
 // 流式响应
    @Test
    public void testDeepSeekStream(@Autowired DeepSeekChatModel deepSeekChatModel) {
        // 调用deepseek对话模型
        Flux<String> stream = deepSeekChatModel.stream("你好,你是谁?");
        stream.toIterable().forEach(System.out::println);
    }

Options配置选项

  • temperature(温度):0-2浮点数值

    • 数值越高 越热情

    • 数值越低 越保守

也可以通过提示词来告诉模型要客观保守,基于事实,相当于我们口头告诉他,而temperature是设置模型的性格

maxTokens

默认低 token

maxTokens:限制AI模型生成的最大token数(近似理解为字数上限)。

  • 需要简洁回复、打分、列表、短摘要等,建议小值(如10~50)。
  • 防止用户跑长对话导致无关内容或花费过多token费用。
  • 如果遇到生成内容经常被截断,可以适当配置更大maxTokens。

stop

  • 截断你不想输出的内容
java 复制代码
  @Test
    public void testDeepSeekOption(@Autowired DeepSeekChatModel deepSeekChatModel) {
        DeepSeekChatOptions options = DeepSeekChatOptions.builder()
                .maxTokens(5)
                .stop(Arrays.asList(","))
                .temperature(1.9)
                .build();
        Prompt prompt = new Prompt("写一句诗描述清晨", options);
        ChatResponse response = deepSeekChatModel.call(prompt);
        System.out.println(response.getResult().getOutput().getText());
    }

阿里百炼模型

我们需要父工程中添加如下依赖

xml 复制代码
<properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.4</spring-ai.version>
        <spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
    </properties>


<dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-bom</artifactId>
        <version>${spring-ai-alibaba.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

子模块中添加如下依赖

xml 复制代码
<!--        百炼模型-->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>

然后去配置文件中配置模型信息

properties 复制代码
# ali百炼
spring.ai.dashscope.api-key=${ALI_API_KEY}
spring.ai.dashscope.chat.options.model=qwen-plus

这样,我们就配置好了ali百炼的大模型,接着创建测试类TestAli,实现与阿里百炼模型的对话

java 复制代码
package com.arguan.ai.quickstart;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestAli {

    @Test
    public void testQwen(@Autowired DashScopeChatModel chatModel) {
        String content = chatModel.call("你好,你是谁");
        System.out.println(content);
    }
}

文生图

java 复制代码
 @Test
    public void text2Img(@Autowired DashScopeImageModel imageModel) {
        DashScopeImageOptions imageOptions = DashScopeImageOptions.builder()
                .withModel("wanx2.1-t2i-turbo")
                .build();

        ImageResponse imageResponse = imageModel.call(
                new ImagePrompt(
                        "程序员阿冠",
                        imageOptions));
        String imageUrl = imageResponse.getResult().getOutput().getUrl();

        // 图片url
        System.out.println(imageUrl);

        // 图片base64
        // imageResponse.getResult().getOutput().getB64Json();

        /*
        按文件流相应
        InputStream in = url.openStream();

        response.setHeader("Content-Type", MediaType.IMAGE_PNG_VALUE);
        response.getOutputStream().write(in.readAllBytes());
        response.getOutputStream().flush();*/
    }

文生语音

java 复制代码
 @Test
    public void testText2Audio(@Autowired DashScopeSpeechSynthesisModel speechSynthesisModel) throws IOException {
        DashScopeSpeechSynthesisOptions options = DashScopeSpeechSynthesisOptions.builder()
                .voice("longyingtian")   // 人声
                //.speed()    // 语速
                .model("cosyvoice-v2")    // 模型
                //.responseFormat(DashScopeSpeechSynthesisApi.ResponseFormat.MP3)
                .build();

        SpeechSynthesisResponse response = speechSynthesisModel.call(
                new SpeechSynthesisPrompt("大家好, 我是程序员阿冠。", options)
        );

        File file = new File(System.getProperty("user.dir") + "/output.mp3");
        try (FileOutputStream fos = new FileOutputStream(file)) {
            ByteBuffer byteBuffer = response.getResult().getOutput().getAudio();
            fos.write(byteBuffer.array());
        } catch (IOException e) {
            throw new IOException(e.getMessage());
        }
    }

语音翻译

java 复制代码
    private static final String AUDIO_RESOURCES_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav";



@Test
    public void testAudio2Text(@Autowired DashScopeAudioTranscriptionModel transcriptionModel) throws MalformedURLException {
        DashScopeAudioTranscriptionOptions transcriptionOptions = DashScopeAudioTranscriptionOptions.builder()
                //.withModel()   模型
                .build();
        AudioTranscriptionPrompt prompt = new AudioTranscriptionPrompt(new UrlResource(AUDIO_RESOURCES_URL), transcriptionOptions);
        AudioTranscriptionResponse response = transcriptionModel.call(prompt);

        System.out.println(response.getResult().getOutput());

    }

多模态

java 复制代码
@Test
    public void testMultimodal(@Autowired DashScopeChatModel dashScopeChatModel
    ) throws MalformedURLException {
        // flac、mp3、mp4、mpeg、mpga、m4a、ogg、wav 或 webm。
        var audioFile = new ClassPathResource("/files/blue.jpg");

        Media media = new Media(MimeTypeUtils.IMAGE_JPEG, audioFile);
        DashScopeChatOptions options = DashScopeChatOptions.builder()
                .withMultiModel(true)
                .withModel("qwen-vl-max-latest").build();

        Prompt prompt = Prompt.builder().chatOptions(options)
                .messages(UserMessage.builder().media(media)
                        .text("识别图片").build())
                .build();
        ChatResponse response = dashScopeChatModel.call(prompt);

        System.out.println(response.getResult().getOutput().getText());
    }

视频生成

首先我们需要添加依赖

xml 复制代码
<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <!-- 请将 'the-latest-version' 替换为最新版本号:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
            <version>2.22.15</version>
        </dependency>

然后添加如下方法

java 复制代码
 @Test
    public void text2Video() throws ApiException, NoApiKeyException, InputRequiredException {
        VideoSynthesis vs = new VideoSynthesis();
        VideoSynthesisParam param =
                VideoSynthesisParam.builder()
                        .model("wanx2.1-t2v-turbo")
                        .prompt("一只小猫在月光下奔跑")
                        .size("1280*720")
                        .apiKey(System.getenv("ALI_API_KEY"))
                        .build();
        System.out.println("please wait...");
        VideoSynthesisResult result = vs.call(param);
        System.out.println(result.getOutput().getVideoUrl());
    }

Ollama

首先我们先在Ollama引擎中下载一个本地大模型,由于我的显卡显存比较小,所以我选择的是qwen3:0.6b;在cmd窗口中执行ollama run qwen3:0.6b,下载好后,我们进入idea进行Ollama的配置,首先在子模块中加入依赖

xml 复制代码
<!--        ollama-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-ollama</artifactId>
        </dependency>

然后我们去配置信息

properties 复制代码
# ollama
spring.ai.ollama.chat.model=qwen3:0.6b

配置完成后,我们创建一个测试类TestOllama

java 复制代码
package com.arguan.ai.quickstart;

import org.junit.jupiter.api.Test;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestOllama {

    @Test
    public void testOllama(@Autowired OllamaChatModel chatModel) {
        String content = chatModel.call("你是谁");
        System.out.println(content);
    }
}

关闭thinkinging

  • 可以在提示词末尾加入"no_think"指令

  • 也可以在命令行窗口中运行本地模型后输入/set nothink 来关闭思考模式

但第二种方式并不能让idea中所运行的本地模型关闭思考模式,由于我使用的SpringAI版本是1.0版本,但是在1.0版本之后才支持了OllamaChatOptions可以设置属性think来关闭思考模式,所以建议大家使用最新版本的SpringAI

流式输出

java 复制代码
 @Test
    public void testStream(@Autowired OllamaChatModel chatModel) {

        Flux<String> stream = chatModel.stream("你是谁/no_think");
        // 阻塞输出
        stream.toIterable().forEach(System.out::println);
    }

多模态

这里需要再Ollama中安装支持多模态的模型 我安装的是gemma3

java 复制代码
 @Test
    public void testMultimodality(@Autowired OllamaChatModel ollamaChatModel) {
        var imageResource = new ClassPathResource("/files/blue.jpg");

        OllamaOptions ollamaOptions = OllamaOptions.builder()
                .model("gemma3")
                .build();

        Media media = new Media(MimeTypeUtils.IMAGE_PNG, imageResource);


        ChatResponse response = ollamaChatModel.call(
                new Prompt(
                        UserMessage.builder().media(media)
                                .text("识别图片").build(),
                        ollamaOptions
                )
        );

        System.out.println(response.getResult().getOutput().getText());
    }

ChatClient

  • ChatClient它支持各种大模型,所以它具有易用性和通用性

首先我们再单独创建一个子模块名为chat-client,只添加阿里百炼的依赖,再创建一个TestChatClient测试类

java 复制代码
package com.arguan.ai.chatclient;

import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.core.publisher.Flux;

@SpringBootTest
public class TestChatClient {

    // 阻塞式输出
    @Test
    public void testChatClient(@Autowired ChatClient.Builder chatClientBuilder) {
        ChatClient chatClient = chatClientBuilder.build();
        String content = chatClient.prompt()
                .user("你好")
                .call()
                .content();
        System.out.println(content);
    }

    // 流式输出
    @Test
    public void testStreamChatClient(@Autowired ChatClient.Builder chatClientBuilder) {
        ChatClient chatClient = chatClientBuilder.build();
        Flux<String> content = chatClient.prompt()
                .user("你好")
                .stream()   
                .content();
        content.toIterable().forEach(System.out::println);
    }
}

多平台多模型动态配置大模型平台实战

首先我们依然去使用刚才的模块chat-client,并添加deepseek和ollama模型的依赖,接着创建一个控制类MorePlatformAndModelController

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.arguan.ai.chatclient.pojo.MorePlatformAndModelOptions;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.HashMap;

@RestController
public class MorPlatformAndModelController {

    // 声明一个map,用于存储不同平台和模型的实例
    HashMap<String, ChatModel> platforms = new HashMap<>();

    public MorPlatformAndModelController(
            OllamaChatModel ollamaChatModel,
            DeepSeekChatModel deepSeekChatModel,
            DashScopeChatModel dashScopeChatModel
    ) {
        // 初始化模型实例
        platforms.put("ollama", ollamaChatModel);
        platforms.put("deepseek", deepSeekChatModel);
        platforms.put("dashscope", dashScopeChatModel);
    }

    @RequestMapping("/chat")
    public Flux<String> chat(String message, MorePlatformAndModelOptions options) {
        // 获取平台和模型
        String platform = options.getPlatform();
        ChatModel chatModel = platforms.get(platform);

        // 创建builder用于创建客户端
        ChatClient.Builder builder = ChatClient.builder(chatModel);

        // 初始化客户端,并设置默认参数
        ChatClient chatClient = builder.defaultOptions(
                ChatOptions.builder()
                        .temperature(options.getTemperature())
                        .model(options.getModel())
                        .build())
                .build();

        Flux<String> content = chatClient.prompt()
                .user(message)
                .stream()
                .content();

        return content;
    }
}

MorePlatformAndModelOptions

java 复制代码
package com.arguan.ai.chatclient.pojo;

import lombok.Data;

@Data
public class MorePlatformAndModelOptions {
    private String platform;
    private String model;
    private Double temperature;
}

我们启动SpringBoot,在网址中输入http://localhost:8080/chat?message=你好\&platform=deepseek\&model=deepseek-reasoner\&temperature=0.8,得到如下回复:

提示词

由于此模块中我们添加了三种模型的依赖,导致ChatClient.Builder在自动注入的时候会导致报错,不知道选择哪一个大模型,因此,我所使用的是自动注入制定的model,在创建builder时建立此模型

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestPrompt {

    @Test
    public void testSystemPrompt(@Autowired DashScopeChatModel chatModel) {
        // 为此客户端设置系统提示词
        // 为ChatClient预设角色:你是什么 你能做什么 你要注意什么 具体应该怎么做
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder.defaultSystem("""
                角色说明:
                你是一名高级Java开发程序设计师
                """).build();
        System.out.println(chatClient.prompt()
//                        .system() 只为当前对话设置系统提示词
                .user("你好").call().content());
    }
}
}
}
content());
    }
}

提示词模版

有时候 我们系统提示词里的内容是不能写死的,那么我们如何传入参数呢?方法如下:

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestPrompt {

    @Test
    public void testSystemPrompt(@Autowired DashScopeChatModel chatModel) {
        // 为此客户端设置系统提示词
        // 为ChatClient预设角色:你是什么 你能做什么 你要注意什么 具体应该怎么做
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder.defaultSystem("""
                角色说明:
                你是一名高级Java开发程序设计师
                姓名:{name} 年龄:{age} 性别:{sex}
                """).build();
        System.out.println(chatClient.prompt()
//                        .system() 只为当前对话设置系统提示词
                .system(p -> p.param("name", "张三")
                        .param("age", 18).
                        param("sex", "男"))
                .user("你好")
                .call()
                .content());
    }
}

这样我们就可以动态的获取信息去补充系统提示词里缺失的内容

伪系统提示词

有的开发者他不会去设置系统提示词,而是全部放在用户提示词中,并将message设置为里面的参数,如何实现呢?如下:

java 复制代码
@Test
    public void testSystemPrompt1(@Autowired DashScopeChatModel chatModel) {
        // 为此客户端设置系统提示词
        // 为ChatClient预设角色:你是什么 你能做什么 你要注意什么 具体应该怎么做
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder.build();
        System.out.println(chatClient.prompt()
//                        .system() 只为当前对话设置系统提示词
                .user(u -> u.text("""
                        角色说明:
                        你是一名高级Java开发程序设计师
                        问题:{question}
                        """).param("question", "如何使用redis解决高并发"))
                .call()
                .content());
    }

提示词设置技巧

简单技巧

  • 文本摘要:将大量文本缩减为简单摘要,捕捉关键点和重要思想

  • 问答:专注于根据用户提出的问题,从提供的文本中获取答案

  • 文本分类:系统的将文本分类到预定义的类别或组中

  • 对话:创建交互式对话

  • 代码生成:根据用户特定要求生成代码片段

高级技巧

  • 指令明确:避免情绪化内容,描述足够清楚,把大模型比作小学生,交代越清楚执行越具体

  • 格式清晰:可以使用markdown格式

Advisor对话拦截日志记录

首先我们要去配置日志拦截的优先级

properties 复制代码
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

创建测试类TestAdvisor

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestAdvisor {

    @Test
    public void testAdvisor(@Autowired DashScopeChatModel chatModel) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
        String content = chatClient.prompt()
                .user("你好")
//                .advisors()
                .call()
                .content();
        System.out.println(content);
    }
}

测试后,控制台会边输出ai的对话结果边输出请求响应的日志信息

Advisor实现敏感词拦截

java 复制代码
 @Test
    public void testAdvisor(@Autowired DashScopeChatModel chatModel) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(new SimpleLoggerAdvisor(),
                        new SafeGuardAdvisor(List.of("阿冠")))
                .build();
        String content = chatClient.prompt()
                .user("阿冠帅不帅")
//                .advisors()
                .call()
                .content();
        System.out.println(content);
    }

自定义拦截实现Reread重读

  • 重读的核心在于让LLMs重新审视输入的问题

自定义拦截器

java 复制代码
package com.arguan.ai.chatclient.util;

import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;

import java.util.Map;

public class ReReadingAdvisor implements BaseAdvisor {

    private static final String DEFAULT_USER_TEXT_ADVISE = """
            {re2_input_query}
            Read the question again:{re2_input_query}
            """;

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
       // 请求之前 重读提示词
        String contents = chatClientRequest.prompt().getContents();

        String re2InputQuery = PromptTemplate.builder().template(DEFAULT_USER_TEXT_ADVISE).build()
                .render(Map.of("re2_input_query", contents));

        ChatClientRequest clientRequest = chatClientRequest.mutate()
                .prompt(Prompt.builder().content(re2InputQuery).build()).build();

        return clientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        return null;
    }

    // 优先级
    @Override
    public int getOrder() {
        return 0;
    }
}

测试方法

java 复制代码
// 敏感词拦截件
    @Test
    public void testReReadingAdvisor(@Autowired DashScopeChatModel chatModel) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(new SimpleLoggerAdvisor(),
                        new ReReadingAdvisor())
                .build();
        String content = chatClient.prompt()
                .user("阿冠帅不帅")
//                .advisors()
                .call()
                .content();
        System.out.println(content);
    }

对话记忆

  • LLM是无状态的,这代表他们不会保留先前交互的信息

首先我们要添加可以自动注入ChatMemory的依赖,否则需要自己去构建

xml 复制代码
<!--        自动注入chatmemory的依赖-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
        </dependency>

测试类

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestMemory {

    @Test
    public void testMemory(@Autowired DashScopeChatModel chatModel) {
        ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
        String memoryId = "ag01";// 当前对话的唯一标识:id

        // 第一轮对话
        UserMessage userMessage1 = new UserMessage("你好,我是阿冠");
        chatMemory.add(memoryId, userMessage1);
        ChatResponse response1 = chatModel.call(new Prompt(chatMemory.get(memoryId)));
        chatMemory.add(memoryId, response1.getResult().getOutput());

        // 第二轮对话
        UserMessage userMessage2 = new UserMessage("我是谁");
        chatMemory.add(memoryId, userMessage2);
        ChatResponse response2 = chatModel.call(new Prompt(chatMemory.get(memoryId)));
        chatMemory.add(memoryId, response2.getResult().getOutput());
        System.out.println(response2.getResult().getOutput().getText());
    }
}

配置聊天记录的最大存储数量

  • 聊天记录每次发给大模型,会消耗token

  • 大模型的token具有上限,发送过多聊天记录,会导致token过长

java 复制代码
// 配置当前测试的内部的测试类来防止影响其他的测试类
    @TestConfiguration
    static class TestConfig {
        @Bean
        public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
            return MessageWindowChatMemory
                    .builder()
                    // 聊天记录最多对话次数
                    .maxMessages(10)
                    .chatMemoryRepository(chatMemoryRepository)
                    .build();
        }
    }

配置多用户隔离记忆

java 复制代码
// 配置多用户记忆隔离
    @Test
    public void testChatOptions(@Autowired DashScopeChatModel chatModel,
                                @Autowired ChatMemory chatMemory) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();

        // 第一个用户的对话
        String content = chatClient.prompt()
                .user("我叫阿冠")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();
        System.out.println(content);
        System.out.println("------------------");

        content = chatClient.prompt()
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();

        System.out.println(content);

        System.out.println("------------------");

        // 第二个用户的对话
        content = chatClient.prompt()
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "2"))
                .call()
                .content();

        System.out.println(content);
    }

数据库存储对话记忆

默认情况,对话内容会存在于jvm虚拟机中会导致:

  • 1 一直存储会撑爆jvm

  • 2 重启就会造成内容丢失

所以,我们应该存储到第三方来保证存储持久化,首先添加依赖

xml 复制代码
<!--        jdbc记忆存储依赖-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>
<!--        jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
<!--        mysql驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

然后添加配置

properties 复制代码
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
# 需要自己定义sql文件
spring.ai.chat.memory.repository.jdbc.schema=classpath:/schema-mysql.sql

测试类

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@SpringBootTest
public class TestJDBCAdvisor {

    @TestConfiguration
    static class ChatMemoryConfig {
        @Bean
        ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
            return MessageWindowChatMemory
                    .builder()
                    .maxMessages(10)
                    .chatMemoryRepository(chatMemoryRepository)
                    .build();
        }
    }

    @Test
    public void testChatOptions(@Autowired DashScopeChatModel chatModel,
                                @Autowired ChatMemory chatMemory) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();

        // 第一个用户的对话
        String content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫阿冠")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();
        System.out.println(content);
        System.out.println("------------------");

        content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();

        System.out.println(content);

        System.out.println("------------------");

        // 第二个用户的对话
        content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "2"))
                .call()
                .content();

        System.out.println(content);
    }
}

Redis存储对话记忆

我们先添加相关依赖

xml 复制代码
<properties>
        <jedis.version>5.2.0</jedis.version>
    </properties>
<!--        redis记忆存储依赖-->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
        </dependency>
<!--        redis依赖-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

接着添加配置文件

properties 复制代码
# redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.timeout=5000
spring.data.redis.password=

项目中应用

java 复制代码
package com.arguan.ai.chatclient.memory;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@SpringBootTest
public class TestRedisMemory {

    @TestConfiguration
    static class Config {

        @Value("${spring.ai.memory.redis.host}")
        private String redisHost;
        @Value("${spring.ai.memory.redis.port}")
        private int redisPort;
        @Value("${spring.ai.memory.redis.password}")
        private String redisPassword;
        @Value("${spring.ai.memory.redis.timeout}")
        private int redisTimeout;


        @Bean
        public RedisChatMemoryRepository redisChatMemoryRepository() {
            return RedisChatMemoryRepository.builder()
                    .host(redisHost)
                    .port(redisPort)
                    // 若没有密码则注释该项
//                        .password(redisPassword)
                    .timeout(redisTimeout)
                    .build();
        }

        @Bean
        ChatMemory chatMemory(RedisChatMemoryRepository chatMemoryRepository) {
            return MessageWindowChatMemory
                    .builder()
                    .maxMessages(10)
                    .chatMemoryRepository(chatMemoryRepository)
                    .build();
        }

    }

    @Test
    public void testChatOptions(@Autowired DashScopeChatModel chatModel,
                                @Autowired ChatMemory chatMemory) {
        ChatClient.Builder builder = ChatClient.builder(chatModel);
        ChatClient chatClient = builder
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();

        // 第一个用户的对话
        String content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫阿冠")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();
        System.out.println(content);
        System.out.println("------------------");

        content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1"))
                .call()
                .content();

        System.out.println(content);

        System.out.println("------------------");

        // 第二个用户的对话
        content = chatClient.prompt()
                .system("""
                        禁止使用表情符号
                        """)
                .user("我叫什么")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "2"))
                .call()
                .content();

        System.out.println(content);
    }
}

启动之后,我们去redis的可视化界面中查看是否记忆存储到了redis当中

我们发现,存储成功了,并根据memoryid进行了对话隔离

多层次记忆架构

痛点

  • 记忆多=聪明,记忆多会触发token上限

  • 聊天记录一旦增多,就会超过token上限

模仿人类

  • 近期记忆:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(ChatMemory)

  • 中期记忆:通过RAG检索相关的历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库)

  • 长期记忆:关键信息的固化总结

    • 方式一:定时批处理

      • 通过定时任务对积累的对话进行总结和提炼

      • 提取关键信息,用户偏好,重要事实等

      • 批处理方式降低计算成本,适合大规模处理

    • 方式二:关键点实时处理

      • 在对话中识别出关键信息点时立即提取并存储

      • 例如:当用户明确表达偏好,提供个人信息或设置持久性指令时

      • 采用"写入触发器"机智,在特定条件下自动更新长期记忆

结构化输出

基础类型:

以Boolean为例,在agent中可以用于判定用于的内容2个分支,不同的分支走不同的逻辑

java 复制代码
package com.arguan.ai.chatclient;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestStructureOut {

    ChatClient chatClient;

    @BeforeEach
    public void init(@Autowired DashScopeChatModel chatModel) {
        chatClient = ChatClient.builder(chatModel).build();
    }

    @Test
    public void testBoolOut() {
        Boolean isComplain = chatClient
                .prompt()
                .system("""
                        请判断用户是否表达了投诉意图?
                        只能用true或false回答,不要输出多余内容
                        """)
                .user("你们家的快递还不错!")
                .call()
                .entity(Boolean.class);
        // 分支逻辑
        if (Boolean.TRUE.equals(isComplain)) {
            System.out.println("用户是投诉,转接人工客服");
        } else {
            System.out.println("用户不是投诉,自动转接客服机器人");
            // todo... dress.class);
        System.out.println(address);
    }
}

另外,还可以通过结构化输出来根据用户输入的收货信息中提取关键信息(Pojo类型)

java 复制代码
 public record Address(
            String name,// 姓名
            String phone,// 手机号
            String province,// 省
            String city,// 市
            String district,// 区
            String detail// 详细地址
    ) {}

    @Test
    public void testEntityOut() {
        Address address = chatClient.prompt()
                .system("""
                        从下面的这段文本中获取收货信息
                        """)
                .user("收货人:薛安琪,电话:15561756197,地址:黑龙江省哈尔滨市浦源路2468号")
                .call()
                .entity(Address.class);
        System.out.println(address);
    }

原理

SpringAI底层是通过BeanOutputConverter转换器来自动设置提示词来提取需要的内容和以什么格式返回,再进行反序列化变为我们Java中的pojo类型

票务助手实战

  • 我要退票---提取任务类型关键字---是否有姓名和预定号---退票方法调用

  • 你好---调用智能客服

xml文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.arguan.ai</groupId>
        <artifactId>string-new-ai-arguan</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <artifactId>more-model-structured-agent</artifactId>
    <name>more-model-structured-agent</name>
    <description>more-model-structured-agent</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.arguan.ai.moremodelstructuredagent.Application</mainClass>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
ns>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

properties 复制代码
spring.ai.dashscope.api-key=${ALI_API_KEY}

AI配置类,用于定义两种情况的对话智能体,一种是处理退票业务的,一种是智能客服

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatProperties;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.prompt.DefaultChatOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient planningChatClient(DashScopeChatModel chatModel,
                                         DashScopeChatProperties options,
                                         ChatMemory chatMemory) {
        DashScopeChatOptions defaultChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
        defaultChatOptions.setTemperature(0.4);

        return ChatClient.builder(chatModel)
                .defaultSystem("""
                        # 票务助手任务拆分规则
                        ## 1 要求
                        ### 1.1 根据用户内容识别任务

                        ## 2 任务
                        ### 2.1 JobType:退票(CANCEL) 要求用户提供姓名和预定号,或者从对话中提取;
                        ### 2.2 JobType:查票(QUERY) 要求用户提供预定号,或者从对话中提取;
                        ### 2.3 JobType:其他(OTHER)
                        """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build()
                )
                .defaultOptions(defaultChatOptions)
                .build();
    }

    // 智能客服
    @Bean
    public ChatClient botChatClient(DashScopeChatModel chatModel,
                                    DashScopeChatProperties options,
                                    ChatMemory chatMemory) {
        DashScopeChatOptions dashScopeChatOptions = DashScopeChatOptions.fromOptions(options.getOptions());
        dashScopeChatOptions.setTemperature(1.2);
        return ChatClient.builder(chatModel)
                .defaultSystem("""
                        你是Arguan航空智能客服代理,请以友好的语气服务用户
                        """)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build()
                )
                .defaultOptions(dashScopeChatOptions)
                .build();
    }
}

定义AiJob类结构化输出模型,JobType用于枚举任务处理类型,Job Record为结构化数据结构,利用SpringAI的entity功能来返回Java对象

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import java.util.Map;

public class AiJob {

    // 结构化数据结构
    record Job(
            JobType jobType, Map<String, String> keyInfos
    ) {}

    // 任务类型枚举
    public enum JobType {
        CANCEL,
        QUERY,
        OTHER,
    }
}

Controller

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;

@RestController
public class MultiModelsController {

    @Autowired
    ChatClient planningChatClient;

    @Autowired
    ChatClient botChatClient;

    @GetMapping(value = "/stream", produces = "text/stream;charset=UTF-8")
    Flux<String> stream(@RequestParam String message) {
        // 创建一个用于接收多条消息的Sink
        Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
        // 推送消息
        sink.tryEmitNext("正在计划任务...<br/>");

        new Thread(() -> {
            AiJob.Job job = planningChatClient.prompt().user(message)
                    .call().entity(AiJob.Job.class);
            switch (job.jobType()) {
                case CANCEL -> {
                    System.out.println(job);
                    if (job.keyInfos().size() == 0) {
                        sink.tryEmitNext("请输入姓名和订单号:");
                    } else {
                        // todo.. 执行业务  ticketService.cancel
                        sink.tryEmitNext("退票成功!");
                    }
                }
                case QUERY -> {
                    System.out.println(job);
                    if (job.keyInfos().size() == 0) {
                        sink.tryEmitNext("请输入订单号:");
                    }
                    // todo.. 执行业务 ticketService.query
                    sink.tryEmitNext("查询预订信息:xxxxxx");
                }
                case OTHER -> {
                    Flux<String> content = botChatClient.prompt().user(message).stream().content();
                    content.doOnNext(sink::tryEmitNext)// 推送每条AI流内容
                            .doOnComplete(() -> sink.tryEmitComplete())
                            .subscribe();
                }
                default -> {
                    System.out.println(job);
                    sink.tryEmitNext("解析失败");
                }
            }
        }).start();

        return sink.asFlux();
    }
}

Function-Call

  • 信息检索:可用于从外部源(数据库,Web服务,文件系统,Web搜索引擎等)检索信息

  • 采取行动:自动执行原本需要人工干预或显式编程的任务

ToolService类,用于提供tools工具,并由此类接收到参数去执行业务方法处理业务逻辑

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ToolService {

    @Autowired
    private TicketService ticketService;

    // @Tool注解告诉大模型我们提供了什么工具
    @Tool(description = "退票")
    public String cancel(
            //@ToolParam注解告诉大模型这个参数的描述
            @ToolParam(description = "预定号,格式可以是纯数字") String ticketNumber,
            @ToolParam(description = "姓名") String name) {
        ticketService.cancel(ticketNumber, name);
        return "退票成功!";
    }
}

ToolsController测试类,用于测试Function-call的使用

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ToolsController {

    ChatClient chatClient;

    public ToolsController(ChatClient.Builder ChatClientBuilder,
                           ToolService toolService) {
        this.chatClient = ChatClientBuilder
                .defaultTools(toolService)// 底层告诉大模型提供了什么工具,需要什么参数
                .build();
    }

    @RequestMapping("/tool")
    public String tool(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
        return chatClient.prompt().user(message).call().content();
    }
}

TicketService票务处理业务类

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import org.springframework.stereotype.Service;

@Service
public class TicketService {
    public void cancel(String ticketNumber, String name) {
        // todo.. 退票业务
        System.out.println("已经取消了名为" + name + "订单号为" + ticketNumber + "的订单");
    }
}

Tools原理

  • 用户输入: "帮我取消订单,订单号是 789,我叫李四"

  • ChatClient 发送请求 + 工具描述到 LLM

  • LLM 识别意图,返回:调用 cancel(ticketNumber="789", name="李四")

  • Spring AI 自动执行 ToolService.cancel("789", "李四")

  • TicketService 执行实际退票业务

  • 返回结果 "退票成功!" 给 LLM

  • LLM 生成友好回复:"已为您取消订单 789,乘客李四。"

  • 用户收到 最终回复

Tools参数无法自动推算问题

  • 温度太低,AI可能缺失自由度变得比较拘谨(从一定程度上可以解决)

  • 也可以通过描述更加准确(推荐)

强行适配Tool参数的幻觉问题

  • 加严参数描述与校验

  • 后端代码加强校验和兜底保护

  • 系统prompt设定限制

  • 加强人工确认

Tools其他注意事项

  • 工具暴露的接口名,方法名,参数名要可读,业务化

  • 避免乱码,缩写

  • 工具方法不适合做超耗时操作,导致用户体验不佳,线程堆积

Tools权限控制

  • 可以利用SpringSecurity限制

相关依赖

xml 复制代码
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-security</artifactId>  
    </dependency>

配置类

java 复制代码
package com.arguan.ai.moremodelstructuredagent.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

        /**
     * 配置并返回基于内存的用户详情服务
     * 创建两个测试用户:普通用户(user1)和管理员(admin)
     * 
     * @return UserDetailsService 包含预配置用户的内存用户详情服务实例
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user1").password(passwordEncoder().encode("pass1")).roles("USER").build();
        UserDetails admin = User.withUsername("admin").password(passwordEncoder().encode("pass2")).roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user, admin);
    }


        /**
     * 配置安全过滤链,定义HTTP请求的访问规则和认证方式
     * 允许/tool端点公开访问,其他所有请求需要认证
     * 启用默认的表单登录功能
     * 
     * @param http HttpSecurity对象,用于配置Web安全策略
     * @return SecurityFilterChain 配置完成的安全过滤链实例
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/tool").permitAll()
                        .anyRequest().authenticated()
                )
                .with(new FormLoginConfigurer<>(), Customizer.withDefaults());
        return http.build();
    }


        /**
     * 配置密码编码器Bean,使用BCrypt强哈希算法对用户密码进行加密
     * BCrypt会自动生成盐值,确保相同密码产生不同的哈希结果
     * 
     * @return PasswordEncoder BCrypt密码编码器实例
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


}

退票方法

java 复制代码
package com.arguan.ai.moremodelstructuredagent;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
public class ToolService {

    @Autowired
    private TicketService ticketService;

        /**
     * 退票工具方法,供AI调用执行退票操作
     * 需要ADMIN角色权限才能执行
     * 
     * @param ticketNumber 预定号,格式可以是纯数字
     * @param name 姓名
     * @return String 包含操作员用户名的退票成功消息
     */
    @Tool(description = "退票")
    @PreAuthorize("hasRole('ADMIN')")
    public String cancel(
            @ToolParam(description = "预定号,格式可以是纯数字") String ticketNumber,
            @ToolParam(description = "姓名") String name) {
        String username  = SecurityContextHolder.getContext().getAuthentication().getName();
        ticketService.cancel(ticketNumber, name);
        return username + "退票成功!";
    }

}
  • 将tools和权限资源一起存储,然后动态设置tools

    • 根据当前用户读取当前用户所属角色的所有tools
java 复制代码
   /**
     * 模拟从数据库中动态根据当前用户角色读取tools
     * 通过反射机制创建工具回调对象,实现动态工具注册
     *
     * @param toolService 工具服务对象实例
     * @return List<ToolCallback> 工具回调列表
     */
    public List<ToolCallback> getToolCallList(ToolService toolService) {

        // todo.. 从数据库中读取的代码

        // 拿一个tool举例
        Method method = ReflectionUtils.findMethod(ToolService.class, "cancel", String.class, String.class);
        ToolDefinition build = ToolDefinition.builder()
                .name("cancel")
                .description("退票")
                .inputSchema("""
                        {
                            "type": "object",
                            "properties": {
                                "ticketNumber": {
                                    "type": "string",
                                    "description": "预定号,可以是纯数字"
                                },
                                "name": {
                                    "type": "string",
                                    "description": "真实人名"
                                }
                            },
                            "required": ["ticketNumber", "name"]
                       }
                        """)
                .build();
        ToolCallback toolCallback = MethodToolCallback.builder()
                .toolDefinition(
                        build
                )
                .toolMethod(method)
                .toolObject(toolService)
                .build();

        return List.of(toolCallback);


        return List.of(toolCallback);
    }

Tools过多影响

  • token上限

  • 选择困难症

向量数据库,一个数据库用来做相似性检索

实现方式:

  • 把所有tools存入到向量数据库

  • 每次对话的时候根据当前对话信息检索到相似的tools(RAG)

  • 动态设置tools

相关推荐
2401_882273721 小时前
SQL如何快速提取分组中最晚时间点数据_结合窗口函数实现
jvm·数据库·python
2301_814809862 小时前
如何用 cookie 的 HttpOnly 与 Secure 属性防范 XSS 攻击
jvm·数据库·python
m0_515098422 小时前
如何用 Object.keys 与 getOwnPropertyNames 遍历键名
jvm·数据库·python
还是阿落呀2 小时前
第三章 添加数据
数据库·mysql
qq_189807032 小时前
golang如何实现日志按级别过滤_golang日志按级别过滤实现教程
jvm·数据库·python
abc123456sdggfd2 小时前
Golang map底层实现原理_Golang map哈希表原理教程【经典】
jvm·数据库·python
roman_日积跬步-终至千里2 小时前
【案例题-知识点(2)】架构风格上(五大类详解)
数据库·架构·系统架构
justjinji2 小时前
JavaScript中利用宏任务拆分阻塞任务的实操案例
jvm·数据库·python
@小柯555m2 小时前
MySql(基础操作符--查找除复旦大学的用户信息)
数据库·sql·mysql