实时 AI 语伴如何切换大模型而不重做语音链路:统一适配、灰度路由与故障回退实战
近期开发者社区明显偏好“快速接入大模型”的内容,但实时 AI 语伴真正让人纠结的,往往不是模型能不能回答,而是:今天用云端 API 做出的 Demo,明天为了隐私改成本地部署,后天又要接某个厂商 SDK,语音链路是否要全部重写?
这种焦虑背后其实是一个架构问题。API、本地部署、SDK 并不是三个完全对等的选择:
- API 与本地部署主要决定模型在哪里运行、由谁维护;
- SDK主要决定应用如何调用某个服务,可能仍然访问云端;
- 实时语音体验还取决于流式输出、取消请求、超时恢复和协议兼容,不能只看模型回答质量。
本文以实时 AI 语伴为例,设计一层独立的 LLM 接入网关。目标不是追求“一句话接入”的演示效果,而是让模型可以替换、灰度和回退,并且不把 RTC、语音识别与语音合成一起拖入改造。
Tencent Conversational AI 支持实时语音交互以及对接多个 LLM 提供方。官方概览:
https://trtc.io/document/conversational-ai-overview?product=conversationalai
一、先把三种接入方式放到同一张决策表里
不要先问“哪个最先进”,而要先确定团队愿意承担什么责任。
| 维度 | 托管模型 API | 自建或本地模型服务 | 厂商 SDK / Agent SDK |
|---|---|---|---|
| 上线成本 | 通常较低 | 需要部署、扩缩容和运维 | 取决于 SDK 封装程度 |
| 数据边界 | 需要审查服务条款与数据路径 | 可由团队控制运行环境 | 需要同时审查 SDK 与后端服务 |
| 协议可替换性 | OpenAI 兼容协议通常较容易适配 | 建议主动暴露兼容协议 | 容易绑定专有对象和回调 |
| 流式输出 | 需验证,不应默认具备 | 取决于推理服务实现 | 取决于 SDK 能力 |
| 请求取消 | 需实测取消是否传到服务端 | 可自行实现 | 不能只看客户端是否停止回调 |
| 运维责任 | 主要由服务方承担 | 主要由自己的团队承担 | 双方共同构成故障面 |
| 适合阶段 | 快速验证、弹性需求 | 明确的数据或定制需求 | 深度使用特定平台能力 |
一个实用判断顺序是:
- 先验证交互契约:是否支持流式返回、取消、超时和请求标识;
- 再验证治理要求:数据能否发送到该服务,日志保存到哪里;
- 然后比较回答质量与成本;
- 最后才决定是否值得承担本地部署或专有 SDK 的绑定成本。
模型榜单回答不了这些问题,必须由产品、安全、算法和后端共同决定。
二、稳定版本的链路应该怎样拆
推荐将实时 AI 语伴拆成以下边界:
用户麦克风 ↓ RTC / 实时媒体传输 ↓ ASR / 语音识别 ↓ 会话控制器 ──→ 安全与业务规则 ↓ LLM Gateway ──→ 云端 API / 本地服务 / 厂商 SDK ↓ TTS / 语音合成 ↓ RTC 播放给用户其中,LLM Gateway 只负责四件事:
- 把应用的统一请求转换成模型请求;
- 把不同返回格式转换成统一文本事件;
- 传播请求标识与取消信号;
- 根据健康状态执行路由和回退。
RTC、ASR、TTS 不应该知道当前使用的是本地模型还是云端 API。这样切换模型时,语音链路不需要跟着重写。
Tencent Conversational AI 的大模型配置文档说明了 OpenAI 兼容模型以及 Dify、Coze 等 Agent 平台的连接方式,并涉及用于路由和观测的请求标识。实际配置项应以官方文档为准:
https://trtc.io/document/68338
三、定义一个不依赖厂商的 LLM 契约
以下 TypeScript 是应用侧适配层示例,不是 Tencent RTC 官方 API。
exporttypeRole="system"|"user"|"assistant";exportinterfaceChatMessage{role:Role;content:string;}exportinterfaceLLMRequest{requestId:string;conversationId:string;messages:ChatMessage[];signal:AbortSignal;metadata:{scene:"voice_companion";locale:string;userConsented:boolean;};}exporttypeLLMEvent=|{type:"text_delta";text:string}|{type:"completed";finishReason:string}|{type:"usage";input?:number;output?:number};exportinterfaceLLMAdapter{readonlyname:string;healthCheck():Promise<boolean>;stream(request:LLMRequest):AsyncIterable<LLMEvent>;}这个契约刻意不暴露某家模型的completion、thread或agent对象。专有字段应留在适配器内部,否则更换模型时,业务代码仍会被绑定。
同时要注意两个边界:
AbortSignal表示应用要求停止当前生成,但不能想当然地认为所有服务端都已停止计算;需要通过服务端日志或供应方文档验证。requestId用于单次请求观测,conversationId用于会话关联,两者不要混用。
四、用同一个适配器覆盖云 API 与本地兼容服务
如果云端 API 和本地推理服务都暴露 OpenAI 兼容接口,可以共用一个 HTTP 适配器,只替换地址、模型名和凭证。
interfaceCompatibleEndpoint{name:string;baseUrl:string;apiKey?:string;model:string;}classCompatibleLLMAdapterimplementsLLMAdapter{constructor(privatereadonlyconfig:CompatibleEndpoint){}getname(){returnthis.config.name;}asynchealthCheck():Promise<boolean>{// 健康检查路径应按所接服务的真实协议实现,不能假定所有服务一致。returnBoolean(this.config.baseUrl);}async*stream(req:LLMRequest):AsyncIterable<LLMEvent>{constresponse=awaitfetch(`${this.config.baseUrl}/chat/completions`,{method:"POST",signal:req.signal,headers:{"Content-Type":"application/json",...(this.config.apiKey?{Authorization:`Bearer${this.config.apiKey}`}:{}),"X-Request-Id":req.requestId},body:JSON.stringify({model:this.config.model,stream:true,messages:req.messages})});if(!response.ok||!response.body){thrownewError(`LLM_HTTP_${response.status}`);}// 生产环境应使用经过测试的 SSE/流式协议解析器。// 不要直接假定一个网络分片就是一个完整 JSON 事件。forawait(consttextofparseCompatibleStream(response.body)){if(text)yield{type:"text_delta",text};}yield{type:"completed",finishReason:"stop"};}}应用侧配置可以写成:
llmRoutes:primary:type:openai-compatiblebaseUrl:${PRIMARY_LLM_BASE_URL}apiKey:${PRIMARY_LLM_API_KEY}model:${PRIMARY_LLM_MODEL}localFallback:type:openai-compatiblebaseUrl:${LOCAL_LLM_BASE_URL}model:${LOCAL_LLM_MODEL}这里的路径和 YAML 字段均为自建网关示例,不代表官方配置格式。接入 Tencent Conversational AI 时,应按照官方大模型配置文档填写实际参数。
SDK 怎么接入
遇到只能通过 SDK 使用的模型,不要让 SDK 对象进入会话控制器,而是额外实现LLMAdapter:
classVendorSDKAdapterimplementsLLMAdapter{readonlyname="vendor-sdk";constructor(privatereadonlyclient:VendorClient){}asynchealthCheck():Promise<boolean>{returnthis.client!==undefined;}async*stream(req:LLMRequest):AsyncIterable<LLMEvent>{constresult=this.client.generateStream({messages:req.messages,requestId:req.requestId});constonAbort=()=>result.cancel?.();req.signal.addEventListener("abort",onAbort,{once:true});try{forawait(constchunkofresult){consttext=normalizeVendorChunk(chunk);if(text)yield{type:"text_delta",text};}yield{type:"completed",finishReason:"stop"};}finally{req.signal.removeEventListener("abort",onAbort);}}}VendorClient、generateStream只是展示适配模式的占位名称,落地时必须替换为所选 SDK 的真实接口。
五、切换模型不能只改一个环境变量
直接把主模型地址从 A 改成 B,风险在于你同时改变了输出节奏、错误格式、取消行为和内容风格。更稳妥的上线流程分为四步。
第 1 步:离线契约测试
为所有适配器执行同一组测试:
constcontractCases=["普通短问答能否返回文本事件","空输入是否被应用层拒绝","请求取消后是否停止继续向 TTS 投递","服务端返回非 2xx 时能否产生标准错误","流式数据被拆包时能否正确重组","同一 requestId 能否贯穿日志"];测试重点不是答案是否一字不差,而是适配器行为是否一致。
第 2 步:影子请求,但不播放影子答案
对已获得用户同意且符合数据规则的流量,可以把同一份脱敏输入发送给候选模型做比较,但只能将主模型结果交给 TTS。
影子链路需要遵守三个限制:
- 不复制用户未同意发送的数据;
- 不把影子模型的输出写入正式会话记忆;
- 不执行影子模型产生的工具调用或业务动作。
如果无法满足这些条件,应改用离线、合成或人工整理的测试集。
第 3 步:小范围灰度
路由键应稳定,避免同一用户每轮对话随机切换模型:
functionchooseRoute(userId:string,rolloutPercent:number){constbucket=stableHash(userId)%100;returnbucket<rolloutPercent?"candidate":"primary";}灰度期间至少比较:
- 首个可播报文本到达时间;
- 完整回答结束时间;
- 空返回、协议错误和超时数量;
- 用户主动停止播报的比例;
- 内容安全拦截与人工反馈结果。
这里不要套用统一的“行业标准值”。应先记录现有版本基线,再结合产品允许的等待时间设置阈值。
第 4 步:保留可见的人工控制
即使模型自动回退成功,用户仍应能:
- 停止当前播报;
- 退出 AI 对话;
- 清除或管理会话数据;
- 对明显不当内容进行反馈;
- 在涉及交易、健康、安全等高风险决定时转向人工或明确的非 AI 流程。
AI 可以生成陪伴式回答,但不应替用户决定是否同意数据使用,也不应通过角色设定掩盖其 AI 身份。
六、实现“只在尚未开口时回退”的路由器
实时语音中最危险的回退方式,是主模型已经说了一半,备用模型又从头回答。用户听到的会是两个互相冲突的答案。
因此需要区分“尚未输出”与“已经输出”:
classLLMRouter{constructor(privatereadonlyprimary:LLMAdapter,privatereadonlyfallback:LLMAdapter){}async*stream(req:LLMRequest):AsyncIterable<LLMEvent>{letemittedText=false;try{forawait(consteventofthis.primary.stream(req)){if(event.type==="text_delta"&&event.text){emittedText=true;}yieldevent;}}catch(error){if(req.signal.aborted)throwerror;if(emittedText){// 已经向用户播报内容,不让备用模型从头续写。thrownewError("PRIMARY_FAILED_AFTER_OUTPUT");}forawait(consteventofthis.fallback.stream(req)){yieldevent;}}}}产品层可以按故障时机处理:
| 故障时机 | 建议处理 |
|---|---|
| 主模型尚未产生文本 | 尝试备用模型,并记录回退原因 |
| 已产生文本但尚未送入 TTS | 丢弃未播放内容后再回退 |
| 已开始语音播放 | 停止本轮,提示用户重试,不自动拼接另一模型答案 |
| 两个模型都不可用 | 结束生成,提供明确的重试或退出入口 |
这比“无限自动重试”更克制。对于陪伴场景,可靠性不是假装永远在线,而是在失败时不制造更混乱的对话。
七、建立最小观测模型
至少为每次模型调用记录以下结构化事件:
CREATETABLEllm_attempts(id BIGSERIALPRIMARYKEY,request_idVARCHAR(128)NOTNULL,conversation_idVARCHAR(128)NOTNULL,adapter_nameVARCHAR(64)NOTNULL,route_roleVARCHAR(16)NOTNULL,started_at TIMESTAMPTZNOTNULL,first_text_at TIMESTAMPTZ,completed_at TIMESTAMPTZ,outcomeVARCHAR(32)NOTNULL,error_codeVARCHAR(64),emitted_textBOOLEANNOTNULLDEFAULTFALSE);CREATEINDEXidx_llm_attempt_requestONllm_attempts(request_id);route_role可记录primary、candidate或fallback;outcome可由应用定义为success、timeout、cancelled、protocol_error等有限枚举。
默认不要把完整用户语音、识别文本和模型回答塞进诊断表。排障字段与内容日志应分开设计,并根据用户同意、业务需要和保存策略处理。
八、效果验证:不要只录一段成功视频
上线前建议按以下清单逐项验收。
接入契约
- 云端 API、本地服务和 SDK 都能映射到统一事件格式;
- 流式解析可处理拆包、粘包和不完整事件;
- 所有模型调用都携带可关联的请求标识;
- 密钥只保存在服务端,不进入客户端安装包和日志;
- 本地服务不可达时不会阻塞整个会话进程。
实时交互
- 用户停止播报后,不再把后续文本送入 TTS;
- 主模型首段输出前失败,可以回退;
- 已经开始播报后失败,不会拼接备用模型答案;
- 用户连续说话时,旧请求不会覆盖新一轮界面状态;
- 模型输出过长时,应用可以安全结束本轮,而不是等待 SDK 自行结束。
灰度与恢复
- 同一用户稳定进入同一灰度分组;
- 候选模型可以单独下线,不影响主模型;
- 熔断后有明确的恢复条件,而不是永久停用;
- 回退原因能通过
requestId查询; - 关闭影子流量后,不再产生额外模型请求。
安全与用户控制
- 进入 AI 语伴前清楚说明 AI 身份及数据处理边界;
- 用户可以停止、退出和管理会话数据;
- 影子请求受用户同意和数据政策约束;
- 高风险问题不会仅依赖模型自动决定;
- 内容审核失败时有可理解的降级提示。
九、常见坑:模型换成功了,产品却更不稳定
1. 把 SDK 当成本地部署
安装在服务器里的 SDK 可能仍然调用厂商云端。判断数据边界时,应检查实际网络路径与服务条款,而不是看依赖包安装在哪里。
2. 只验证最终文本,不验证流式行为
两个模型最终答案都正确,不代表实时体验相同。一个模型可能很晚才返回整段内容,另一个可能持续输出短片段;这会直接影响 TTS 的启动与停顿。
3. 备用模型使用不同的人设和安全规则
故障回退后语气突然改变,通常不是 RTC 问题,而是备用模型没有使用同一套系统约束。公共业务规则应由会话控制器装配,模型适配器只负责协议转换。
4. 失败后自动重放用户整段语音
回退应复用已经确认的文本输入,而不是默认重新上传音频。否则可能造成重复识别、额外数据传输和不一致结果。
5. 把“客户端停止显示”当成真正取消
UI 不再显示,不等于模型服务已经终止生成。至少要分别记录:应用发出取消、适配器收到取消、上游连接结束。无法验证服务端取消时,要在容量与成本评估中保留这个不确定性。
6. 只留一个全局模型开关
全局开关适合紧急停用,但不适合灰度。稳定路由至少应支持主模型、候选模型和备用模型,并保留快速回滚能力。
十、可复用总结
把实时 AI 语伴从 Demo 推向可靠实现,可以复用下面这条路线:
统一 LLM 契约 → 为 API、本地服务、SDK 编写独立适配器 → 执行流式、取消、错误和请求标识契约测试 → 影子验证,不播放也不执行候选结果 → 按稳定路由键灰度 → 只在尚未播报时自动回退 → 用阶段事件观测故障 → 始终保留停止、退出与人工决策入口大模型真正改善的是自然语言理解与生成,并不能自动解决实时媒体传输、协议差异、取消传播、数据治理和故障责任。把这些边界拆清后,团队就不必在“云 API、本地部署、厂商 SDK”之间做一次性押注,而可以让选择保持可逆。
社交娱乐中的 AI 虚拟陪伴、角色对话等场景可参考 Tencent RTC 的方案页面:
https://trtc.io/solutions/social-entertainment
**关系披露:**作者与 Tencent RTC 存在内容合作关系;本文以 Tencent RTC 官方文档作为实现事实参考,示例中的应用侧适配器、数据表和路由策略为通用工程设计,不代表官方 API 或固定配置。