feat(feedback): 置信度评分与用户反馈机制 & 归档 confidence-feedback change
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user