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