# 英文口语 · 单轮内容评语(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