【SpringAI翻车笔记】02-ChatClient的角色预设+结构化输出+流式输出+日志打印 的 使用

友友们,咱们本篇就介绍ChatClient的相关使用


SpringAI对我们调用大模型服务器进行了的封装,那么它里面有两个对象:ChatClient和ChatModel,其中ChatClient是对ChatModel的进一步封装,整体的格式是链式调用,偏我们的现实思维,更加易用。

一、ChatClient的使用

1. 直接查阅文档并复制

我们怎么去使用呢?🤔,直接啥都不知道就写代码吗?🐒

最简单的办法就是查阅官方使用文档👉ChatClientFluentAPI

查阅之后 如下:

2. 在我们的idea中创建一个控制器去调用大模型

java 复制代码
@RestController
@RequestMapping("/chat")
public class ChatClientController {
    private final ChatClient chatClient;

    //对ChatClient对象的注入使用的是构造方法注入
    public ChatClientController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai")
    String generation(String userInput) {
        //提示词
        return this.chatClient.prompt()
                //用户提示词
                .user(userInput)
                //调用
                .call()
                //返回响应
                .content();
    }
}

⚠️注意哦:yml配置文件得格式得正确,我们的spring才读取得到

yml 复制代码
spring:
  application:
    name: spring-ai-demo
  ai:
    openai:
      # 下面是我们的DeepSeek的api相关配置
      api-key:  #你自己的API秘钥
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat  # 模型
          temperature: 0.7      # 模型的参数0-2之间 数值越高,那么结果更加随机 数值越低,那么数值就越聚焦

3. 代码解释

ChatClient = 对 ChatModel 的高级封装 + 链式调用 + 贴近人类思维的调用方式


那么你想,我们使用构造方法注入的话,那我们的ChatClient对象只能在本例中使用了呀,那其他类要使用怎么办呢?

😄还记得SpringIOC中有一个简单的道理吗?就是说你把第三方的东西交给我们的spring容器管理起来,然后你使用的时候直接打上一个标签@Autowired 注入进来就行了,那么我们怎么做❓

我们写一个配置类,然后让Spring先管理这个配置类(@Configuration) 我们再将我们的对象通过方法返回交给Spring管理起来(@Bean)

咱们开始实践:👇

4. 将ChatClient对象配置交给Spring管理

咱先写一个配置类

java 复制代码
@Configuration //让Spring对这个配置类进行管理
public class ChatClientConfiguration {
}

在这个配置类中使用方法,方法返回一个对象,对方法打上@Bean注解将这个返回的对象注入到spring中

java 复制代码
@Configuration
public class ChatClientConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
       return chatClientBuilder
                .build();
    }
}

图解:


5. 从IOC容器中获取ChatClient对象

那么获取就很简单啦🎉,直接使用@Autowired注解

java 复制代码
@RestController
@RequestMapping("/chat")
public class ChatClientController {
    
    @Autowired //将ChatClient注入进来
    private ChatClient chatClient;


    @GetMapping("/ai")
    String generation(String userInput) {
        //提示词
        return this.chatClient.prompt()
                //用户提示词
                .user(userInput)
                //调用
                .call()
                //返回响应
                .content();
    }
}

OK,此时我们运行程序,进行测试:


二、角色预设

那么我们现在可以知道了,得出的结果如下所示:


1. 设置系统提示词指定角色预设

java 复制代码
@Configuration
public class ChatClientConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
       return chatClientBuilder
               //在此处指定一个系统提示词进行角色预设
               .defaultSystem("你叫谈简特,是tan的一名助手,精通聊天话术,主要的工作就是提供情绪价值")
                .build();
    }
}

加入系统提示词:

2. 结果


三、结构化输出

我们上述返回的都是String类型,那么我们可不可以返回结构化对象呢??

SpringAI⽀持将ChatModel/ChatClient ⽅法的返回类型从String更改为其他类型。

我们通过 entity() ⽅法将模型输出转为⾃定义实体。

但是我们得确保输出格式符合JSON规范。

如下所示👇:

1. 需求

我们生成一个菜单

2. 创建一个实体类

借助JDK16提供的新关键词record来定义⼀个实体类

record 就是 Java 官方给你做的「极简实体类模板」,一行代码 = 一个完整的实体类!

java 复制代码
/**
* 等价于手动写一个超级完整的 Java 实体类,
* 但你不用写 getter、equals、* hashCode、toString、构造器!
*/
record Recipe(String dish, List<String> ingredients) {} 
@GetMapping("/ai")
public Recipe generation(String userInput) {
		//提示词
    return this.chatClient.prompt()
            //用户提示词
            .user(String.format("请帮我生成%s的食谱", userInput))
            //调用
            .call()
            //返回响应:使用结构化对象
            entity(Recipe.class);
}        

3. 解释


4. 结果

它很智能,直接就给我们将数据给填充好了,如下所示👇


四、流式输出

你会发现哈,上述都是我们的模型将结果处理好之后,一次性给你返回的,那么我们怎么做到让他一个字一个字的蹦出来,有着流式体验呢?

SpringAI使⽤ChatClient 的stream() ⽅法⽣成Flux 流。

如下所示👇

1.代码

java 复制代码
    @GetMapping("/ai")
    public Flux<String> generation(String userInput) {
        //提示词
        return this.chatClient.prompt()
                //用户提示词
                .user( userInput)
                //调用 次数使用call()是一次性返回 使用stream()流式输出
                .stream()
                //返回响应:使用结构化对象
                .content();
    }

2.对比之前

3. 乱码问题

你使用流式输出 就会出现问题:

比如:如下👇乱码

4. 解决乱码问题

java 复制代码
    @GetMapping(value = "/ai", produces = "text/html;charset=utf-8")
    public Flux<String> generation(String userInput) {
        //提示词
        return this.chatClient.prompt()
                //用户提示词
                .user( userInput)
                //调用 次数使用call()是一次性返回 使用stream()流式输出
                .stream()
                //返回响应:使用结构化对象
                .content();
    }

5.结果


五、⽇志打印

SpringAI借助Advisors来实现⽇志打印的功能。、

1. 核心本质

  • Advisors = Spring AI 的 AOP 拦截器
  • 链式执行,可拦截请求 + 拦截响应
  • 每个 Advisor 都能在对话前后加自定义逻辑
  • 作用:统一处理、增强功能、不侵入业务代码

2. 常用功能场景

  • 敏感词过滤
  • 聊天历史记录
  • 对话上下文管理
  • 日志打印

3. SimpleLoggerAdvisor(内置日志工具)

  • 作用:自动打印 AI 对话的请求 + 响应日志
  • 可配置:日志级别、日志格式

4. 两种使用方式

① 全局生效(所有对话都打印)

java 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder){
    return builder
        .defaultSystem("系统提示词")
        .defaultAdvisors(new SimpleLoggerAdvisor()) // 全局
        .build();
}

例如我们的代码:

java 复制代码
@Configuration
public class ChatClientConfiguration {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
       return chatClientBuilder
               //在此处指定一个系统提示词进行角色预设
               .defaultSystem("你叫谈简特,是tan的一名助手,精通聊天话术,主要的工作就是提供情绪价值")
               .defaultAdvisors(new SimpleLoggerAdvisor())//全局
               .build();
    }
}

② 单次生效(仅当前请求打印)

优先级 高于全局

java 复制代码
chatClient.prompt()
    .user(userInput)
    .advisors(new SimpleLoggerAdvisor()) // 单次
    .call()
    .content();

5. 开启日志配置

yaml 复制代码
logging:
  level:
    org.springframework.ai.chat.client.advisor: debug

为什么必须开启日志级别?

宝贝,这个超级关键,我用最简单的话告诉你👇

因为
SimpleLoggerAdvisor 输出的是 DEBUG 级别的日志

而 Spring Boot 默认只打印 INFO 级别及以上不打印 DEBUG

也就是说:

  • 不开启 debug → 日志被屏蔽 → 看不见任何输出
  • 开启 debug → 日志放行 → 能看到请求/响应详情

打个比方:

日志就像喇叭

  • INFO = 正常说话(默认能听见)
  • DEBUG = 小声嘀咕(默认听不见)

SimpleLoggerAdvisor 就是用**小声嘀咕(debug)**说话的。

你必须告诉系统:
"我要听小声嘀咕!"

它才会输出。


6. 日志结果

请求:

控制台日志:

bash 复制代码
2026-04-14T10:59:09.425+08:00 DEBUG 2892 --- [spring-ai-demo] [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: AdvisedRequest[chatModel=OpenAiChatModel [defaultOptions=OpenAiChatOptions: {"streamUsage":false,"model":"deepseek-chat","temperature":0.7}], userText=你是谁, systemText=你叫谈简特,是tan的一名助手,精通聊天话术,主要的工作就是提供情绪价值, chatOptions=OpenAiChatOptions: {"streamUsage":false,"model":"deepseek-chat","temperature":0.7}, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@39eb2d2c, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@4f0e1ac7, SimpleLoggerAdvisor, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@4e479638, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@162b58b3], advisorParams={}, adviseContext={}, toolContext={}]
2026-04-14T10:59:12.346+08:00 DEBUG 2892 --- [spring-ai-demo] [oundedElastic-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {
  "result" : {
    "metadata" : {
      "finishReason" : "STOP",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "role" : "ASSISTANT",
        "messageType" : "ASSISTANT",
        "finishReason" : "STOP",
        "refusal" : "",
        "index" : 0,
        "id" : "599d877f-f300-4bb8-9a5b-fb558d23e0e0"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "我是谈简特,专门为你提供情绪支持和聊天陪伴的助手。有什么想聊的,或者需要我帮忙的吗? 😊"
    }
  },
  "metadata" : {
    "id" : "599d877f-f300-4bb8-9a5b-fb558d23e0e0",
    "model" : "deepseek-chat",
    "rateLimit" : {
      "requestsReset" : 0.0,
      "tokensLimit" : 0,
      "requestsLimit" : 0,
      "tokensRemaining" : 0,
      "tokensReset" : 0.0,
      "requestsRemaining" : 0
    },
    "usage" : {
      "promptTokens" : 27,
      "completionTokens" : 28,
      "totalTokens" : 55,
      "nativeUsage" : {
        "promptTokens" : 27,
        "totalTokens" : 55,
        "completionTokens" : 28
      },
      "generationTokens" : 28
    },
    "promptMetadata" : [ ],
    "empty" : true
  },
  "results" : [ {
    "metadata" : {
      "finishReason" : "STOP",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "role" : "ASSISTANT",
        "messageType" : "ASSISTANT",
        "finishReason" : "STOP",
        "refusal" : "",
        "index" : 0,
        "id" : "599d877f-f300-4bb8-9a5b-fb558d23e0e0"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "我是谈简特,专门为你提供情绪支持和聊天陪伴的助手。有什么想聊的,或者需要我帮忙的吗? 😊"
    }
  } ]
}

觉得有用的小伙伴别忘了 点赞、收藏、关注 哦,下期继续分享 Spring AI 入门干货,我们下期见!👋

相关推荐
鬼先生_sir3 天前
Spring AI Alibaba 1.1.2.2 完整知识点库
人工智能·ai·agent·源码解析·springai
鬼先生_sir5 天前
Spring AI Alibaba 用户使用手册
java·人工智能·springai
盐水冰12 天前
【SpringAI】认识与应用开发
人工智能·springai
冲上云霄的Jayden13 天前
Spring Ai WebClient、RestClient设置代理
spring·webclient·代理·restclient·gemini·region·springai
学java的冲鸭14 天前
【SpringAI第四章】函数调用
java·ai·springai
Chan1614 天前
SpringAI:RAG 最佳实践与调优
java·spring boot·ai·java-ee·intellij-idea·rag·springai
xdscode14 天前
Spring AI 中的 Flux 与 SSE:流式输出完全解析
java·flux·sse·springai·stream流式输出
梵得儿SHI16 天前
(第四篇)Spring AI 实战进阶:Ollama+Spring AI 构建离线私有化 AI 服务(脱离 API 密钥的完整方案)
人工智能·数据安全·springai·离线私有化ai服务·springai深度集成·模型优化与资源控制·离线rag知识库
yc_xym21 天前
SpringAI快速入门
java·springai·deepseek