feat(chat): 新增聊天调用日志与动态配置支持
- 新增 KeyboardUserCallLog 实体及对应 Mapper、Service,用于记录每次聊天请求的模型、token、耗时、错误码等 - ChatController.talk() 在流式输出前后采集元数据,异步落库,支持错误码记录 - AppConfig 新增 QdrantConfig,支持 vectorSearchLimit 动态配置 - QdrantVectorService 改为从 Nacos 动态读取搜索条数,替代硬编码 limit=1 - UserController 登出时先清除用户会话再清除 token,避免并发异常
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -12,7 +13,9 @@ import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserCallLog;
|
||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import com.yolo.keyborad.service.KeyboardUserCallLogService;
|
||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||
import io.qdrant.client.grpc.JsonWithInt;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -28,9 +31,10 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
@@ -57,6 +61,9 @@ public class ChatController {
|
||||
@Resource
|
||||
private KeyboardCharacterService keyboardCharacterService;
|
||||
|
||||
@Resource
|
||||
private KeyboardUserCallLogService callLogService;
|
||||
|
||||
|
||||
@PostMapping("/talk")
|
||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||
@@ -89,6 +96,14 @@ public class ChatController {
|
||||
throw new BusinessException(ErrorCode.CHAT_CHARACTER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 初始化调用日志
|
||||
String requestId = IdUtil.fastSimpleUUID();
|
||||
long startTime = System.currentTimeMillis();
|
||||
AtomicReference<String> modelRef = new AtomicReference<>();
|
||||
AtomicInteger inputTokens = new AtomicInteger(0);
|
||||
AtomicInteger outputTokens = new AtomicInteger(0);
|
||||
AtomicReference<String> errorCodeRef = new AtomicReference<>();
|
||||
|
||||
// 3. LLM 流式输出
|
||||
Flux<ChatStreamMessage> llmFlux = client
|
||||
.prompt(character.getPrompt())
|
||||
@@ -103,10 +118,35 @@ public class ChatController {
|
||||
.user(StpUtil.getLoginIdAsString())
|
||||
.build())
|
||||
.stream()
|
||||
.content()
|
||||
.concatMap(chunk -> {
|
||||
.chatResponse()
|
||||
.concatMap(response -> {
|
||||
// 提取 metadata
|
||||
if (response.getMetadata() != null) {
|
||||
var metadata = response.getMetadata();
|
||||
if (metadata.getModel() != null) {
|
||||
modelRef.set(metadata.getModel());
|
||||
}
|
||||
if (metadata.getUsage() != null) {
|
||||
var usage = metadata.getUsage();
|
||||
if (usage.getPromptTokens() != null) {
|
||||
inputTokens.set(usage.getPromptTokens());
|
||||
}
|
||||
if (usage.getCompletionTokens() != null) {
|
||||
outputTokens.set(usage.getCompletionTokens());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 获取内容
|
||||
String content = response.getResult().getOutput().getText();
|
||||
if (content == null || content.isEmpty()) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
// 拆成单字符
|
||||
List<String> chars = chunk.codePoints()
|
||||
List<String> chars = content.codePoints()
|
||||
.mapToObj(cp -> new String(Character.toChars(cp)))
|
||||
.toList();
|
||||
|
||||
@@ -127,7 +167,10 @@ public class ChatController {
|
||||
return Flux.fromIterable(batched)
|
||||
.map(s -> new ChatStreamMessage("llm_chunk", s));
|
||||
})
|
||||
.doOnError(error -> log.error("LLM调用失败", error))
|
||||
.doOnError(error -> {
|
||||
log.error("LLM调用失败", error);
|
||||
errorCodeRef.set("LLM_ERROR");
|
||||
})
|
||||
.onErrorResume(error ->
|
||||
Flux.just(new ChatStreamMessage("error", "LLM服务暂时不可用,请稍后重试"))
|
||||
);
|
||||
@@ -151,13 +194,39 @@ public class ChatController {
|
||||
Flux<ChatStreamMessage> merged =
|
||||
Flux.merge(llmFlux, searchFlux)
|
||||
.concatWith(doneFlux);
|
||||
|
||||
// 7. SSE 包装
|
||||
return merged.map(msg ->
|
||||
ServerSentEvent.builder(msg)
|
||||
.event(msg.getType())
|
||||
.build()
|
||||
);
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
// 7. SSE 包装并记录调用日志
|
||||
return merged
|
||||
.doFinally(signalType -> {
|
||||
// 异步保存调用日志
|
||||
Mono.fromRunnable(() -> {
|
||||
try {
|
||||
KeyboardUserCallLog callLog = new KeyboardUserCallLog();
|
||||
SaTokenContextMockUtil.setMockContext(()->{
|
||||
StpUtil.setTokenValueToStorage(tokenValue);
|
||||
callLog.setUserId(StpUtil.getLoginIdAsLong());
|
||||
});
|
||||
callLog.setRequestId(requestId);
|
||||
callLog.setFeature("chat_talk");
|
||||
callLog.setModel(modelRef.get());
|
||||
callLog.setInputTokens(inputTokens.get());
|
||||
callLog.setOutputTokens(outputTokens.get());
|
||||
callLog.setTotalTokens(inputTokens.get() + outputTokens.get());
|
||||
callLog.setSuccess(errorCodeRef.get() == null);
|
||||
callLog.setLatencyMs((int) (System.currentTimeMillis() - startTime));
|
||||
callLog.setErrorCode(errorCodeRef.get());
|
||||
callLog.setCreatedAt(new Date());
|
||||
callLogService.save(callLog);
|
||||
} catch (Exception e) {
|
||||
log.error("保存调用日志失败", e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic()).subscribe();
|
||||
})
|
||||
.map(msg ->
|
||||
ServerSentEvent.builder(msg)
|
||||
.event(msg.getType())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ public class UserController {
|
||||
@GetMapping("/logout")
|
||||
@Operation(summary = "退出登录", description = "退出登录接口")
|
||||
public BaseResponse<Boolean> logout() {
|
||||
StpUtil.logout(StpUtil.getLoginIdAsLong());
|
||||
StpUtil.logoutByTokenValue(StpUtil.getTokenValue());
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
@@ -115,4 +116,5 @@ public class UserController {
|
||||
public BaseResponse<Boolean> resetPassWord(@RequestBody ResetPassWordDTO resetPassWordDTO) {
|
||||
return ResultUtils.success(userService.resetPassWord(resetPassWordDTO));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user