1. 为什么要在医疗 SDK 里塞一条 AI 语音管线
1.1 远程医疗对"实时"的要求远超一般会议
远程问诊、多学科会诊、术后随访,这些场景大家可能都接触过。传统的视频问诊做到音视频不卡顿、不花屏,已经能满足大部分需求。但当你要把 AI 助手塞进问诊过程里,事情就完全不一样了。
我接手这个项目时,客户提了个很直接的需求:医生在问诊时,AI 要能"听着"医患对话,实时把关键信息整理成结构化病历,同时在医生询问某个罕见病用药时,AI 要能即时给出参考建议,还要用语音播报出来。说白了,就是要做一个能听、能想、能说的实时语音助手,嵌入到原有的医疗音视频咨询系统里。
这里的关键词是"实时"。视频通话里你延迟两秒,人眼还能容忍,但语音对话延迟超过 1 秒,医生的使用体验就会断崖式下降。因为语音交互的节奏感很强,你问一句,系统要想半分钟才回答,这AI助手就没人愿意用了。所以整个管线的延迟预算非常紧张,每个环节都必须卡着时间算。
1.2 私有化是底线,不是可选项
再说私有化。医疗行业的音视频数据,尤其是问诊内容,涉及患者隐私和病历信息。数据不能出医院内网,不能经过第三方公有云服务平台,这是客户反复强调的底线。所以这条 AI 语音管线不能简单调云端 API,而是要在客户机房或者内网环境里,用私有化部署的 ASR、LLM、TTS 组件搭起来。
这带来的直接后果是:你不能指望云端那种超大算力集群,必须在有限的计算资源下跑实时推理。我项目里那台服务器配置其实不算差,但要同时跑四个模型服务,还得给视频编码留资源,每一路并发都要精打细算。
所以"微议医疗 SDK + WebRTC + 实时 AI 语音管线"这个组合,本质上是在解决两个问题:第一,把大模型的实时语音能力无缝植入到已有的音视频通话流程里;第二,让这条管线在私有化环境下跑得动、跑得稳、跑得准。下面我把整个接入过程拆开讲,包括架构设计、核心实现、踩坑记录,希望能给做同类项目的同行省点时间。
2. 管线架构:ASR/LLM/TTS 怎么在 WebRTC 链路里串起来
2.1 整体框架与音频流向
刚开始设计时,我面临一个选择:AI 语音管线是放在客户端单独跑,还是由 SDK 统一接管。客户明确要求医生端使用体验越简单越好,医生打开问诊界面,点击"AI 助手"按钮,剩下的全部自动完成。所以最终架构是把整个管线逻辑嵌入到 SDK 底层,对上层业务只暴露开关和回调。
音频流向是这样的:
- 医生和患者的麦克风采集,通过 WebRTC 上行推流给对端,同时 SDK 在本地把混音后的音频流旁路一份,送入 ASR 引擎。
- ASR 识别出文本后,送入 LLM 服务进行语义理解和生成回复,同时把问诊中的关键实体(主诉、现病史、既往史、用药情况)抽取出来做结构化记录。
- LLM 生成的回复文本送入 TTS 引擎合成音频,合成后的 PCM 数据通过 SDK 内部的音频混合模块,混入到发送给医生的下行音频流中。
也就是说,AI 的"声音"不是独立播放出来的,而是作为一路虚拟音频源,混入 WebRTC 的音频轨道里。这样做的好处是:医生不需要戴耳机,也不需要额外调节音量,AI 的声音和患者的声音走同一个播放链路,音量平衡由 SDK 自动控制。
2.2 三个关键组件的选型逻辑
ASR 部分,我最终选的是支持流式识别的方案,因为医疗场景里长句非常多,"患者自述""现病史"这种长段落,非流式识别要等说完才能出结果,延迟不可接受。流式识别可以边说话边出字,医生的话音刚落不久,完整句子基本就能出来。选型时重点看三个指标:流式支持、医疗术语识别准确率、热词注入能力。医疗术语本身就是难点,很多药名和病名是生僻词,必须通过热词表动态调整识别结果。
LLM 部分,第一版我尝试过一次性把全文交给模型做分析。实测发现这种方式有两个问题:一是延迟不稳定,长文本生成时间波动大,作为实时助手体验很差;二是上下文一长,模型容易"跑偏",回答一些无关内容。后来改为只维护最近几轮对话的上下文窗口,问题描述和回答控制在合理长度内,效果明显稳定。选模型时,我优先考虑 7B 级别能私有化部署的模型,因为 13B 往上推理速度明显下降,在只有一张推理卡的环境里很难同时支撑多路并发。
TTS 部分,医疗场景对音色要求比较特殊,不能太机械,也不能太夸张。我试过几种方案,最后选的是延迟低、声音自然的方案。TTS 最容易踩的坑是首包延迟——从收到文本到发出第一个音频帧的时间。有些系统整句话合成完毕才开始输出,这个时间往往要好几秒,根本没法用。必须选支持流式合成的,也就是边合成边输出音频帧。
2.3 "AI 发言"如何安全混入通话
AI 合成的音频要混入 WebRTC 通话,没有经验的话容易忽略一个问题:回声。如果 AI 的声音通过扬声器放出来,又被麦克风重新采集进去,就会形成回声甚至啸叫。传统的回声消除是基于远端参考信号的,但 AI 音频是从 SDK 内部混进去的,WebRTC 自带的回声消除模块未必能正确处理这一路信号。
我在实现里做了两件事来解决:
- AI 音频混入时,把这一路标记为"虚拟远端信号",在混音模块里预先做一次回声抵消处理,消除该路音频中可能被麦克风采集的成分。
- 播放 AI 音频时,自动降低音量到合理档位,与正常通话音量拉开距离,同时用侧音检测规避啸叫风险。
这个部分没有现成的标准方案可抄,完全是在调试中一点点试出来的。后面我会单独讲这个坑。
3. 接入实操:从初始化到完整跑通
3.1 阶段一:音频采集与预处理
SDK 初始化时,要先把 AI 管线的开关打开,告诉底层这次通话需要使用实时 AI 能力。微议医疗 SDK 的接口大致是长这样的:
// 初始化 AI 语音管线 AIConfig config; config.enableASR = true; config.enableLLM = true; config.enableTTS = true; config.asrServer = "10.20.30.40:8080"; // 内网 ASR 服务地址 config.llmServer = "10.20.30.41:8081"; // 内网 LLM 服务地址 config.ttsServer = "10.20.30.42:8082"; // 内网 TTS 服务地址 config.sampleRate = 16000; config.vadThreshold = 0.6; // VAD 激活阈值 config.noiseSuppression = true; sdk->initAIPipeline(config);这一步有几个关键点需要说明:
采样率统一用 16kHz。虽然 WebRTC 通话通常是 48kHz 采样,但语音识别引擎多数在 16kHz 下效果最好,所以在送入 ASR 之前要做一次采样率转换。SDK 提供的音频预处理模块会自动做这个转换,不需要业务层关心。但你要知道这背后的原因——ASR 模型的训练数据大多基于 16kHz,匹配采能够最大程度保证识别准确率。
降噪必须开。医院环境并不安静,走廊广播声、监护仪滴答声、键盘敲击声,这些都会干扰识别。SDK 里面的噪声抑制模块基于深度学习的降噪算法,实测在平稳噪声环境下能明显降低误识别率。但要注意降噪强度不能拉满,太激进会把说话人的尾音也削掉,导致识别丢字。
VAD(语音活动检测)阈值的设置要按场景调。医疗问诊的特点是停顿多,医生在写病历、看检查报告时可能长时间不说话。如果阈值太低,患者说话间隙的呼吸声也被当成有效语音送进 ASR,产出一堆无意义的语气词;阈值太高又会导致快速对话时丢句。我用 0.6 作为默认值,实际在不同科室场景下还会微调。
3.2 阶段二:ASR 动态识别与断句策略
ASR 接入的核心不是把音频流送进去就完事,而是如何处理识别结果的输出节奏。流式识别会持续产出一个"中间结果"和一个"最终结果"。中间结果是不断更新的半成品,最终结果是当前句子定稿。
实际业务中,我们需要拿到"最终结果"才把文本送入 LLM,因为中间结果很可能被修正。比如患者说"我今天有点头晕",中间结果可能先出"我今天有点头",然后才补上"晕"。医生这边如果看到了不完整的句子,影响倒不大,但 LLM 如果基于不完整句子做分析,就会产生错误回答。
我采取的策略是:
- 中间结果只用于在界面上做提示性展示,不进入 LLM。
- 当 VAD 判定一句话结束(静音超过一定时长),ASR 会返回最终结果,此时立刻触发 LLM 请求。
这里有个坑:医疗场景的句子往往非常长,患者可能一口气说两分钟,中间只有短暂停顿。VAD 的"静音判定时间"如果设太短,长句会被从中切断;设太长,整个管线响应节奏会被拖慢。我最终把静音判定设置为 600ms~800ms 之间,经过多轮实测,对语速中等的医患对话比较合适。
ASR 还有一个医疗场景特有的需求:热词初始化。每次问诊开始前,我会从业务系统拉取当前科室相关的术语表,比如心内科的"房颤、支架、心率变异性",内分泌科的"二甲双胍、糖化血红蛋白",注入到 ASR 引擎的动态词表里。这样做能明显提升专业术语的识别准确率,比模型本身自带的知识库好用得多。热词注入之后要测一下,有些 ASR 引擎对热词的权重设置很敏感,热词权重太高会强行匹配,把正常的句子扭曲成热词组合。
3.3 阶段三:模型调用与会话上下文管理
LLM 接入的难点不在接口调用,而在上下文管理和答复策略。问诊是持续的对话过程,患者前面说的症状、医生之前给出的初步判断,都会影响当前回答。所以 SDK 内部维护了一个按会话维度隔离的上下文窗口。
一个小细节:每一路通话都要有独立的会话 ID,用于区分不同患者的对话上下文。刚开始我图省事,把上下文放在共享内存里,结果发现两路并发会互相串场——A 患者的症状被带进了 B 患者的问诊,这个错误非常严重,尤其在医疗场景,绝对不能容忍。
上下文窗口我限制在最近 6 轮对话以内。为什么是 6?因为医疗问诊的对话轮次一多,早期信息的重要性会下降,而且历史太长会增加模型推理时间。6 轮是我测下来精度和延迟比较均衡的点。关键症状和用药信息,我会通过提示词要求 LLM 在每次回复时同步更新一份"结构化问诊摘要",这样即使上下文窗口滚动掉了旧信息,摘要里仍然保留关键内容。
提示词设计也是要反复调的。初始版本我给的提示词很简单:"你是一位医疗助手,请回答医生的问题。"实测效果很一般,回答过于笼统。后来改成结构化提示词:
你是全科医疗助手,在远程问诊过程中辅助医生。规则: 1. 回答必须简洁,不超过100字。 2. 基于已有对话上下文,不要臆测患者未提及的症状。 3. 如果信息不足,明确回复"信息不足,建议进一步问询"。 4. 回答内容仅供参考,不构成诊断结论,需提醒以医生诊断为准。 5. 对话结束时,输出结构化问诊摘要,格式为:主诉|现病史|既往史|用药情况|医生建议。加上这 5 条规则之后,回答质量明显提升,尤其是"信息不足要明说"这一条,大大减少了模型乱猜的情况。医疗场景最怕的不是模型不会答,而是模型一本正经地给出错误答案。所以提示词里必须约束它"不知为不知"。
LLM 服务端的推理参数也有讲究。温度设为 0.2,降低随机性;top_p 设为 0.9;最大生成 Token 数设为 256。医疗回答不需要长篇大论,限制生成长度既保证响应速度,也降低收回无关内容的概率。实测下来,设置这些参数后,回答内容和上下文的相关度明显增强。
3.4 阶段四:TTS 合成与音频回播
TTS 合成的首选方案一定是流式,理由前面已经说过。流式 TTS 会先把第一句话的前几个字合成出来,立刻交给播放模块,剩余内容在后台继续生成。这样首包延迟能控制在 300ms~500ms 左右,人耳基本感受不到等待。
流式 TTS 在实现时有个问题:文本怎么切分。如果把整句话一次性发给 TTS 引擎,首包延迟依然很高,因为引擎要等拿到完整的文本才开始处理。正确做法是先做一句拆解,只把模型的回复按标点拆分成短句序列,第一句优先合成播放,后面的句子排队合成。我用中文标点(句号、感叹号、问号)作为切分点,实测效果最自然。
TTS 播放端的混音流程,我在 SDK 里做了独立处理:
// TTS 音频回调,将 PCM 数据混入下行音频流 void onTTSAudioChunk(const int16_t* pcm, int samples, int sampleRate) { // 1. 音量归一化,控制在用户语音的 70% 左右 applyGain(pcm, samples, 0.7f); // 2. 追加到 AI 播放缓冲区 aiPlaybackQueue.push(pcm, samples); // 3. 混入下行 WebRTC 音频轨道 sdk->mixAudioIntoRemoteStream(aiPlaybackQueue, sampleRate); }这里有个细节:混音的时机。TTS 音频到达的时候,下行链路里可能刚好有正在播放的扬声器音频,直接混入会造成重叠。我的做法是给 AI 播放缓冲区设置一个"播放闸门"——只有当前没有 AI 音频在播时才接受新的 TTS 片段。如果医生连续问了好几个问题,LLM 连续返回多段回复,TTS 会生成一个音频队列,逐段播放,避免多段 AI 语音重叠发声。
音量和音色方面,医疗场景的 AI 助手声音不宜有太强的"机械感"。我在 TTS 引擎里选了偏温和平稳的音色,并将播放速率设定在 1.0 倍速,确保医生能在不打断诊疗节奏的情况下听清内容。AI 语音播放时,如果检测到医患双方开始说话,SDK 会自动降低 AI 音量或者暂停当前播放,这是通过混音模块里内置的信号检测实现的,避免 AI 的声音干扰到真实对话。
4. 踩过的坑与排查实录
4.1 回音与啸叫:最容易被低估的问题
这个坑我花了两周才彻底解决。
现象是:AI 开始语音播报后,对端(患者端)能听到明显的回声,而且 AI 说话时间一长,整个通话链路里开始出现电流啸叫。
排查链路时,我一开始怀疑是 WebRTC 的 AEC(回声消除)配置有问题。检查了半天,发现医生端扬声器放出来的 AI 声音被麦克风采回去,WebRTC 的 AEC 确实做了消除,但因为 AI 音频是后混入的,不是原始采集进来的声音,AEC 拿不到这个信号的完整参考,所以消除不干净。
最终的解决方案分两步:
- SDK 内部增加了"AI 语音回声预估"模块:在把 AI 音频混入下行流的同时,生成一份完全相同的声音指纹,交给 AEC 做参考信号,这样 AEC 就知道"这段声音是系统自己发出来的",消除起来有据可依。
- 播放侧加功率限制:AI 音频输出时的最大音量不能超过正常通话音量的某个阈值(实测 75% 左右比较安全),从源头上降低被麦克风拾取后的回声强度。
另外还有一个特别容易忽略的:AI 播放期间,医生端的麦克风灵敏度要适当地临时降低。这是混音模块里加的一个侧链控制逻辑,AI 说话时自动压低采集音量,AI 说完立刻恢复。听起来粗暴,但实测效果立竿见影。副作用是 AI 说话期间医生插话的音量会变小,会有一点点影响体验,但因为 AI 播报时间通常很短,整体可以接受。
4.2 延迟叠加:全链路实时性预算
项目上线后的第一轮体验测试,医生反馈"AI 反应太慢",说句话要等 3~4 秒才听到 AI 回答。这个延迟在演示环节还能忍受,但真实问诊中不可接受。
逐段梳理延迟分布:
- 医生说完话,到 ASR 输出最终结果:约 500ms~800ms。
ASR 结果 + 上下文 + 中间摘要拼接发送给 LLM:网络与排队延迟约 200ms~400ms。- LLM 首 Token 生成:约 600ms~1000ms,具体取决于当前并发和学生请求数量。
- LLM 生成完整回答:约 800ms~1200ms。
- TTS 首包合成:约 300ms~500ms。
- 音频混入并送达播放端:约 50ms~100ms。
满打满算,最坏情况要 3.8 秒,刚好和测试体验吻合。优化思路不是让某个单一环节大幅提速,而是尽量让流水线重叠起来:
- ASR 出中间结果时就开始预加载 LLM 的上下文,等最终结果出来直接发送,省掉一次构建上下文的时间。
- LLM 采用流式输出,而不是等完整回答生成完毕再交给 TTS。每生成一句完整的话,就立刻送入 TTS 合成。也就是说,LLM 还在生成第二句话的时候,TTS 已经开始播第一句话了,这就是"流式级联"。
改造成流式级联之后,医生感知到响应时间大约是 1.2s~1.8s,接近正常人聊天的响应节奏。技术上的核心就是别等完整结果,让下游模型拿到上游的部分输出就能开跑。
4.3 并发与资源管理:一机多路的稳定性
私有化部署的一个特点是:服务器是客户定的,你没法按需扩容。我项目里的推理服务器,同时跑 ASR、LLM、TTS 三个服务,还要承受 4~6 路并发问诊。这比单独部署三个服务要复杂得多,主要难点是资源争抢。
第一轮压测在 4 路并发时,出现了明显的音频卡顿和 LLM 响应超时。看监控发现:LLM 服务在并行推理时 GPU 利用率拉满,ASR 服务的计算资源被挤占,实时识别跟不上音频流速度。
解决办法是给三个服务做显式的 QoS 资源控制:
- 在容器编排层给每个服务设置独立的 CPU 和内存配额,ASR 和 TTS 优先保证实时性,LLM 虽然耗资源但不能无限制占用。
- ASR 的并发限制在 2 路独立识别,多余请求排队;TTS 的合成任务池限制在 4 个并行任务,超出后按队列优先级处理。
- LLM 服务的批处理开启,多个并发请求合并成一个批次推理,GPU 利用率提升比较明显。实测 4 并发时,单个请求的首 Token 时间从 1.4s 下降到 0.7s。
另外,音频编解码也吃 CPU。同样是 4 路 720p 视频通话,软编 4 路 720p 在普通 X 86 CPU 上就能吃掉 3~4 个核,如果再叠加 AI 管线的计算量,CPU 会爆。我最终把视频编码改为硬件编码,腾出 CPU 资源给 ASR 和 TTS 使用。这个取舍在方案设计阶段就该想清楚,否则上线前手忙脚乱。
4.4 并发与资源之外:两个容易被忽略的隐藏问题
音频格式不匹配导致的"沙沙声"。有一次集成测试时发现 TTS 播放的 AI 声音有明显"沙沙"的背景噪声,排查半天发现不是降噪问题,而是 PCM 数据的位深和采样率没有对齐。TTS 引擎输出的是 16bit/24kHz,而 WebRTC 下行音频是 16bit/48kHz,SDK 里做了格式转换但转换参数写错,导致高位数据被截断,声音就带上了噪声。这类问题完全靠耳朵听很难定位,我后来写了个音频数据分析工具,用频谱图才看出来异常。集成不同厂商的 TTS 引擎时,一定要在 transformer 层统一做一次音频格式校验。
ASR 的"它不讲武德"问题——语义断句。正常的语音识别会根据停顿和语气做断句,但医疗场景里有大量"短期""长期""保守治疗"这样的表达,ASR 经常在"长期"后面断句,导致送入 LLM 的句子语义不通。我在 ASR 引擎里加了基于领域词典的分词重排规则,把医学术语作为整体对象参与断句判断,效果明显。这类问题没有统一解法,取决于 ASR 引擎是否支持自定义断句模型。如果引擎不支持,折中方案是把多个相邻短句合并为一个逻辑单元,先不送 LLM,等上下文语义更清晰再处理,不过这样会增加一点延迟。
4.5 排查实录速查表
共享一份我踩坑踩出来的排查速查表,供做类似项目时参考:
| 症状 | 可能原因 | 排查工具/手段 | 建议处理 |
|---|---|---|---|
| AI 声音听到回声 | AI 音频无参考送入 AEC | 抓取 AEC 输入输出对比,用音频软件打时间戳 | AI 音频叠加参考信号,降低 AI 播放音量 |
| AI 回复延迟超 3 秒 | 流水线未级联,串行等待 | 在每个环节打时间戳日志 | 改为流式 ASR / 流式 LLM / 流式 TTS 级联 |
| 识别结果出现大量错字 | 采样率或格式不匹配 | 检查 ASR 输入音频格式 | 统一为 16kHz/16bit/mono |
| AI 声音与患者声音重叠 | 混音队列控制缺失 | 观察 AI 播放缓冲区状态 | 增加播放闸门,串行播放多段 TTS |
| 多路并发时识别速率跟不上 | ASR 资源被 LLM 挤占 | 监控各服务 CPU/GPU 占用 | 容器 QoS 配额 + 并发限制 |
| 说完话后较长时间才出回复 | VAD 静音判定时间过长 | 录制音频分析静音段 | 将静音判定时间调至 600ms 左右 |
| TTS 播报中医生插话被压制 | 侧链音量控制副作用 | 录音回放确认 | 优化侧链恢复时间,播报结束立即恢复 |
表格里的每条都是实际遇到并解决过的,不是理论推演。排查排版时从现象倒推,优先级最高的永远是"先确认数据长什么样",打开音视频流看一眼,很多问题比看代码更快定位。
5. 一点个人体会
这套管线上线稳定运行之后,我复盘了整个项目的技术决策过程,最深的体会是这样一句话:医疗场景的 AI 实时语音接入,难点不在于单个模型效果好不好,而在于整条链路在资源受限的环境下拼得稳不稳。
我见过很多团队在做类似 Demo 时,跑在开发机上一切都很流畅,一落到客户环境就问题频出。原因就是开发机算力充裕、网络稳定,不用考虑芯片共享、延迟叠加和音频链路回环。真正的工程化,是把每一毫秒的计算开销、每一路的资源占用、每一种音频链路异常都算进预算里。
记得项目上线前,客户提了一个要求:AI 助手的所有回答都要带"仅供参考,不构成诊断依据"的提示说明,而且要求 SDK 能记录每一次 AI 交互的完整音频和文本日志,便于事后审计。这个需求一开始我觉得很麻烦,后来意识到这正是医疗场景的行业特性。于是我在 SDK 里增加了一条记录链路:ASR 识别的中间结果、最终文本、送入 LLM 的上下文、LLM 的完整回答、TTS 播音的音频文件,全部按询问会话维度落盘保存。这个功能上线后客户满意度反而比 AI 语音本身还高,因为医生团队感受到了系统对合规性的尊重。
最后想分享一个实用心得:这类项目千万别只盯着"识别准确率""回答准确率"这些单点指标,一定要建立全链路的可观测体系。我在每一路通话的音频流关键节点都加了数据监控:音量电平、回声残余、识别置信度、首包延迟、播放队列深度。这些数据是排查问题、优化体验的线索来源。有一次半夜被叫起来处理一个"偶发卡顿"问题,就是靠监控曲线发现是某一路通话的播放队列在特定时长后堆积导致的,不看数据根本猜不到原因。
如果条件允许,可以把 ASR 和 TTS 独立成可替换的插件模块,不同科室、不同产品形态可以选择不同的引擎组合。我已经在某个子项目里把 ASR 换成了另一家引擎,只动了配置没改业务代码,这个设计省了大功夫。医疗 AI 语音这块才刚刚起步,后续如果要扩展方言识别、实时翻译、多模态病历录入,管线框架的扩展性是非常重要的。