Explorar o código

docs: 单轮内容评语 MVP 设计

锁定 per-turn 粒度 + 每轮 /speak 后立刻并发评估的时机约束,
补齐 Azure 四维发音分之外的内容评语链路(LLM 生成 SentenceCard
所需的 highlights/corrections/suggestions)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee hai 2 meses
pai
achega
c1f8bb8743
Modificáronse 1 ficheiros con 227 adicións e 0 borrados
  1. 227 0
      doc/ContentEvaluationDesign.md

+ 227 - 0
doc/ContentEvaluationDesign.md

@@ -0,0 +1,227 @@
+# 英文口语 · 单轮内容评语(MVP 设计)
+
+> 状态:设计收敛,待实现
+> 日期:2026-04-23
+> 归属后端:`cococlass-english-speaking-api`
+> 前端消费:`PPT` → `src/views/Editor/EnglishSpeaking/` + `enspeak` 原型的 `SentenceCard` 契约
+> 关联设计:`doc/EnglishSpeakingIntegration.md`
+
+---
+
+## 1. 背景与问题
+
+现有评分链路覆盖**发音侧**四维:`accuracy / fluency / completeness / prosody`(Azure Speech SDK 的 Pronunciation Assessment)。发音侧回答的是"听起来像不像母语者",不回答"说了什么是否合适"。
+
+enspeak 原型 `DetailedReport.tsx:55` 的 `SentenceCard` 已经给出 UI 契约:每个学生句除发音四分外,还展示 `feedback.{highlights, corrections, suggestions}`。这部分目前为空,本设计补齐。
+
+MVP 目标:用一次 LLM 调用生成这三段结构化评语,不引入任何新 provider,不改动对话主流程。
+
+---
+
+## 2. 核心约束
+
+1. **评分只在结果页展示**:`content_feedback` 永远**不出现在 `/speak` 的响应里**(无论 SSE 流还是最终帧)。对话进行中前端拿不到任何评分数据,避免分心 + 避免评分阻塞对话节奏。
+2. **评分数据的唯一出口是 `/api/speaking/dialogue/report`**:学生/教师打开结果页时按 `sessionId` 拉取,拿到每轮的发音四分 + `content_feedback`。
+3. **评估粒度 = 每轮(per-turn)**:每一轮学生发言独立评估、独立存库。结果页按轮次渲染卡片,OverallReport 的 `overallScore` / 聚合数据由**前端从 per-turn 结果计算**(平均分、top-N 亮点),不引入 session 级 LLM 调用。
+4. **评估时机 = 每轮 `/speak` 完成后立即后台触发,绝不延后到会话结束批量跑**:
+   - `asyncio.create_task(_evaluate_pronunciation)` 在本轮 `/speak` 返回前就挂起,与 AI 回复并发
+   - N 轮对话的 N 次评估**摊在对话进行的整个时间窗口内并发完成**,结果页首屏延迟 ≈ 最后一轮的单轮评估延迟,而非 N × 单轮延迟
+   - 失败隔离:某一轮 Azure / LLM 失败不影响其他轮次;学生中途放弃对话时已完成轮次的评分仍可查看
+5. **评估生成是 fire-and-forget**:`/speak` 立即返回,不等待任何评分完成。每轮内部串行 Azure → Content LLM(Content LLM 需要 Azure 的四分作为输入,故不能并行)。
+6. **结果页需容忍 pending**:打开结果页时可能最近几轮的评估还没跑完,前端应轮询 `/report` 或按 `status` 字段显示"评估中"占位。
+
+---
+
+## 3. 范围
+
+**In scope**
+- 每轮学生发言结束后,在已有的后台评估任务里多挂一次 LLM 调用,产出 `content_feedback`
+- 对 `PronunciationEvaluation` 表加一列 `content_feedback JSON`
+- `GET /api/speaking/dialogue/report` 返回值把 `content_feedback` 并入每个 `evaluation`
+
+**Out of scope(后续迭代)**
+- Session 级的连贯性/任务达成评语(本期 per-turn only)
+- 独立的"内容分"数值(本期只出评语文字,不出 0–100 分)
+- LLM judge 的 self-consistency / calibration / 金标对比
+
+---
+
+## 4. 数据契约
+
+### 4.1 输出结构(与 enspeak `SentenceCard.feedback` 对齐)
+
+```jsonc
+{
+  "highlights": ["..."],                              // 1–2 条,中文,≤30 字
+  "corrections": [
+    {
+      "original":    "I go to park yesterday",       // 英文原句
+      "corrected":   "I went to the park yesterday", // 英文改正
+      "explanation": "过去式应用 went,park 前加 the" // 中文解释
+    }
+  ],
+  "suggestions": ["..."]                              // 1–2 条,中文,≤30 字
+}
+```
+
+- 空数组合法(没话可说时不强凑)
+- 整个对象允许为 `null`(LLM 失败时的降级)
+
+### 4.2 数据库变更
+
+`pronunciation_evaluation` 表新增一列:
+
+```python
+content_feedback: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
+```
+
+无独立状态字段:
+- `status == "completed"` 且 `content_feedback == null` → 内容评语调用失败(LLM 超时/解析失败),不阻塞发音结果
+- `status == "failed"` → 发音失败,内容评语跳过不调用
+
+---
+
+## 5. 架构
+
+```
+/speak (SSE)
+  │
+  ├─ 同步:ASR → LLM 对话回复
+  │
+  └─ 后台 asyncio.create_task(_evaluate_pronunciation)
+       │
+       ├─ Azure Pronunciation Assessment  →  4 个分数 + word_analysis
+       │
+       └─ [新增] ContentEvaluator.evaluate()
+              · 输入:4 分 + 学生转录 + AI 上一句
+              · 输出:{highlights, corrections, suggestions}
+              · 失败:log 并置 content_feedback=null,不影响 status
+```
+
+新增模块一个:`app/service/speaking/content_evaluator.py`
+
+---
+
+## 6. LLM 调用规格
+
+### 6.1 Provider
+
+复用现有 `LLMProvider`(onehub_llm)。无需新 provider 协议。
+
+### 6.2 模型与参数
+
+- 模型:`gpt-4o-mini` 或等价轻量模型(onehub 内部选)
+- `temperature = 0`
+- `response_format = { "type": "json_schema", "json_schema": FEEDBACK_SCHEMA }`
+  - onehub 不支持 json_schema 时降级为 `{ "type": "json_object" }` + prompt 里加 schema 说明
+- 超时:10s,失败即降级 `content_feedback=null`
+
+### 6.3 Prompt(system)
+
+```
+You are an English tutor evaluating a student's single spoken turn in an
+open dialogue. You receive:
+- Azure pronunciation scores (accuracy/fluency/completeness/prosody, 0–100)
+- The immediate prior AI turn (context)
+- The student's transcript
+
+Produce JSON with three arrays:
+- highlights:  1–2 Chinese sentences praising specific strengths. Reference a
+               pronunciation dimension if that score is ≥ 85. ≤ 30 chars each.
+- corrections: grammar / word-choice fixes. Each item:
+                 { original (EN), corrected (EN), explanation (ZH, ≤ 30 chars) }
+- suggestions: 1–2 Chinese actionable improvements. Reference a pronunciation
+               dimension if that score is < 70. ≤ 30 chars each.
+
+Rules:
+- Empty arrays are valid. Do not invent errors to fill quota.
+- If the student only said a filler ("yes", "ok", "hmm"), return all empty
+  arrays plus one encouragement in highlights.
+- Never include the scores as raw numbers in output text; describe
+  qualitatively ("发音准确度很高" not "accuracy 92").
+```
+
+### 6.4 User payload
+
+```json
+{
+  "pronunciation": {
+    "accuracy": 72, "fluency": 85, "completeness": 90, "prosody": 60
+  },
+  "ai_said":      "What did you do last weekend?",
+  "student_said": "I go to park with my family and play ball."
+}
+```
+
+历史消息**只给 AI 上一句**(per-turn 评语场景足够;深层连贯性留给后续 session-level 迭代)。
+
+---
+
+## 7. 失败策略
+
+| 场景 | status | content_feedback | 前端表现 |
+|---|---|---|---|
+| Azure 成功 + LLM 成功 | completed | `{...}` | 完整卡片 |
+| Azure 成功 + LLM 超时 | completed | `null` | 只显示发音四分 |
+| Azure 成功 + JSON 解析失败 | completed | `null` | 只显示发音四分 |
+| Azure 失败 | failed | `null`(不调用) | 卡片置错误态 |
+
+前端 `hasEvaluation = score !== undefined || feedback` 的逻辑原样兼容。
+
+---
+
+## 8. 成本与性能预算
+
+- 每轮:输入 ~500 token,输出 ~200 token
+- 按 `gpt-4o-mini` ≈ $0.15 / 1M input、$0.60 / 1M output 估算:**单轮 < $0.0002**
+- 延迟:10s 内必须完成或放弃。用户在对话中不等此结果(已是后台 fire-and-forget),延迟只影响"查看结果页"的 pending 窗口大小
+- 不做 self-consistency / 多次采样,MVP 接受 ±10 分波动(本期无数字分,只有文字评语,波动容忍度高)
+
+---
+
+## 9. API 契约改动
+
+`GET /api/speaking/dialogue/report` 返回的 `evaluations[i]` 加一个字段:
+
+```jsonc
+{
+  "round": 1,
+  "status": "completed",
+  "accuracy_score": 72,
+  "fluency_score": 85,
+  "completeness_score": 90,
+  "prosody_score": 60,
+  "content_feedback": {           // ← 新增,可为 null
+    "highlights":  [...],
+    "corrections": [...],
+    "suggestions": [...]
+  }
+}
+```
+
+PPT 前端 `src/types/englishSpeaking.ts` 的 evaluation 类型同步加 `contentFeedback`。
+
+---
+
+## 10. 测试约束
+
+- `tests/service/speaking/test_content_evaluator.py`
+  - mock LLM provider,验证 prompt 组装
+  - JSON schema 合法 / 非法 / 超时三条分支
+  - 空学生转录 / 全 filler 转录 → 走 "all empty + encouragement" 分支
+- `tests/service/speaking/test_dialogue_service.py`(扩充)
+  - Azure 成功 + content 成功 → 两个字段都写入
+  - Azure 成功 + content 失败 → status completed,content_feedback null
+  - Azure 失败 → content 不被调用
+- 不覆盖真实 LLM 调用(集成测试在后续阶段)
+
+---
+
+## 11. 实施步骤(给实现计划用的骨架)
+
+1. DB 迁移:`pronunciation_evaluation.content_feedback JSON nullable`
+2. 新建 `app/service/speaking/content_evaluator.py`,实现 `evaluate(transcript, prior_ai_turn, pron_scores) -> dict | None`
+3. 在 `dialogue_service._evaluate_pronunciation` 的 Azure 调用成功分支后追加 content 评估
+4. `GET /report` 返回值补 `content_feedback`
+5. PPT 前端 `EnglishSpeaking/` 下报告渲染补 `feedback` 展示(enspeak `SentenceCard` 三段 UI 直接移植)
+6. Unit tests