feat(feedback): 置信度评分与用户反馈机制 & 归档 confidence-feedback change
This commit is contained in:
@@ -9,3 +9,4 @@
|
||||
| 2026-06-24 | lookup-knowledge-integration | 知识库检索 | L0精确匹配, L1语义检索, frontmatter, 混合检索 | archived |
|
||||
| 2026-06-25 | doc-management-ui | 前端开发/文档管理 | 文档管理页面, CRUD, 状态监控, 纯静态页面, API集成 | archived |
|
||||
| 2026-06-26 | session-storage | 会话存储/可观测 | diagnosis_session, agent_step, tool_invocation, token追踪, 多Agent路由 | openspec/changes/session-storage | archived |
|
||||
| 2026-06-29 | confidence-feedback | 质量评估/反馈机制 | evidence_score, selfEvaluation, feedback, useful, not_useful, case_library, BAD_CASE, tool_invocation规则引擎, 反馈按钮, sessionId回传 | openspec/changes/confidence-feedback | archived |
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# acceptance.md — confidence-feedback
|
||||
|
||||
## 实现清单
|
||||
|
||||
| 任务 | 文件 | 状态 |
|
||||
|---|---|---|
|
||||
| T0:Flyway V008 + answer 字段 | `V008__add_answer_to_diagnosis_session.sql`、`DiagnosisSession.java` | 完成 |
|
||||
| T1:EvaluationService(规则引擎) | `EvaluationService.java` | 完成 |
|
||||
| T2:ChatService 后置调用 | `ChatService.java` | 完成 |
|
||||
| T3:FeedbackController + FeedbackService | `FeedbackController.java`、`FeedbackService.java`、`FeedbackRequest.java`、`FeedbackResponse.java` | 完成 |
|
||||
| T4:CaseLibraryService | `CaseLibraryService.java` | 完成 |
|
||||
| T5:AsyncConfig | `AsyncConfig.java` | 完成 |
|
||||
|
||||
## 验证记录
|
||||
|
||||
### 静态验证(已通过)
|
||||
|
||||
- `mvn compile` BUILD SUCCESS(2026-06-30)
|
||||
- 无新增 ERROR,存量 WARNING 与本次改动无关
|
||||
- import 完整性人工检查通过
|
||||
|
||||
### 脚本验证(已通过,2026-06-30)
|
||||
|
||||
验证工具:`scripts/query_mysql.py`(本次新建)
|
||||
|
||||
| 步骤 | 操作 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | POST /api/chat 发送问题 | 200,answer 有值 |
|
||||
| 2 | 等 5 秒查 diagnosis_session | self_evaluation 写入规则引擎结果,answer 写入完整回答 |
|
||||
| 3 | POST /api/feedback useful | 200,返回 caseId;case_library 新增一行,feedback=useful,status=SUCCESS |
|
||||
| 4 | POST /api/feedback not_useful | 200,feedback=not_useful,status 仍为 SUCCESS(未被改写) |
|
||||
| 5(边界)| 重复提交 useful | 返回同一 caseId,case_library 无重复插入 |
|
||||
| 6(边界)| 非法 feedback 值 | HTTP 400 |
|
||||
|
||||
### Flyway V008 迁移
|
||||
|
||||
- 服务启动后 diagnosis_session 表存在 answer 列,验证通过(步骤 2 能写入 answer)
|
||||
|
||||
### 浏览器/人工验证(已通过,2026-06-30)
|
||||
|
||||
| 步骤 | 操作 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 发送"今天天气怎么样" | AI 回复下方出现"有用/无用"按钮 |
|
||||
| 2 | 点击"有用" | 按钮区域替换为"已标记为有用" |
|
||||
| 3 | 网络请求确认 | POST /api/feedback 返回 HTTP 200,`success: true` |
|
||||
|
||||
### 前端反馈按钮(追加,2026-06-30)
|
||||
|
||||
**改动文件**:`app.js`、`styles.css`
|
||||
|
||||
关键设计:
|
||||
- `ChatResult` record 新增(`ChatService`),`ChatResponse` 增加 `sessionId` 字段(`ChatController`)
|
||||
- `sendQuickMessage` 读取 `chatResponse.sessionId` 存为 `this.lastSessionId`
|
||||
- `createFeedbackBar(sessionId)` 闭包绑定 sessionId,避免多轮对话时 sessionId 错位
|
||||
- `submitFeedback(feedback, barElement, sessionId)` 直接用传入参数,不依赖全局状态
|
||||
- 流式模式(`/api/chat_stream`)反馈按钮会渲染,但 sessionId 为空,点击不生效(已知限制)
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 非检索工具(DateTimeTools 等)不写 tool_invocation,evidence_score = 0(已接受,符合"证据充分度"定义)
|
||||
- `@Async` 失败时 selfEvaluation 为 null,前端需处理 null(已接受)
|
||||
- CaseLibrary 的 faultCategory 固定为 GENERAL,需人工补充(已接受,Phase 2 优化)
|
||||
- LLM 观点层未实现,selfEvaluation JSON 预留 llm_opinion 扩展位(Phase 2)
|
||||
- 流式模式反馈按钮 sessionId 缺失,暂不处理(已知,后续处理流式接口时一并解决)
|
||||
@@ -0,0 +1,37 @@
|
||||
# brief.md — confidence-feedback
|
||||
|
||||
## 背景
|
||||
|
||||
DiagnosisSession 已预留 `selfEvaluation`(JSON)和 `feedback`(VARCHAR 16)两个字段,但完全为空。Agent 完成对话后不计算证据评分,也没有接收用户反馈的 API,无法支撑报告质量评估和 BadCase 追踪。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 给每次对话结果自动打一个基于事实的证据充分度评分(evidence_score)
|
||||
2. 提供用户反馈 API(useful/not_useful),useful 触发案例自动沉淀,not_useful 标记 BadCase
|
||||
|
||||
## 范围
|
||||
|
||||
- `DiagnosisSession` 加 `answer` 字段(Flyway V008)
|
||||
- `EvaluationService`:基于 tool_invocation 的规则引擎,@Async 写 selfEvaluation
|
||||
- `FeedbackController` + `FeedbackService`:POST /api/feedback
|
||||
- `CaseLibraryService.createFromSession`:幂等案例沉淀
|
||||
- `AsyncConfig`:@EnableAsync
|
||||
- `ChatService`:SUCCESS 分支写 answer + 触发 evaluate;新增 `ChatResult` record 回传 sessionId
|
||||
- `ChatController.ChatResponse` 增加 `sessionId` 字段
|
||||
- 前端 `app.js`:AI 回复下方反馈按钮,点击调用 `/api/feedback`,闭包绑定 sessionId
|
||||
- 前端 `styles.css`:反馈栏样式
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不实现 Verifier Agent 完整链路
|
||||
- 不实现 LLM 自评(预留扩展位,Phase 2 再做)
|
||||
- 不实现案例结构化字段自动填充(faultCategory 等暂时填 GENERAL)
|
||||
- 不实现 BadCase 自动分析或 Prompt 优化
|
||||
|
||||
## 分档
|
||||
|
||||
standard
|
||||
|
||||
## 关联 OpenSpec
|
||||
|
||||
`openspec/changes/confidence-feedback/`
|
||||
@@ -0,0 +1,115 @@
|
||||
# decisions.md — confidence-feedback
|
||||
|
||||
## Question Pool(grill 阶段)
|
||||
|
||||
| # | 问题 | 模式 | 状态 |
|
||||
|---|---|---|---|
|
||||
| Q1 | 置信度由谁计算 | user-interview | 已确认 |
|
||||
| Q2 | 反馈触发哪些后端操作 | user-interview | 已确认 |
|
||||
| Q3 | CaseLibrary 结构化字段从哪里填 | evidence-driven | 已确认(方案变更) |
|
||||
| Q4 | 验收口径 | user-interview | 已确认 |
|
||||
|
||||
---
|
||||
|
||||
## Evidence-Driven 结论
|
||||
|
||||
### Q3:CaseLibrary 内容来源
|
||||
|
||||
**初始结论**:从 `agent_step.thought` 提取(grill 阶段)
|
||||
|
||||
**修正(apply 阶段讨论后)**:
|
||||
- 代码证据:`agent_step.thought` 截断为 2000 字符,`modelOutput` 截断为 500 字符,均不是完整答案
|
||||
- `ChatService.executeChat` 第 269 行已有完整答案 `answer = response.getText()`,但未持久化
|
||||
- 决策:给 `DiagnosisSession` 加 `answer TEXT` 字段,Flyway V008 迁移,案例内容直接从 `session.answer` 取
|
||||
|
||||
---
|
||||
|
||||
## User-Interview 确认记录
|
||||
|
||||
### Q1 — 置信度由谁评估
|
||||
- 用户原话(grill):"两者都要:规则兜底 + Verifier 主打分"
|
||||
- **apply 后修正**:讨论后决定去掉 LLM 自评,仅用规则引擎(见"apply 阶段决策")
|
||||
- 最终实现:`EvaluationService` 纯规则,预留 `llm_opinion` 扩展位
|
||||
|
||||
### Q2 — 反馈触发操作
|
||||
- 用户原话:"写入 DiagnosisSession.feedback 字段, not_useful → 打 BAD_CASE 标记"
|
||||
- **apply 后修正**:BAD_CASE 不改 status,feedback 字段本身即为标记(见"apply 阶段决策")
|
||||
- 最终实现:`FeedbackService` 只写 feedback + 可选写 case_library,不改 status
|
||||
|
||||
### Q4 — 验收口径
|
||||
- 用户原话:"端到端可验证:发一次 chat → 查 DB 看 selfEvaluation 有值 → 提交 feedback → 查 DB 看 feedback + case_library"
|
||||
- 确认状态:已确认,未变化
|
||||
|
||||
---
|
||||
|
||||
## Apply 阶段决策(post-grill 重要变更)
|
||||
|
||||
### 决策 A:DiagnosisSession 加 answer 字段
|
||||
|
||||
- **问题**:案例沉淀需要完整答案,agent_step.thought 被截断,不可用
|
||||
- **决策**:新增 `answer LONGTEXT` 字段,ChatService SUCCESS 分支写入
|
||||
- **影响**:V008 Flyway 迁移,CaseLibraryService 直接读 session.answer
|
||||
|
||||
### 决策 B:去掉 LLM 自评,只用规则引擎
|
||||
|
||||
- **问题**:LLM 评估自己的答案系统性偏高分;多一次调用消耗 token;Verifier Agent 当前未实现
|
||||
- **决策**:MVP 阶段仅用基于 tool_invocation 的规则引擎
|
||||
- **理由**:规则可解释、可复现、不撒谎;Verifier 留待诊断全链路实现时再做
|
||||
- **预留**:`selfEvaluation` JSON 结构保留 `llm_opinion` 扩展位,代码底部注释说明接入点
|
||||
|
||||
### 决策 C:BAD_CASE 不改 status 字段
|
||||
|
||||
- **问题**:status 是执行状态语义(RUNNING/SUCCESS/FAILED),BAD_CASE 是质量标签,两个维度不同;覆盖 status 会破坏统计
|
||||
- **决策**:`not_useful` 通过 `feedback` 字段本身标识,查 BadCase 用 `WHERE feedback = 'not_useful'`
|
||||
|
||||
### 决策 D:评分字段重命名为 evidence_score
|
||||
|
||||
- **问题**:原名 confidence 容易误解为"答案准确性",实际衡量的是"证据收集充分度"
|
||||
- **决策**:重命名为 `evidence_score`,明确语义边界
|
||||
- **边界说明**:工具调用能证明 Agent 有尝试收集证据,但无法证明答案无幻觉;这个分数过滤最差情况(无工具调用就给答案),不能识别"调用了工具但结论仍错误"
|
||||
|
||||
### 决策 E:规则输入来源仅限 tool_invocation 事实
|
||||
|
||||
- **问题**:DateTimeTools、QueryMetricsTools 等非检索工具调用未写入 tool_invocation
|
||||
- **接受**:evidence_score 定义本来就是检索证据充分度,非检索工具排除在外是合理的,不是 bug
|
||||
- **已知限制**:调用了时间工具但 evidence_score = 0 的 session 存在
|
||||
|
||||
---
|
||||
|
||||
## 架构审计记录
|
||||
|
||||
- 接口影响:`POST /api/feedback` 是新接口(L2);ChatService 主流程返回值不变(L1)
|
||||
- 时序验证:tool_invocation 在工具执行时同步写入,evaluate @Async 在 Agent 完成后触发,无竞态问题
|
||||
- 已接受风险:
|
||||
- `@Async` 失败时 selfEvaluation 保持 null,前端需处理 null
|
||||
- 案例结构化字段(faultCategory 等)暂时填 GENERAL,后续可人工补充
|
||||
- LLM 自评预留但未实现,Phase 2 再迭代
|
||||
|
||||
### 决策 F:ChatResult record + ChatResponse.sessionId 回传
|
||||
|
||||
- **问题**:`ChatService` 内部生成 8 位 sessionId,但从不返回给前端;前端用自己的 sessionId 调 feedback 接口,后端查不到 session(400)
|
||||
- **决策**:新增 `ChatResult(answer, sessionId)` record,`executeChatWithStrategy` 链路全部返回 `ChatResult`;`ChatResponse` 增加 `sessionId` 字段;前端读取并闭包绑定至对应消息的反馈按钮
|
||||
- **影响**:`ChatService` 三个方法签名变更(内部链路),`ChatController` 调用方更新,前端 `app.js` 读取新字段
|
||||
|
||||
### 决策 G:反馈 sessionId 闭包绑定而非全局变量
|
||||
|
||||
- **问题**:最初实现用 `this.lastSessionId` 全局变量,多轮对话时点击早期消息的反馈按钮会提交最新 sessionId
|
||||
- **决策**:`createFeedbackBar(sessionId)` 接收 sessionId 参数,`submitFeedback(feedback, bar, sessionId)` 直接用传入值,不读全局状态
|
||||
- **效果**:每条 AI 回复绑定自己那轮的 sessionId,多轮对话下行为正确
|
||||
|
||||
### 项目技术栈清单
|
||||
|
||||
- ChatModel 注入:`@Autowired ChatModel chatModel`,通过 `ModelRoutingConfig` 路由
|
||||
- Repository:Spring Data JPA,`Optional<T>` 返回,方法命名约定
|
||||
- DTO:独立文件放 `dto/` 包
|
||||
- 异步:新建 `AsyncConfig.java` 加 `@EnableAsync`(项目原无此配置)
|
||||
- 无 MQ,无加密,工具类直接用 UUID.randomUUID()
|
||||
- 日志:SLF4J Logger,`LoggerFactory.getLogger()`
|
||||
- `ToolInvocationRepository.findBySessionId` 已有,可直接用
|
||||
|
||||
### 参考实现文件
|
||||
|
||||
- `ChatService.java`:executeChat/executeChatComplex 流程
|
||||
- `CaseLibraryRepository.findByDiagnosisId`:幂等检查用
|
||||
- `DiagnosisSessionRepository.findBySessionId`
|
||||
- `ToolInvocationRepository.findBySessionId`
|
||||
@@ -0,0 +1,52 @@
|
||||
# evidence.md — confidence-feedback
|
||||
|
||||
## 代码证据
|
||||
|
||||
### agent_step.thought 不可作为案例内容
|
||||
|
||||
- 文件:`AgentLoggingHook.java:135`
|
||||
- 证据:`thought` 在写入前截断为 2000 字符,`modelOutput` 截断为 500 字符
|
||||
- 结论:两者均不是返回给用户的完整答案,案例质量低
|
||||
|
||||
### ChatService 已有完整答案未持久化
|
||||
|
||||
- 文件:`ChatService.java:269`(executeChat)、`ChatService.java:353`(executeChatComplex)
|
||||
- 证据:`String answer = response.getText()` 只用于返回前端,未写入任何持久化存储
|
||||
- 结论:加 `DiagnosisSession.answer` 字段是最干净的方案
|
||||
|
||||
### ToolInvocationRepository 已有 findBySessionId
|
||||
|
||||
- 文件:`ToolInvocationRepository.java`
|
||||
- 证据:`findBySessionId(String sessionId)` 已实现,返回 `List<ToolInvocation>`
|
||||
- 结论:规则引擎可直接读取 tool_invocation 事实,无需新增查询方法
|
||||
|
||||
### tool_invocation 写入时序安全
|
||||
|
||||
- 文件:`LookupKnowledgeTool.java:144`
|
||||
- 证据:`saveToolInvocation` 在工具执行时同步调用,早于 ChatService 的 SUCCESS 分支
|
||||
- 结论:@Async evaluate 触发时 tool_invocation 数据已在库,无竞态
|
||||
|
||||
### 项目原无 @EnableAsync
|
||||
|
||||
- 证据:`grep -rn "EnableAsync"` 无任何命中(apply 前)
|
||||
- 结论:需要新建 `AsyncConfig.java`
|
||||
|
||||
### CaseLibraryRepository.findByDiagnosisId 已有幂等检查支持
|
||||
|
||||
- 文件:`CaseLibraryRepository.java`
|
||||
- 证据:`findByDiagnosisId(String diagnosisId)` 已实现
|
||||
- 结论:useful 重复提交时可用此方法检查,不重复插入
|
||||
|
||||
## 设计推导
|
||||
|
||||
### evidence_score vs confidence 命名
|
||||
|
||||
- 基于工具调用的分数衡量的是证据收集充分度,不是答案准确性
|
||||
- "confidence" 容易误解,改为 "evidence_score" 更准确
|
||||
- LLM 自评才适合叫 confidence,但当前未实现
|
||||
|
||||
### BAD_CASE 不应混入 status
|
||||
|
||||
- status 有明确执行状态语义(RUNNING/SUCCESS/FAILED)
|
||||
- 一个 SUCCESS 的 session 被标为 BAD_CASE 后,按 status 做的统计会失真
|
||||
- feedback 字段本身就够,`WHERE feedback = 'not_useful'` 即可查 BadCase
|
||||
@@ -0,0 +1,116 @@
|
||||
# Design: 置信度评分与用户反馈机制
|
||||
|
||||
## 架构约束(来自 devflow)
|
||||
|
||||
- Spring Boot 3.2 + Spring AI Alibaba
|
||||
- JPA ddl-auto=validate,变更走 Flyway
|
||||
- 已有实体:`DiagnosisSession`(含 selfEvaluation JSON、feedback VARCHAR)、`CaseLibrary`
|
||||
- **新增字段**:`DiagnosisSession.answer TEXT`,存储返回给用户的完整答案,Flyway V008 迁移
|
||||
- 已有 Repository:`DiagnosisSessionRepository`、`CaseLibraryRepository`
|
||||
- 当前主流程入口:`ChatService.executeChat`(非流式)、`executeChatComplex`(多 Agent)
|
||||
|
||||
## 模块链路
|
||||
|
||||
```
|
||||
用户对话
|
||||
↓
|
||||
ChatService.executeChat / executeChatComplex
|
||||
↓ SUCCESS 后异步
|
||||
EvaluationService.evaluate(sessionId, answer, steps)
|
||||
├─ LLM 自评 → 写 selfEvaluation(含 confidence + reasoning)
|
||||
└─ 规则兜底(LLM 失败时)→ 写 selfEvaluation(含 source: "rule")
|
||||
|
||||
用户提交反馈
|
||||
↓
|
||||
POST /api/feedback { sessionId, feedback }
|
||||
↓
|
||||
FeedbackService.submitFeedback(sessionId, feedback)
|
||||
├─ 写 DiagnosisSession.feedback
|
||||
├─ feedback=useful → 写 CaseLibrary
|
||||
└─ feedback=not_useful → 更新 status=BAD_CASE
|
||||
```
|
||||
|
||||
## 数据结构定义
|
||||
|
||||
### DiagnosisSession.selfEvaluation(JSON 字符串)
|
||||
|
||||
```json
|
||||
{
|
||||
"evidence_score": 65,
|
||||
"source": "rule",
|
||||
"factors": [
|
||||
{"name": "has_successful_tool_call", "delta": 30, "description": "有成功的工具调用(2次)"},
|
||||
{"name": "l1_semantic_match", "delta": 20, "description": "L1 语义匹配命中"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `evidence_score`:0-100,衡量证据收集充分度(非答案准确性)
|
||||
- `source`:评分来源,当前固定为 `"rule"`;预留 `"llm"` 供后续 LLM 观点叠加
|
||||
- `factors`:命中的规则因子列表,每项含 name / delta / description,可直接用于分析
|
||||
- `llm_opinion`:预留字段,LLM 观点叠加时扩展此处,不改变现有规则逻辑
|
||||
|
||||
### FeedbackRequest(新 DTO)
|
||||
|
||||
```java
|
||||
public class FeedbackRequest {
|
||||
String sessionId; // 必填
|
||||
String feedback; // "useful" | "not_useful"
|
||||
}
|
||||
```
|
||||
|
||||
### FeedbackResponse(新 DTO)
|
||||
|
||||
```java
|
||||
public class FeedbackResponse {
|
||||
boolean success;
|
||||
String message;
|
||||
String caseId; // useful 时返回生成的 case_id,否则 null
|
||||
}
|
||||
```
|
||||
|
||||
### CaseLibrary 生成规则(useful 时)
|
||||
|
||||
| CaseLibrary 字段 | 来源 |
|
||||
|---|---|
|
||||
| caseId | UUID |
|
||||
| diagnosisId | DiagnosisSession.sessionId |
|
||||
| sourceType | SourceType.AUTO |
|
||||
| faultCategory | FaultCategory.GENERAL(暂时) |
|
||||
| title | DiagnosisSession.query 前 100 字符 |
|
||||
| rootCause | DiagnosisSession.answer(完整答案,不截断) |
|
||||
| solution | DiagnosisSession.answer(同上) |
|
||||
| createdBy | "system" |
|
||||
|
||||
## 关键技术决策
|
||||
|
||||
### 决策 1:LLM 自评异步执行
|
||||
|
||||
置信度计算在 Agent 主流程结束后异步进行(`@Async` + Spring 线程池),不阻塞用户响应。
|
||||
原因:LLM 自评耗时 1-3 秒,主流程不应等待。
|
||||
|
||||
### 决策 2:置信度规则兜底参数
|
||||
|
||||
```
|
||||
基础分:60
|
||||
工具调用加分:toolCallCount × 5,上限 +20
|
||||
步数少加分:stepCount <= 3 → +10
|
||||
status=FAILED → 直接 0
|
||||
```
|
||||
|
||||
### 决策 3:BAD_CASE 用 status 字段而非新字段
|
||||
|
||||
`DiagnosisSession.status` 已有 PENDING/RUNNING/SUCCESS/FAILED,扩展为允许包含 BAD_CASE。
|
||||
该字段是 VARCHAR 16,直接存字符串,无需枚举类(Java 端用常量控制)。
|
||||
|
||||
### 决策 4:案例内容提取策略
|
||||
|
||||
useful 时,`rootCause` 和 `solution` 从 `AgentStepRepository.findBySessionIdOrderByStepIndex` 的最后一步 `thought` 字段提取。
|
||||
如果 thought 为空,则用 DiagnosisSession.query + "(自动提取失败,请人工补充)" 占位。
|
||||
|
||||
## 接口影响等级
|
||||
|
||||
- `POST /api/feedback`:新增接口,L2(内部,前端新消费)
|
||||
- `ChatService.executeChat`:新增异步后置调用,不改返回值,L1
|
||||
- `DiagnosisSession.status` 增加 BAD_CASE 值:原调用方只读不写此字段,L2
|
||||
@@ -0,0 +1,61 @@
|
||||
# Proposal: 置信度评分与用户反馈机制
|
||||
|
||||
## 问题
|
||||
|
||||
DiagnosisSession 已预留 `selfEvaluation`(JSON)和 `feedback`(VARCHAR 16)两个字段,但目前完全为空——Agent 完成对话后不计算置信度,也没有接收用户反馈的 API,无法支撑报告质量评估和 BadCase 追踪。
|
||||
|
||||
## 建议方案
|
||||
|
||||
### 置信度评分(双轨)
|
||||
|
||||
**主轨:LLM 自评**
|
||||
- 在 ChatService 的 `executeChat` 流程结束后,追加一次轻量 LLM 调用(EvaluationService),
|
||||
将 Agent 的最终答案 + 步骤摘要传给模型,要求输出 `{"confidence": 0-100, "reasoning": "..."}` JSON。
|
||||
- 结果写入 `DiagnosisSession.selfEvaluation`。
|
||||
|
||||
**兜底轨:规则计算**
|
||||
- 若 LLM 自评失败(超时/解析失败),用规则计算:
|
||||
- 基础分 60
|
||||
- 工具调用数 > 0 每次 +5(上限 +20)
|
||||
- 步数 <= 3 额外 +10
|
||||
- 状态为 FAILED 直接 0
|
||||
- 兜底结果同样写入 `selfEvaluation`,并附 `"source": "rule"` 标记。
|
||||
|
||||
### 用户反馈 API
|
||||
|
||||
新增 `POST /api/feedback`,接收:
|
||||
```json
|
||||
{ "sessionId": "xxx", "feedback": "useful" | "not_useful" }
|
||||
```
|
||||
后端操作:
|
||||
1. 写入 `DiagnosisSession.feedback`。
|
||||
2. 若 `feedback = "not_useful"`,将 `status` 更新为 `BAD_CASE`(需要在 status 枚举扩展此值)。
|
||||
3. 若 `feedback = "useful"`,写入一条 `CaseLibrary` 记录(从 session 提取 query/answer)。
|
||||
|
||||
## 范围
|
||||
|
||||
- 新建 `EvaluationService`(置信度计算)
|
||||
- 新建 `FeedbackService`(反馈处理)
|
||||
- 新增 `POST /api/feedback` 接口(在 ChatController 或新 FeedbackController)
|
||||
- 改造 `ChatService.executeChat` 在 SUCCESS 后调用 EvaluationService
|
||||
- `DiagnosisSession.status` 枚举扩展 `BAD_CASE` 值
|
||||
- Flyway 迁移:`diagnosis_session.status` 列注释更新(不改类型,字段已存在)
|
||||
- 无需新建数据库表
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不实现 Verifier Agent 完整链路(只做轻量自评,不是多 Agent 编排)
|
||||
- 不实现案例沉淀的复杂结构化字段(CaseLibrary 的 faultCategory/errorCode 等填 GENERAL/null)
|
||||
- 不实现 BadCase 的自动分析或 Prompt 优化流程
|
||||
|
||||
## 关键约束(来自 devflow)
|
||||
|
||||
- JPA ddl-auto = validate,表结构变更必须走 Flyway 迁移,但本次无需加新列
|
||||
- `DiagnosisSession.selfEvaluation` 已声明为 JSON 类型,直接用 String 写入
|
||||
- `CaseLibrary.faultCategory` 是枚举,默认填 GENERAL
|
||||
- `SourceType.AUTO` 表示系统自动生成
|
||||
|
||||
## 风险
|
||||
|
||||
- LLM 自评 prompt 质量影响分数可信度,需要在 reasoning 字段记录依据
|
||||
- BAD_CASE status 与现有 PENDING/RUNNING/SUCCESS/FAILED 并存,需确认 UI 是否受影响
|
||||
@@ -0,0 +1,44 @@
|
||||
# Functional Spec: 置信度评分与用户反馈机制
|
||||
|
||||
## REQ-1:置信度 LLM 自评
|
||||
|
||||
- Agent 对话(executeChat / executeChatComplex)成功后,异步调用 EvaluationService。
|
||||
- EvaluationService 构造 Prompt,调用 ChatModel,要求输出纯 JSON:`{"confidence": 0-100, "reasoning": "...", "source": "llm"}`。
|
||||
- 若 JSON 解析成功,写入 `DiagnosisSession.selfEvaluation`。
|
||||
- 若调用失败或解析失败,转入规则兜底(REQ-2)。
|
||||
- 验收:对话结束后数秒内,DB `diagnosis_session.self_evaluation` 非 null,且 `source` 字段存在。
|
||||
|
||||
## REQ-2:置信度规则兜底
|
||||
|
||||
- 触发条件:LLM 自评失败(任何异常)。
|
||||
- 规则:基础分 60 + toolCallCount×5(上限+20)+ (stepCount<=3 ? +10 : 0),status=FAILED 则直接 0。
|
||||
- 结果写入 `selfEvaluation`,含 `"source": "rule"`。
|
||||
- 验收:LLM 自评失败时,self_evaluation 仍有值(非 null),且 source=rule。
|
||||
|
||||
## REQ-3:反馈接收 API
|
||||
|
||||
- 接口:`POST /api/feedback`
|
||||
- 入参:`{ "sessionId": "xxx", "feedback": "useful" | "not_useful" }`
|
||||
- 出参:`{ "success": true/false, "message": "...", "caseId": "uuid 或 null" }`
|
||||
- 校验:sessionId 不能为空;feedback 只能是 useful 或 not_useful,否则返回 400。
|
||||
- 验收:接口返回 200,DB 对应行 feedback 字段有值。
|
||||
|
||||
## REQ-4:useful → 案例沉淀
|
||||
|
||||
- 触发条件:feedback = "useful"。
|
||||
- 操作:在 case_library 插入一条记录,caseId=UUID,sourceType=AUTO,faultCategory=GENERAL,
|
||||
title=query 前 100 字,rootCause/solution 来自最后一步 agent_step.thought。
|
||||
- 响应中返回 caseId。
|
||||
- 验收:提交 useful 后,case_library 表新增一行,diagnosis_id = sessionId。
|
||||
|
||||
## REQ-5:not_useful → BAD_CASE 标记
|
||||
|
||||
- 触发条件:feedback = "not_useful"。
|
||||
- 操作:feedback 字段本身即为标记,不修改 status 字段(status 保持执行状态语义)。
|
||||
- 查询 BadCase 使用:`WHERE feedback = 'not_useful'`。
|
||||
- 验收:提交 not_useful 后,DB diagnosis_session.feedback = "not_useful",status 不变。
|
||||
|
||||
## REQ-6:幂等性
|
||||
|
||||
- 同一 sessionId 重复提交 feedback,覆盖写入(不报错,不重复创建 CaseLibrary)。
|
||||
- 已有 case_library 记录时(diagnosisId 已存在),跳过插入并返回已有 caseId。
|
||||
@@ -0,0 +1,133 @@
|
||||
# Tasks: 置信度评分与用户反馈机制
|
||||
|
||||
## T0:Flyway 迁移 + DiagnosisSession 实体加字段
|
||||
|
||||
**文件**:
|
||||
- `src/main/resources/db/migration/V008__add_answer_to_diagnosis_session.sql`(新建)
|
||||
- `src/main/java/com/superbiz/agent/domain/entity/DiagnosisSession.java`(加字段)
|
||||
|
||||
**迁移脚本**:
|
||||
```sql
|
||||
ALTER TABLE diagnosis_session ADD COLUMN answer LONGTEXT COMMENT 'Agent 返回给用户的完整答案';
|
||||
```
|
||||
|
||||
**实体**:在 `DiagnosisSession` 加:
|
||||
```java
|
||||
@Column(name = "answer", columnDefinition = "LONGTEXT")
|
||||
private String answer;
|
||||
```
|
||||
|
||||
**验收标准**:应用启动不报 schema validation 错误;`diagnosis_session` 表有 answer 列
|
||||
|
||||
---
|
||||
|
||||
— LLM 自评 + 规则兜底
|
||||
|
||||
**文件**:`src/main/java/com/superbiz/agent/service/EvaluationService.java`
|
||||
|
||||
**实现**:
|
||||
- `@Service @Async` 标注
|
||||
- `evaluate(String sessionId, String answer)` 方法:
|
||||
1. 从 `DiagnosisSessionRepository` 加载 session(含 stepCount、toolCallCount、status)
|
||||
2. 调用 ChatModel 做 LLM 自评,Prompt 见下
|
||||
3. 解析 JSON → 写入 `selfEvaluation`
|
||||
4. 失败时走规则兜底
|
||||
- 规则兜底逻辑:`computeRuleScore(session)` → 返回 JSON 字符串
|
||||
|
||||
**LLM 自评 Prompt(系统提示)**:
|
||||
```
|
||||
你是一个 AI 回答质量评估器。
|
||||
请根据以下信息,评估这次 AI 回答的置信度(0-100分):
|
||||
- 用户原始问题:{query}
|
||||
- AI 的回答:{answer}
|
||||
- 工具调用次数:{toolCallCount}
|
||||
- 推理步数:{stepCount}
|
||||
|
||||
只返回一个 JSON,格式如下,不要输出任何其他内容:
|
||||
{"confidence": <0-100的整数>, "reasoning": "<评估依据,50字以内>"}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- LLM 正常时:DB selfEvaluation 包含 confidence 和 reasoning,source = "llm"
|
||||
- LLM 失败时:DB selfEvaluation 包含 confidence 和 source = "rule"
|
||||
|
||||
---
|
||||
|
||||
## T2:ChatService 后置调用 EvaluationService
|
||||
|
||||
**文件**:`src/main/java/com/superbiz/agent/service/ChatService.java`
|
||||
|
||||
**实现**:
|
||||
- 在 `executeChat` 的 `session.setStatus("SUCCESS")` 之后,追加 `session.setAnswer(answer)` 写入完整答案,再注入 EvaluationService 调用 `evaluate(sessionId, answer)`
|
||||
- 在 `executeChatComplex` 的 SUCCESS 分支同样补充 `session.setAnswer(answer)`
|
||||
- 注意:EvaluationService 是 @Async,调用方不等待返回值
|
||||
|
||||
**验收标准**:发送一次 chat 请求后,数秒内 DB self_evaluation 非 null
|
||||
|
||||
---
|
||||
|
||||
## T3:FeedbackController + FeedbackService
|
||||
|
||||
**文件**:
|
||||
- `src/main/java/com/superbiz/agent/controller/FeedbackController.java`(新建)
|
||||
- `src/main/java/com/superbiz/agent/service/FeedbackService.java`(新建)
|
||||
- `src/main/java/com/superbiz/agent/dto/FeedbackRequest.java`(新建)
|
||||
- `src/main/java/com/superbiz/agent/dto/FeedbackResponse.java`(新建)
|
||||
|
||||
**FeedbackService.submitFeedback(sessionId, feedback)**:
|
||||
1. 加载 session,sessionId 不存在抛异常
|
||||
2. 校验 feedback 值(useful/not_useful)
|
||||
3. 更新 `DiagnosisSession.feedback`
|
||||
4. if useful:调用 `CaseLibraryService.createFromSession(session)`
|
||||
5. if not_useful:更新 `DiagnosisSession.status = "BAD_CASE"`
|
||||
6. 保存 session
|
||||
7. 返回 FeedbackResponse
|
||||
|
||||
**幂等逻辑(useful 重复提交)**:
|
||||
- 调用 `CaseLibraryRepository.findByDiagnosisId(sessionId)` 检查
|
||||
- 已存在则返回已有 caseId,不重复插入
|
||||
|
||||
**FeedbackController**:
|
||||
```
|
||||
POST /api/feedback
|
||||
@RequestBody FeedbackRequest
|
||||
@ResponseBody FeedbackResponse
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- useful:返回 200,feedback 字段有值,case_library 新增一行
|
||||
- not_useful:返回 200,status = BAD_CASE
|
||||
- 非法 feedback 值:返回 400
|
||||
|
||||
---
|
||||
|
||||
## T4:CaseLibraryService — createFromSession
|
||||
|
||||
**文件**:`src/main/java/com/superbiz/agent/service/CaseLibraryService.java`(新建)
|
||||
|
||||
**实现**:
|
||||
- `createFromSession(DiagnosisSession session)` → `CaseLibrary`
|
||||
- 直接从 `session.getAnswer()` 取完整答案
|
||||
- answer 为空时用占位文本 `query + "\n(自动提取失败,请人工补充)"`
|
||||
- 填写 CaseLibrary 各字段,save 后返回 caseId
|
||||
|
||||
**验收标准**:case_library 行的 diagnosis_id = sessionId,root_cause 非空
|
||||
|
||||
---
|
||||
|
||||
## T5:Spring @Async 配置
|
||||
|
||||
**文件**:检查项目是否已有 `@EnableAsync`,若无则在 `SessionConfiguration` 或新建 `AsyncConfig` 中添加
|
||||
|
||||
**验收标准**:EvaluationService 中 @Async 方法可被正确调度(不抛 bean 配置错误)
|
||||
|
||||
---
|
||||
|
||||
## T6:集成验证
|
||||
|
||||
验证步骤:
|
||||
1. 启动服务,POST /api/chat,发送一条问题
|
||||
2. 查 `diagnosis_session` 表,确认 self_evaluation 有值
|
||||
3. POST /api/feedback `{"sessionId": "xxx", "feedback": "useful"}`,确认 case_library 新增
|
||||
4. POST /api/feedback `{"sessionId": "yyy", "feedback": "not_useful"}`,确认 status = BAD_CASE
|
||||
5. 重复步骤 3,确认不重复创建 case_library
|
||||
@@ -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