feat(chat): 新增聊天润色与向量搜索接口
- ChatController 提供 /chat/talk SSE 流式对话,融合 LLM 输出与 Qdrant 向量检索 - 新增 ChatReq、ChatStreamMessage 等 DTO 与 Service 骨架 - 调整向量维度与集合名称,开放跨域并补充错误码
This commit is contained in:
104
src/main/java/com/yolo/keyborad/controller/ChatController.java
Normal file
104
src/main/java/com/yolo/keyborad/controller/ChatController.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/8 15:05
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/chat")
|
||||
@Slf4j
|
||||
@CrossOrigin
|
||||
@Tag(name = "聊天", description = "聊天接口")
|
||||
public class ChatController {
|
||||
|
||||
@Resource
|
||||
private ChatClient client;
|
||||
|
||||
@Resource
|
||||
private OpenAiEmbeddingModel embeddingModel;
|
||||
|
||||
@Resource
|
||||
private QdrantVectorService qdrantVectorService;
|
||||
|
||||
@Resource
|
||||
private KeyboardCharacterService keyboardCharacterService;
|
||||
|
||||
|
||||
@PostMapping("/talk")
|
||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
|
||||
public Flux<ServerSentEvent<ChatStreamMessage>> testTalk(@RequestBody ChatReq chatReq){
|
||||
|
||||
KeyboardCharacter character = keyboardCharacterService.getById(chatReq.getCharacterId());
|
||||
|
||||
// 1. LLM 流式输出
|
||||
Flux<ChatStreamMessage> llmFlux = client
|
||||
.prompt(character.getPrompt() +
|
||||
"\nUser message: %s".formatted(chatReq.getMessage()))
|
||||
.system("""
|
||||
Format rules:
|
||||
- Return EXACTLY 3 replies.
|
||||
- Use "<SPLIT>" as the separator.
|
||||
- reply1<SPLIT>reply2<SPLIT>reply3
|
||||
""")
|
||||
.user(chatReq.getMessage())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.user(StpUtil.getLoginIdAsString())
|
||||
.build())
|
||||
.stream()
|
||||
.content()
|
||||
.map(chunk -> new ChatStreamMessage("llm_chunk", chunk));
|
||||
|
||||
// 2. 向量搜索Flux(一次性发送搜索结果)
|
||||
Flux<ChatStreamMessage> searchFlux = Mono
|
||||
.fromCallable(() -> qdrantVectorService.searchText(chatReq.getMessage()))
|
||||
.subscribeOn(Schedulers.boundedElastic()) // 避免阻塞 event-loop
|
||||
.map(list -> new ChatStreamMessage("search_result", list))
|
||||
.flux();
|
||||
// 3. 结束标记
|
||||
Flux<ChatStreamMessage> doneFlux =
|
||||
Flux.just(new ChatStreamMessage("done", null));
|
||||
|
||||
// 4. 合并所有Flux
|
||||
Flux<ChatStreamMessage> merged =
|
||||
Flux.merge(llmFlux, searchFlux)
|
||||
.concatWith(doneFlux);
|
||||
|
||||
// 5. SSE 包装
|
||||
return merged.map(msg ->
|
||||
ServerSentEvent.builder(msg)
|
||||
.event(msg.getType())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/save_embed")
|
||||
@Operation(summary = "保存润色后的句子", description = "保存润色后的句子")
|
||||
@Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
|
||||
public Flux<String> testTalkWithVector(@RequestBody ChatReq chatReq) {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,6 @@ public class DemoController {
|
||||
@Operation(summary = "测试聊天接口", description = "测试接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
|
||||
public Flux<String> testTalk(@DefaultValue("you are so cute!") String userInput){
|
||||
String delimiter = "/t";
|
||||
|
||||
return client
|
||||
.prompt("""
|
||||
You're a 25-year-old guy—witty and laid-back, always replying in English.
|
||||
@@ -83,7 +81,7 @@ public class DemoController {
|
||||
""")
|
||||
.user(userInput)
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.user(StpUtil.getLoginIdAsString()) // ✅ 这里每次请求都会重新取当前登录用户
|
||||
.user(StpUtil.getLoginIdAsString())// ✅ 这里每次请求都会重新取当前登录用户
|
||||
.build())
|
||||
.stream()
|
||||
.content();
|
||||
@@ -99,12 +97,6 @@ public class DemoController {
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "IOS内购凭证校验", description = "IOS内购凭证校验")
|
||||
public BaseResponse<String> iosPay(@RequestBody IosPayVerifyReq req) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/testSaveEmbed")
|
||||
@Operation(summary = "测试存储向量接口", description = "测试存储向量接口")
|
||||
@Parameter(name = "userInput",required = true,description = "测试存储向量接口")
|
||||
|
||||
Reference in New Issue
Block a user