做会议纪要这件事我大概烦了三年。每次开完一个小时的会录音文件躺在手机里想整理成文字却要重新听一遍正常语速一小时大概六千到八千字整理纪要至少要花掉我同样长的时间甚至更久。后来我决定不再跟自己的耳朵较劲直接用腾讯云AI语音识别搭了一个会议小帮手把开会录音丢进去自动出带时间戳的转写文本再配合简单的后处理脚本直接生成结构化会议纪要。这篇文章就把整个落地过程完整记录下来包括接口选型、参数调优、代码实现以及我在真实会议场景里踩过的坑和解决办法给同样被会议纪要折磨的朋友一条能直接照抄的路径。1. 整体设计与思路拆解1.1 会议场景的真实痛点开会这件事核心矛盾从来不是“开”的过程而是会后的整理。我观察了一下自己所在团队的日常一场常规项目周会大概一个半小时涉及七八个人讨论内容包括进度同步、问题讨论、任务分派、风险预警。会后我通常需要产出三类东西一份完整的会议记录存档备查一份精简版纪要发给没能参会的人还有一张任务清单列清楚谁负责什么、什么时候交付。以前这三件事全靠手动完成听一遍录音是跑不掉的而且只听一遍往往还不够遇到声音重叠、术语含糊的地方还要反复回放。更烦的是时间成本的不确定性。录音一小时整理两小时是常态。如果会议涉及专业术语多比如某个内部系统的模块名、某个协议的编号识别和理解成本还会再翻倍。所以当腾讯云AI语音识别这类产品成熟到可以稳定输出带标点、带时间戳、甚至带说话人标记的转写文本时我第一时间想到的就是能不能把“听录音”这一步彻底自动化掉让人的精力集中在最后的校验和决策整理上。1.2 技术选型为什么不自己做ASR动手之前我认真考虑过自训练模型这条路。当时也调研了一些开源的语音识别方案比如主流的Whisper系列模型。本地部署Whisper的优势是没有调用成本、数据不出内网听起来挺诱人。但我做了个简单测算就放弃了这个方向一个像样的语音识别服务要保障实时率、支持流式识别、做好说话人分离、还要处理各种口音和噪声环境这部分工程量远远超过“跑通一个模型”。Whisper的中文识别准确率对标准普通话确实不错但遇到会议场景里常见的口语化表达、中英混杂、人名地名、行业黑话效果就时好时坏。而且本地跑大模型对硬件有要求公司配的普通办公笔记本根本跑不动实时推理云GPU实例的费用算下来也不比API调用便宜多少。相比之下腾讯云AI语音识别这类云服务解决了最麻烦的几个问题中文识别经过大规模数据调优效果稳定接口直接提供时间戳、热词、说话人分离等会议场景刚需能力不需要自己维护推理服务按量付费用多少花多少。对个人开发者或小团队来说把精力集中在业务逻辑上而不是死磕声学模型是性价比最高的选择。1.3 腾讯云AI语音识别的关键能力拆解我在选型和实际使用中重点关注了这几个会议场景核心能力录音文件识别这是会议小帮手的主力接口。支持提交录音文件URL或直接上传音频数据服务端异步识别任务完成后返回完整转写结果。适合离线处理场景比如会议结束后把录音丢进去稍等片刻拿结果。实时语音识别基于WebSocket的流式识别边说边出结果。这个能力我用在了一个附加功能上——开会时如果忘记录音或者想临时查看某句话的实时转写可以直接开着手机录音并把音频流转发到识别服务。不过主流程我仍然以录音文件识别为准因为实时识别的结果在会议中途不好整理反而容易分心。热词定制会议中的人名、产品名、系统名、专业术语通用模型经常识别错。热词表可以在不重新训练的情况下大幅提升特定词汇的识别准确率这个功能对会议场景来说几乎是刚需。说话人分离按说话人身份区分转写文本输出每条结果的说话人ID和时间戳。虽然它的分离效果不能做到像人工标注那样完美但对于分辨“谁说了哪段话”已经足够实用。智能标点与数字转换自动补充标点符号把“六点五”这类口语数字转成规范写法或保留为数字显著降低后处理成本。这些能力组合起来正好覆盖了“会议录音→转写文本→结构化纪要”的完整链路。2. 核心功能设计与实操要点2.1 会议小帮手的功能清单与优先级在写代码之前我先列了一个功能清单按照使用频率和必要性排了优先级功能模块优先级说明录音文件转写P0核心功能会议录音输入转写文本输出时间戳对齐P0每条识别结果带起止时间方便定位说话人标记P1区分发言人辅助整理讨论脉络热词优化P1提升人名、专有名词的识别准确率文本清洗P1去语气词、合并断句、规范数字自动摘要P2提取关键词和行动项生成精简纪要实时语音识别P2临时录音场景的补充能力P0和P1是必须做的P2视时间精力决定。我建议大家在动手开发前也先做这一步明确“最小可用版本”的边界否则很容易陷入细节泥潭。2.2 两种接入模式怎么选腾讯云AI语音识别提供了多种接入方式我重点对比了其中的录音文件识别和实时语音识别。录音文件识别适合“事后整理”场景流程是提交录音文件地址或上传音频数据服务端异步处理调用方轮询或通过回调获取结果。我的会议小帮手主流程就走这条链路稳定性最高实现也最简单不需要维护长连接。实时语音识别适合“边说边转”场景基于WebSocket协议需要自己管理连接状态、分帧发送音频数据、接收中间识别结果。我后来在移动端加的原生录音转发功能就是走这条路但只作为辅助工具没有让它成为核心。从实现成本来看录音文件识别接口只需要两个请求加一个轮询循环实时识别则涉及WebSocket帧格式、二进制分片、断线重连等一堆逻辑。如果你是第一次做这类项目强烈建议先跑通录音文件识别把主流程吃透之后再考虑加实时能力。2.3 系统处理链路设计整个会议小帮手的处理链路被我设计成四个阶段采集阶段用手机录音或会议软件自带的录音功能统一转成符合要求的音频格式。我一般用WAV或MP3采样率16kHz或8kHz单声道即可。会议场景不需要立体声双声道反而会增加文件大小和接口消耗。识别阶段调用腾讯云ASR的录音文件识别接口传入音频地址或音频数据等待异步识别结果。得到的结果是JSON结构包含多条识别结果文本、每条的开始/结束时间、说话人ID、置信度等信息。清洗阶段对原始转写文本做后处理包括过滤语气词、合并句子、规范数字和标点、处理说话人分离后的文本块排序。这一步决定了最终纪要的质量不能省略。生成阶段基于清洗后的文本提取关键词、定位动作项、生成精简版纪要和完整版记录。我目前用规则加字符串匹配的方式实现简单直接够用如果你有条件接入大模型做语义总结效果会更好。这个链路并不复杂但每一步都有细节要抠下面我把最关键的部分展开讲。3. 实操过程与核心环节实现3.1 开通服务与密钥准备这一步没什么玄学注册账号后在控制台找到腾讯云AI语音识别服务开通录音文件识别能力然后在访问管理里创建一组API密钥拿到SecretId和SecretKey。这里有一个我自己掉过坑的细节密钥权限别图省事给到最高权限建议只授权语音识别相关的接口权限。毕竟密钥是要写在代码里的万一代码落到公共仓库最小权限可以把损失降到最低。另外腾讯云API调用都有签名机制我用的是官方推荐的TC3-HMAC-SHA256签名方式后面代码里会展示完整的实现。3.2 录音文件识别完整代码实现录音文件识别的调用过程分两步先提交识别任务再轮询获取结果。以Python为例我把整个调用封装成一个类核心代码如下import hashlib import hmac import json import time import requests from datetime import datetime class TencentASRClient: def __init__(self, secret_id, secret_key): self.secret_id secret_id self.secret_key secret_key self.endpoint asr.tencentcloudapi.com self.service asr self.version 2019-06-14 self.region ap-guangzhou def _sign(self, payload, timestamp): # TC3-HMAC-SHA256 签名 date datetime.utcfromtimestamp(timestamp).strftime(%Y-%m-%d) canonical_headers fcontent-type:application/json; charsetutf-8\nhost:{self.endpoint}\nx-tc-action:createrecTask\n signed_headers content-type;host;x-tc-action hashed_payload hashlib.sha256(payload.encode(utf-8)).hexdigest() canonical_request ( fPOST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload} ) credential_scope f{date}/{self.service}/tc3_request hashed_canonical_request hashlib.sha256(canonical_request.encode(utf-8)).hexdigest() string_to_sign ( fTC3-HMAC-SHA256\n{timestamp}\n{credential_scope}\n{hashed_canonical_request} ) secret_date hmac.new( (TC3 self.secret_key).encode(utf-8), date.encode(utf-8), hashlib.sha256 ).digest() secret_service hmac.new( secret_date, self.service.encode(utf-8), hashlib.sha256 ).digest() secret_signing hmac.new( secret_service, tc3_request.encode(utf-8), hashlib.sha256 ).digest() signature hmac.new( secret_signing, string_to_sign.encode(utf-8), hashlib.sha256 ).hexdigest() authorization ( fTC3-HMAC-SHA256 Credential{self.secret_id}/{credential_scope}, fSignedHeaders{signed_headers}, Signature{signature} ) return authorization def create_task(self, audio_url, hotword_id): # 提交录音文件识别任务 payload { EngineModelType: 16k_zh_large, ChannelNum: 1, ResTextFormat: 4, SourceType: 0, Url: audio_url, ConvertNumMode: 1, FilterDirty: 1, FilterPunc: 0, SpeakerDiarization: 1, SpeakerNumber: 8, HotwordId: hotword_id } if hotword_id: payload[HotwordId] hotword_id body json.dumps(payload) timestamp int(time.time()) authorization self._sign(body, timestamp) url fhttps://{self.endpoint}/ headers { Authorization: authorization, Content-Type: application/json; charsetutf-8, Host: self.endpoint, X-TC-Action: CreateRecTask, X-TC-Timestamp: str(timestamp), X-TC-Version: self.version, X-TC-Region: self.region } resp requests.post(url, headersheaders, databody.encode(utf-8)) result resp.json() if Response in result and Data in result[Response]: return result[Response][Data][TaskId] else: raise Exception(f创建任务失败: {result}) def query_task(self, task_id): # 查询识别结果 payload json.dumps({TaskId: task_id}) timestamp int(time.time()) authorization self._sign(payload, timestamp) url fhttps://{self.endpoint}/ headers { Authorization: authorization, Content-Type: application/json; charsetutf-8, Host: self.endpoint, X-TC-Action: DescribeTaskStatus, X-TC-Timestamp: str(timestamp), X-TC-Version: self.version, X-TC-Region: self.region } resp requests.post(url, headersheaders, datapayload.encode(utf-8)) return resp.json()调用的几个参数说明一下EngineModelType引擎模型我用的是“16k_zh_large”适合16kHz采样率的中文普通话录音识别速度和准确率平衡得比较好。如果你的录音是电话录音那种8kHz音质就要对应换引擎。FilterDirty过滤脏词开启后会把一些不雅或不必要的词汇替换掉开会录音基本都会开。ConvertNumMode数字转换我设为1让识别结果尽量把口语数字转成阿拉伯数字比如“一二三”变成“123”后面做文本处理方便很多。SpeakerDiarization开启说话人分离SpeakerNumber设为预估的发言人数我一般设8因为团队会议基本不超过8个人。这个值如果设得太大分离准确性会下降设得太小又会把不同的人合并成同一个说话人。3.3 轮询获取结果与结果解析提交任务后服务端返回一个TaskId接下来就是轮询。任务状态正常几分钟内会从“识别中”变成“识别完成”我写了一个带超时控制的轮询函数def get_result(self, task_id, max_wait600): start time.time() while time.time() - start max_wait: resp self.query_task(task_id) response resp.get(Response, {}) status response.get(Status) if status 2: # 识别完成 result response.get(Result, ) return self._parse_result(result) elif status 3: # 识别失败 raise Exception(f识别失败: {resp}) time.sleep(5) raise TimeoutError(识别超时)识别结果是一大段JSON字符串根据我在创建任务时设置的ResTextFormat4返回的是包含时间戳、说话人ID、句子完整性较高的结构化数据。解析逻辑大致是这样def _parse_result(self, result_str): try: data json.loads(result_str) except json.JSONDecodeError: # 兼容部分返回格式不是标准JSON的情况 data json.loads(result_str.replace(False, false).replace(True, true)) sentences [] for item in data.get(Result, []): sentences.append({ text: item.get(Text, ), start_time: item.get(StartTime, 0), end_time: item.get(EndTime, 0), speaker: item.get(SpeakerId, -1) }) return sentences拿到这样的结构化句子列表之后会议纪要的生成就方便多了后面的后处理脚本直接基于这个列表做。3.4 热词表配置和使用热词是会议场景提准确率最简单有效的手段不花一分钱推理成本就是纯配置。我在腾讯云ASR控制台里创建了一个热词表把参会人姓名、内部项目代号、产品名、常用的技术名词都加了进去。比如我们团队的项目叫“灵枢”第一次识别直接被转成了“零书”加了热词之后立刻纠正过来。还有同事的英文名比如“Lucas”被识别成“卢卡斯”甚至“撸卡死”加了热词就完美解决。创建热词表的接口也可以在代码里调用但我更推荐直接在控制台维护因为改动频繁界面操作更直观。创建后拿到HotwordId在CreateRecTask的请求体里带上即可。热词表有个数量上限我印象中几百个词条以内没问题超过了就要考虑精简。开一次会真正需要加的词其实也就二三十个完全够用。3.5 实时语音识别的WebSocket接入要点虽然主流程用的是录音文件识别但我也基于WebSocket写了一个实时识别的辅助模块用来应对“没提前录音、现场需要临时转写”的情况。腾讯云实时语音识别的WebSocket协议大体流程是先通过临时密钥或签名换取一个WebSocket连接地址建立连接后发送一个包含配置信息的请求帧然后持续发送二进制音频数据帧服务端通过文本帧不断返回中间识别结果。用Python实现的话核心伪代码是这样的import asyncio import websockets import json async def realtime_asr(audio_stream, secret_id, secret_key): # 1. 生成带签名的WebSocket URL具体生成方式参考官方文档 url build_signed_ws_url(secret_id, secret_key) async with websockets.connect(url, subprotocols[binary]) as ws: # 2. 发送请求参数帧 req { type: full, # 一次性返回最终结果 voice_format: 1, # 1: pcm, 4: mp3 engine_model_type: 16k_zh_large, hotword_id: , need_vad: 1, voice_id: str(uuid.uuid4()) } await ws.send(json.dumps(req)) # 3. 按数据块发送音频同时接收识别结果 for chunk in audio_stream: await ws.send(chunk) response await ws.recv() handle_response(response)实时识别的坑主要集中在音频格式上PCM裸流、采样率、位深必须和参数里设置的一致不然识别效果会非常差。WebSocket连接也需要自己做心跳保活。对这个项目来说实时识别只是配角我就不展开全部细节了大家按官方文档操作都能跑通。4. 让转写结果变成“会议纪要”的后处理4.1 文本清洗与句子合并语音识别的原始结果直接拿来当纪要肯定不行口语特征太重。我的清洗脚本做了三件事第一是去语气词。中文会议口语里“嗯”“啊”“呃”“就是说”“然后”“这个这个”出现的频率极高但它们在书面纪要里没有价值。我用一个停用词列表简单匹配过滤效果还不错。需要注意的是像“然后”在某些语境下是有实际连接意义的不能一刀切全删我设置了“连续出现两个及以上语气词才删”的规则保留单次的语义连接词。第二是断句合并。语音识别经常把同一句话切成两段比如“我们下周”和“要上线这个功能”中间还有短暂停顿。我根据时间戳间隔来判断如果前一句的结束时间和后一句的开始时间间隔小于500毫秒且有明显的上下文衔接就把两句合并成一句。这里不需要复杂的模型简单的规则就能明显提升可读性。第三是规范数字和标点。ConvertNumMode1已经把大部分口语数字转成了阿拉伯数字但还有少数漏网的比如“百分之五”这种我用正则做了补充替换。标点方面识别接口默认输出的中文标点基本可用个别长句缺少句号的情况我按“句末语气词停顿时间大于800毫秒”的规则补上了句号。4.2 说话人信息整理与结构化输出开启SpeakerDiarization后每条转写结果都带一个SpeakerId但SpeakerId只是编号并不知道是谁。我的处理方式是在脚本里手动建立一个映射表先看一遍转写结果根据发言频率和发言内容特征大致判断编号对应的人也可以在会议结束后让主持人帮忙标注一下谁是谁几分钟就能搞定。有了说话人信息后输出结构就清晰多了。完整版纪要的格式我设计成Markdown# 会议记录项目周会 时间2025-01-10 14:00 - 15:30 参会人A编号0、B编号1、C编号2、D编号3 ## 讨论内容 **A**00:01:23: 这周主要完成了登录模块的重构下周开始接支付... **B**00:04:56: 支付接口的对公认证还没有通过需要法务配合... ## 行动项 - [ ] 跟进支付接口认证 - 负责人B - 截止时间下周一 - [ ] 输出登录模块测试报告 - 负责人A - 截止时间周五前4.3 基于关键词与规则的自动摘要自动摘要是让纪要“精简”的关键。我一开始想过用大模型但为了控制成本和依赖先用了规则方案实测下来效果足够应付大多数会议。行动项提取的逻辑很简单查找包含“需要”“要”“必须”“尽快”“负责”“跟进”“确认”“办”等行为动词的句子提取主语根据说话人ID映射的参会人再结合时间词“明天”“下周一”“本周五”等补上截止时间。这一步不需要理解复杂语义规则匹配加关键词定位就能命中七八成。关键词提取用词频加TF-IDF的思路统计清洗后文本里每个词的出现频率滤掉停用词和太通用的词比如“问题”“情况”“项目”剩余的高频词就是会议核心主题。我把这些词按权重排序取前10个作为“会议关键词”附在纪要顶部。需要说明的是规则方案的摘要提取更接近“信息压缩”而非“语义理解”。如果哪天需要更贴近人类语感的会议总结我把这个模块替换成大模型提示词工程也不难前面的结构化转写结果就是现成的优质上下文。4.4 输出多种纪要格式不同的人需要不同颗粒度的纪要。我最终的输出脚本支持四种格式完整逐字稿带时间戳和说话人的全文转写适合存档备查。精简纪要按讨论主题分段每段用发言人加摘要句概括去掉口语冗余。行动项清单只保留动作、负责人、时间生成待办事项。一句话总结基于关键词排名生成一两句话概括会议核心内容适合快速同步给没参会的人。这四种格式都是用同一个清洗后的数据结构生成的脚本里写几个不同的渲染函数就行成本很低但非常实用。5. 常见问题与排查技巧实录5.1 高频报错及其原因实操过程中我整理了一张问题速查表遇到问题对着查就行现象可能原因解决方案返回签名错误或SignatureExpired本地系统时间不准同步系统时间服务器时间偏差不能超过5分钟EngineModelType参数不合法音频采样率和模型不匹配确认音频是16kHz还是8kHz选择对应引擎任务提交成功但一直转码中音频格式不合规或文件过大转成WAV/MP3用URL方式提交大文件识别结果中文乱码请求或响应编码不是UTF-8统一使用UTF-8编码说话人分离结果混乱SpeakerNumber设置过小将SpeakerNumber调大到接近实际人数热词不生效热词表未绑定或词条过多确认请求体中带了HotwordId精简热词表其中签名错误是最常见的十个人有八个第一次都会栽在这。我特意在打包脚本里加了一个自动校时函数每次请求前先和NTP对一下时间用这个方式彻底告别了SignatureExpired。5.2 会议场景特有的识别陷阱有些坑是会议场景独有的文档里不会写我实际踩过之后才总结出来。第一个是录音设备距离问题。用手机开会的远场录音多人同时说话时识别率会急剧下降。腾讯云ASR接口针对近场语音优化更好最好的做法是发言人尽量靠近麦克风或者用会议室的拾音设备。如果实在只能远场录音建议在提交前先用音频工具做一次简单的降噪和音量归一化。第二个是密集术语场景。一次评审会上全程都在聊内部系统架构涉及大量英文缩写和自定义名词我第一次跑出来的识别结果几乎没法看。后来把关键术语全部加进热词表效果立刻好了很多。我现在的做法是会议前先找主持人要一份议程文档把里面的名词全部提取出来塞进热词表准确率直接从不到七成拉到九成以上。第三个是静音段落误切分。有的人讲话停顿特别多识别服务可能会把一句话切断清理脚本合并句子时要注意不要误把“两个人交替发言”合并成“一个人说话”这里需要同时参考说话人ID。5.3 延迟与成本优化思路录音文件识别的耗时和音频时长成正比一段一小时的音频大概需要几分钟处理时间。我在优化延迟上做了两件事一是把长音频分段提交比如每段十分钟并行识别再合并结果总时延能缩短不少二是在轮询时用指数退避策略前几次间隔短一点后面拉长避免空转请求。成本方面录音文件识别按音频时长计费不开SpeakerDiarization会更便宜但会议场景强烈建议开省下的整理时间远超这点费用差距。另外腾讯云有预付费资源包常开会的人可以买一个比按量后付费划算一些。还有一个隐藏成本点是“重复识别”。同一段录音不小心提交了两次钱就白花了。我在脚本里加了录音文件MD5记录机制提交前先查一下这个文件是否已经识别过识别过就直接复用缓存结果避免重复扣费。6. 实测效果与后续扩展方向这套会议小帮手上线用了两个月处理了三十多场会议的录音最直接的改变是会议纪要的整理时间从原来的一到两小时压缩到十几分钟。我只需要把录音文件丢进脚本等待转写完成然后在结构化的转写文本上快速过一遍修正少数识别错误再让脚本生成行动项清单一份能直接发出去的纪要就出来了。在实际使用中我也摸索出了一些经验分享几个最实用的经验一是“不要追求完美识别”。语音识别永远有误差但不影响纪要使用。参会人真正关注的不是每个字都对而是行动项有没有遗漏、关键结论有没有记错。转写准确率在九成左右时人工校对十分钟就够强行追求九十九分反而会花掉大量时间。经验二是“准备比事后补救重要”。现在的流程是开会前一天把议程和参会人名单发给脚本自动生成热词表会议结束时录音上传后立刻开始转写会议结束后清理脚本自动输出四份格式的纪要草稿。这一套串起来基本上散会后十分钟内纪要的初稿就能出来。经验三是“给行动项加上人工确认环节”。自动提取的行动项虽然准确率不错但偶尔会把“他需要提交周报”这类第三人称表述错误归因。我在脚本里对每个行动项都附带了原句引用生成纪要前快速扫一眼原句就能避免大部分乌龙。后续我还想把以下功能加进去支持从视频会议软件直接拉取录音文件、增加声纹识别来自动标注发言人身份、把行动项直接同步到项目管理工具里、用大模型生成更自然的会议总结。这些都是在现有框架上的增量开发底层的录音文件识别和后处理链路已经稳定随时可以往上叠新功能。如果你也想做类似的会议小帮手我的建议是先跑通最基础的录音文件转写拿到带时间戳和说话人的文本再把热词表用起来然后写清洗和结构化输出脚本最后才是实时识别、自动摘要这类锦上添花的能力。一步步来每步都能立刻感受到效率的提升。这套思路不仅适用于会议场景访谈记录、课堂录音、直播字幕原理都是相通的换一个输入源就是另一款生产力工具。