
你真正开会的时候脑子通常同时在跑三条线程听人讲话、组织自己的回应、还要把结论记下来。结果就是一个小时过去记录页上只躺着半页残废的笔记。我前几年在团队里最头疼的就是这个尤其是需求评审和跨部门对齐会开完两天后没人能说清楚当时到底定了谁负责哪件事。后来我花了一个下午用腾讯云 AI 语音识别搭了个会议小帮手录音文件扔上去几分钟拿回带时间戳、带说话人标记的转写文本再稍微做点后处理就变成一份能直接分发的会议纪要。这篇文章就讲整套方案是怎么落地的选型思路、接口怎么接、哪些参数值得调、哪些坑我已经替你踩过以及怎么把一堆原始文本加工成待办清单。想照着搭一套或者只想评估能不能用、成本如何、效果怎样这篇都覆盖了。1. 先想清楚会议小帮手到底要解决什么问题1.1 开会的真实痛点不只是“没人记纪要”开会没人记纪要只是表面现象更深的问题是记录这件事本身和开会流程是脱节的。参会人一边听一边写写下的往往是自己关心的部分结论、责任人、截止时间很容易漏会后要追忆只能靠零散的聊天记录。这个问题在只有录音没有转写的情况下也解决不彻底因为回听一小时音频的成本太高没人愿意干。我当时的诉求很明确要一份能检索的、有时间轴的、能指出“谁在什么时候说了什么”的文本而不是一份凭记忆整理出来的美化版纪要。1.2 方案选型为什么用云端 ASR 而不是本地模型做会议记录转写市面上大体有两条路线。第一条是用开源的本地语音识别模型部署在自己机器上优点是数据不出内网、离线可用、没有单次调用费用缺点是模型体积大、需要 GPU 或至少不错的 CPU、工程维护成本高而且中文会议场景常用的说话人分离、时间戳对齐、热词定制这些能力都要自己额外开发。第二条就是用云服务商的成熟语音识别 API。腾讯云 AI 语音识别这类方案的优势是开箱即用接口稳定、支持说话人分离、可以返回逐句时间戳、有热词机制针对专有名词纠错收费按实际识别的音频时长算对中小团队来说成本非常可控。我的选择很直接先用云 API 在最短时间内把流程跑通如果后续对隐私有更高要求再考虑引入本地模型替换识别链路架构上把识别模块隔离好替换成本不高。1.3 我圈定的功能边界动手之前先把需求砍干净。第一版我只做三件事录音文件自动上传、异步转写拿到结构化文本、按说话人整理成会议纪要初稿。不做实时字幕不做自动生成摘要里的智能压缩不做会议系统对接。把边界切小有个重要原因腾讯云语音识别接口本身只负责“音频转文本”会议纪要的质量还需要后处理环节不断调功能再多容易一手抓不深。事实证明这个优先级是对的后面我在转写文本的清洗和待办提取上花的时间远多于接 API 的时间。2. 整体设计以录音文件识别为主线实时识别做备选2.1 两条技术路线怎么选腾讯云 AI 语音识别按场景分好几类会议小帮手主要面对两种录音文件识别和实时语音识别。录音文件识别是异步任务先把完整音频传上去服务端排队处理几分钟后任务结果可取它支持长音频、质量更稳我把它定为主链路。实时语音识别走流式 WebSocket边说话边出字适合现场投屏字幕但客户端复杂度高而且直播场景需要处理断句和重连对于“会后再整理纪要”这个目标来说不划算。另外还有一句话识别处理 60 秒以内的短音频我一般用来做录音质量自检和单句测试不承担主流程。我把两条路线的差异整理成一个直观对比对比项录音文件识别实时语音识别一句话识别适用音频长度长音频几小时量级流式持续60 秒以内延迟要求分钟级可接受秒级出字低延迟说话人分离支持支持需要额外处理不支持工程复杂度低上传加轮询即可高需维护连接与重连最低推荐场景会后转写纪要现场字幕快速试听测试2.2 这套小帮手的处理链路整套流程可以画成一条单向流水线录音设备产出音频文件 → 统一转成 wav 或 mp3 → 上传到对象存储 COS → 提交录音文件识别任务 → 轮询任务状态 → 拿到带时间戳和说话人标签的 JSON → 后处理脚本清洗、分人、提取待办 → 输出 Markdown 会议纪要。关键设计点是不要把识别结果只当成一个字符串而是要保留住时间戳和说话人维度这两个维度后面做纪要定位时特别有用。比如“这段结论是谁拍板的”“这个问题在第几分钟开始被讨论”没有时间戳根本无从查证。2.3 录音质量是识别效果的上限先解决这个第一次测试识别率特别低后来发现不是 API 的问题是录音质量太差。会议场景常见的录音问题包括设备离人太远导致音量低、多人同时发言导致叠音、电话会议里双声道串扰。我的处理方式是引导大家用手机录音时尽量靠近发言人如果用的是电脑会议软体录音导出时尽量选择单声道 16kHz外接领夹麦或桌面麦对识别率的提升比任何参数都大。音频的采样率建议在预处理时统一确认声卡导出的某些格式可能是 44.1kHz语音 API 通常支持重新采样但在上传前自己转成 16kHz 会让行为更可控。我用了 pydub 或 ffmpeg 做统一转码脚本里留好接口后面批量处理录音时非常省事。3. 准备工作开通服务、配权限、装环境3.1 开通服务与控制台快速验证第一步在控制台开通腾讯云的语音识别服务并把对象存储 COS 也开通因为录音文件识别的标准姿势是先把音频放在 COS 上。开通后在语音识别控制台找到“录音文件识别”的功能页正常会提供音频上传试听功能建议先传一段 30 秒左右的真实会议录音做一次在线识别。这一步花不了几分钟但它能提前暴露很多问题音频格式是否支持、识别引擎选得对不对、语速和噪音对结果的影响有多大。我身边不少人是直接写代码后才发现问题其实控制台先验证是最快的路径。3.2 密钥和权限的最小化配置调用腾讯云语音识别接口需要一组 API 密钥也就是 SecretId 和 SecretKey。我建议不要用主账号密钥而是在访问管理里建一个子账号只授予语音识别服务和 COS 的相关权限原则上能跑通业务就给最小权限。密钥不要硬编码进脚本更不要提交到代码仓库我习惯从环境变量读取本地开发时放 .env 文件并加入 .gitignore。对象存储桶同样别图省事设成公有读开发阶段可以用预签名 URL 方式让识别服务访问私人桶里的文件后面我会给出具体代码这比把桶开放给公网安全得多。会议录音涉及讨论内容访问权限和审计要提前想清楚。3.3 安装依赖环境用 Python 3.8 以上就够了依赖主要是两个官方 SDK语音识别部分用腾讯云的 Python SDK包含 asr 模块上传文件用 COS 的 Python SDK。再装一个 pydub 用来做音频格式转换ffmpeg 作为它的后端。安装命令我放在下面pip install tencentcloud-sdk-python-asr cos-python-sdk-v5 pydub # 如果系统没有 ffmpeg还需要单独安装macOS 可用 brew install ffmpeg装完之后先做一次最简单的连通性验证哪怕是调用一句话识别传一个测试音频也行确认密钥、地域、网络链路都没问题再开始写正式脚本。4. 核心实现用腾讯云 AI 语音识别把录音变成文本4.1 小文件直连识别一句话识别兜底先说说我怎么用一句话识别做快速验证。会议里偶尔会有“某段关键发言只有几十秒”的情况或者只想确认某段录音能否被识别直接走文件上传链路太重了。一句话识别接口可以直接传音频数据返回识别文本。示例代码如下import base64 import os from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models SECRET_ID os.getenv(TENCENTCLOUD_SECRET_ID) SECRET_KEY os.getenv(TENCENTCLOUD_SECRET_KEY) REGION ap-guangzhou _client None def get_client(): global _client if _client is None: cred credential.Credential(SECRET_ID, SECRET_KEY) _client asr_client.AsrClient(cred, REGION) return _client def recognize_short_audio(audio_path: str, fmt: str wav) - str: with open(audio_path, rb) as f: data f.read() req models.SentenceRecognitionRequest() req.ProjectId 0 req.SubServiceType 2 req.EngineModelType 16k_zh req.VoiceFormat fmt req.Data base64.b64encode(data).decode() req.DataLen len(data) resp get_client().SentenceRecognition(req) return resp.Result这个接口对音频时长有限制超过 60 秒的音频不能走这条路。我在脚本里把它定位成一个工具函数主要用来在开会前快速检查录音设备是否正常或者在调参时对比同一段音频在不同引擎模型下的识别结果。4.2 长录音推荐COS 上传 录音文件识别正式的会议转写走录音文件识别核心步骤有三个上传音频到 COS、提交识别任务、轮询任务状态。上传我建议用 COS SDK 的 upload_file 方法它内部处理了文件分块比一次性读进内存更省资源。提交任务时把音频的访问地址传给接口并打开说话人分离。下面是我整理的完整示例import os import time from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.asr.v20190614 import asr_client, models from qcloud_cos import CosConfig, CosS3Client SECRET_ID os.getenv(TENCENTCLOUD_SECRET_ID) SECRET_KEY os.getenv(TENCENTCLOUD_SECRET_KEY) REGION ap-guangzhou BUCKET your-bucket-1250000000 cos_client CosS3Client(CosConfig(RegionREGION, SecretIdSECRET_ID, SecretKeySECRET_KEY)) def upload_and_get_url(local_path: str, cos_key: str, expire: int 3600) - str: cos_client.upload_file( BucketBUCKET, Keycos_key, LocalFilePathlocal_path, ContentTypeaudio/wav, ) url cos_client.get_presigned_url( MethodGET, BucketBUCKET, Keycos_key, Expiredexpire ) return url def submit_asr_task(audio_url: str) - int: cred credential.Credential(SECRET_ID, SECRET_KEY) http_profile HttpProfile() http_profile.endpoint asr.tencentcloudapi.com client_profile ClientProfile() client_profile.httpProfile http_profile client asr_client.AsrClient(cred, REGION, client_profile) req models.CreateRecTaskRequest() req.EngineModelType 16k_zh req.SourceType 0 # 0 表示URL方式1 表示COS对象方式 req.ChannelNum 1 req.ResTextFormat 2 # 中文分词结果便于后续做关键词提取 req.ConvertNumMode 1 # 数字统一转成阿拉伯数字 req.SpeakerDiarization 1 # 开启说话人分离 req.SpeakerNumber 4 # 预估说话人数 req.FilterDirty 0 req.FilterModal 1 # 过滤语气词和停顿 req.Url audio_url resp client.CreateRecTask(req) return resp.Data.TaskId这里有个容易忽略的点如果 COS 桶是私有读写识别服务访问对象需要可信的地址预签名 URL 是最直接的开发方案。SourceType 字段在官方文档里区分“语音URL”和“COS对象”两种开发阶段用 0 配合预签名 URL 排障最快。等流程稳定了再看是否需要换成 SourceType1 的标准方式这个根据你拿到的文档版本调整即可。4.3 轮询任务结果解析时间戳和说话人录音文件识别是异步任务提交后不能立刻拿结果。腾讯云的接口会返回一个 TaskId之后轮询查询任务状态。我在脚本里用循环加 sleep 的方式等待遇到失败状态直接抛出错误信息方便日志里排查。下面这段代码会拉取结果并解析成结构化对象import json import time from tencentcloud.common import credential from tencentcloud.asr.v20190614 import asr_client, models SECRET_ID os.getenv(TENCENTCLOUD_SECRET_ID) SECRET_KEY os.getenv(TENCENTCLOUD_SECRET_KEY) REGION ap-guangzhou def fetch_asr_result(task_id: int, max_wait: int 600) - dict: cred credential.Credential(SECRET_ID, SECRET_KEY) client asr_client.AsrClient(cred, REGION) req models.DescribeTaskStatusRequest() req.TaskId task_id waited 0 while waited max_wait: resp client.DescribeTaskStatus(req) status resp.Data.Status # 状态码含义随 SDK/API 版本可能不同常见情况2 表示完成3 表示失败 if status 2: return json.loads(resp.Data.Result) if status 3: raise RuntimeError(resp.Data.ErrorMsg) time.sleep(5) waited 5 raise TimeoutError(识别任务超时) task_id submit_asr_task(audio_url) detail fetch_asr_result(task_id) for item in detail[Result]: start item[StartMs] / 1000 end item[EndMs] / 1000 speaker item.get(SpeakerId, 0) print(f[{start:7.2f} - {end:7.2f}s] 说话人{speaker}: {item[Text]})关于状态码我特别提醒一句不同 SDK 版本对状态的定义在文档里表述略有出入第一次运行时先把 status 原样打印出来对照文档确认不要盲信网上的代码片段。返回的 Result 是一个 JSON 字符串里面每一项包含句子文本、开始时间、结束时间和说话人编号这就是后面所有后处理的基础数据。4.4 必须调好的关键参数我整理了一张参数速查表都是会议场景里直接影响效果的参数建议值原因EngineModelType16k_zh中文普通话会议电话录音用 8k 系列ChannelNum1单声道双声道电话会议可设 2ResTextFormat2返回中文分词结果方便后续做关键词提取ConvertNumMode1把“二零二四”统一转成“2024”纪要更干净SpeakerDiarization1开启说话人分离SpeakerNumber预估人数设得越接近真实分人越准FilterModal1过滤“嗯”“啊”等语气词FilterDirty视情况会议如果比较随意建议保持 0 以免误杀这里最值得花时间调的是 SpeakerNumber。说话人分离本质是聚类如果预估人数和实际差太多会出现两个人被合成一个人或者一个人被拆成两个身份的情况。我的经验是固定每周例会填参会人数减一主持人话少的情况跨部门拉会对不上号时宁多勿少宁可多拆再人工合并也比漏掉身份好。另外如果团队沟通里中英混杂较多比如技术术语经常夹英文单词可以试试把引擎 Model 换成 16k_mix 之类的混语模型效果会有明显变化。5. 会议记录的后处理从一段段文字变成能用的纪要5.1 按说话人切分先恢复对话脉络拿到 JSON 结果后第一步不是直接拼文本而是把句子按说话人重新组织。这样一份会议记录不再是混沌的流水账而是一条条清晰的发言轮次。我用 defaultdict 把同一个 SpeakerId 的句子聚合按时间排序后输出完整的发言段落from collections import defaultdict def group_by_speaker(parsed: dict) - dict: turns defaultdict(list) for item in parsed[Result]: speaker item.get(SpeakerId, 0) turns[speaker].append(item) grouped {} for spk, items in turns.items(): start items[0][StartMs] / 1000 end items[-1][EndMs] / 1000 text .join(i[Text] for i in items) grouped[spk] {start: start, end: end, text: text} return grouped这一步最直观的收益是纪要里可以说“关于预算的问题从第 12 分钟开始讨论某同事在第 14 分钟提出了关键结论”而不是含糊地说“大家讨论了一下”。时间戳让纪要具备可回溯性这是录音文本最值钱的部分。5.2 清洗文本与自动提取待办原始转写文本里即使开了语气词过滤也还会有“然后”“那个”“就是说”这类口头语直接发给所有人既不专业也难读。我做了两层清洗先删掉常见口头禅再用规则把重复的短句合并。第二层更关键的是待办提取。会议纪要最后一定要落到“谁、什么时候、做什么”我写了一个轻量扫描脚本把包含“负责”“跟进”“确认”“提交”“下周”“本周”等动作词且长度大于一定字数的句子挑出来人工在这个候选列表里勾选确认。脚本本身不复杂但能节省大量通读时间ACTION_WORDS [负责, 跟进, 确认, 提交, 输出, 安排, 尽快, 本周, 下周] def extract_actions(sentences): todos [] for s in sentences: if len(s) 8: continue if any(w in s for w in ACTION_WORDS): todos.append(s) return todos提取结果整理成待办列表后我一般还会配上对应的时间戳这样每个人回看时都能直接定位到原始发言。如果团队有统一的任务池这步生成的结构化文本可以直接转成工单后期扩展空间很大。5.3 摘要生成与归档会议纪要除了全文还需要一段摘要核心结论要能在 30 秒内读完。摘要我分两层做第一层是规则法把每个人发言段落的首句和末尾总结句拼出来因为多数人在发言最后会收束观点第二层是可选的大模型摘要把清洗后的全文喂给任意国内大模型接口让模型输出“结论、分歧、待办”三个板块。用大模型时注意一次别喂全量长文本可以按说话人切片后分别摘要再合并成总摘要效果更稳。归档环节我直接把 Markdown 文件按日期命名放进私有仓库和音频文件关联存储既方便搜索也方便事后回溯。6. 实测踩坑记录与排查速查6.1 识别率低的几类原因我第一版上线后遇到的最典型问题是识别率突然下降。排查后发现会议现场换了个会议室回音变大麦克风离发言人两米以上识别文本里大量同音字错误。这类问题靠调参数救不回来只能从录音源头解决。第二类是引擎模型不匹配普通话会议用了默认通用模型专业术语频繁出错后来把热词表建起来把项目代号、人名、系统名称都加进去专有名词识别率明显提升。第三类是说话人分离不准实际原因是 SpeakerNumber 填少了一位调整后基本稳定。6.2 任务排队与回调的问题录音文件识别是异步机制偶尔会遇到任务在队列里卡住的情况。实际测试发现提交任务后可能在几分钟内一直显示处理中如果超过预期时间还不动先检查 URL 是否还能访问预签名 URL 有过期时间过期后任务会失败而很多人以为是服务端问题。我的脚本里把轮询超时设成了 10 分钟并加了重试机制失败一次就会用新 URL 重新提交一次正式环境跑下来稳定很多。此外接口还支持回调 URL 方式如果服务能暴露公网地址可以注册回调让服务端主动通知避免轮询消耗但对大多数内部小工具来说轮询已经足够。6.3 并发、计费与隐私的一些提醒批量处理历史录音时很容易踩并发限制。我曾一次性提交了几十条历史会议音频结果部分任务提示频率超限后来在脚本里用信号量把并发控制在个位数一个个排队处理。计费方面录音文件识别按时长计费实时识别也是按时长计费但两者有免费额度和单价差异建议按自己的会议时长先估算一轮别等到账单出来才发现路径选错了。隐私方面如果会议内容敏感要评估录音合规性最好在上传前脱敏或明确授权并且在转写完成后删除云端音频只保留文本避免长期留存带来的风险。6.4 排查速查表现象可能原因处理办法识别结果全是同音字录音音质差或噪音大换麦克风位置先做 16kHz 单声道转码专有名词错误频繁缺少热词建热词表把项目名、人名、缩写加进去说话人混乱SpeakerNumber 不准调整预估数宁多勿少任务一直处理中URL 过期或队列忙重新生成预签名 URL确认超时时间提交任务报错权限或地域不一致核对密钥权限、桶地域、SDK 版本费用异常跑了实时识别或重复调用检查账单明细确认用的路径这张表是我自己在两次排障中沉淀出来的方向比细节更重要遇到问题时先判断是“输入音频问题”还是“参数配置问题”还是“权限链路问题”逐层排查能省很多时间。7. 给想复刻这套方案的人几点实在建议如果你也打算搭这么一个小帮手我建议三点。第一从一场真实的 15 分钟小会开始不要一开始就上一个月的历史录音先用一场会走通全流程确认格式、参数、输出模板都符合团队习惯再批量推广。第二热词表一定要在第一次转写完成后立刻维护一次把识别错误的项目代号、产品名、英文缩写补进去第二次跑效果会完全不同。第三把脚本写成命令行工具而不是一次性脚本输入音频路径、输出纪要路径做成参数后面接入新的录音来源时改动最小。我后来还加了两个小扩展一个是对接每周例会提醒提前一天自动检查上一场纪要的待办有没有闭环另一个是把实时语音识别接到现场投屏给远程参会的人看字幕。这两个功能都是建立在转换链路已经跑通的基础上加得很轻松。说到底工具的价值不在于开头那点 API 调用而在于你愿意为“会后能不能找回现场”这件事投入多少细节。