简介:Cluelessly 是一套面向开发者与 AI 应用爱好者的 AI 会议纪要助手源码,聚焦会议场景下的实时转录、对话分析与行动项提取,适合希望自建本地会议助理、研究 OpenAI Realtime API 落地实践的中高级开发者参考。项目采用 Laravel 12 搭配 Vue 3.5 与 TypeScript、Inertia.js 构建前后端,桌面端基于 NativePHP/Electron,AI 能力由 OpenAI Realtime API 驱动,样式使用 Tailwind CSS 4.1.1,本地对话数据以 SQLite 存储,并支持跨会话上下文保护与见解生成。压缩包共 347 个文件,约 65.05MB,其中 155 个 vue 组件、77 个 php 后端逻辑、41 个 ts 脚本构成主体,另含 md 文档、sh 脚本、json 与 yml 配置及少量图片资源,目录结构完整。已有 115 人学习关注,读者可据此理解实时转录链路、动作项抽取与桌面端集成思路,快速搭建可运行的会议助手原型。
1. 当会议录音变成一份能直接发出去的纪要
上周三下午四点,我开完一场跨部门需求评审,录音时长 47 分钟。按老办法,我得戴上耳机重听一遍,边听边敲字,整理出待办、结论、争议点,再排版成纪要发群里——这套流程走完,一个下午就没了。后来我把这段录音丢进一套本地跑的 AI 会议纪要助手,从上传到拿到结构化纪要,用了不到 90 秒,待办事项还自动带上了负责人和截止时间。
这就是「Cluelessly - AI 会议纪要助手(源码)」这个方向要解决的问题:把一段会议录音或转写文本,自动变成一份有摘要、有决议、有待办、有发言归类的纪要。它适合三类人——每天被会议纪要追着跑的项目成员、想把纪要能力集成进自己系统的开发者、以及想拿一套可改源码做二次开发的技术团队。源码在手,意味着你能改提示词、换模型、调输出格式,而不是被某个闭源工具卡死。
2. 会议纪要助手的技术链路:从音频到结构化文本
2.1 为什么是「转写 + 大模型抽取」两段式,而不是端到端
很多人第一反应是找一个能直接「音频进、纪要出」的模型。我试过,翻车概率很高。原因在于会议音频里混着口音、多人抢话、专业术语,直接端到端做,模型既要扛声学识别又要扛语义归纳,任何一环出错都会污染最终结果,而且你根本不知道错在哪。
稳妥的链路是拆成两段:第一段做语音转文字(ASR),把音频变成带时间戳的文本;第二段用大语言模型(LLM)对文本做结构化抽取。这样拆的好处是每一段都能单独验证——转写错了,你去看 ASR 结果;抽取错了,你去调提示词。常见做法是 ASR 用本地可部署的开源方案,LLM 用 API 或本地量化模型,两者解耦。
提示:如果你的会议本身就有实时字幕或转写文本,可以跳过 ASR,直接把文本喂给抽取层,省掉一大半算力。
2.2 结构化抽取要抽哪几个字段
纪要不是一段摘要就完事。真正能用的纪要,字段是固定的。我一般会定义这么几个输出字段:
| 字段 | 含义 | 是否必填 |
|---|---|---|
| summary | 整体摘要,3~5 句 | 是 |
| decisions | 已达成的决议列表 | 是 |
| todos | 待办事项,含负责人、事项、截止时间 | 是 |
| issues | 悬而未决的争议点 | 否 |
| speakers | 发言人及其主要观点 | 否 |
字段定死之后,提示词就好写了——你是在让模型填一张表,而不是让它自由发挥。自由发挥的纪要,十有八九会漏掉待办,或者把「我们再讨论一下」当成决议。
2.3 用 Python 跑通最小抽取链路
下面这段代码是抽取层的核心,输入是转写文本,输出是 JSON 格式的纪要。我用的是通用的 OpenAI 兼容接口写法,你换成任何兼容的模型端点都能跑。
import json from openai import OpenAI client = OpenAI( api_key="your-api-key", # 换成你自己的 key base_url="https://your-endpoint/v1" # 换成兼容端点 ) # 系统提示词:把模型角色锁死在「会议纪要抽取器」上 SYSTEM_PROMPT = """你是一个会议纪要抽取引擎。 输入是一段会议转写文本,你需要输出严格的 JSON,不要输出任何解释。 JSON 结构如下: { "summary": "3到5句的整体摘要", "decisions": ["决议1", "决议2"], "todos": [ {"owner": "负责人", "task": "待办内容", "deadline": "截止时间或未提及"} ], "issues": ["争议点1"], "speakers": [{"name": "发言人", "points": ["观点1"]}] } 如果某个字段在文本中没有对应内容,返回空数组或空字符串。""" def extract_minutes(transcript: str) -> dict: resp = client.chat.completions.create( model="your-model-name", # 换成你的模型名 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": transcript} ], temperature=0.2, # 低温度,减少自由发挥 response_format={"type": "json_object"} # 强制 JSON 输出 ) return json.loads(resp.choices[0].message.content) if __name__ == "__main__": text = "(这里放你的会议转写文本)" result = extract_minutes(text) print(json.dumps(result, ensure_ascii=False, indent=2))逻辑说明:系统提示词负责定义输出结构,temperature=0.2是为了压住模型的发散倾向,response_format强制 JSON 能省掉大量解析容错代码。参数上,temperature别超过 0.4,否则待办字段会开始编造截止时间;model选长上下文版本,因为一场会的转写文本动辄几千到上万字,上下文不够会被截断,截断位置之后的待办全丢。
2.4 转写层怎么接:分片与时间戳对齐
如果走 ASR,音频要先切片。常见做法是按静音检测切,而不是按固定时长切——固定切会把一句话劈成两半,转写出来就是残句。切片后每片单独转写,再按时间戳拼回完整文本。拼接时保留[mm:ss]前缀,方便后面定位「这句话是谁在什么时候说的」。
# 伪代码示意:按静音切分后逐片转写并拼接 segments = split_by_silence(audio_path, min_silence_ms=500) full_text = [] for seg in segments: text = asr_transcribe(seg.path) # 调用你的 ASR ts = format_timestamp(seg.start_ms) # 转成 mm:ss full_text.append(f"[{ts}] {text}") transcript = "\n".join(full_text)min_silence_ms这个参数很关键,设太小会把正常换气切成碎片,设太大则多人抢话的段落会被合并成一片,发言人区分就废了。我一般从 500 毫秒起步,根据实际录音质量上下调。
3. 提示词工程:让模型稳定吐出可用的纪要字段
3.1 少样本示例比长篇规则更管用
写提示词时,很多人喜欢堆规则:「你要注意区分决议和讨论」「待办必须带负责人」。规则写多了,模型反而顾此失彼。我的血泪经验是:给两个输入输出示例,比写二十条规则有效。
示例要覆盖边界情况。比如给一个「有人说了待办但没指定负责人」的例子,让模型学会填「未指定」而不是瞎编一个人名。再给一个「讨论很激烈但没有结论」的例子,让模型学会把内容放进issues而不是decisions。
3.2 分步抽取:先摘要,再抽待办
一次性让模型输出所有字段,长会议下容易漏。更稳的做法是分两步:第一步只出摘要和决议,第二步拿着摘要和原文专门抽待办。第二步的提示词可以更聚焦:「以下文本中,哪些句子表达了未来要执行的动作?提取负责人、动作、时间。」
def extract_todos(transcript: str, summary: str) -> list: prompt = f"""已知会议摘要:{summary} 请从下面的会议原文中抽取所有待办事项。 只输出 JSON 数组,每项包含 owner、task、deadline。 没有明确负责人的填"未指定",没有明确时间的填"未提及"。 会议原文: {transcript}""" resp = client.chat.completions.create( model="your-model-name", messages=[{"role": "user", "content": prompt}], temperature=0.1, response_format={"type": "json_object"} ) return json.loads(resp.choices[0].message.content)分步的代价是多一次调用,但换来的是待办召回率明显提升。我实测过,一场 40 分钟的会,单步抽取平均漏 2~3 条待办,分步后基本能全捞出来。
3.3 输出格式校验:别信模型一定给合法 JSON
即使开了response_format,模型偶尔还是会吐出带 markdown 代码块包裹的 JSON,或者多一个逗号。上线前一定要加一层校验和修复。
import re def safe_parse(raw: str) -> dict: # 去掉可能的 ```json ``` 包裹 raw = re.sub(r"^```(json)?|```$", "", raw.strip(), flags=re.MULTILINE) try: return json.loads(raw) except json.JSONDecodeError: # 兜底:截取第一个 { 到最后一个 } start, end = raw.find("{"), raw.rfind("}") if start != -1 and end != -1: return json.loads(raw[start:end+1]) raise这层兜底看着土,但能救回不少线上请求。校验失败时不要静默丢弃,记日志,方便回头调提示词。
4. 避坑与排查:纪要助手最容易翻车的五个地方
4.1 现象:待办里出现原文根本没有的人名
原因:模型在补全结构时,看到owner字段为空,会倾向于「编一个合理的」填进去。这是语言模型的通病,不是你的提示词写错了。
解决:在提示词里明确写「没有明确负责人时填『未指定』」,并在后处理里加一道校验——如果owner不在原文出现过的姓名列表里,就替换成「未指定」。姓名列表可以从转写文本里用简单的正则或分词先抽一遍。
4.2 现象:长会议后半段的待办全丢
原因:转写文本超过模型上下文窗口,被静默截断。很多接口不会报错,直接截掉尾部。
解决:先估算 token 数,超过窗口的按议题或时间切成多段,分别抽取后再合并去重。合并时按task文本做相似度去重,避免同一件事被抽两遍。
4.3 现象:决议和讨论混在一起
原因:提示词没有给「决议」下可操作的定义。模型不知道「我们倾向于方案 A」算不算决议。
解决:在提示词里给判定标准——「只有当出现明确的确认、拍板、通过类表述时才算决议;出现『可能』『再看看』『倾向于』的一律归入 issues」。标准要具体到词,不要写「根据语义判断」。
4.4 现象:发言人张冠李戴
原因:ASR 只给文本不给说话人,或者说话人分离(diarization)做错了。多人抢话时尤其严重。
解决:如果录音质量允许,先做说话人分离再转写;如果做不到,就在纪要里弱化发言人字段,只保留「主要观点」,别硬标名字。标错了比不标更误导人。
4.5 现象:同一场会跑两次,结果差很多
原因:temperature设高了,或者模型本身有随机性。纪要这种要发出去的东西,结果不稳定是致命的。
解决:temperature压到 0.1~0.2,能设随机种子的接口就固定种子。另外把提示词版本化,每次改动都记录,出问题能回滚。
5. 把纪要助手用出进阶价值:三个我常做的动作
第一个动作是给纪要加「回链」。每一条待办和决议后面,附上它在转写文本里的时间戳。这样别人看纪要时,对某条结论有疑问,能直接跳回原话核对。实现上就是在抽取时让模型顺带输出source_ts字段,虽然会稍微增加输出长度,但可信度提升明显。
第二个动作是做增量纪要。会议开了一小时,中途休息时先跑一次抽取,拿到阶段性纪要;会议结束后再跑一次全量,两次结果做 diff,只把新增的决议和待办推送给参会人。这样大家不用等会开完才看到东西。
第三个动作是建一个「纪要质量抽检」的小流程。每周随机抽两份纪要,人工核对待办召回率和决议准确率,把错例收集起来反哺提示词。我坚持这么做之后,提示词的迭代方向从「拍脑袋改」变成了「按错例改」,效果稳定得多。
验证一套纪要助手好不好用,我的标准很朴素:拿一场你自己开过的会,把录音丢进去,看输出的待办里有没有你当时真正记下来的那几条。如果漏了,去看是转写漏了还是抽取漏了,定位到具体环节再调。别一上来就换模型,八成问题出在提示词和分片策略上。
我自己踩过最深的一个坑,是早期图省事,把整场会的转写文本一股脑塞给模型,结果待办漏了一半,还以为是模型不行,换了好几个模型都一样。后来才发现是上下文超了被截断,改成按议题分段抽取,问题当场消失。从那以后我养成了一个习惯:任何跟长文本打交道的大模型链路,第一步永远是先量 token,再谈效果。希望帮到你。
本文还有配套的精品资源,点击获取