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:
Obltv
2026-06-16 09:24:36 +08:00
parent 7aa67cfc2f
commit e925050207
40 changed files with 2618 additions and 41 deletions

View File

@@ -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>

View File

@@ -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);
}
}
};
}

View File

@@ -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;
}
}
}

View File

@@ -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";
}

View File

@@ -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();
}
}

View File

@@ -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"));
}
}

View File

@@ -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>

View File

@@ -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));
}
}