一、问题拆解:演讲陪练和对话陪练是两种完全不同的评估问题
对话陪练(销售话术、客诉处理)评估的是"说什么"——AI 扮演客户,看学员的应对是否得当,核心是内容与策略的匹配。
演讲陪练评估的是"怎么说"——同样一份 PPT、同一篇讲稿,讲得流畅与讲得磕绊是两种结果,全程念稿与脱稿转述是两种水平,压着时间讲完与超时一半更是天壤之别。核心是表现与基准的偏离度。
这个差异带来三个全新的工程问题:
问题一:内容基准从哪来? 对话陪练的基准是"标准应对话术",通常几句话就够。演讲陪练的基准是"这 31 页 PPT 每页应该讲什么"——一份 31 页的方案汇报,全程应该讲 40 分钟,每页都有该覆盖的要点。基准必须做到页级粒度,否则评估无从谈起。
问题二:翻页节奏怎么感知? 现场演讲中"讲到哪页了"由演讲者翻页决定,AI 陪练要跟着学员的节奏走:学员说"下一页",界面要翻页;学员第 3 页还没讲完就开始讲第 4 页的内容,AI 要知道当前应该停在第 3 页——否则"这一页漏讲了"的判定全部错位。
问题三:表现质量怎么量化? "讲得好不好"在人工评委那里是感觉,在 AI 这里必须拆成可计算的信号:语速(每分钟字数)、停顿(异常长停顿的次数与分布)、口头禅密度("嗯""然后"的每分钟频次)、脱稿程度(演讲内容与 PPT 原文的逐字重合度)、时间管控(实际时长 vs 预算时长)。
三个问题对应本文的三个核心模块:页级内容基准、翻页感知状态机、多维评分引擎。先看整体架构。
二、整体架构:从五步创建流程到陪练引擎
演讲陪练在产品上是一个五步创建流程:创建对话剧本 → 演讲稿设置 → 创建评分标准 → 创建陪练计划 → 添加学员。流程背后是四个引擎模块:
┌──────────────────────────────────────────────────────────────┐
│ 陪练创建流程(管理端) │
│ ① 对话剧本 ② 演讲稿设置 ③ 评分标准 │
│ (场景设定) (页级讲稿+关键词) (维度/权重/细则) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ PPT 解析器 │ │ 评分标准编辑器 │ │
│ │ 逐页提取结构 │ │ AI 生成维度 │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │ 讲稿生成器 │ │ 细则结构化 │ │
│ │ (AI帮写/全篇) │ │ (评分点抽取) │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│ 内容基准 + 评分标准(陪练配置)
┌──────────────────────────▼───────────────────────────────────┐
│ 演练执行引擎(学员端) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ 流式 ASR │──▶│ 翻页感知 │──▶│ 实时反馈控制器 │ │
│ │ 语音流采集 │ │ 状态机 │ │ (漏讲提示/翻页提示) │ │
│ └─────┬────┘ └──────┬───────┘ └────────────────────┘ │
│ │ │ │
│ │ ┌──────▼──────────────────────────────┐ │
│ └───────▶│ 多维评分引擎(演练结束触发) │ │
│ │ 内容覆盖度(50%) × 语言表达(30%) │ │
│ │ × 时间管控(20%) │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│
┌──────▼─────────────┐
│ 评分报告生成 │
│ 维度分/雷达图/逐页批注│
└────────────────────┘
管理端先把"考题"出好(内容基准 + 评分标准),学员端演练时实时感知节奏、结束后整体评分。两侧的关键数据模型如下逐一展开。
三、页级内容基准:PPT 解析与"讲稿 + 关键词"双基准设计
3.1 PPT 解析器:逐页结构提取
陪练创建的第一步是把上传的 PPT 解析成逐页结构。解析用 Apache POI(.pptx 结构化解析)+ 视觉模型兜底(图表类内容转文字描述):
/**
* PPT 解析器:输出页级结构数据
*/
@Service
public class PptParser {
public List<PptPage> parse(InputStream pptStream) {
XMLSlideShow ppt = new XMLSlideShow(pptStream);
List<PptPage> pages = new ArrayList<>();
int index = 1;
for (XSLFSlide slide : ppt.getSlides()) {
PptPage page = new PptPage();
page.setPageIndex(index++);
page.setTitle(extractTitle(slide)); // 页标题
page.setBulletPoints(extractBullets(slide)); // 要点列表
page.setTables(extractTables(slide)); // 表格数据
page.setNotes(extractNotes(slide)); // 备注页(如有)
page.setImageCount(countImages(slide));
// 图表/复杂图形:截图后走视觉模型生成文字描述
if (page.getImageCount() > 0 || hasCharts(slide)) {
page.setVisualDescription(
visionModel.describe(renderSlideToImage(slide))
);
}
pages.add(page);
}
return pages;
}
}
/**
* 解析出的页结构
*/
@Data
public class PptPage {
private Integer pageIndex;
private String title; // "产品背景"
private List<String> bulletPoints; // 要点
private List<TableData> tables;
private String notes; // PPT 备注栏
private String visualDescription; // 图表的文字描述
private Integer imageCount;
}
视觉模型兜底是必要的:方案汇报类 PPT 大量使用图表(架构图、数据图),纯文本解析会漏掉这些页的核心信息。渲染成图片后用视觉模型生成"本页展示的是三层系统架构,包含接入层/服务层/数据层"这类描述,讲稿生成和评分时都用得上。
3.2 双基准设计:演讲稿 + 关键词
演讲稿设置界面为每页提供两个输入:演讲稿(该页应该讲什么,2000 字上限)和关键词(逗号分隔的必须命中的要点)。这不是两个冗余字段,而是两种互补的评估基准:
/**
* 页级内容基准:双基准模型
*
* 演讲稿(Script):语义基准 —— 评分时做整体语义对齐,
* 判断"讲的内容对不对、全不全"
* 关键词(Keywords):点命中基准 —— 评分时做精确命中检查,
* 判断"关键点有没有讲"
*
* 设计动机:
* - 只有讲稿:语义相似度对"关键术语漏讲"不敏感
* (学员讲了一大段相关但无关痛痒的话,语义分不低)
* - 只有关键词:无法评估内容的连贯性和完整性
*/
@Data
public class PageContentBaseline {
private Integer pageIndex;
/** 演讲稿(语义基准) */
private String script; // 该页的标准讲稿
/** 关键词(命中基准) */
private List<String> keywords; // 必须覆盖的关键要点
/** 预计讲解时长(秒)—— 讲稿字数 / 语速换算,用于时间预算 */
private Integer estimatedDurationSec;
public void calculateDuration() {
// 中文演讲语速基准:200-240 字/分钟
int charCount = script.replaceAll("\\s", "").length();
this.estimatedDurationSec = charCount / 4; // 约 240字/分 → 4字/秒
}
}
时间预算从讲稿推导:每页讲稿按 240 字/分钟换算出预计讲解时长,全篇累加就是这场演讲的时间预算——评分标准里的"时间管控"维度(规定时长内完成、超时扣分)就有了客观基准,而不是拍脑袋定一个总时长。
3.3 讲稿生成器:"AI 帮写"与"AI 写全篇讲稿"
31 页 PPT 每页都手写讲稿,对管理者是不小的负担。产品提供两级 AI 辅助:单页"AI 帮写"(按页触发,可逐页调整)和"AI 写全篇讲稿"(一键生成全部)。
/**
* 讲稿生成器
*
* 两种触发粒度:
* 1. 单页生成(AI帮写):只看当前页 + 全局上下文摘要
* 2. 全篇生成(AI写全篇讲稿):带前后页语境的连贯生成
*/
@Service
public class ScriptGenerator {
/**
* 单页讲稿生成
* 上下文策略:当前页完整信息 + 前后页标题(保持衔接)+ 演讲整体信息
*/
public String generatePageScript(ScriptGenRequest req) {
PptPage current = req.getCurrentPage();
String prevTitles = joinTitles(req.getPages(), current.getPageIndex() - 2, 2);
String nextTitles = joinTitles(req.getPages(), current.getPageIndex(), 2);
return llmClient.chat(String.format("""
你是一位资深演讲教练。请为 PPT 的指定页面撰写演讲稿。
## 演讲信息
- 演讲主题:%s(如"甲方方案汇报")
- 听众:%s
- 总页数:%d,当前页:第 %d 页
- 前两页标题:%s(衔接上文用,不要重复讲)
- 后两页标题:%s(为下文铺垫)
## 当前页内容
- 页标题:%s
- 要点:%s
- 图表描述:%s
- PPT备注:%s
## 撰写要求
1. 口语化演讲风格(是说出来的话,不是书面文档)
2. 覆盖所有要点,图表页要"讲图"(描述图表说明什么问题)
3. 开头自然承接上一页,结尾为下一页做钩子
4. 长度:%s 字左右(按语速 %d 字/分钟 × 本页预算 %d 秒换算)
5. 用户如有补充指令,优先遵循:%s
只输出讲稿正文。
""",
req.getSpeechTitle(), req.getAudience(),
req.getTotalPages(), current.getPageIndex(),
prevTitles, nextTitles,
current.getTitle(), String.join(";", current.getBulletPoints()),
defaultString(current.getVisualDescription()), defaultString(current.getNotes()),
req.getTargetCharCount(), 240, req.getEstimatedDurationSec(),
defaultString(req.getUserInstruction())
));
}
/**
* 全篇讲稿生成:逐页生成但带"已生成讲稿的尾部上下文"
*
* 为什么不一次生成全部?
* - 31 页 × 300 字 = 9300 字,单次生成质量衰减明显(后几页明显敷衍)
* - 逐页生成可以逐页控制长度(按各页时间预算)
* - 用户体验:生成过程逐页落库,可中断、可从某页重新生成
*/
public void generateFullScript(Long trainingId) {
List<PptPage> pages = pptRepo.getPages(trainingId);
Deque<String> recentTails = new ArrayDeque<>(3); // 最近3页讲稿的结尾
for (PptPage page : pages) {
ScriptGenRequest req = buildRequest(trainingId, page)
.withRecentContext(recentTails); // 注入前文尾部,保证衔接
String script = generatePageScript(req);
baselineRepo.saveScript(trainingId, page.getPageIndex(), script);
recentTails.addLast( StringUtils.abbreviate(script, 100) );
if (recentTails.size() > 3) recentTails.pollFirst();
// 逐页推送前端(流式体验:编辑器里逐页填充)
sseEmitter.push(trainingId, page.getPageIndex(), script);
}
}
}
"全篇生成"为什么不一次生成? 这是实践中验证过的决策:长文本单次生成时,模型对后段内容的投入度明显下降(第 25 页之后的讲稿会变得模板化)。逐页生成 + 尾部上下文注入(最近三页讲稿的结尾),既保证了页间衔接,又让每页都获得完整的模型注意力。代价是 31 次 LLM 调用,但可以并行化(只有承接关系存在时串行,用小上下文并行 + 后处理校验衔接)。
关键词生成与讲稿同源——从讲稿中抽取该页的 3-8 个核心要点(产品名、数字、关键概念),供管理者确认或修改。管理者的修改权重高于 AI:改过的关键词在后续重新生成讲稿时作为约束注入。
四、翻页节奏感知:双触发的状态机
演练执行时,引擎必须知道"现在讲到第几页"。这是所有页级评估的前提。
4.1 翻页状态机
翻页采用显式指令 + 内容感知双触发:
┌─────────────────────────────┐
│ PageState 状态机(第 N 页) │
└─────────────────────────────┘
┌──────────┐ 显式指令 ┌──────────┐ 显式指令 ┌──────────┐
│ 第 N 页 │──────────▶│ 第 N+1 页 │──────────▶│ 第 N+2 页 │
│ SPEAKING │ "下一页" │ SPEAKING │ │ │
└────┬─────┘ └──────────┘ └──────────┘
│
│ 内容完成度 ≥ 90% 且停顿 > 2s(隐式信号)
▼
┌──────────┐ 学员内容跳跃(讲到下一页的标题/关键词)
│ 翻页建议 │────────────────────────────────────┐
│ (浮层提示) │ │
└──────────┘ ▼
自动跟随翻页
/**
* 翻页感知状态机
*
* 双触发设计:
* 1. 显式触发:学员说"下一页/翻页/过"——语音指令关键词命中
* 2. 隐式触发:内容完成度达标 + 自然停顿 → 弹"建议翻页"提示
* (不自动翻,把控制权留给学员——真实演讲也是自己翻页)
*
* 特殊处理:内容跳跃跟随 —— 学员开始讲下一页的标题/关键词时,
* 状态机自动跟随到下一页(演讲者忘了翻页是真实高频场景)
*/
@Service
public class PageTurnStateMachine {
private static final List<String> TURN_COMMANDS = Arrays.asList(
"下一页", "翻页", "下一张", "过", "next", "next page"
);
/** 当前页状态 */
private int currentPage = 1;
private List<PageContentBaseline> baselines;
/**
* 处理流式 ASR 的新增文本片段
*/
public PageTurnEvent onTranscriptDelta(String delta, long timestampMs) {
// 1. 显式翻页指令检测
if (containsTurnCommand(delta)) {
return turnToPage(currentPage + 1, TurnTrigger.EXPLICIT);
}
// 2. 内容完成度检测(当前页关键词命中 + 语义覆盖)
double coverage = coverageCalculator.computeIncremental(
currentPage, delta);
if (coverage >= 0.9 && isLongPause(timestampMs)) {
// 内容讲完 + 停顿:建议翻页(浮层提示,不强制)
return PageTurnEvent.suggestTurn(currentPage);
}
// 3. 内容跳跃跟随:新文本与下一页的关键词/标题高度匹配
if (matchesNextPageContent(delta)) {
return turnToPage(currentPage + 1, TurnTrigger.CONTENT_JUMP);
}
return PageTurnEvent.none();
}
private boolean matchesNextPageContent(String delta) {
PageContentBaseline next = baselines.get(currentPage); // 下一页
if (next == null) return false;
// 命中下一页标题或 ≥2 个关键词 → 判定为内容跳跃
boolean titleHit = delta.contains(next.getPageTitle());
long keywordHits = next.getKeywords().stream()
.filter(delta::contains).count();
return titleHit || keywordHits >= 2;
}
}
"内容跳跃跟随"是体验的关键细节:真实演练中,演讲者讲到兴头上忘了翻页是高频事件——学员已经开讲"第二部分:产品能力",界面还停在第 5 页。状态机检测到新文本命中下一页的标题/关键词时自动跟随,页级评估的锚点就不会错位。而显式指令永远优先——学员说"下一页"立即翻,无论内容完成度多少(回练某页时学员会主动跳页)。
4.2 页级时间轴的沉淀
翻页事件与 ASR 时间戳共同沉淀出"页级时间轴",评分阶段消费:
{
"practiceId": "p_20261014_001",
"totalDurationSec": 2380,
"pageTimeline": [
{
"page": 1, "startMs": 0, "endMs": 62000, "durationSec": 62 },
{
"page": 2, "startMs": 62000, "endMs": 118000, "durationSec": 56 },
{
"page": 3, "startMs": 118000, "endMs": 285000, "durationSec": 167 }
],
"turnEvents": [
{
"page": 2, "trigger": "EXPLICIT", "atMs": 62000 },
{
"page": 3, "trigger": "CONTENT_JUMP", "atMs": 118000 }
]
}
五、多维评分引擎:产品评分标准的技术化
评分标准创建界面定义了演讲陪练的评分体系——以"甲方方案汇报"为例的三个维度:
| 维度 | 权重 | 评分细则(界面原文摘录) |
|---|---|---|
| 内容掌握程度 | 50% | 熟悉度(15分):不逐字念PPT,能脱稿/用自己语言转述;不卡顿、不频繁看幻灯片。重点把控(12分):能识别PPT核心信息…… |
| 语言表达与呈现 | 30% | 语言表达(12分):吐字清晰、语速适中;专业术语准确;口语流畅,无大量口头禅(嗯、然后)。语气感染力(10分):节奏有变化…… |
| 时间管控 | 20% | 按时完成:在规定时长内完成汇报。轻微超时酌情扣分;超时较多大幅扣分;提前过多收尾、内容仓促也扣分。超时≤30s…… |
这个评分体系的技术挑战在于:界面上的细则是给人看的自然语言,评分引擎需要把它转成可计算的规则。 我们的方案是"细则结构化"——创建评分标准时,把自然语言细则抽取成"评分点 + 计算方式"的结构化规则。
5.1 评分标准的数据模型
/**
* 评分维度
*/
@Data
public class ScoringDimension {
private String name; // "内容掌握程度"
private Double weight; // 0.5(权重 50%)
private List<ScoringPoint> points;
}
/**
* 评分点:自然语言细则 → 结构化规则
*
* 每个评分点绑定一个或多个"信号计算器",
* 引擎运行时按计算器输出打分。
*/
@Data
public class ScoringPoint {
private String description; // "不逐字念PPT,能脱稿/用自己语言转述"
private Double maxScore; // 15
/** 计算器类型(引擎按此路由到对应的信号算法) */
private SignalCalculatorType calculator;
// SCRIPT_DEPENDENCE - 脱稿程度(讲稿逐字重合度)
// KEYWORD_COVERAGE - 关键词覆盖度
// SPEECH_RATE - 语速区间
// FILLER_DENSITY - 口头禅密度
// PAUSE_ANOMALY - 异常停顿
// TERM_ACCURACY - 专业术语准确(ASR 置信度 + 术语表比对)
// TIME_BUDGET - 时间管控
// RHYTHM_VARIATION - 节奏变化(可选,需较长时间轴)
/** 计算器参数(如语速区间 180-260 字/分) */
private Map<String, Object> params;
}
5.2 AI 生成评分维度:细则到规则的自动转换
产品支持"AI 生成"评分维度——输入演讲主题,AI 输出维度、权重和细则。生成时同步产出结构化规则,人工校对后生效:
/**
* 评分标准 AI 生成器
*
* 关键设计:让 LLM 一次性产出"自然语言细则 + 结构化规则"对,
* 人工校对自然语言时同步看到规则映射,避免规则与细则脱节。
*/
@Service
public class ScoringStandardGenerator {
public ScoringStandardDraft generate(String speechTitle, String audience,
Integer totalPages,
Integer plannedDurationMin) {
String prompt = String.format("""
你是一位企业演讲培训专家。请为以下演讲场景设计评分标准。
## 演讲信息
- 主题:%s
- 听众:%s
- PPT 页数:%d 页
- 规定时长:%d 分钟
## 要求
1. 设计 3-4 个评分维度,权重合计 100%%。
(典型维度:内容掌握程度、语言表达与呈现、时间管控——
可按演讲类型调整,述职汇报侧重逻辑,产品发布侧重感染力)
2. 每个维度下 2-4 个评分点,标注该点的满分。
3. 每个评分点的细则用自然语言描述(给培训管理员看),
同时指定对应的计算器类型和参数(给评分引擎用)。
## 可用计算器类型
SCRIPT_DEPENDENCE(脱稿度:讲稿逐字重合度,越低越好)
KEYWORD_COVERAGE(关键词覆盖:页级关键词命中率)
SPEECH_RATE(语速:字/分钟,参数给 [min, max] 区间)
FILLER_DENSITY(口头禅:嗯/然后/那个 的每分钟频次)
PAUSE_ANOMALY(异常停顿:超过 N 秒的停顿次数)
TIME_BUDGET(时间:实际 vs 预算的偏离规则)
TERM_ACCURACY(术语准确:ASR 低置信片段占比 + 术语表比对)
## 输出 JSON
{
"dimensions": [
{
"name": "内容掌握程度",
"weight": 0.5,
"points": [
{
"description": "熟悉度:不逐字念PPT,能脱稿/用自己的语言转述;不卡顿、不频繁停顿",
"maxScore": 15,
"calculator": "SCRIPT_DEPENDENCE",
"params": {
"verbatimThreshold": 0.6, "minParaphraseRatio": 0.7 }
}
]
}
]
}
""", speechTitle, audience, totalPages, plannedDurationMin);
LlmResponse resp = llmClient.chat(prompt, Map.of("temperature", 0.4));
return parseDraft(resp.getContent());
}
}
为什么让 AI 同时输出细则和规则? 如果只生成自然语言细则、再靠人工映射计算器,31 页 × 多维度的映射工作没人愿意做。让 LLM 在生成细则时就指定计算器类型(从枚举里选),规则与细则天然绑定,人工校对变成"确认"而非"翻译"。
5.3 信号计算器:五个核心算法
评分引擎运行时按页级时间轴 + 全程 ASR 转写计算各信号:
/**
* 信号计算器实现(核心五个)
*/
@Service
public class SignalCalculators {
/**
* 1. 脱稿程度(SCRIPT_DEPENDENCE)
*
* 原理:把学员的转写文本与该页讲稿做 n-gram 重合度分析。
* - 3-gram 重合度 > 0.6:存在大段逐字背诵/念稿
* - 关键指标:n-gram 覆盖率(讲稿的 3-gram 有多少出现在学员语音里)
* - 区分"转述"与"背诵":语义相似度高 + n-gram 重合度低 = 好的脱稿
*/
public double calcScriptDependence(String transcript, String script) {
Set<String> scriptTrigrams = ngrams(script, 3);
Set<String> spokenTrigrams = ngrams(transcript, 3);
// 讲稿 trigram 在学员语音中的出现比例(越高越像念稿)
double verbatimRatio = (double) countOverlap(scriptTrigrams, spokenTrigrams)
/ scriptTrigrams.size();
// 语义相似度(转述是否保留了讲稿的内容)
double semanticSim = embeddingClient.similarity(
embeddingClient.embed(transcript),
embeddingClient.embed(script));
// 脱稿分 = 语义保持 × (1 - 逐字比例)
// 内容讲对了但用自己的话 = 满分;内容对但逐字背诵 = 中等;内容不对 = 低分
return semanticSim * (1 - normalize(verbatimRatio));
}
/**
* 2. 关键词覆盖度(KEYWORD_COVERAGE)
*
* 按页计算:该页的关键词在对应页时间段内的命中比例。
* "时间归属"很重要——第 5 页的关键词在第 3 页讲了不算命中。
*/
public double calcKeywordCoverage(PracticeSession session) {
int totalKeywords = 0, hitKeywords = 0;
for (PageTimelineSegment seg : session.getPageTimeline()) {
PageContentBaseline baseline = session.getBaseline(seg.getPage());
String segTranscript = session.getTranscriptBetween(
seg.getStartMs(), seg.getEndMs());
for (String keyword : baseline.getKeywords()) {
totalKeywords++;
if (segTranscript.contains(keyword)) {
hitKeywords++;
} else {
// 精确 miss 后做同义词/形态变体匹配
if (synonymMatcher.match(segTranscript, keyword)) {
hitKeywords++;
} else {
session.recordMissedKeyword(seg.getPage(), keyword);
}
}
}
}
return totalKeywords == 0 ? 1.0 : (double) hitKeywords / totalKeywords;
}
/**
* 3. 语速(SPEECH_RATE)
*
* 有效语速:剔除停顿后的"说话时间"内每分钟字数。
* 演讲推荐区间 180-260 字/分钟(界面细则"语速适中"的量化)。
*/
public double calcSpeechRate(PracticeSession session) {
long speechTimeMs = session.getTotalDurationMs() - session.getTotalPauseMs(1000);
int charCount = session.getTranscript().replaceAll("\\s", "").length();
double charsPerMin = charCount / (speechTimeMs / 60000.0);
// 区间评分:180-260 满分,线性衰减到区间外
if (charsPerMin >= 180 && charsPerMin <= 260) return 1.0;
if (charsPerMin < 180) return Math.max(0, charsPerMin / 180.0);
return Math.max(0, 1 - (charsPerMin - 260) / 120.0); // 380字/分 → 0分
}
/**
* 4. 口头禅密度(FILLER_DENSITY)
*
* "嗯、然后、那个、就是说" 的每分钟频次。
* 依赖 ASR 的词级时间戳(或逐句转写),按句统计。
* 基准:≤2 次/分钟 满分,≥8 次/分钟 零分,线性插值。
*/
public double calcFillerDensity(PracticeSession session) {
List<String> fillers = Arrays.asList("嗯", "呃", "然后", "那个", "就是说", "这个");
long fillerCount = 0;
for (AsrSentence sentence : session.getSentences()) {
for (String filler : fillers) {
fillerCount += countOccurrences(sentence.getText(), filler);
}
}
double perMinute = fillerCount / (session.getTotalDurationMs() / 60000.0);
return clamp01(1 - (perMinute - 2) / 6.0);
}
/**
* 5. 时间管控(TIME_BUDGET)
*
* 对应界面细则:"轻微超时酌情扣分;超时较多大幅扣分;
* 提前过多收尾、内容仓促也扣分"
*
* 量化规则:
* - |偏离| ≤ 5%:满分(正常节奏浮动)
* - 超时 5%-15%:线性扣分至 60%
* - 超时 >15%:线性扣分至 20%
* - 提前 >15%(讲太快/漏内容):线性扣分至 30%
* (提前收尾会连带触发内容覆盖度扣分,双重信号定位"仓促")
*/
public double calcTimeBudget(long actualMs, long plannedMs) {
double deviation = (double) (actualMs - plannedMs) / plannedMs;
if (Math.abs(deviation) <= 0.05) return 1.0;
if (deviation > 0) {
// 超时
if (deviation <= 0.15) return 1 - (deviation - 0.05) / 0.10 * 0.4;
return Math.max(0.2, 0.6 - (deviation - 0.15) * 1.0);
} else {
// 提前
double early = -deviation;
if (early <= 0.15) return 1 - (early - 0.05) / 0.10 * 0.3;
return Math.max(0.3, 0.7 - (early - 0.15) * 0.8);
}
}
}
"提前收尾也扣分"的设计与产品细则一致且必要:只罚超时不罚提前,学员的最优策略就变成飞快念完。提前收尾触发双信号——时间管控扣分 + 内容覆盖度下降(漏讲导致关键词 miss),两个维度交叉印证"内容仓促"。
5.4 总分合成与评分报告
/**
* 总分合成:维度加权 + LLM 总评生成
*/
@Service
public class ScoreComposer {
public PracticeReport compose(PracticeSession session) {
double totalScore = 0;
for (ScoringDimension dimension : session.getScoringStandard().getDimensions()) {
double dimensionScore = 0;
for (ScoringPoint point : dimension.getPoints()) {
// 路由到对应计算器
double signal = calculatorRegistry
.get(point.getCalculator())
.calculate(session, point.getParams());
dimensionScore += signal * point.getMaxScore();
}
totalScore += dimensionScore * dimension.getWeight();
}
// LLM 生成总评与逐页批注(人话版的反馈)
String commentary = generateCommentary(session, totalScore);
return PracticeReport.builder()
.totalScore(totalScore)
.dimensionScores(calcDimensionBreakdown(session))
.missedKeywords(session.getMissedKeywords()) // 漏讲要点清单(逐页)
.fillerStatistics(session.getFillerStats())
.pageTimeline(session.getPageTimeline())
.commentary(commentary)
.build();
}
/**
* 总评生成:数字分 → 有指导价值的人话
* 界面"总评"字段(如"熟悉方案,不念PPT;能抓住重点……")
* 是给"合格表现"的描述模板,AI 总评对齐这个风格
*/
private String generateCommentary(PracticeSession session, double score) {
return llmClient.chat(String.format("""
你是一位资深演讲教练。请基于以下评估数据,为学员的演练写一段总评。
## 评估数据
- 总分:%.1f / 100
- 各维度得分:%s
- 漏讲关键词(按页):%s
- 口头禅频次:%.1f 次/分钟
- 语速:%.0f 字/分钟
- 时间:实际 %d 分钟 / 预算 %d 分钟
- 翻页方式分布:%s
## 要求
1. 先一句话总体定性(达到了什么水平)
2. 指出 1-2 个最突出的优点(有数据支撑)
3. 指出 1-2 个最需要改进的问题,每个问题给出具体的改进动作
(如"第 12 页漏讲了'成本对比'这个关键点,建议在讲完竞品分析后
主动引到成本话题"——要具体到页码和关键词,不要说"加强重点把控")
4. 全文 150 字以内,语气是教练而不是考官
""",
score, formatDimensions(session),
formatMissedKeywords(session),
session.getFillerStats().getPerMinute(),
session.getSpeechRate(),
session.getActualDurationMin(), session.getPlannedDurationMin(),
formatTurnTriggers(session)
));
}
}
六、实时反馈架构:流式 ASR 双通道
演练进行中的实时提示(翻页建议、漏讲提醒)和结束后的整体评分,对语音识别的要求不同——前者要快(秒级),后者要准(整段重识别)。我们采用双通道:
学员语音 ──┬──▶ 实时通道:流式 ASR ──▶ 翻页状态机 / 实时提示
│ (Web RTC 采集 → 云端流式识别,延迟 300-800ms)
│
└──▶ 离线通道:整段音频留存 ──▶ 演练结束后录音文件识别
(词级时间戳、标点、数字归一化全开,评分更准)
为什么评分用离线通道重跑? 流式识别为了低延迟做了很多妥协(无标点、时间戳精度低、长句切分随意),直接影响填充词统计和停顿检测的准确度。离线通道的录音文件识别提供词级时间戳和完整标点,是 FillerDensity、PauseAnomaly 这些细粒度信号的正确数据源。两个通道的成本可以接受——流式按分钟计费,离线识别只对演练录音跑一次。
实时提示的"打扰度"控制在产品层有明确规则:翻页建议用浮层(视觉信号,不插入语音);漏讲提醒只在翻页前一刻出现(该页即将结束但你还有 2 个关键词没讲——给学员一次补救机会,而不是全程打断);口头禅、语速问题只在结束报告中反馈(演讲中纠正这些会严重干扰节奏)。
七、效果数据与总结
这套演讲陪练引擎在内测阶段的关键数据(以 31 页"甲方方案汇报"演练为例):
| 指标 | 数值 |
|---|---|
| 陪练创建耗时 | 上传 PPT + AI 全篇讲稿 + AI 评分标准,约 15 分钟 |
| 演练时长 | 中位数 38 分钟(预算 40 分钟) |
| 翻页判定准确率 | 显式指令 100%,内容跳跃跟随 P95 准确(错位多发生在关键词跨页复用时) |
| 漏讲关键词检出率 | 与人工标注对比 91% |
| 脱稿度评分合理性 | 与 3 位人工评委的相关系数 0.82 |
| 口头禅检出 | ASR 标点修复后,误报率 < 8% |
| 评分-报告产出延迟 | 演练结束后 ≤ 90 秒 |
企学宝技术团队总结的四条核心经验:
内容基准做"双基准"(讲稿 + 关键词)是评分可信的前提。 语义对齐管"讲得全不全",关键词命中管"要点漏没漏"——只用其一都会出现明显的评估盲区。页级时间轴 + 关键词的时间归属判定(第 5 页的关键词必须在第 5 页的时间段里讲)是双基准落地的关键工程细节。
评分标准的技术化本质是"自然语言细则 → 结构化规则"的转换。 让 AI 生成细则时同步生成计算器映射,人工从"翻译规则"降级为"确认规则",这套流程让企业培训管理员(非技术角色)也能搭建可计算的评分体系。信号计算器的枚举是有限的(七八个),但组合出的评分标准是无限的。
翻页感知要把控制权留给学员。 显式指令永远优先、隐式信号只建议不强制、内容跳跃自动跟随但记录触发方式——真实演讲中翻页权在演讲者手里,陪练系统应该模拟这种真实感,同时在报告里反映"3 次翻页都是内容跳跃触发的"这类行为信号。
实时提示的价值在"少而准"而不是"多而全"。 演练场景中任何打断都会破坏心流。翻页建议用视觉浮层、漏讲提醒只在页尾出现一次、表达类问题一律留到报告——克制的信息策略反而让学员信任并依赖这些提示。
演讲陪练与对话陪练共用同一个剧本陪练底盘(会话管理、学员分配、练习计划),差异全部收敛在"内容基准模型 + 评估管道"这一层——这也是引擎架构上最重要的约束:新场景的接入成本应该体现在配置里,而不是新系统里。