diff --git a/pom.xml b/pom.xml index d93520b..ca6086c 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ 21 1.0.3 3.19.0 + 1.18.40 @@ -46,6 +47,11 @@ commons-lang3 ${commons-lang3.version} + + org.projectlombok + lombok + ${lombok.version} + diff --git a/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java b/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java new file mode 100644 index 0000000..dd85e1d --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/advisor/MyLoggerAdvisor.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java new file mode 100644 index 0000000..5c10c5c --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java b/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java new file mode 100644 index 0000000..602d7b7 --- /dev/null +++ b/src/main/java/com/hanserwei/airobot/controller/ChatClientController.java @@ -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 generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) { + return chatClient.prompt() + .user(message) // 提示词 + .stream() // 流式输出 + .content(); + + } +} \ No newline at end of file diff --git a/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java b/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java index ff4ced5..8fdce1e 100644 --- a/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java +++ b/src/main/java/com/hanserwei/airobot/controller/DeepSeekR1ChatController.java @@ -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", "
") : rawContent; + + // 在正式回答内容之前,添加一个分隔线 + if (isTextResponse + && needSeparator.compareAndSet(true, false)) { + processed = "
" + processed; // 使用 HTML 的
标签实现 + } + + return processed; }); - - - } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3be31b2..c3f06da 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,10 +8,13 @@ spring: base-url: https://api.deepseek.com # DeepSeek 的请求 URL, 可不填,默认值为 api.deepseek.com chat: options: - model: deepseek-reasoner # 使用哪个模型 + model: deepseek-chat # 使用哪个模型 temperature: 0.8 # 温度值 jasypt: encryptor: password: ${jasypt.encryptor.password} algorithm: PBEWithHMACSHA512AndAES_256 - iv-generator-classname: org.jasypt.iv.RandomIvGenerator \ No newline at end of file + iv-generator-classname: org.jasypt.iv.RandomIvGenerator +logging: + level: + org.springframework.ai.chat.client.advisor: debug \ No newline at end of file