|
|
@@ -30,17 +30,20 @@ public class ChatController {
|
|
|
private final PromptService promptService;
|
|
|
private final ChatPersistenceService persistenceService;
|
|
|
private final RerankerService rerankerService;
|
|
|
+ private final QACacheService qaCache;
|
|
|
private final JdbcTemplate jdbc;
|
|
|
private final QwenProperties props;
|
|
|
|
|
|
public ChatController(RetrieverService rs, LLMService ls, PromptService ps,
|
|
|
ChatPersistenceService cps, RerankerService rrs,
|
|
|
+ QACacheService qaCache,
|
|
|
JdbcTemplate jdbc, QwenProperties props) {
|
|
|
this.retrieverService = rs;
|
|
|
this.llmService = ls;
|
|
|
this.promptService = ps;
|
|
|
this.persistenceService = cps;
|
|
|
this.rerankerService = rrs;
|
|
|
+ this.qaCache = qaCache;
|
|
|
this.jdbc = jdbc;
|
|
|
this.props = props;
|
|
|
}
|
|
|
@@ -48,10 +51,46 @@ public class ChatController {
|
|
|
@PostMapping("/ask")
|
|
|
public ResponseEntity<Map<String, Object>> chatAsk(@RequestBody ChatRequest request) {
|
|
|
String query = request.getMessage();
|
|
|
+ String normalized = qaCache.normalize(query);
|
|
|
String cid = request.getConversationId() != null && !request.getConversationId().isBlank()
|
|
|
? request.getConversationId()
|
|
|
: UUID.randomUUID().toString();
|
|
|
|
|
|
+ // 检查全局缓存(24 小时有效,不区分用户)
|
|
|
+ Map<String, Object> cached = qaCache.get(normalized);
|
|
|
+ if (cached != null) {
|
|
|
+ String answer = (String) cached.get("answer");
|
|
|
+ String intent = (String) cached.getOrDefault("intent", "");
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
|
+ persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
+ persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
+ return ResponseEntity.ok(Map.of(
|
|
|
+ "answer", answer,
|
|
|
+ "sources", sources,
|
|
|
+ "intent", intent,
|
|
|
+ "conversation_id", cid,
|
|
|
+ "cached", true
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未命中缓存:尝试抢占处理权,避免并发重复调 LLM
|
|
|
+ if (!qaCache.tryMarkPending(normalized)) {
|
|
|
+ Map<String, Object> waited = qaCache.waitForCache(normalized);
|
|
|
+ if (waited != null) {
|
|
|
+ String answer = (String) waited.get("answer");
|
|
|
+ String intent = (String) waited.getOrDefault("intent", "");
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> sources = (List<Map<String, Object>>) waited.getOrDefault("sources", List.of());
|
|
|
+ persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
+ persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
+ return ResponseEntity.ok(Map.of(
|
|
|
+ "answer", answer, "sources", sources, "intent", intent,
|
|
|
+ "conversation_id", cid, "cached", true
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
String intent = retrieverService.classifyIntent(query);
|
|
|
List<Map<String, Object>> docs = retrieverService.search(query, intent, 20);
|
|
|
docs = rerankerService.rerank(docs, query, 5);
|
|
|
@@ -65,6 +104,9 @@ public class ChatController {
|
|
|
persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
|
|
|
+ // 写入全局缓存
|
|
|
+ qaCache.put(normalized, answer, intent, sources);
|
|
|
+
|
|
|
return ResponseEntity.ok(Map.of(
|
|
|
"answer", answer,
|
|
|
"sources", sources,
|
|
|
@@ -76,10 +118,25 @@ public class ChatController {
|
|
|
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
public Flux<ServerSentEvent<String>> chatStream(@RequestBody ChatRequest request) {
|
|
|
String query = request.getMessage();
|
|
|
+ String normalized = qaCache.normalize(query);
|
|
|
final String cid = request.getConversationId() != null && !request.getConversationId().isBlank()
|
|
|
? request.getConversationId()
|
|
|
: UUID.randomUUID().toString();
|
|
|
|
|
|
+ // 检查全局缓存
|
|
|
+ Map<String, Object> cached = qaCache.get(normalized);
|
|
|
+ if (cached != null) {
|
|
|
+ return streamCached(cached, cid, query, normalized);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未命中缓存:尝试抢占处理权,避免并发重复调 LLM
|
|
|
+ if (!qaCache.tryMarkPending(normalized)) {
|
|
|
+ Map<String, Object> waited = qaCache.waitForCache(normalized);
|
|
|
+ if (waited != null) {
|
|
|
+ return streamCached(waited, cid, query, normalized);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
final String intent = retrieverService.classifyIntent(query);
|
|
|
|
|
|
return retrieverService.searchReactive(query, intent, 20)
|
|
|
@@ -113,6 +170,10 @@ public class ChatController {
|
|
|
String finalAnswer = cleanAnswer(fullAnswer.toString());
|
|
|
persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
persistenceService.saveMessage(cid, "assistant", finalAnswer, intent, sources);
|
|
|
+
|
|
|
+ // 写入全局缓存
|
|
|
+ qaCache.put(normalized, finalAnswer, intent, sources);
|
|
|
+
|
|
|
sink.tryEmitComplete();
|
|
|
})
|
|
|
.doOnError(sink::tryEmitError)
|
|
|
@@ -122,6 +183,35 @@ public class ChatController {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /** 将缓存命中结果以流式 SSE 形式返回 */
|
|
|
+ private Flux<ServerSentEvent<String>> streamCached(Map<String, Object> cached, String cid,
|
|
|
+ String query, String normalized) {
|
|
|
+ String answer = (String) cached.get("answer");
|
|
|
+ String intent = (String) cached.getOrDefault("intent", "");
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> sources = (List<Map<String, Object>>) cached.getOrDefault("sources", List.of());
|
|
|
+
|
|
|
+ persistenceService.saveMessage(cid, "user", query, intent, null);
|
|
|
+ persistenceService.saveMessage(cid, "assistant", answer, intent, sources);
|
|
|
+
|
|
|
+ return Flux.create(sink -> {
|
|
|
+ sink.next(ServerSentEvent.<String>builder().event("intent").data(intent).build());
|
|
|
+ sink.next(ServerSentEvent.<String>builder().event("status").data("命中缓存,直接返回...").build());
|
|
|
+ // 将缓存答案按段落拆分发送,模拟流式体验
|
|
|
+ String[] chunks = answer.split("(?<=\\n)");
|
|
|
+ for (String chunk : chunks) {
|
|
|
+ sink.next(ServerSentEvent.<String>builder().data(chunk).build());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String meta = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(Map.of(
|
|
|
+ "intent", intent, "sources", sources, "conversation_id", cid, "cached", true
|
|
|
+ ));
|
|
|
+ sink.next(ServerSentEvent.<String>builder().event("meta").data(meta).build());
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ sink.complete();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
// ============================================================
|
|
|
// 图片对话 API(Qwen VL 分析 + OCR → RAG 检索 → 联网搜索)
|
|
|
// ============================================================
|