mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
feat: 新增通用链路追踪模块与 RAG 对话全链路埋点
- 新增 ruoyi-common-trace 模块:TraceContext 上下文、TraceNodeAspect 注解切面、数据库双表持久化 - ChatServiceFacade / KnowledgeRetrievalServiceImpl 完成 RAG 对话 retrieval → rerank → llm-call 全链路采集 - 新增 TraceController 提供列表/详情/节点查询 API 及数据统计 - 支持 trace.enabled 开关、payload 长度截断、错误安全降级 - TraceNodeTemplate 模板封装标准节点生命周期,减少重复样板代码
This commit is contained in:
@@ -29,6 +29,11 @@
|
||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-trace</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
|
||||
@@ -50,6 +50,16 @@ import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.ruoyi.common.trace.config.TraceProperties;
|
||||
import org.ruoyi.common.trace.constant.TraceConstants;
|
||||
import org.ruoyi.common.trace.core.DefaultTraceStreamSpan;
|
||||
import org.ruoyi.common.trace.core.TraceContext;
|
||||
import org.ruoyi.common.trace.core.TraceScope;
|
||||
import org.ruoyi.common.trace.core.TraceStreamSpan;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.domain.TraceRun;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ChatServiceFactory;
|
||||
@@ -62,14 +72,19 @@ import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
|
||||
import org.ruoyi.service.knowledge.retriever.CustomVectorRetriever;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.ruoyi.trace.RagTraceNodeTypes;
|
||||
import org.ruoyi.trace.RagTracePayloadBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 聊天服务门面层
|
||||
@@ -106,6 +121,10 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
private final ToolProviderFactory toolProviderFactory;
|
||||
|
||||
private final TraceRecordService traceRecordService;
|
||||
|
||||
private final TraceProperties traceProperties;
|
||||
|
||||
/**
|
||||
* 内存实例缓存,避免同一会话重复创建
|
||||
* Key: sessionId, Value: MessageWindowChatMemory实例
|
||||
@@ -133,14 +152,10 @@ public class ChatServiceFacade implements IChatService {
|
||||
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 2. 构建上下文消息列表
|
||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
||||
|
||||
chatRequest.setEmitter(emitter);
|
||||
chatRequest.setUserId(userId);
|
||||
chatRequest.setTokenValue(tokenValue);
|
||||
chatRequest.setChatModelVo(chatModelVo);
|
||||
chatRequest.setContextMessages(contextMessages);
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
@@ -151,18 +166,145 @@ public class ChatServiceFacade implements IChatService {
|
||||
return sseEmitter;
|
||||
}
|
||||
|
||||
// 4. 路由服务提供商
|
||||
String providerCode = chatModelVo.getProviderCode();
|
||||
TraceRunHandle traceRun = startRagTraceRun(chatRequest, userId);
|
||||
try (TraceScope ignored = openTraceScope(traceRun, userId)) {
|
||||
// 4. 构建上下文消息列表,普通 RAG 检索会在当前 trace 上下文中执行
|
||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
||||
chatRequest.setContextMessages(contextMessages);
|
||||
|
||||
// 5. 路由并发起普通 RAG 对话
|
||||
executeNormalRagChat(chatRequest, userId, tokenValue, contextMessages, traceRun);
|
||||
} catch (RuntimeException | Error e) {
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, e);
|
||||
throw e;
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private void executeNormalRagChat(ChatRequest chatRequest, Long userId, String tokenValue,
|
||||
List<ChatMessage> contextMessages, TraceRunHandle traceRun) {
|
||||
String providerCode = chatRequest.getChatModelVo().getProviderCode();
|
||||
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
||||
|
||||
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
|
||||
TraceStreamSpan llmSpan = startLlmCallSpan(traceRun, chatRequest);
|
||||
try {
|
||||
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue, chatRequest,
|
||||
traceRun, llmSpan);
|
||||
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatRequest.getChatModelVo(), chatRequest);
|
||||
streamingChatModel.chat(contextMessages, handler);
|
||||
} catch (RuntimeException | Error e) {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.finishError(e);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (llmSpan != null) {
|
||||
llmSpan.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 发起对话
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(contextMessages, handler);
|
||||
return emitter;
|
||||
private TraceRunHandle startRagTraceRun(ChatRequest chatRequest, Long userId) {
|
||||
if (!traceProperties.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String traceId = UUID.randomUUID().toString().replace("-", "");
|
||||
long startMillis = System.currentTimeMillis();
|
||||
TraceRun run = new TraceRun();
|
||||
run.setTraceId(traceId);
|
||||
run.setTraceName(RagTraceNodeTypes.TRACE_NAME_RAG_CHAT);
|
||||
run.setBusinessType(RagTraceNodeTypes.BUSINESS_TYPE_RAG_CHAT);
|
||||
run.setBusinessId(chatRequest.getSessionId() == null ? null : chatRequest.getSessionId().toString());
|
||||
run.setUserId(userId);
|
||||
run.setTenantId(safeGetTenantId());
|
||||
run.setStatus(TraceConstants.STATUS_RUNNING);
|
||||
run.setStartTime(new Date(startMillis));
|
||||
run.setMetadata(RagTracePayloadBuilder.chatRequestSummary(chatRequest));
|
||||
|
||||
try {
|
||||
traceRecordService.startRun(run);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 RAG chat trace run 失败,traceId={}", traceId, e);
|
||||
}
|
||||
return new TraceRunHandle(traceId, startMillis, run.getBusinessId(), run.getTenantId());
|
||||
}
|
||||
|
||||
private TraceScope openTraceScope(TraceRunHandle traceRun, Long userId) {
|
||||
if (traceRun == null) {
|
||||
return null;
|
||||
}
|
||||
return TraceContext.begin(traceRun.traceId, RagTraceNodeTypes.BUSINESS_TYPE_RAG_CHAT,
|
||||
traceRun.businessId, userId, traceRun.tenantId);
|
||||
}
|
||||
|
||||
private TraceStreamSpan startLlmCallSpan(TraceRunHandle traceRun, ChatRequest chatRequest) {
|
||||
if (traceRun == null || StringUtils.isBlank(TraceContext.getTraceId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String nodeId = UUID.randomUUID().toString().replace("-", "");
|
||||
long startMillis = System.currentTimeMillis();
|
||||
TraceNode node = new TraceNode();
|
||||
node.setTraceId(traceRun.traceId);
|
||||
node.setNodeId(nodeId);
|
||||
node.setParentNodeId(TraceContext.currentNodeId());
|
||||
node.setDepth(TraceContext.depth());
|
||||
node.setNodeName("llm-call");
|
||||
node.setNodeType(RagTraceNodeTypes.NODE_LLM_CALL);
|
||||
node.setClassName(ChatServiceFacade.class.getName());
|
||||
node.setMethodName("executeNormalRagChat");
|
||||
node.setStatus(TraceConstants.STATUS_RUNNING);
|
||||
node.setStartTime(new Date(startMillis));
|
||||
node.setInputPayload(RagTracePayloadBuilder.streamInputSummary(chatRequest));
|
||||
|
||||
try {
|
||||
traceRecordService.startNode(node);
|
||||
TraceContext.pushNode(nodeId);
|
||||
return new DefaultTraceStreamSpan(traceRecordService, traceProperties, traceRun.traceId, nodeId, startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 LLM trace 节点失败,traceId={}, nodeId={}", traceRun.traceId, nodeId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishTraceRun(TraceRunHandle traceRun, String status, Throwable error) {
|
||||
if (traceRun == null || !traceRun.finished.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
traceRecordService.finishRun(traceRun.traceId, status, TracePayloadUtils.error(error, traceProperties),
|
||||
new Date(), System.currentTimeMillis() - traceRun.startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("结束 RAG chat trace run 失败,traceId={}", traceRun.traceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String safeGetTenantId() {
|
||||
try {
|
||||
return LoginHelper.getTenantId();
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 trace tenantId 失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TraceRunHandle {
|
||||
|
||||
private final String traceId;
|
||||
private final long startMillis;
|
||||
private final String businessId;
|
||||
private final String tenantId;
|
||||
private final AtomicBoolean finished = new AtomicBoolean(false);
|
||||
|
||||
private TraceRunHandle(String traceId, long startMillis, String businessId, String tenantId) {
|
||||
this.traceId = traceId;
|
||||
this.startMillis = startMillis;
|
||||
this.businessId = businessId;
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,6 +651,11 @@ public class ChatServiceFacade implements IChatService {
|
||||
* @return 标准的流式响应处理器
|
||||
*/
|
||||
protected StreamingChatResponseHandler createResponseHandler(Long userId, String tokenValue,ChatRequest chatRequest) {
|
||||
return createResponseHandler(userId, tokenValue, chatRequest, null, null);
|
||||
}
|
||||
|
||||
private StreamingChatResponseHandler createResponseHandler(Long userId, String tokenValue, ChatRequest chatRequest,
|
||||
TraceRunHandle traceRun, TraceStreamSpan streamSpan) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
@@ -540,19 +687,35 @@ public class ChatServiceFacade implements IChatService {
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), fullMessage, RoleType.ASSISTANT.getName(), chatRequest.getModel());
|
||||
}
|
||||
|
||||
if (streamSpan != null) {
|
||||
streamSpan.finishSuccess();
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_SUCCESS, null);
|
||||
|
||||
// 关闭SSE连接
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
log.info("消息结束,已保存到数据库");
|
||||
} catch (Exception e) {
|
||||
log.error("完成响应时出错: {}", e.getMessage(), e);
|
||||
if (streamSpan != null) {
|
||||
streamSpan.finishError(e);
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// 发送错误事件
|
||||
SseMessageUtils.sendError(userId, error.getMessage());
|
||||
log.error("流式响应错误: {}", error.getMessage());
|
||||
try {
|
||||
SseMessageUtils.sendError(userId, error.getMessage());
|
||||
} finally {
|
||||
if (streamSpan != null) {
|
||||
streamSpan.finishError(error);
|
||||
}
|
||||
finishTraceRun(traceRun, TraceConstants.STATUS_ERROR, error);
|
||||
log.error("流式响应错误: {}", error.getMessage(), error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ package org.ruoyi.service.retrieval.impl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.trace.config.TraceProperties;
|
||||
import org.ruoyi.common.trace.constant.TraceConstants;
|
||||
import org.ruoyi.common.trace.core.TraceContext;
|
||||
import org.ruoyi.common.trace.core.TraceNodeTemplate;
|
||||
import org.ruoyi.common.trace.domain.TraceNode;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
import org.ruoyi.domain.bo.rerank.RerankRequest;
|
||||
import org.ruoyi.domain.bo.rerank.RerankResult;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
@@ -13,10 +20,13 @@ import org.ruoyi.mapper.knowledge.KnowledgeFragmentMapper;
|
||||
import org.ruoyi.service.rerank.RerankModelService;
|
||||
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.ruoyi.trace.RagTraceNodeTypes;
|
||||
import org.ruoyi.trace.RagTracePayloadBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -34,6 +44,8 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
private final VectorStoreService vectorStoreService;
|
||||
private final RerankModelFactory rerankModelFactory;
|
||||
private final KnowledgeFragmentMapper fragmentMapper;
|
||||
private final TraceRecordService traceRecordService;
|
||||
private final TraceProperties traceProperties;
|
||||
|
||||
/**
|
||||
* 粗召回默认扩大倍数
|
||||
@@ -53,33 +65,40 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
public List<KnowledgeRetrievalVo> retrieve(QueryVectorBo queryVectorBo) {
|
||||
log.info("开始知识库检索, kid={}, query={}", queryVectorBo.getKid(), queryVectorBo.getQuery());
|
||||
|
||||
// 1. 粗召回阶段 (向量检索 + 关键词搜索)
|
||||
List<KnowledgeRetrievalVo> coarseResults = performCoarseRetrieval(queryVectorBo);
|
||||
log.debug("粗召回返回 {} 条结果", coarseResults.size());
|
||||
return TraceNodeTemplate.withNode(traceRecordService, traceProperties,
|
||||
"retrieval", RagTraceNodeTypes.NODE_RETRIEVAL,
|
||||
KnowledgeRetrievalServiceImpl.class.getName(), "retrieve",
|
||||
RagTracePayloadBuilder.retrievalInputSummary(queryVectorBo),
|
||||
() -> {
|
||||
// 1. 粗召回阶段 (向量检索 + 关键词搜索)
|
||||
List<KnowledgeRetrievalVo> coarseResults = performCoarseRetrieval(queryVectorBo);
|
||||
log.debug("粗召回返回 {} 条结果", coarseResults.size());
|
||||
|
||||
if (coarseResults.isEmpty()) {
|
||||
return coarseResults;
|
||||
}
|
||||
if (coarseResults.isEmpty()) {
|
||||
return coarseResults;
|
||||
}
|
||||
|
||||
// 2. 初始化原始索引
|
||||
for (int i = 0; i < coarseResults.size(); i++) {
|
||||
coarseResults.get(i).setOriginalIndex(i);
|
||||
}
|
||||
// 2. 初始化原始索引
|
||||
for (int i = 0; i < coarseResults.size(); i++) {
|
||||
coarseResults.get(i).setOriginalIndex(i);
|
||||
}
|
||||
|
||||
// 3. 重排序阶段 (可选)
|
||||
List<KnowledgeRetrievalVo> finalResults = coarseResults;
|
||||
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
|
||||
StringUtils.isNotBlank(queryVectorBo.getRerankModelName())) {
|
||||
finalResults = performRerank(queryVectorBo, coarseResults);
|
||||
}
|
||||
// 3. 重排序阶段 (可选)
|
||||
List<KnowledgeRetrievalVo> finalResults = coarseResults;
|
||||
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
|
||||
StringUtils.isNotBlank(queryVectorBo.getRerankModelName())) {
|
||||
finalResults = performRerank(queryVectorBo, coarseResults);
|
||||
}
|
||||
|
||||
// 4. 应用分值阈值过滤 (重排分值或 RRF 分值)
|
||||
double threshold = queryVectorBo.getRerankScoreThreshold() != null ?
|
||||
queryVectorBo.getRerankScoreThreshold() : 0.0;
|
||||
|
||||
return finalResults.stream()
|
||||
.filter(res -> res.getScore() >= threshold)
|
||||
.collect(Collectors.toList());
|
||||
// 4. 应用分值阈值过滤 (重排分值或 RRF 分值)
|
||||
double threshold = queryVectorBo.getRerankScoreThreshold() != null ?
|
||||
queryVectorBo.getRerankScoreThreshold() : 0.0;
|
||||
|
||||
return finalResults.stream()
|
||||
.filter(res -> res.getScore() >= threshold)
|
||||
.collect(Collectors.toList());
|
||||
},
|
||||
RagTracePayloadBuilder::retrievalOutputSummary);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,6 +182,9 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
* 重排序阶段
|
||||
*/
|
||||
private List<KnowledgeRetrievalVo> performRerank(QueryVectorBo queryVectorBo, List<KnowledgeRetrievalVo> coarseResults) {
|
||||
int topN = queryVectorBo.getRerankTopN() != null ? queryVectorBo.getRerankTopN() : queryVectorBo.getMaxResults();
|
||||
TraceNodeHandle traceNode = startTraceNode("rerank", RagTraceNodeTypes.NODE_RERANK, "performRerank",
|
||||
RagTracePayloadBuilder.rerankInputSummary(queryVectorBo, coarseResults.size(), topN));
|
||||
try {
|
||||
RerankModelService rerankModel = rerankModelFactory.createModel(queryVectorBo.getRerankModelName());
|
||||
|
||||
@@ -170,9 +192,6 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
.map(KnowledgeRetrievalVo::getContent)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// topN 默认为 maxResults
|
||||
int topN = queryVectorBo.getRerankTopN() != null ? queryVectorBo.getRerankTopN() : queryVectorBo.getMaxResults();
|
||||
|
||||
RerankRequest rerankRequest = RerankRequest.builder()
|
||||
.query(queryVectorBo.getQuery())
|
||||
.documents(contents)
|
||||
@@ -194,12 +213,18 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
coarseResults.sort((a, b) -> b.getScore().compareTo(a.getScore()));
|
||||
|
||||
// 截断到 topN
|
||||
return coarseResults.subList(0, Math.min(topN, coarseResults.size()));
|
||||
List<KnowledgeRetrievalVo> rerankedResults = coarseResults.subList(0, Math.min(topN, coarseResults.size()));
|
||||
finishTraceNode(traceNode, TraceConstants.STATUS_SUCCESS, null,
|
||||
RagTracePayloadBuilder.rerankOutputSummary(rerankedResults));
|
||||
return rerankedResults;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("重排序流程失败: {}", e.getMessage());
|
||||
int limit = queryVectorBo.getMaxResults() != null ? queryVectorBo.getMaxResults() : 10;
|
||||
return coarseResults.subList(0, Math.min(limit, coarseResults.size()));
|
||||
List<KnowledgeRetrievalVo> fallbackResults = coarseResults.subList(0, Math.min(limit, coarseResults.size()));
|
||||
finishTraceNode(traceNode, TraceConstants.STATUS_ERROR, e,
|
||||
RagTracePayloadBuilder.rerankOutputSummary(fallbackResults));
|
||||
return fallbackResults;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +267,52 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
return fusedResults;
|
||||
}
|
||||
|
||||
private TraceNodeHandle startTraceNode(String nodeName, String nodeType, String methodName, String inputPayload) {
|
||||
if (!traceProperties.isEnabled() || StringUtils.isBlank(TraceContext.getTraceId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String traceId = TraceContext.getTraceId();
|
||||
String nodeId = UUID.randomUUID().toString().replace("-", "");
|
||||
long startMillis = System.currentTimeMillis();
|
||||
TraceNode node = new TraceNode();
|
||||
node.setTraceId(traceId);
|
||||
node.setNodeId(nodeId);
|
||||
node.setParentNodeId(TraceContext.currentNodeId());
|
||||
node.setDepth(TraceContext.depth());
|
||||
node.setNodeName(nodeName);
|
||||
node.setNodeType(nodeType);
|
||||
node.setClassName(KnowledgeRetrievalServiceImpl.class.getName());
|
||||
node.setMethodName(methodName);
|
||||
node.setStatus(TraceConstants.STATUS_RUNNING);
|
||||
node.setStartTime(new Date(startMillis));
|
||||
node.setInputPayload(inputPayload);
|
||||
|
||||
try {
|
||||
traceRecordService.startNode(node);
|
||||
TraceContext.pushNode(nodeId);
|
||||
return new TraceNodeHandle(traceId, nodeId, startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("写入 RAG 检索 trace 节点失败,traceId={}, nodeId={}", traceId, nodeId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishTraceNode(TraceNodeHandle traceNode, String status, Throwable error, String outputPayload) {
|
||||
if (traceNode == null || !traceNode.finished.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
traceRecordService.finishNode(traceNode.traceId, traceNode.nodeId, status,
|
||||
TracePayloadUtils.error(error, traceProperties), outputPayload,
|
||||
new Date(), System.currentTimeMillis() - traceNode.startMillis);
|
||||
} catch (Exception e) {
|
||||
log.warn("结束 RAG 检索 trace 节点失败,traceId={}, nodeId={}", traceNode.traceId, traceNode.nodeId, e);
|
||||
} finally {
|
||||
TraceContext.popNode();
|
||||
}
|
||||
}
|
||||
|
||||
private QueryVectorBo copyOf(QueryVectorBo original, int maxResults) {
|
||||
QueryVectorBo copy = new QueryVectorBo();
|
||||
copy.setQuery(original.getQuery());
|
||||
@@ -253,4 +324,18 @@ public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService
|
||||
copy.setBaseUrl(original.getBaseUrl());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static final class TraceNodeHandle {
|
||||
|
||||
private final String traceId;
|
||||
private final String nodeId;
|
||||
private final long startMillis;
|
||||
private final AtomicBoolean finished = new AtomicBoolean(false);
|
||||
|
||||
private TraceNodeHandle(String traceId, String nodeId, long startMillis) {
|
||||
this.traceId = traceId;
|
||||
this.nodeId = nodeId;
|
||||
this.startMillis = startMillis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.ruoyi.trace;
|
||||
|
||||
/**
|
||||
* RAG trace 业务与节点类型常量。
|
||||
*/
|
||||
public final class RagTraceNodeTypes {
|
||||
|
||||
private RagTraceNodeTypes() {
|
||||
}
|
||||
|
||||
public static final String BUSINESS_TYPE_RAG_CHAT = "RAG_CHAT";
|
||||
public static final String TRACE_NAME_RAG_CHAT = "rag-chat";
|
||||
|
||||
public static final String NODE_RETRIEVAL = "RETRIEVAL";
|
||||
public static final String NODE_RERANK = "RERANK";
|
||||
public static final String NODE_LLM_CALL = "LLM_CALL";
|
||||
public static final String NODE_STREAM = "STREAM";
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package org.ruoyi.trace;
|
||||
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.trace.util.TracePayloadUtils;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG trace payload 摘要构建器。
|
||||
*/
|
||||
public final class RagTracePayloadBuilder {
|
||||
|
||||
private static final int MAX_RESULT_SUMMARY_SIZE = 5;
|
||||
|
||||
private RagTracePayloadBuilder() {
|
||||
}
|
||||
|
||||
public static String chatRequestSummary(ChatRequest request) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("requestPresent", request != null);
|
||||
if (request == null) {
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
ChatModelVo model = request.getChatModelVo();
|
||||
payload.put("sessionId", request.getSessionId() == null ? null : request.getSessionId().toString());
|
||||
payload.put("model", request.getModel());
|
||||
payload.put("providerCode", model == null ? null : model.getProviderCode());
|
||||
payload.put("knowledgeId", request.getKnowledgeId());
|
||||
payload.put("hasKnowledge", request.getKnowledgeId() != null);
|
||||
payload.put("contentLength", length(request.getContent()));
|
||||
payload.put("contextMessageCount", request.getContextMessages() == null ? null : request.getContextMessages().size());
|
||||
payload.put("enableWorkFlow", request.getEnableWorkFlow());
|
||||
payload.put("isResume", request.getIsResume());
|
||||
payload.put("enableThinking", request.getEnableThinking());
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String retrievalInputSummary(QueryVectorBo query) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("queryPresent", query != null);
|
||||
if (query == null) {
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
payload.put("kid", query.getKid());
|
||||
payload.put("queryLength", length(query.getQuery()));
|
||||
payload.put("maxResults", query.getMaxResults());
|
||||
payload.put("vectorModelName", query.getVectorModelName());
|
||||
payload.put("embeddingModelName", query.getEmbeddingModelName());
|
||||
payload.put("enableHybrid", query.getEnableHybrid());
|
||||
payload.put("hybridAlpha", query.getHybridAlpha());
|
||||
payload.put("similarityThreshold", query.getSimilarityThreshold());
|
||||
payload.put("enableRerank", query.getEnableRerank());
|
||||
payload.put("rerankModel", query.getRerankModelName());
|
||||
payload.put("rerankTopN", query.getRerankTopN());
|
||||
payload.put("rerankScoreThreshold", query.getRerankScoreThreshold());
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String retrievalOutputSummary(List<KnowledgeRetrievalVo> results) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("resultCount", results == null ? 0 : results.size());
|
||||
payload.put("results", summarizeResults(results));
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String rerankInputSummary(QueryVectorBo query, int candidateCount, Integer topN) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("candidateCount", candidateCount);
|
||||
payload.put("rerankModel", query == null ? null : query.getRerankModelName());
|
||||
payload.put("topN", topN);
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String rerankOutputSummary(List<KnowledgeRetrievalVo> results) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("resultCount", results == null ? 0 : results.size());
|
||||
payload.put("results", summarizeResults(results));
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String streamInputSummary(ChatRequest request) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("sessionId", request == null || request.getSessionId() == null ? null : request.getSessionId().toString());
|
||||
payload.put("model", request == null ? null : request.getModel());
|
||||
payload.put("contextMessageCount", request == null || request.getContextMessages() == null ? null : request.getContextMessages().size());
|
||||
payload.put("contentLength", request == null ? null : length(request.getContent()));
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
public static String streamOutputSummary(int responseLength) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("responseLength", responseLength);
|
||||
return TracePayloadUtils.toJson(payload);
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> summarizeResults(List<KnowledgeRetrievalVo> results) {
|
||||
List<Map<String, Object>> summaries = new ArrayList<>();
|
||||
if (results == null || results.isEmpty()) {
|
||||
return summaries;
|
||||
}
|
||||
|
||||
int limit = Math.min(MAX_RESULT_SUMMARY_SIZE, results.size());
|
||||
for (int i = 0; i < limit; i++) {
|
||||
KnowledgeRetrievalVo result = results.get(i);
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("rank", i + 1);
|
||||
item.put("id", result == null ? null : result.getId());
|
||||
item.put("docId", result == null ? null : result.getDocId());
|
||||
item.put("knowledgeId", result == null || result.getKnowledgeId() == null ? null : result.getKnowledgeId().toString());
|
||||
item.put("idx", result == null ? null : result.getIdx());
|
||||
item.put("score", result == null ? null : result.getScore());
|
||||
item.put("rawScore", result == null ? null : result.getRawScore());
|
||||
item.put("originalIndex", result == null ? null : result.getOriginalIndex());
|
||||
item.put("sourceName", result == null ? null : result.getSourceName());
|
||||
item.put("contentLength", result == null ? null : length(result.getContent()));
|
||||
summaries.add(item);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private static Integer length(String value) {
|
||||
return value == null ? null : value.length();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.ruoyi.trace;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RagTracePayloadBuilderTest {
|
||||
|
||||
@Test
|
||||
void chatRequestSummaryShouldHandleNullsAndAvoidPromptBody() {
|
||||
ChatRequest request = new ChatRequest();
|
||||
request.setSessionId(100L);
|
||||
request.setModel("qwen-plus");
|
||||
request.setKnowledgeId("200");
|
||||
request.setContent("secret prompt body");
|
||||
|
||||
ChatModelVo model = new ChatModelVo();
|
||||
model.setProviderCode("dashscope");
|
||||
request.setChatModelVo(model);
|
||||
|
||||
String payload = RagTracePayloadBuilder.chatRequestSummary(request);
|
||||
|
||||
assertTrue(payload.contains("\"sessionId\":100"));
|
||||
assertTrue(payload.contains("\"contentLength\":18"));
|
||||
assertTrue(payload.contains("\"providerCode\":\"dashscope\""));
|
||||
assertFalse(payload.contains("secret prompt body"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retrievalInputSummaryShouldUseSummaryOnly() {
|
||||
QueryVectorBo query = new QueryVectorBo();
|
||||
query.setKid("200");
|
||||
query.setQuery("private retrieval query");
|
||||
query.setMaxResults(5);
|
||||
query.setEnableRerank(true);
|
||||
query.setRerankModelName("gte-rerank");
|
||||
query.setRerankTopN(null);
|
||||
|
||||
String payload = RagTracePayloadBuilder.retrievalInputSummary(query);
|
||||
|
||||
assertTrue(payload.contains("\"kid\":\"200\""));
|
||||
assertTrue(payload.contains("\"queryLength\":23"));
|
||||
assertTrue(payload.contains("\"enableRerank\":true"));
|
||||
assertFalse(payload.contains("private retrieval query"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retrievalOutputSummaryShouldAvoidFragmentContent() {
|
||||
KnowledgeRetrievalVo result = new KnowledgeRetrievalVo();
|
||||
result.setId("fragment-1");
|
||||
result.setDocId("doc-1");
|
||||
result.setIdx(1);
|
||||
result.setScore(0.85);
|
||||
result.setContent("sensitive knowledge fragment");
|
||||
|
||||
String payload = RagTracePayloadBuilder.retrievalOutputSummary(List.of(result));
|
||||
|
||||
assertTrue(payload.contains("\"resultCount\":1"));
|
||||
assertTrue(payload.contains("\"contentLength\":28"));
|
||||
assertTrue(payload.contains("\"fragment-1\""));
|
||||
assertFalse(payload.contains("sensitive knowledge fragment"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void summariesShouldAcceptNullValuesWithoutMapOfNpe() {
|
||||
assertTrue(RagTracePayloadBuilder.chatRequestSummary(null).contains("\"requestPresent\":false"));
|
||||
assertTrue(RagTracePayloadBuilder.retrievalInputSummary(null).contains("\"queryPresent\":false"));
|
||||
assertTrue(RagTracePayloadBuilder.retrievalOutputSummary(null).contains("\"resultCount\":0"));
|
||||
assertTrue(RagTracePayloadBuilder.rerankInputSummary(null, 0, null).contains("\"candidateCount\":0"));
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,11 @@
|
||||
<artifactId>ruoyi-common-log</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-trace</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- excel-->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.system.controller.monitor;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.trace.domain.bo.TraceRunBo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceDetailVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceNodeVo;
|
||||
import org.ruoyi.common.trace.domain.vo.TraceRunVo;
|
||||
import org.ruoyi.common.trace.service.TraceRecordService;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 链路追踪监控
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/monitor/trace")
|
||||
public class TraceController extends BaseController {
|
||||
|
||||
private final TraceRecordService traceRecordService;
|
||||
|
||||
/**
|
||||
* 获取链路追踪运行列表
|
||||
*/
|
||||
@SaCheckPermission("monitor:trace:list")
|
||||
@GetMapping("/run/list")
|
||||
public TableDataInfo<TraceRunVo> list(TraceRunBo bo, PageQuery pageQuery) {
|
||||
return traceRecordService.pageRuns(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取链路追踪运行详情
|
||||
*/
|
||||
@SaCheckPermission("monitor:trace:query")
|
||||
@GetMapping("/run/{traceId}")
|
||||
public R<TraceRunVo> run(@PathVariable String traceId) {
|
||||
return R.ok(traceRecordService.getRun(traceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取链路追踪节点列表
|
||||
*/
|
||||
@SaCheckPermission("monitor:trace:query")
|
||||
@GetMapping("/node/list/{traceId}")
|
||||
public R<List<TraceNodeVo>> nodes(@PathVariable String traceId) {
|
||||
return R.ok(traceRecordService.listNodes(traceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取链路追踪完整详情
|
||||
*/
|
||||
@SaCheckPermission("monitor:trace:query")
|
||||
@GetMapping("/detail/{traceId}")
|
||||
public R<TraceDetailVo> detail(@PathVariable String traceId) {
|
||||
return R.ok(traceRecordService.getDetail(traceId));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user