资讯动态

视频课程自动摘要与知识点提取流水线:ASR+LLM工程实践

发布时间:2026/10/2 12:02:56 来源:尧图企业网站定制
做视频课程自动摘要和知识点提取这条流水线我前前后后折腾了三个多月其中一版方案彻底推翻重来。最开始我也以为很简单拿ASR把视频转成文字再丢给LLM让它整理一下就是自动摘要了。结果真正试完才发现LLM拿到一段乱七八糟的转写文本后生成的摘要既不像摘要也不像提纲甚至会一本正经地给你编课程里完全没讲过的定义。后来反复调了几轮才明白这个项目的关键不在某个单一模型而在ASR和LLM之间那一大段没人替你做的“脏活累活”清洗、标点修复、术语纠正、文本切分、时间戳映射、输出校验。这篇文章就把我最终落地的架构、每一步的选型理由、以及调试过程中踩过的坑完整写出来给正在做视频课自动化、培训内容结构化、知识库建设的朋友一份可以直接参考的工程笔记。1. 先想清楚这条流水线到底在解决什么问题1.1 为什么是 ASR LLM 两段式而不是一键工具市面上确实有不少“一键生成课程笔记”的产品我也在早期试用过几个。它们背后的逻辑通常是开发者在内部把ASR转写和LLM摘要封装成一个黑盒用户拿到的是最终输出中间层层细节完全不可控。对个人用户来说偶尔处理一两个视频这种工具确实够用。但一旦进入生产环境问题马上暴露出来。我自己试过用这类工具处理几十个小时的课程遇到的典型问题非常一致同一个视频课程第一次跑和第二次跑摘要结果有时会有肉眼可见的差异领域术语一旦被语音识别识别错整个知识点的表达就会跟着错你根本不知道去哪里干预课程里某个关键概念出现在视频的第几分几秒工具给不出准确的时间戳想单独修复ASR的错误或者单独调整摘要风格也没有任何接口。所以我最终放弃了“全家桶”式的封装老老实实采用ASR LLM 的两段式架构中间自己写清洗和切分逻辑。这样做的价值有三个第一可调试性每一级输入输出都落盘出问题能定位到具体环节第二可替换性ASR模块和LLM模块互相解耦今天用A家的语音识别明天想换B家的只动一个接口第三可重跑性单独一个环节挂了不需要从头把视频再转一遍断点续跑成本低很多。一句话总结我的架构思路ASR负责把声音变成“带时间戳的结构化文本”LLM负责把文本变成“结构化的摘要与知识点”中间的所有加工都是为了让这两个模型各自在最舒服的输入条件下工作。1.2 一条可落地的流水线怎么拆我把整个流程拆成了八个环节每个环节只干一件明确的事环节输入输出关键技术点1. 音轨抽取MP4/MKV视频纯音频文件ffmpeg提取避免直接转ASR时解压视频流浪费资源2. 音频预处理原始音频标准化音频单声道、16kHz采样率、响度归一化3. ASR转写标准化音频分段文本时间戳需要segment级时间戳最好有word级4. 文本清洗原始转写干净文本标点修复、同音词纠正、断句合并5. 文本切分干净文本多个chunk按语义段落和token预算共同约束6. 逐段LLM处理每个chunk段落摘要知识点结构化输出JSON模式7. 摘要合并各段摘要全文摘要分层摘要或一次长文合并视课程长度定8. 结构化输出合并结果Markdown/JSON关联时间戳、生成目录层级这个拆法最关键的一点是每个环节之间用磁盘上落地的JSON文件作为交接物而不是让数据一直在内存里以一个“大对象”的形式流动。这样做在初期看起来有点笨但等到某一步报错、需要重跑的时候你会感谢当初这个决定。后面我会在断点续跑那部分详细说。2. 工具选型把每一环的“性价比”算明白2.1 ASR引擎本地模型还是云端APIASR选型这件事我一开始走了不少弯路。总想找一个“中文准、英文也准、带标点、带时间戳、还能免费跑”的方案后来意识到这种完美的引擎不存在只能根据自己项目的实际约束做取舍。我做的主要是中文课程但课程里夹杂大量英文技术名词所以重点关注三件事中文普通话的准确率、英文术语的保留程度、以及时间戳的精度。我把主流的ASR方案拉在一起做了个对比方案中文准确率英文识别时间戳成本隐私要求适合场景Faster-Whisper本地高很高segment/word可选仅硬件投入数据不出内网批量离线处理FunASR Paraformer本地很高中等有仅硬件投入数据不出内网中文普通话为主商用云端API各家很高很高有按小时/次数计费数据出库处理频率低、快速出结果我最终选的是Faster-Whisper模型用的large-v3计算精度float16。理由很简单本地部署可以用GPU并发批量跑长时间课程的成本几乎是零而且它能直接输出带时间戳的segments回调接口设计也干净。对中文优化更强的FunASR在我这里作为备选专门用来处理那种“主讲人口语特别重、普通话不太标准”的课程。如果你只是内部试用不一定需要上large模型。Faster-Whisper的medium甚至small模型在中文普通话语速正常的课程上已经不错处理速度会快很多。我建议先拿两三个有代表性的视频分别用小模型和大模型各跑一遍做对比别凭参数猜。2.2 LLM模型不要一上来就上最大参数LLM的选型比ASR更需要克制。很多朋友看到摘要任务就觉得“模型越大越好”实际跑下来你会发现对自动摘要和知识点提取来说上下文窗口长度、输出格式的稳定性、接口的并发限制、单位Token的成本这四样东西的影响通常比模型本身的推理能力要大。线上环境我优先用云端API主要看中三点响应速度稳定、不需要自己维护推理服务、JSON结构化输出支持得比较好。模型方面我在不同阶段试过DeepSeek、通义千问、GLM和GPT-4系列最终线上主力用的是国产开源模型在云端部署的接口中文摘要效果好、成本也低。本地测试则用Ollama或vLLM跑小尺寸模型来调试prompt等prompt基本定型后再切到线上大模型做最终生成。这里有一个很实际的建议在选LLM之前先把你的输出格式定死。比如你要让LLM输出JSON就必须确认这个模型对JSON Mode或者结构化输出的支持是否稳定。有些模型聊天能力很强但让它严格输出一段合法JSON经常会有多余的解释文字这在流水线里就是灾难。我自己的经验是单纯“摘要写得好”的模型不一定是好选择必须优先选“格式守得住”的模型。2.3 编排框架自己写脚本还是上框架这一步我纠结了很久。早期我尝试过用Dify这类可视化平台来搭知识库流水线确实拖拽节点很爽但你一旦要处理“ASR结果异常为空”“某个chunk调用失败要重试三次”“LLM返回了非法JSON需要回退解析”这类条件逻辑可视化编排就会变成一团乱线。后来我把编排层改成了纯Python脚本加并发线程池中间结果全部落盘。不是说框架不能用而是你要分清边界框架适合做固定流程的可视化拼装和展示但业务逻辑里一旦充满分支判断和错误处理原生代码的灵活性和可排查性一定更强。我现在的结构是一个pipeline.py负责调度全流程按步骤编号执行每一步的执行结果单独存成一个JSON文件文件名带上任务ID和步骤ID如果某一步失败重新执行时检查JSON是否已存在存在就跳过并发逻辑只放在“切分后的chunk”这一层因为chunk之间天然没有依赖关系这套结构让我后面换模型、改prompt、调并发数的时候都没有推翻重写过。框架可以学、可以用但别让框架替你决定业务分支。3. 实操过程从MP4到结构化摘要3.1 音轨提取与预处理别在源头埋雷第一步看起来没什么技术含量但很多人栽在这里。直接拿一个MP4文件丢给ASR模型模型内部也要先解出音频流再做识别这个过程既慢又容易受视频编码影响。所以我永远先单独抽取音轨。ffmpeg -i lecture.mp4 -vn -ac 1 -ar 16000 -f wav -y lecture_16k.wav这条命令做了三件事丢弃视频流-vn、强制单声道-ac 1、把采样率降到16000Hz。为什么是16kHz因为主流ASR模型的训练数据基本都在16kHz采样率附近高于这个值的信息对识别没有帮助反而增加解算量低于这个值则可能丢失有效频段。注意不要做激进降噪我试过给音频加highpass、lowpass滤波器结果反而把一些轻声细语的解释性话语给滤掉了。ASR模型本身就见过各种室内噪声普通课程场景里的环境音它基本能扛住你强行“帮他”降噪往往帮倒忙。如果原始音轨的音量偏小或者飘忽不定还可以加一段响度归一化ffmpeg -i lecture.mp4 -vn -ac 1 -ar 16000 -af loudnormI-16:LRA11:TP-1.5 -f wav -y lecture_norm.wav响度归一化对ASR准确率的影响在部分设备录制的课程上非常明显有的老师讲课时离麦克风忽远忽近不归一化会出现连续漏字。3.2 ASR转写拿到带时间戳的干净文本转写这一步我用Faster-Whisper代码非常简洁from faster_whisper import WhisperModel model WhisperModel(large-v3, devicecuda, compute_typefloat16) segments, info model.transcribe( lecture_16k.wav, languagezh, vad_filterTrue, vad_parametersdict(min_silence_duration_ms500), word_timestampsTrue, ) results [] for seg in segments: results.append({ id: seg.id, start: round(seg.start, 2), end: round(seg.end, 2), text: seg.text.strip(), })这里有两个值得说的地方。第一个是vad_filterTrue。VAD是语音活动检测它会先把大段静音、音乐、掌声之类没内容的片段切掉再交给识别模型。这么做不仅能大幅缩短转写时间还能避免ASR在静音处产生一些无意义的“幻听”文本。我遇到过最典型的情况是一段课程在开头放了30秒等待音乐没开VAD时ASR把音乐识别成一段莫名其妙的“歌词”后面LLM差点把这段当课程内容总结进去。第二个是Faster-Whisper的segments是一个生成器真正的解码是在你遍历它的时候才发生的。很多人发现转写脚本“半天不出结果”其实不是卡住了而是遍历还没走完。这在批量处理长课程时容易让人误判我一般会在遍历时打印进度避免反复手动取消任务。word_timestampsTrue我建议视场景开。做摘要本身用段级时间戳就够但如果你后面要给知识点标题做“点击跳转到视频对应位置”的功能word级时间戳会更有用。代价是转写耗时显著增加所以我是把这项做成可选参数不是每个任务都开。3.3 清洗与切分先做“LLM友好化”从ASR拿到的原始文本直接把“你好大家好欢迎来到本节课”这种无标点长文本丢给LLM效果会很差。LLM可以用上下文推断断句点但它会把大量“推理能力”消耗在补标点上留给真正语义理解的余量就少了。所以我会先做一次轻量清洗再做切分。清洗的核心是两类操作。第一类是把ASR凑出来的断句碎片合并成完整句子比如前一句以逗号结尾、下一句开头是紧跟的话说明它可能是一个句子的两个半段。第二类是术语纠正这个最实在。比如课程里频繁出现“Kubernetes”ASR经常转写成“quineterous”或者“库伯内特斯”你可以维护一张同音词替代表在输入LLM之前先用最简单的方式替换掉。import re GLOSSARY { 库伯内特斯: Kubernetes, quarterly: Kubernetes, # 拼写纠正示例 德克亚: Docker, } def clean_transcript(text: str) - str: for wrong, right in GLOSSARY.items(): text text.replace(wrong, right) text re.sub(r([。])\s*([,]), r\1, text) return text.strip()切分这一步要同时考虑两个维度语义完整性和token预算。我最后采用的规则是优先按句号、问号、感叹号、分号这些强断句符切开再把句子按顺序拼接成chunk每个chunk控制在2000到2500个中文字符左右。因为中文在模型里大约是1个字对应0.7到1.5个token2500个字符换算下来在3000到3500个token左右再算上给LLM输出的预留空间整段输入很安全。def split_transcript(text: str, max_chars: int 2400) - list[str]: sentences re.split(r(?[。]), text) chunks, current [], for sent in sentences: sent sent.strip() if not sent: continue if len(current) len(sent) max_chars and current: chunks.append(current) current current sent if current: chunks.append(current) return chunks这里有个重要原则不要用固定字符数硬切。硬切会把一个概念的解释切到两个chunk里导致LLM看到的上下文不完整摘要里经常出现“前言不搭后语”。宁可让某个chunk稍微长一点也要保证语义边界完整。3.4 Prompt设计摘要与知识点分开生成Prompt是整条流水线里最值得反复打磨的一环。我的做法是“段落摘要”和“知识点提取”分开生成用同一个模型但用两个不同的用户消息。混在一起生成的输出经常有一方质量被另一方稀释分开之后各自的效果都会更稳定。先看段落摘要的prompt模板你是一名课程内容分析助手。请阅读下面这段视频课程转写文本生成段落摘要。 要求 1. 摘要不超过150个汉字概括本段的核心观点不要罗列细节。 2. 只能依据文本中出现的内容不得补充课程之外的知识。 3. 用词客观不要使用“精彩”“深入浅出”这类评价性词汇。 转写文本 {chunk_text}知识点提取的prompt则要求输出结构化JSON从这段转写文本中提取课程知识点输出JSON数组每个元素格式 { point: 知识点名称20字以内, statement: 一句话解释不超过40字, keywords: [关键词1, 关键词2, 关键词3], evidence: 从原文中摘录的一句话用于证明该知识点确实被讲过 } 要求 - 每个chunk最多提取5个知识点 - point名称必须基于原文术语不要自己改写专业名词 - 如果原文没有明确表述的知识点不要强行提取 - evidence必须原样引用文本中的句子不得改写 转写文本 {chunk_text}添加evidence字段是我在踩了许多幻觉坑之后想出来的强制约束。让LLM把每个知识点后面挂一句原文证据测试下来能大幅降低“捏造知识点”的概率。这个字段你可以不用在最终输出里展示但生成时必须有它相当于给模型划定了一个“禁出圈”的范围。3.5 批量调用、断点续跑与分层合并chunk之间没有依赖关系所以这层是最适合并发加速的地方。我用Python的ThreadPoolExecutor保持worker数量在4到6个。并行数不是越大越好太多并发很容易触发云端API的限流反而因为重试浪费更多时间。from concurrent.futures import ThreadPoolExecutor, as_completed def call_llm(chunk_idx: int, chunk_text: str) - dict: response client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: KNOWLEDGE_PROMPT.format(chunk_textchunk_text)}, ], response_format{type: json_object}, ) return {chunk_idx: chunk_idx, result: response.choices[0].message.content} with ThreadPoolExecutor(max_workers4) as pool: futures {pool.submit(call_llm, i, c): i for i, c in enumerate(chunks)} for future in as_completed(futures): data future.result() save_json(foutput/chunk_{data[chunk_idx]}.json, data)断点续跑的关键在于每个chunk的结果独立落盘。如果跑到第18个chunk时网络断了重跑时只需要检查chunk_18.json是否存在已完成的17个文件直接跳过。不要把这些结果堆在内存里最后一起写那等于把所有鸡蛋放在一个篮子里。所有chunk生成了段落摘要和知识点之后最后再调用一次LLM做合并。因为如果课程很长直接把所有chunk摘要拼起来让LLM通读输入token可能超限而且它会在“只见树木不见森林”的状态下输出一个不够连贯的全文摘要。分层合并的做法是先把摘要文本按每5个chunk一组让LLM合并成一组“中层摘要”再把所有中层摘要放进最终一次调用生成全文大纲和摘要。这个层级数要根据课程总时长调整1小时以内的课程通常两级就够了。4. 常见问题与排查技巧我把踩过的坑列成速查表4.1 ASR侧发音、倍速和术语坑问题现象可能原因我的处理方法中文术语反复识别错领域名词不在ASR词表中建立术语替换表或在转写后用Levenshtein距离做模糊替换课程是1.5倍速播放漏字严重ASR对快速语音不敏感预处理时用ffmpeg的atempo0.9把音频慢速还原静音片段被识别成无意义文本没有开VAD开启vad_filter并设置min_silence_duration_ms为400-600长句没有标点读起来像流水ASR模型的标点预测弱用FunASR的标点模型二次修复或交给LLM时明确要求补标点两个老师同时说话文本串行错乱多人重叠加噪音引入说话人分离工具如果成本高先接受串行标注为“多人对话”倍速这个问题是非常容易忽略的。很多机构的内部课程为了压缩时长直接压成1.25倍甚至1.5倍速ASR在这种音频上的错误率会明显上升。我实验过几种处理方案效果最稳定的是在预处理阶段用ffmpeg -af atempo0.85把音频慢放回来代价是音频变长但转写准确率能恢复不少。这是一个典型的“做一步预处理省百次排查”的案例。如果你在转写结果里频繁看到漏词、吞字先别急着换模型回到音频这一步检查一下语速。4.2 LLM侧幻觉、截断和JSON解析坑LLM侧最让人头疼的永远是幻觉和格式不稳定。幻觉的处理我前面提到了evidence字段。另一个很有效的补充手段是在调用LLM之前给出一段“反例”作为few-shot以下是一个错误示例 原文提到“提高吞吐量通常需要分布式架构” 错误输出为“分布式架构能解决所有系统性能问题”。 原因是输出扩展了原文之外的内容这是不允许的。这类反例对任何摘要提取类任务都非常有效。比单纯写“不要编造”效果强很多因为模型对“要做什么”的指令感知往往没有对“这个例子就是错的”来得强。截断问题主要出现在chunk太大或max_tokens设置过小。我见过不少同学把max_tokens设成512让LLM在输出中途戛然而止然后还以为是模型理解不了。处理方式很简单先估算预期输出长度。一个chunk大概提5个知识点每个知识点JSON字段约50到80个token再加上可能的paddingmax_tokens至少要给800到1024。宁可稍微浪费一点额度也不能让它截断。JSON解析的问题最常见的现象是LLM返回的内容里夹杂了“好的这是您需要的JSON”这类开场白。纯靠正则去解析很脆弱我建议做三级降级策略import json def safe_json_parse(text: str): try: return json.loads(text) except Exception: start text.find({) end text.rfind(}) 1 if start 0 and end start: return json.loads(text[start:end]) return None如果这个函数返回None再走重试逻辑重试时在prompt末尾追加“请只输出JSON不要输出任何解释文字”。我上线至今这一套组合方案基本能稳定接住绝大多数异常输出。4.3 流水线侧限流、重试、成本与幂等工程侧的问题往往是代码写起来简单但运行几天后才暴露。我遇到最多的三个问题依次是限流、重复扣费、中间结果丢失。限流好理解云端API都有QPS和每分钟Token限制。解决办法是给每类请求加一个速率控制最简单的可以用ratelimit库或者自己写一个简单的信号量。重试策略我固定用指数退避第一次重试等2秒第二次4秒第三次8秒再失败就标记为失败任务最后统一扫描重跑。重复扣费这个问题非常隐蔽。最早的版本里如果网络超时导致调用方没收到响应我下意识地直接重试同一条请求结果相同prompt被处理了两次账单上就会出现双倍费用。解决的方式是给每个chunk生成一个全局唯一的request_id重试时带着同一个request_id服务端如果支持去重最好不支持也能在日志里排查出来。云端模型接口大部分没有天然的幂等保障所以这个请求ID最好还是自己设计进去。中间结果丢失是另一个教训。早期我把所有结果先放在内存里最后统一写盘结果有一次跑到第50多个chunk时进程被OOM杀掉前50个全部白跑。后来改成每个chunk出结果就立刻写盘这个改动让我每天省下大量重跑时间。5. 效果评估与继续迭代5.1 用人工抽检代替ROUGE打分很多人会问自动摘要效果怎么量化。学术上确实有ROUGE指标但在生产环境里它和“这份摘要到底能不能直接用”的相关性很低。我最后的评估方案很朴素每批处理完随机抽10%的视频人工对照原文和摘要打分主要看三点——知识点覆盖率是否达标、有没有编造内容、时间戳定位是否准确。这个抽检表格我放在一个简单的Excel里字段就三个视频标题、课程时长、最终评分1到5分。坚持抽检了两周后我根据结果连续调了三轮prompt重点解决“知识点提取过浅”的问题。第二轮之前很多知识点提取出来只是原文术语的复述没有解释含义后来我在prompt里加了一句“statement字段不需要包含术语本身而是要解释这个术语在课程中的实际含义”输出质量立刻有明显提升。如果你是个人在跑这套流水线也建议至少抽几段人工看一眼。模型输出的摘要读起来“似乎还行”的时候往往半数的知识点已经偏了只有逐句对照原文才能发现。5.2 后续还能加的三个方向这套流水线跑通之后最自然的几个扩展方向我列在这里供你参考。第一做智能问答。把抽取出的知识点结构化之后可以继续接入向量库做成一个“课程知识库问答”的接口。这一步本质上只是把知识点的文本向量化并建立索引难度不大但用户价值很高很多课程平台都在做这件事。第二做说话人分离。如果课程里有多位老师或师生对话可以在ASR之前叠加一个说话人分离模型给每段文本打上说话人标签这样LLM生成摘要时能区分“谁讲了什么观点”输出会更立体。第三生成练习题。知识点的statement和evidence字段可以直接作为出题素材让LLM围绕每个知识点生成一道判断题或选择题再配合时间戳定位回原视频一个很简洁的“视频学习练习回溯”闭环就出来了。这套流水线我还在持续用每个环节都留下了重新调优的余地。如果你也在搭类似的项目我真心建议把第3步和第5步的中间结果先落盘成JSON这两步做好了后面无论换ASR模型还是换LLM都不会推倒重来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑