feat(feedback): 置信度评分与用户反馈机制 & 归档 confidence-feedback change
This commit is contained in:
@@ -87,15 +87,16 @@ public class ChatController {
|
||||
|
||||
// 根据问题复杂度自动选择单 Agent 或多 Agent
|
||||
logger.info("开始 ReactAgent 对话(支持自动工具调用)");
|
||||
String fullAnswer = chatService.executeChatWithStrategy(chatModel, toolCallbacks,
|
||||
ChatService.ChatResult result = chatService.executeChatWithStrategy(chatModel, toolCallbacks,
|
||||
request.getQuestion(), history);
|
||||
|
||||
String fullAnswer = result.answer();
|
||||
|
||||
// 更新会话历史
|
||||
session.addMessage(request.getQuestion(), fullAnswer);
|
||||
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
|
||||
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
|
||||
request.getId(), session.getMessagePairCount());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(ChatResponse.success(fullAnswer)));
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(ChatResponse.success(fullAnswer, result.sessionId())));
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("对话失败", e);
|
||||
@@ -537,11 +538,13 @@ public class ChatController {
|
||||
private boolean success;
|
||||
private String answer;
|
||||
private String errorMessage;
|
||||
private String sessionId;
|
||||
|
||||
public static ChatResponse success(String answer) {
|
||||
public static ChatResponse success(String answer, String sessionId) {
|
||||
ChatResponse response = new ChatResponse();
|
||||
response.setSuccess(true);
|
||||
response.setAnswer(answer);
|
||||
response.setSessionId(sessionId);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ public class DiagnosisSession {
|
||||
@Column(name = "tool_call_count")
|
||||
private Integer toolCallCount;
|
||||
|
||||
@Column(name = "answer", columnDefinition = "LONGTEXT")
|
||||
private String answer;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "self_evaluation", columnDefinition = "JSON")
|
||||
private String selfEvaluation;
|
||||
|
||||
@@ -46,6 +46,9 @@ public class ChatService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
|
||||
|
||||
/** 封装 answer + 后端生成的 sessionId,用于 feedback 关联 */
|
||||
public record ChatResult(String answer, String sessionId) {}
|
||||
|
||||
@Autowired
|
||||
private InternalDocsTools internalDocsTools;
|
||||
|
||||
@@ -73,6 +76,9 @@ public class ChatService {
|
||||
@Autowired
|
||||
private AgentStepRepository agentStepRepository;
|
||||
|
||||
@Autowired
|
||||
private EvaluationService evaluationService;
|
||||
|
||||
/** 多 Agent Chat 的 Prompt */
|
||||
private String chatPlannerPrompt;
|
||||
private String chatExecutorPrompt;
|
||||
@@ -233,9 +239,9 @@ public class ChatService {
|
||||
* 执行 ReactAgent 对话(非流式)
|
||||
* @param agent ReactAgent 实例
|
||||
* @param question 用户问题
|
||||
* @return AI 回复
|
||||
* @return ChatResult(answer + sessionId)
|
||||
*/
|
||||
public String executeChat(ReactAgent agent, String question) throws GraphRunnerException {
|
||||
public ChatResult executeChat(ReactAgent agent, String question) throws GraphRunnerException {
|
||||
logger.info("========================================");
|
||||
logger.info("📝 用户问题: {}", question);
|
||||
|
||||
@@ -267,15 +273,18 @@ public class ChatService {
|
||||
|
||||
// 更新诊断会话
|
||||
session.setStatus("SUCCESS");
|
||||
session.setAnswer(answer);
|
||||
session.setTotalDurationMs((int) duration);
|
||||
backfillSessionMetrics(session);
|
||||
diagnosisSessionRepository.save(session);
|
||||
|
||||
evaluationService.evaluate(sessionId, answer);
|
||||
|
||||
logger.info("⏱️ 总耗时: {} ms", duration);
|
||||
logger.info("📏 输出长度: {} 字符", answer.length());
|
||||
logger.info("========================================");
|
||||
|
||||
return answer;
|
||||
return new ChatResult(answer, sessionId);
|
||||
} catch (Exception e) {
|
||||
session.setStatus("FAILED");
|
||||
diagnosisSessionRepository.save(session);
|
||||
@@ -291,9 +300,9 @@ public class ChatService {
|
||||
* @param toolCallbacks 工具回调
|
||||
* @param question 用户问题
|
||||
* @param history 历史消息
|
||||
* @return AI 回复
|
||||
* @return ChatResult(answer + sessionId)
|
||||
*/
|
||||
public String executeChatWithStrategy(ChatModel chatModel, ToolCallback[] toolCallbacks,
|
||||
public ChatResult executeChatWithStrategy(ChatModel chatModel, ToolCallback[] toolCallbacks,
|
||||
String question, List<Map<String, String>> history) throws GraphRunnerException {
|
||||
if (QuestionComplexity.isComplex(question)) {
|
||||
logger.info("📊 问题判定为复杂,使用多 Agent(Planner + Executor)执行");
|
||||
@@ -309,7 +318,7 @@ public class ChatService {
|
||||
/**
|
||||
* 多 Agent 复杂对话执行(Planner + Executor + Supervisor)
|
||||
*/
|
||||
public String executeChatComplex(ChatModel chatModel, ToolCallback[] toolCallbacks,
|
||||
public ChatResult executeChatComplex(ChatModel chatModel, ToolCallback[] toolCallbacks,
|
||||
String question, List<Map<String, String>> history) throws GraphRunnerException {
|
||||
String sessionId = UUID.randomUUID().toString().substring(0, 8);
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -356,20 +365,23 @@ public class ChatService {
|
||||
}
|
||||
|
||||
session.setStatus("SUCCESS");
|
||||
session.setAnswer(answer);
|
||||
session.setTotalDurationMs((int) duration);
|
||||
backfillSessionMetrics(session);
|
||||
diagnosisSessionRepository.save(session);
|
||||
|
||||
evaluationService.evaluate(sessionId, answer);
|
||||
|
||||
logger.info("⏱️ 多 Agent 总耗时: {} ms", duration);
|
||||
logger.info("📏 输出长度: {} 字符", answer.length());
|
||||
|
||||
return answer;
|
||||
return new ChatResult(answer, sessionId);
|
||||
|
||||
} catch (Exception e) {
|
||||
session.setStatus("FAILED");
|
||||
diagnosisSessionRepository.save(session);
|
||||
logger.error("多 Agent 执行失败", e);
|
||||
return "执行失败: " + e.getMessage();
|
||||
return new ChatResult("执行失败: " + e.getMessage(), sessionId);
|
||||
} finally {
|
||||
SessionContextHolder.clear();
|
||||
}
|
||||
|
||||
@@ -632,8 +632,12 @@ class SuperBizAgentApp {
|
||||
if (data.code === 200 || data.message === 'success') {
|
||||
// data.data 是 ChatResponse 对象
|
||||
const chatResponse = data.data;
|
||||
|
||||
|
||||
if (chatResponse && chatResponse.success) {
|
||||
// 保存后端返回的 sessionId,用于 feedback 提交
|
||||
if (chatResponse.sessionId) {
|
||||
this.lastSessionId = chatResponse.sessionId;
|
||||
}
|
||||
// 成功:添加实际响应消息(即使 answer 为空也显示)
|
||||
const answer = chatResponse.answer || '(无回复内容)';
|
||||
this.addMessage('assistant', answer);
|
||||
@@ -848,24 +852,78 @@ class SuperBizAgentApp {
|
||||
}
|
||||
|
||||
messageContentWrapper.appendChild(messageContent);
|
||||
|
||||
// assistant 消息末尾加反馈栏(流式消息完成后由 handleStreamComplete 添加)
|
||||
if (type === 'assistant' && !isStreaming) {
|
||||
messageContentWrapper.appendChild(this.createFeedbackBar(this.lastSessionId));
|
||||
}
|
||||
|
||||
messageDiv.appendChild(messageContentWrapper);
|
||||
|
||||
if (this.chatMessages) {
|
||||
this.chatMessages.appendChild(messageDiv);
|
||||
|
||||
|
||||
// 如果是第一条消息,移除居中样式并添加动画
|
||||
if (isFirstMessage && this.chatContainer) {
|
||||
this.chatContainer.classList.remove('centered');
|
||||
// 添加动画类
|
||||
this.chatContainer.style.transition = 'all 0.5s ease';
|
||||
}
|
||||
|
||||
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
// 创建反馈栏,sessionId 闭包绑定,避免多轮对话时错位
|
||||
createFeedbackBar(sessionId) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'feedback-bar';
|
||||
|
||||
const usefulBtn = document.createElement('button');
|
||||
usefulBtn.className = 'feedback-btn';
|
||||
usefulBtn.title = '有用';
|
||||
usefulBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||
|
||||
const notUsefulBtn = document.createElement('button');
|
||||
notUsefulBtn.className = 'feedback-btn';
|
||||
notUsefulBtn.title = '无用';
|
||||
notUsefulBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||
|
||||
usefulBtn.addEventListener('click', () => this.submitFeedback('useful', bar, sessionId));
|
||||
notUsefulBtn.addEventListener('click', () => this.submitFeedback('not_useful', bar, sessionId));
|
||||
|
||||
bar.appendChild(usefulBtn);
|
||||
bar.appendChild(notUsefulBtn);
|
||||
return bar;
|
||||
}
|
||||
|
||||
// 提交反馈
|
||||
async submitFeedback(feedback, barElement, sessionId) {
|
||||
if (!sessionId) return;
|
||||
|
||||
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId, feedback })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
barElement.innerHTML = `<span class="feedback-done">${feedback === 'useful' ? '已标记为有用' : '已标记为无用'}</span>`;
|
||||
} else {
|
||||
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = false);
|
||||
this.showNotification('反馈提交失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = false);
|
||||
this.showNotification('反馈提交失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加带加载动画的消息
|
||||
addLoadingMessage(content) {
|
||||
const messageDiv = document.createElement('div');
|
||||
@@ -946,12 +1004,17 @@ class SuperBizAgentApp {
|
||||
handleStreamComplete(assistantMessageElement, fullResponse) {
|
||||
if (assistantMessageElement) {
|
||||
assistantMessageElement.classList.remove('streaming');
|
||||
const messageContentWrapper = assistantMessageElement.querySelector('.message-content-wrapper');
|
||||
const messageContent = assistantMessageElement.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.innerHTML = this.renderMarkdown(fullResponse);
|
||||
// 高亮代码块
|
||||
this.highlightCodeBlocks(messageContent);
|
||||
}
|
||||
// 流式完成后追加反馈栏
|
||||
if (messageContentWrapper && !messageContentWrapper.querySelector('.feedback-bar')) {
|
||||
messageContentWrapper.appendChild(this.createFeedbackBar(this.lastSessionId));
|
||||
}
|
||||
}
|
||||
// 保存流式消息到历史记录
|
||||
if (fullResponse) {
|
||||
|
||||
@@ -1157,9 +1157,58 @@ body {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.mode-dropdown {
|
||||
width: 100%;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 反馈栏 */
|
||||
.feedback-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.feedback-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #e8eaed;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #9aa0a6;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feedback-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.feedback-btn:hover {
|
||||
border-color: #1a73e8;
|
||||
color: #1a73e8;
|
||||
background: #e8f0fe;
|
||||
}
|
||||
|
||||
.feedback-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.feedback-done {
|
||||
font-size: 0.75rem;
|
||||
color: #34a853;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user