fix(chat): 增强聊天接口参数校验与异常处理
- 新增消息长度、空值、人设存在性等校验 - 补充 LLM 与向量搜索异常捕获及降级 - 统一返回错误码与日志,提升鲁棒性
This commit is contained in:
@@ -23,6 +23,11 @@ public enum ErrorCode {
|
||||
FILE_NAME_ERROR(40002, "文件名错误"),
|
||||
FILE_TYPE_ERROR(40004, "文件类型不支持,仅支持图片格式"),
|
||||
FILE_SIZE_EXCEED(40005, "文件大小超出限制,最大支持5MB"),
|
||||
CHAT_MESSAGE_EMPTY(40006, "聊天消息不能为空"),
|
||||
CHAT_CHARACTER_ID_EMPTY(40007, "键盘人设ID不能为空"),
|
||||
CHAT_CHARACTER_NOT_FOUND(40008, "键盘人设不存在"),
|
||||
CHAT_MESSAGE_TOO_LONG(40009, "聊天消息过长,最大支持1000字符"),
|
||||
CHAT_SAVE_DATA_EMPTY(40010, "保存数据不能为空"),
|
||||
TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"),
|
||||
TOKEN_INVALID(40103, "令牌无效"),
|
||||
TOKEN_TIMEOUT(40104, "令牌已过期"),
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.QdrantPayloadMapper;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
||||
@@ -13,7 +16,6 @@ import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||
import io.qdrant.client.grpc.JsonWithInt;
|
||||
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;
|
||||
@@ -37,10 +39,12 @@ import java.util.Map;
|
||||
@RestController
|
||||
@RequestMapping("/chat")
|
||||
@Slf4j
|
||||
@CrossOrigin
|
||||
@Tag(name = "聊天", description = "聊天接口")
|
||||
public class ChatController {
|
||||
|
||||
// 最大消息长度限制
|
||||
private static final int MAX_MESSAGE_LENGTH = 1000;
|
||||
|
||||
@Resource
|
||||
private ChatClient client;
|
||||
|
||||
@@ -57,8 +61,35 @@ public class ChatController {
|
||||
@PostMapping("/talk")
|
||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||
public Flux<ServerSentEvent<ChatStreamMessage>> talk(@RequestBody ChatReq chatReq){
|
||||
// 1. 参数校验
|
||||
if (chatReq == null) {
|
||||
log.error("聊天请求参数为空");
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
if (chatReq.getCharacterId() == null) {
|
||||
log.error("键盘人设ID为空");
|
||||
throw new BusinessException(ErrorCode.CHAT_CHARACTER_ID_EMPTY);
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(chatReq.getMessage())) {
|
||||
log.error("聊天消息为空");
|
||||
throw new BusinessException(ErrorCode.CHAT_MESSAGE_EMPTY);
|
||||
}
|
||||
|
||||
if (chatReq.getMessage().length() > MAX_MESSAGE_LENGTH) {
|
||||
log.error("聊天消息过长,长度: {}", chatReq.getMessage().length());
|
||||
throw new BusinessException(ErrorCode.CHAT_MESSAGE_TOO_LONG);
|
||||
}
|
||||
|
||||
// 2. 验证键盘人设是否存在
|
||||
KeyboardCharacter character = keyboardCharacterService.getById(chatReq.getCharacterId());
|
||||
// 1. LLM 流式输出
|
||||
if (character == null) {
|
||||
log.error("键盘人设不存在,ID: {}", chatReq.getCharacterId());
|
||||
throw new BusinessException(ErrorCode.CHAT_CHARACTER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 3. LLM 流式输出
|
||||
Flux<ChatStreamMessage> llmFlux = client
|
||||
.prompt(character.getPrompt())
|
||||
.system("""
|
||||
@@ -79,12 +110,12 @@ public class ChatController {
|
||||
.mapToObj(cp -> new String(Character.toChars(cp)))
|
||||
.toList();
|
||||
|
||||
// 你可以在这里按 3~5 个字符再拼一拼
|
||||
// 按 3 个字符批量发送
|
||||
List<String> batched = new ArrayList<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String ch : chars) {
|
||||
sb.append(ch);
|
||||
if (sb.length() >= 3) { // 这里的 3 可以自己调
|
||||
if (sb.length() >= 3) {
|
||||
batched.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
}
|
||||
@@ -95,25 +126,33 @@ public class ChatController {
|
||||
|
||||
return Flux.fromIterable(batched)
|
||||
.map(s -> new ChatStreamMessage("llm_chunk", s));
|
||||
});
|
||||
// .map(chunk -> new ChatStreamMessage("llm_chunk", chunk));
|
||||
})
|
||||
.doOnError(error -> log.error("LLM调用失败", error))
|
||||
.onErrorResume(error ->
|
||||
Flux.just(new ChatStreamMessage("error", "LLM服务暂时不可用,请稍后重试"))
|
||||
);
|
||||
|
||||
// 2. 向量搜索Flux(一次性发送搜索结果)
|
||||
// 4. 向量搜索Flux(一次性发送搜索结果)
|
||||
Flux<ChatStreamMessage> searchFlux = Mono
|
||||
.fromCallable(() -> qdrantVectorService.searchText(chatReq.getMessage()))
|
||||
.subscribeOn(Schedulers.boundedElastic()) // 避免阻塞 event-loop
|
||||
.map(list -> new ChatStreamMessage("search_result", list))
|
||||
.doOnError(error -> log.error("向量搜索失败", error))
|
||||
.onErrorResume(error ->
|
||||
Mono.just(new ChatStreamMessage("search_result", new ArrayList<>()))
|
||||
)
|
||||
.flux();
|
||||
// 3. 结束标记
|
||||
|
||||
// 5. 结束标记
|
||||
Flux<ChatStreamMessage> doneFlux =
|
||||
Flux.just(new ChatStreamMessage("done", null));
|
||||
|
||||
// 4. 合并所有Flux
|
||||
// 6. 合并所有Flux
|
||||
Flux<ChatStreamMessage> merged =
|
||||
Flux.merge(llmFlux, searchFlux)
|
||||
.concatWith(doneFlux);
|
||||
|
||||
// 5. SSE 包装
|
||||
// 7. SSE 包装
|
||||
return merged.map(msg ->
|
||||
ServerSentEvent.builder(msg)
|
||||
.event(msg.getType())
|
||||
@@ -123,13 +162,30 @@ public class ChatController {
|
||||
|
||||
|
||||
@PostMapping("/save_embed")
|
||||
@Operation(summary = "保存润色后的句子", description = "保存润色后的句子")
|
||||
@Parameter(name = "userInput",required = true,description = "测试聊天接口",example = "talk to something")
|
||||
public BaseResponse<Boolean> testTalkWithVector(@RequestBody ChatSaveReq chatSaveReq) {
|
||||
@Operation(summary = "保存润色后的句子", description = "保存用户选择的润色句子到向量数据库")
|
||||
public BaseResponse<Boolean> saveChatEmbed(@RequestBody ChatSaveReq chatSaveReq) {
|
||||
// 1. 参数校验
|
||||
if (chatSaveReq == null) {
|
||||
log.error("保存请求参数为空");
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(chatSaveReq.getUserText())) {
|
||||
log.error("用户原始文本为空");
|
||||
throw new BusinessException(ErrorCode.CHAT_SAVE_DATA_EMPTY);
|
||||
}
|
||||
|
||||
// 2. 生成嵌入向量
|
||||
float[] embed = embeddingModel.embed(chatSaveReq.getUserText());
|
||||
|
||||
// 3. 设置用户ID
|
||||
chatSaveReq.setUserId(StpUtil.getLoginIdAsLong());
|
||||
|
||||
// 4. 转换为Qdrant payload并保存
|
||||
Map<String, JsonWithInt.Value> map = QdrantPayloadMapper.toQdrantPayload(chatSaveReq);
|
||||
qdrantVectorService.upsertPoint(IdUtil.getSnowflakeNextId(), embed, map);
|
||||
|
||||
log.info("聊天嵌入保存成功,用户ID: {}, 文本长度: {}", chatSaveReq.getUserId(), chatSaveReq.getUserText().length());
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user