第29章:ASR 后处理:主题分段与段落划分
本章分析 internal/service/live_material_asr_postprocess.go——ASR 链路的 LLM 增强环节。两条 LLM 流水线并行产出「主题分段」(给人看的目录)与「全文段落」(给 AI 切片吃的语料),全部围绕一个关键词:容错。窗口化、传输重试、输出修复、服务端硬校验,一层层把不可靠的模型输出驯化成可信数据。
流程
输入是 live_asr 原始 JSON 与音频总时长,输出 summaries + paragraphs。处理分两条对称的流水线:
主题分段(给人看):分句列表按 60 分钟窗口切分 → 每窗发 LLM(asrSummariesSystemPrompt:输出 {"items":[{title,start_index,end_index}]},title ≤6 字、每段 5~60 分钟)→ 窗内结果按 2 分钟间隙合并相邻同主题 → 服务端硬校验(时长越界丢弃、索引越界截断)→ 汇总成 summaries。
段落划分(给 AI 吃):分句列表按 25 分钟窗口切分 → 每窗发 LLM(asrParagraphsSystemPrompt:区间必须恰好全覆盖无遗漏无重叠、单说话人、拼接 <200 字)→ 输出不合格时进入修复轮次(把错误说明拼进 prompt 再问一次)→ 校验通过后按区间聚合 utterances(正文拼接、words 合并、时间取区间首尾)→ paragraphs。
两窗处理用 errgroup 并行(asrLLMWindowParallelism = 8 上限),单窗失败整体失败。LLM 网络异常(非内容异常)有 3 次指数退避重试(100ms 基数)。
flowchart TD
A[live_asr 原始 JSON] --> B[FormatUtterancesForAPI 分句]
B --> C1[60min 窗口 → LLM 主题分段 → 合并间隙 → 硬校验]
B --> C2[25min 窗口 → LLM 段落划分 → 修复重试 → 硬校验]
C1 --> D1[asr_summaries]
C2 --> D2[asr_paragraphs 含 words]
C1 & C2 -.调试.-> E[asr staging 落盘]
实现
这是全仓库 LLM 工程化最深的代码,关键机制逐个看:
- 窗口化(Map-Reduce):
asrLLMWindowMaxRunes = 16000限制单窗 prompt 的字符预算,长直播切成多窗并行(8 并发上限)再归并——既绕过上下文限制又控制延迟。窗口大小两档(总结 60 分钟 / 段落 25 分钟)对应不同任务的粒度需求。 - 提示词即契约:两个系统提示词把输出格式、字段、约束(title 长度、区间全覆盖、说话人唯一、200 字上限)写得极其具体,甚至给了坏例(「坏例:今天给大家介绍优惠活动」)。这些约束与 model 层常量(第 10 章 ASRSummarySegment 注释)一致——同一套业务约束在提示词、数据模型、服务端校验三处对齐。
- 服务端硬校验兜底:模型说的不算,代码验证的才算——时长 5~60 分钟之外丢弃、索引越界截断、段落 <200 字超限按句号兜底拆分(
asrParagraphMaxRunes)。LLM 输出永远被当作「建议」而非「事实」。 - 修复轮次:段落划分的输出若校验失败(有遗漏/重叠),把错误描述拼进 RepairPrompt 再问一次(debug 结构里的
RepairPrompt/RepairRaw字段记录这轮),一轮修复不行才失败——比盲目重试省 token 且收敛快。 - 传输与内容错误分流:
asrLLMTransportMaxAttempts=3只对网络/接口异常重试;内容不合格走修复轮次而不是重试——两类失败的处理路径完全不同。 - 调试结构 asrLLMWindowDebug:每窗的 user_prompt、原始响应、修复轮、最终区间全部落盘(接第 28 章的 recorder)——任何段落划分争议可以完整回放。
📌 设计决策
- 段落约束「单说话人 + ≤200 字 + 全覆盖」直接服务 AI 切片:切片消费段落(第 36 章),段落粒度决定 LLM 选段的可操作性与 clips 回写精度。
- 主题分段允许「不覆盖全文、区间可相交」(提示词第 5 条)而段落必须全覆盖——前者是目录(挑重点),后者是语料(要完整),约束差异源于用途差异。
- LLM 失败不阻断 ASR 主结果:转写文本已入库(live_asr),后处理失败素材仍是 completed 但段落空——核心资产优先落袋,增强层可重跑。
代码示例
常量族——所有预算与阈值集中声明:
// internal/service/live_material_asr_postprocess.go(节选)
const (
asrLLMWindowMaxRunes = 16000 // 单窗 user 侧句段列表最大 rune 数
asrParagraphMaxRunes = 200 // 段落正文上限;超限按句号兜底拆分
asrSummaryMinDurationMs = int64(5 * 60 * 1000)
asrSummaryMaxDurationMs = int64(60 * 60 * 1000)
asrSummaryMergeGapMs = int64(2 * 60 * 1000) // 同主题合并间隙
asrSummaryTitleMaxRunes = 6
asrParagraphWindowMs = int64(25 * 60 * 1000)
asrSummaryWindowMs = int64(60 * 60 * 1000)
asrLLMTransportMaxAttempts = 3 // 仅网络/接口异常重试
asrLLMWindowParallelism = 8 // Map 阶段多窗并行上限
)
段落划分的系统提示词(约束的可执行形态):
const asrParagraphsSystemPrompt = `你是直播 ASR 段落划分助手。根据带编号的 ASR 句段列表,将全文划分为连续段落。
要求:
1. 只输出一个 JSON 对象,格式严格为 {"items":[...]},不要输出其它文字或 markdown。
2. items 每项格式:{"start_index":0,"end_index":3},为句段编号闭区间(含两端)。
3. 所有编号必须恰好覆盖输入中的全部句段一次:无遗漏、无重叠。
4. 每个区间内只能有一个说话人(speaker 相同)。
5. 每个区间拼接后的正文字数必须小于 200 字。
6. 按时间顺序输出区间。`
小结
- 两条 LLM 流水线:主题分段(60min 窗、可相交)与段落划分(25min 窗、全覆盖)。
- 容错四层:窗口化预算、传输重试、修复轮次、服务端硬校验。
- 提示词/模型注释/校验代码三处约束对齐,LLM 输出被当建议不当事实。
思考题
- 修复轮次只做一轮,若加多轮修复,如何设计「失败原因分类 → 不同修复策略」的收敛机制?
- 段落 200 字上限与 AI 切片选段粒度的关系是什么?调到 400 字会改善还是恶化选段质量?
项目信息
- GitHub仓库:Chyona/live-mixer