diff --git a/devflow/index.md b/devflow/index.md index d64e92b..c4947e3 100644 --- a/devflow/index.md +++ b/devflow/index.md @@ -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 | diff --git a/devflow/projects/2026-06-29-confidence-feedback/acceptance.md b/devflow/projects/2026-06-29-confidence-feedback/acceptance.md new file mode 100644 index 0000000..4d17c24 --- /dev/null +++ b/devflow/projects/2026-06-29-confidence-feedback/acceptance.md @@ -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 缺失,暂不处理(已知,后续处理流式接口时一并解决) diff --git a/devflow/projects/2026-06-29-confidence-feedback/brief.md b/devflow/projects/2026-06-29-confidence-feedback/brief.md new file mode 100644 index 0000000..10e3bb1 --- /dev/null +++ b/devflow/projects/2026-06-29-confidence-feedback/brief.md @@ -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/` diff --git a/devflow/projects/2026-06-29-confidence-feedback/decisions.md b/devflow/projects/2026-06-29-confidence-feedback/decisions.md new file mode 100644 index 0000000..93eefb2 --- /dev/null +++ b/devflow/projects/2026-06-29-confidence-feedback/decisions.md @@ -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` 返回,方法命名约定 +- DTO:独立文件放 `dto/` 包 +- 异步:新建 `AsyncConfig.java` 加 `@EnableAsync`(项目原无此配置) +- 无 MQ,无加密,工具类直接用 UUID.randomUUID() +- 日志:SLF4J Logger,`LoggerFactory.getLogger()` +- `ToolInvocationRepository.findBySessionId` 已有,可直接用 + +### 参考实现文件 + +- `ChatService.java`:executeChat/executeChatComplex 流程 +- `CaseLibraryRepository.findByDiagnosisId`:幂等检查用 +- `DiagnosisSessionRepository.findBySessionId` +- `ToolInvocationRepository.findBySessionId` diff --git a/devflow/projects/2026-06-29-confidence-feedback/evidence.md b/devflow/projects/2026-06-29-confidence-feedback/evidence.md new file mode 100644 index 0000000..8d2c3de --- /dev/null +++ b/devflow/projects/2026-06-29-confidence-feedback/evidence.md @@ -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` +- 结论:规则引擎可直接读取 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 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/.archive-ready b/openspec/changes/archive/2026-06-30-confidence-feedback/.archive-ready new file mode 100644 index 0000000..e69de29 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/.committed b/openspec/changes/archive/2026-06-30-confidence-feedback/.committed new file mode 100644 index 0000000..e69de29 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/design.md b/openspec/changes/archive/2026-06-30-confidence-feedback/design.md new file mode 100644 index 0000000..67b0806 --- /dev/null +++ b/openspec/changes/archive/2026-06-30-confidence-feedback/design.md @@ -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 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/proposal.md b/openspec/changes/archive/2026-06-30-confidence-feedback/proposal.md new file mode 100644 index 0000000..f2a2925 --- /dev/null +++ b/openspec/changes/archive/2026-06-30-confidence-feedback/proposal.md @@ -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 是否受影响 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/specs/functional-spec.md b/openspec/changes/archive/2026-06-30-confidence-feedback/specs/functional-spec.md new file mode 100644 index 0000000..c4ff349 --- /dev/null +++ b/openspec/changes/archive/2026-06-30-confidence-feedback/specs/functional-spec.md @@ -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。 diff --git a/openspec/changes/archive/2026-06-30-confidence-feedback/tasks.md b/openspec/changes/archive/2026-06-30-confidence-feedback/tasks.md new file mode 100644 index 0000000..006405f --- /dev/null +++ b/openspec/changes/archive/2026-06-30-confidence-feedback/tasks.md @@ -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 diff --git a/src/main/java/com/superbiz/agent/controller/ChatController.java b/src/main/java/com/superbiz/agent/controller/ChatController.java index aebedbd..24eeddf 100644 --- a/src/main/java/com/superbiz/agent/controller/ChatController.java +++ b/src/main/java/com/superbiz/agent/controller/ChatController.java @@ -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; } diff --git a/src/main/java/com/superbiz/agent/domain/entity/DiagnosisSession.java b/src/main/java/com/superbiz/agent/domain/entity/DiagnosisSession.java index bd49d03..c531dfb 100644 --- a/src/main/java/com/superbiz/agent/domain/entity/DiagnosisSession.java +++ b/src/main/java/com/superbiz/agent/domain/entity/DiagnosisSession.java @@ -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; diff --git a/src/main/java/com/superbiz/agent/service/ChatService.java b/src/main/java/com/superbiz/agent/service/ChatService.java index c311fd7..d97237b 100644 --- a/src/main/java/com/superbiz/agent/service/ChatService.java +++ b/src/main/java/com/superbiz/agent/service/ChatService.java @@ -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> 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> 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(); } diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js index 19f074b..bf524f3 100644 --- a/src/main/resources/static/app.js +++ b/src/main/resources/static/app.js @@ -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 = ``; + + const notUsefulBtn = document.createElement('button'); + notUsefulBtn.className = 'feedback-btn'; + notUsefulBtn.title = '无用'; + notUsefulBtn.innerHTML = ``; + + 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 = ``; + } 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) { diff --git a/src/main/resources/static/styles.css b/src/main/resources/static/styles.css index 5cbda3e..0afc6c1 100644 --- a/src/main/resources/static/styles.css +++ b/src/main/resources/static/styles.css @@ -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; +} +