feat(ai): 新增 ChatClient 配置与控制器

- 添加 ChatClientConfig 配置类,初始化 ChatClient 并配置系统提示和顾问
- 创建 ChatClientController 控制器,支持普通对话与流式对话接口- 引入 lombok依赖并添加 MyLoggerAdvisor 日志顾问实现
- 调整 DeepSeekR1ChatController,优化流式输出内容处理逻辑
- 更新 application.yml 中默认模型名称及日志级别配置
This commit is contained in:
hanserwei
2025-10-21 14:53:01 +08:00
parent 62cf0ed548
commit ef527aab00
6 changed files with 140 additions and 6 deletions

View File

@@ -0,0 +1,30 @@
package com.hanserwei.airobot.advisor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
@Slf4j
public class MyLoggerAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
log.info("## 请求入参: {}", chatClientRequest);
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
log.info("## 请求出参: {}", chatClientResponse);
return chatClientResponse;
}
@Override
public int getOrder() {
return 1; // order 值越小,越先执行
}
@Override
public String getName() {
// 获取类名称
return this.getClass().getSimpleName();
}
}

View File

@@ -0,0 +1,27 @@
package com.hanserwei.airobot.config;
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
/**
* 初始化 ChatClient 客户端
*
* @param chatModel 模型
* @return ChatClient
*/
@Bean
public ChatClient chatClient(DeepSeekChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
.defaultAdvisors(new SimpleLoggerAdvisor(),
new MyLoggerAdvisor())
.build();
}
}

View File

@@ -0,0 +1,46 @@
package com.hanserwei.airobot.controller;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
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;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/v2/ai")
public class ChatClientController {
@Resource
private ChatClient chatClient;
/**
* 普通对话
*
* @param message 对话输入内容
* @return 对话结果
*/
@GetMapping("/generate")
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
// 一次性返回结果
return chatClient.prompt()
.user(message)
.call()
.content();
}
/**
* 流式对话
* @param message 对话输入内容
* @return 对话结果
*/
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
return chatClient.prompt()
.user(message) // 提示词
.stream() // 流式输出
.content();
}
}

View File

@@ -12,6 +12,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@RestController
@RequestMapping("/v1/ai")
public class DeepSeekR1ChatController {
@@ -29,6 +32,9 @@ public class DeepSeekR1ChatController {
// 构建提示词
Prompt prompt = new Prompt(new UserMessage(message));
// 使用原子布尔值跟踪分隔线状态(每个请求独立)
AtomicBoolean needSeparator = new AtomicBoolean(true);
// 流式输出
return chatModel.stream(prompt)
.mapNotNull(chatResponse -> {
@@ -39,11 +45,27 @@ public class DeepSeekR1ChatController {
// 推理结束后的正式回答
String text = deepSeekAssistantMessage.getText();
// 是否是正式回答
boolean isTextResponse = false;
// 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答
return StringUtils.isNotBlank(reasoningContent) ? reasoningContent : text;
String rawContent;
if (Objects.isNull(text)) {
rawContent = reasoningContent;
} else {
rawContent = text;
isTextResponse = true; // 标记为正式回答
}
// 处理换行
String processed = StringUtils.isNotBlank(rawContent) ? rawContent.replace("\n", "<br>") : rawContent;
// 在正式回答内容之前,添加一个分隔线
if (isTextResponse
&& needSeparator.compareAndSet(true, false)) {
processed = "<hr>" + processed; // 使用 HTML 的 <hr> 标签实现
}
return processed;
});
}
}