feat(feedback): 置信度评分与用户反馈机制 & 归档 confidence-feedback change

This commit is contained in:
zhuyongxin
2026-06-30 18:01:06 +08:00
parent 3ffa5cc366
commit 2a796da490
16 changed files with 771 additions and 18 deletions
@@ -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();
}
+66 -3
View File
@@ -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) {
+50 -1
View File
@@ -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;
}