AI会议纪要助手源码解析:从语音识别到LLM结构化的完整链路
简介Cluelessly 是一套面向开发者和产品团队的 AI 会议纪要助手源码以实时转录、动作项提取、对话存储和见解生成为核心解决多人会议后信息散落、难以复盘与跟进的问题。资源共 347 个文件zip 压缩包约 65.05MB其中 155 个 Vue 文件覆盖前端交互与组件逻辑77 个 PHP 文件构建 Laravel 后端服务另含 TypeScript 类型定义、Shell 部署脚本、Markdown 文档及 SQLite 本地数据库存储整体按桌面端、后端、前端与配置文件分区结构清晰、便于定位。当前已有 115 人学习/下载适合具备 PHP 与 Vue 基础、希望深入实战项目的开发者参考。这套源码中读者可以获得完整的 Laravel 12 与 Vue 3.5 桌面应用实现包含 OpenAI Realtime API 集成示例、会议上下文保护机制、前端 TypeScript 类型定义与构建配置以及部署所需的环境配置示例、命令行入口等配套文件。借助这些内容既能快速搭建一套可运行的 AI 会议助手也能学习桌面端、前后端与 AI 服务结合的系统设计思路适合二次开发或学习参考。1. Cluelessly 这个 AI 会议纪要助手到底在解决什么问题很多团队买回来的“AI 会议纪要”工具实际只是把转写文本丢给大模型“总结一下”结果生成的纪要既没有说话人归因也没有时间锚点会后想回听原话还得满屏翻找。Cluelessly 这类带源码的 AI 会议纪要助手价值不在“摘要”这一步而在于把录音、转写、说话人分离、结构化输出整条流水线串起来让纪要能直接当作项目资产去用。它面向三类人被会议纪要淹没的项目经理、要给团队搭内部工具的后端工程师以及想研究 ASR 与 LLM 结合链路的学生。如果只想要一个能用的云端纪要买现成的更快但如果你想知道每一步怎么拆、参数怎么调、翻车怎么修这套源码思路值得完整走一遍。2. 一条会议纪要的完整链路从录音流到结构化文档2.1 转写层与结构化层为什么必须分开选型拿到“AI 会议纪要助手源码”后第一步不是急着改代码而是把链路拆成三层采集层负责拿到合格的音频文件转写层负责把音频变成带时间戳的文本结构化层负责把文本整理成会议结论。常见做法里转写层用本地 ASR 模型结构化层用 LLM。很多同学的第一反应是“既然用了大模型为什么不直接让它听音频”这个想法在成本和效果上都走不通主流 LLM 对多人口音、会议室混响、叠话场景的理解还不够稳而且单次输入的音频时长有限一段 40 分钟会议直接塞进去模型可能只盯着前 10 分钟。分开选型的理由有三条。第一ASR 模型在处理中文会议上的延迟更低本地跑还能避开音频文件外传的合规问题。第二转写层和结构化层对模型的要求完全不同转写要的是“逐字准”结构化要的是“归纳稳”硬用一个模型两头兼顾往往两头都差。第三分开之后可以独立替换今天用开源转写模型明天换更强的商业 API结构化层的 Prompt 和输出协议不用跟着动。这种多个模型各干一段的架构其实就是多 AI 协作落地时最典型的形态。转写层和结构化层之间靠一个中间协议衔接通常是一份带时间戳的 JSON 文本段数组。转写层只承诺“这段音频从第几秒到第几秒说了什么”不负责判断“这句话重不重要”判断丢给下一层。这样设计的好处是两层可以单独测转写出错就修 ASR 参数结构化出错就修 Prompt不用每次都在整条链路上猜黑匣子。2.2 说话人分离纪要里“谁说了什么”的源头会议纪要和普通摘要的本质区别是“谁说了什么”。没有说话人信息的纪要读起来像一段匿名评论项目复盘时根本没法追责。处理这个问题的常见做法是 Speaker Diarization说话人分离先从音频里切出若干声纹簇再和转写文本做时间对齐给每段文本贴上 spk_id。这一层的技术选型比较固定主流方案基本都在做两件事先把纯音频段和静音段分开再对有声段做聚类。聚类这一步会用到嵌入特征和谱聚类参数上最重要的是 min_speakers 和 max_speakers。给得太宽模型会把两个人的声音聚成一类纪要里就只剩两三个说话人给得太窄模型会把一个人在不同情绪下的声音拆成两个人纪要里凭空多出四五个人。我一般会用会前议题人数做参考比如参会 5 人min_speakers 设 3max_speakers 设 5宁可少拆也不要多拆因为后续 LLM 还能通过语义修正角色称呼但多拆出来的“幽灵说话人”会让结构化结果彻底混乱。说话人分离还会直接影响时间戳的可读性。很多纪要工具只输出“第 12 分钟张三说”这是把每个说话人最长的一段连续发言当作锚点忽略了他中途被别人打断的情况。实际做的时候我会把一段连续发言再按下一次说话人切换切成子块每条子块都带自己的起止时间这样回听定位能精确到一句话而不是半页。2.3 输出协议一份能用纪要么长什么样结构化层最容易被低估很多源码默认输出一大段 Markdown看起来漂亮但没法被系统消费。我在自己的方案里会把输出定义成五段固定结构会议背景、决议列表、待办列表、争议点列表、时间索引。决议和待办是机器能解析的字段背景和时间索引是给人看的。字段设计上一条待办至少要包含负责人、动作、截止时间、证据文本四项。负责人不能靠猜要从说话人分离结果里绑定如果声纹簇没有对应到名字宁可留空让用户补也不要让模型编一个。证据文本则是从转写原文里摘出来的那句话通常限制在 50 字以内用来做会后回听的跳转锚点。下面是字段参考。字段类型必填说明summarystring是决议的概括控制在 80 字内speaker_idstring是对应 spk_id 的显示名timestampfloat是决议首次出现的录音秒数quotestring是原文摘录用于定位和校验action_itemsarray否子任务列表可嵌套confict_reasonstring否争议双方的核心分歧点这层输出协议定了之后上面所有层的代码都不用动只改结构化层的 Prompt 就能适配不同团队的习惯。比如研发团队会关注技术方案取舍销售团队会关注客户承诺时间这些都是在结构化层做配置而不是去改转写逻辑。2.4 源码工程通常的骨架配置位比功能位更值钱拿到一份署名“Cluelessly - AI 会议纪要助手源码”的工程公开版和自用版差异会很大但骨架基本是五块采集端、转写服务、结构化服务、导出模板、配置文件。采集端负责读取录音文件或直接监听会议系统输出流转写服务包装 ASR 模型结构化服务调用 LLM导出模板负责把结构化 JSON 渲染成 Markdown 或邮件配置文件把模型名、密钥、路径等全部外置。我在自己的方案里会把接口按可替换模型设计不直接依赖某一家厂商的 SDK。转写和结构化都走抽象类现场只需要改 config.yaml 里两个字段就能从本地模型切到云端 API。这个小设计带来的好处是当某个模型服务涨价或升级时整条链路只需要重新验证单个接口的输入输出不用重新测全流程。有些人会觉得这只是工程洁癖但经历过一次云厂商接口大版本升级导致纪要格式全乱的教训之后你会发现这层抽象是后悔药。3. 把源码跑起来本地最小可复现的 Python 实现3.1 准备环境与依赖只需要 Python 3.10 和三行命令说实话AI 会议纪要助手的源码再花哨核心依赖也就两个一个 ASR 模型一个 LLM 客户端。我这里用 Python 3.10 作为基线版本因为绝大多数 ASR 库和 OpenAI SDK 在 3.10 上的兼容性最稳3.11 偶尔会遇到二进制包编译问题3.9 又少了语言特性。安装命令如下。pip install openai-whisper openai pyyaml不要贪多装全套的语音处理库。很多项目把 pydub、librosa 都拉进来最后发现根本用不上还增加了音频格式解码的兼容性负担。openai-whisper 自带 ffmpeg 的调用封装openai 库负责调用 LLMpyyaml 负责读配置三个依赖足够跑通最小闭环。如果你对部署体积有要求可以把 openai-whisper 换成 faster-whisper推理速度快一些但接口有差异后面所有示例我以 openai-whisper 为准来写。3.2 转写并带时间戳第一个能跑的脚本转写是整个链路里最需要稳定输出的环节。下面这段代码是整套方案的地基读入音频文件模型推理输出带起止时间的文本段。参数我直接写在函数里方便你复制后改成自己的路径。import whisper model whisper.load_model(medium) # 中文会议建议 medium 起步 result model.transcribe( meeting_20250723.wav, languagezh, # 固定语言别让模型自动猜 fp16False, # CPU 必须关GPU 可开 verboseFalse, ) segments [] for seg in result[segments]: segments.append({ start: round(seg[start], 2), end: round(seg[end], 2), spk: None, # 说话人字段稍后由 diarization 填充 text: seg[text].strip(), }) print(f共识别 {len(segments)} 段文本) open(segments.json, w, encodingutf-8).write(json.dumps(segments, ensure_asciiFalse))这里的语言参数是第一个关键坑中文会议一定不要用自动检测。自动检测模型在前几秒出现英文开场白或音乐时会把整段识别切到英文模式输出一堆拼音和错误标点。设置了 languagezh 之后无论中间出现几个英文技术名词模型都会保持中文语法框架去解析。fp16 这个参数如果你是在 CPU 上跑铁定要设成 False否则直接报精度错误如果是 NVIDIA 显卡开 fp16 能让 medium 模型的推理速度快一半但显存低于 6G 时容易溢出需要降到 small。3.3 用 LLM 把转写变成纪要提示词与结构化输出转写完成之后下一步是把 segments 拼装成可读文本交给 LLM 做结构化。这里有一个容易忽略的细节不要让 LLM 自己拼接长文本要在代码里把每段文本前标上序号和时间LLM 只需要处理“已经有格式”的输入出错率会显著下降。from openai import OpenAI client OpenAI() # 读取环境变量中的密钥 transcript for idx, seg in enumerate(segments): transcript f[{idx}] [{seg[start]:.0f}-{seg[end]:.0f}s] {seg[text]}\n prompt f 你是一名会议纪要整理助手。下面是带时间戳的转写文本。 请提取决议、待办和争议点为每一条附上原始时间戳和原文摘录。 不要添加转写中没有的内容。 输出 JSON格式为: {{decisions:[{{summary:,timestamp:0,quote:}}], action_items:[{{owner:,task:,deadline:,timestamp:0}}], disputes:[{{topic:,sides:[,],timestamp:0}}]}} 转写文本: {transcript[:12000]} response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0.2, response_format{type: json_object}, ) print(response.choices[0].message.content)transcript 字段截断到 12000 个字符这是我在实践中验证过的安全范围。以 8k 上下文窗口的模型为例输入占了 6000 左右的 token输出 JSON 留 2000 到 3000 token模型有足够空间展开思维链又不会因为太长而丢尾巴。temperature 设 0.2 是为了让输出格式稳定设太低会显得机械设太高格式很容易跑偏。此外这里的“转写模型负责转录、大模型负责提炼”就是很典型的双模型协作方式两个模型各自发挥优势效果比单模型硬扛要好。4. 四个必调参数切分窗口、Prompt、说话人人数与时间锚点4.1 切分窗口先决定一段喂给模型多少字参数调优的起点不是 Prompt而是切分窗口。现在的上下文窗口基础理论很简单模型在窗口内的注意力会随着文本长度增长而稀释超过一定比例后中间部分容易被忽略末尾部分反而被“记住”。所以不管模型说明书上写“支持 128K 上下文”我一般只用它 30% 到 50% 的容量。给 8k 上下文的模型输入限制在 12000 到 15000 字符输出预留 2000 到 3000 token。下面是具体的估算关系供你抄完直接用。# 输入字符上限 ≈ (context_token - output_token) × 1.6 # 例如 8k 上下文输出留 2.5k token则输入可用 5.5k token # 5.5k token 约等于 8500 到 11000 字符中文按每 token 1.5 到 2 字估算如果会议转写文本超过这个上限常见做法是先按时间窗口切成多个块每块单独做结构化得到多个“纪要片段”再通过一次合并 Prompt 把片段汇总成最终纪要。切分时每块之间保持 30 秒到 60 秒的重叠防止一句话被从中间断开后上下文丢失。这个重叠时间值看起来是玄学但如果你把重叠设成 0测试几次就会发现被切在边界的决策要点隔三差五就会漏掉。4.2 Prompt 设计让模型“先摘录、再归纳”结构化 Prompt 最容易犯的错是直接问“这段会议的重点是什么”这种开放式问题会诱导模型编出“会议很成功团队士气高昂”之类的空话。我会给模型两条硬约束第一每条结论必须附带原文摘录第二如果没有对应原文就标记为“无来源”。这两条约束能有效压制幻觉因为摘录本质上逼着模型回到文本里找证据。一份完整的中文 Prompt 模板我建议至少包含下面几个部分。角色设定你是产品团队的会议纪要助手 输入约定每行包含序号、时间、说话人占位符和原文 输出协议JSON schema字段名固定 证据要求每条结论必须带 quote 原文 禁忌不得添加转写中不存在的议题;不得把玩笑或寒暄作为决议这里把“说话人占位符”放进去是很关键的一步。如果转写层已经做了说话人分离文本那一行会带着 [spk_0] 这样的标记Prompt 里要求模型引用时保留这个标记后续做人员归因就非常方便。如果还没做分离宁愿留空也不要让模型猜人名否则后果就是给虚构人物派活。4.3 说话人绑定参数min_speakers 和 max_speakers 怎么给说话人分离是链路里最需要凭经验调的部分。没有源码题可以参考时我就用一个笨办法先拿会议前 3 分钟做试跑听一遍实际有几人在说话然后以此为准设范围。比如试跑确认是 5 个人就设 min_speakers4、max_speakers5这个范围会让聚类结果相对保守宁可合并相近声纹也不拆出不存在的说话人。这里给一个参数表是几个典型场景的推荐值。场景推荐 min_speakers推荐 max_speakers备注双人产品评审22固定值防止把一方一分多五人项目周会35实际可能有人全程不说话十人跨部门对齐58远程会议声纹差异小用户访谈录音13访谈者可能被静音过滤min_speakers 设短了会带来一个麻烦两个人声纹相近时聚成一类纪要里所有干活的任务都算到一个人头上。这种情况的修复代价很高因为下游结构化层已经生成了带 spk_id 的字段再想拆只能重跑转写。所以我的经验是宁可先让 min_speakers 偏小取保守值后面通过看三个方向的数据来确认是否要重跑spk_id 是否超过预期、单人发言总时长是否异常、表决类内容是否集中在一个簇里。4.4 时间锚点把录音秒数映射回会议系统时间最后一个是常被忽略的时间锚点。ASR 输出的 start/end 是相对音频文件的秒数而人们在会上记录的是“上午十点张三说了一个方案”。如果不做对齐导出纪要里的 timestamp 和日历会议时间对不上回听定位功能就废了。最常见的对齐方式是拿音频文件里的一个基准点做偏移计算。比如会议开始有人说了句“开始吧”音频文件里它出现在 5.3 秒处而会议系统里这场会议开始时间是 09:30:00那么偏移量就是 09:30:00 - 5.3秒。所有后续 timestamp 加上这个偏移就得到日历时间。这个偏移值需要做成可配置项因为会议系统录屏有延迟Zoom 和本地方案之间通常差 1 到 2 秒。不配置直接硬编码换一个录制环境纪要的时间就全错。# 偏移公式 会议时间 录音秒数 offset_seconds # offset_seconds calendar_start_time - audio_start_moment # 示例: 录音第 5.3 秒对应 09:30:00, offset 34200 - 5.3 34194.7实测环境里能到秒级一致就很够用了不需要追求毫秒级因为纪要回听的粒度本来就是“大概在那个时间附近”精确过头反而让人找不到。这个参数我没见过几个开源项目默认配好通常都要自己改。5. 避坑五条让纪要翻车的真实问题与排查路径5.1 长会议过半纪要开始胡说上下文窗口被塞满现象前 20 分钟的内容摘要质量很高到第四十分钟时纪要里开始出现“与会者一致认为”这种套话甚至出现了根本没讨论过的结论。原因转写全文被一口气灌进 LLM输入超过窗口的 60% 之后模型开始丢失中间段的注意力用通用话术补位。解决把转写文本按时间窗口切成多段每段独立结构化再做一次汇总。汇总 Prompt 里明确带上每段的摘要结果而不是把这 40 分钟重新输入一遍。5.2 全场只有一个说话人说话人分离失效现象转写文本里明明有“那边”“我觉得”“我补充一下”但所有段落都归到同一个人。原因音频被转写成单声道后声纹特征被压缩或者 VAD语音活动检测把所有静音段都切掉之后聚类算法丢了足够多的特征点。解决先把录音检查一遍确认不是压缩太狠的低码率文件然后缩小 min_speakers 和 max_speakers 的范围很多人话音调接近时聚类算法会把它们当成同一个人固定范围后准确率会明显提升。5.3 时间戳对不上回听找不到原话现象纪要上写着“12:03 提出预算调整”回听录音时这段内容出现在 12:18。原因会议录制系统本身有延迟或者录音文件在导出时被剪辑过ASR 返回的时间戳是相对音频文件内部的不是相对会议日历的。解决按 4.4 节的方式配置 offset_seconds用开场第一句话做校准点。这个参数要允许在会议开始后手动修正因为有些系统在唤醒后还会继续缓存缓冲一两秒的偏差会随时间累积成几分钟。5.4 客户名全被写成同音错字GLS 表没做现象客户公司“未动科技”被转写成“威动科技”产品名“天枢”被写成“天书”LLM 结构化时完美继承了错字。原因ASR 模型对专有名词没有任何先验知识低频词组识别准确率很低而且 LLM 拿到的转写文本就是错的再聪明的模型也无法凭空纠错。解决在转写前后各加一层处理先在 ASR 文本后处理时用同音词替换表把已知专有名词替换回去再把专有名词清单写进结构化 Prompt。这一步叫 glossary 定制几乎所有商用会议纪要系统都有开源项目里反而是空白。5.5 纪要里全是寒暄和争吵没有结论现象生成的纪要记录了“大家早上好”“感谢张工的介绍”“两个人对排期争议很大”但读完不知道这个会最终定了什么。原因结构化 Prompt 没有给内容分优先级模型把对话里的社交行为和情绪表达都当成有效信息。解决在 Prompt 里加内容优先级规则明确“决议优先级高于争议争议高于待办待办高于寒暄”并且要求模型先把带“拍板、决定、就这么定”这类词汇的句子优先提取。优先级规则一旦生效纪要从流水账变成资产这一步往往比换更大的模型效果还明显。6. 进阶用自动化脚本验证纪要质量把流程接成可控的 agent6.1 自动质量打分给纪要挑刺的脚本用多了之后你会发现LLM 生成纪要的质量是有波动的不能每次开会都手动读一遍检查。我的习惯是写一个打分脚本用三个硬指标筛一遍输出待办是否有负责人、是否有来源时间戳、引用内容是否在原文里存在。这三个指标能过滤掉大多数幻觉和空话。import json def score_summary(gold, summary): score 0 # 指标1: 待办覆盖率 gold_items {i[task] for i in gold[action_items]} out_items {i[task] for i in summary[action_items]} score len(gold_items out_items) / max(len(gold_items), 1) * 40 # 指标2: 时间戳可回验 has_ts sum(1 for i in summary[action_items] if i.get(timestamp) is not None) \ sum(1 for i in summary[decisions] if i.get(timestamp) is not None) total len(summary[action_items]) len(summary[decisions]) score has_ts / max(total, 1) * 30 # 指标3: 引用覆盖 quote_hits sum(0 if not i.get(quote) or len(i.get(quote)) 4 else 1 for i in summary[action_items] summary[decisions]) score quote_hits / max(total, 1) * 30 return round(score) gold json.load(open(gold.json)) summary json.load(open(output.json)) print(得分:, score_summary(gold, summary))这个脚本本身是给人看的不是给机器看的。我通常会让结构化服务每生成一份纪要就自动跑一次打分低于 60 分则自动触发一次“重生成”并带着低分原因作为额外提示词。这种把失败反馈回传给模型的机制比盲目调 temperature 有效得多因为模型能看到自己哪个字段不达标第二次生成会刻意补齐。整个链路从“录音进来、纪要出去”变成了一个半自动的 AI agent 流程。6.2 增量纪要不重新跑全文只追加新结论连续开很久的项目例会纪要会越滚越长每天重新跑整场录音不是聪明的做法。更好的方式是按天增量生成第一次跑生成全量纪要后续每次只把新增的转写文本结构化然后做一个“合并摘要”操作让模型比对旧纪要和新结论只更新有变化的部分。这样省 token 是其次重要的是避免了多天会议汇总时出现“昨天和今天结论互相矛盾”的错乱。合并时要让模型优先保留时间更晚的结论并标注每个决议的提出日期方便跟进人追查上下文。6.3 把命令接成一个自动化链条做到这一步你已经有了转写脚本、结构化脚本、打分脚本、增量脚本把它们串起来的方式很简单一个入口函数检查 raw 目录下的新音频按文件名排序调用上一章的函数最后把 JSON 渲染成 Markdown。这个入口函数不需要做成服务做成定时任务就够了。我自己实践下来这种批处理方案的稳定性远高于常驻服务没有要维护的进程没有异常恢复逻辑出了问题看任务日志一目了然。整个方案到这里已经可以作为一个团队工具落地真没必要一开始就上微服务。我做这套流程时的最大教训是永远不要信任某一次 Prompt 的输出一定要让结构化前保留原文摘录这样出错时至少能顺着引用找到那一句原话。纪要工具本质是给人省时间的如果生成的纪要还需要人去猜来源那它就是在浪费时间。希望这个方案能帮你在下一次会议后直接拿到能派活的项目资产而不是另一份需要返工的流水账。本文还有配套的精品资源点击获取