1. 这套架构到底在解决什么问题
把大模型跑在远端、把语音和图像生成留在本机,这个思路我第一次听到的时候觉得有点绕——为什么不干脆全部塞进一台机器里?后来自己动手搭了一遍才明白,这不是为了炫技,而是被现实逼出来的最优解。
先说最直接的痛点:显存。一个能聊得下去的大模型,量化之后动辄也要十几 GB 的显存占用,而本机同时还要跑语音识别、语音合成、图像生成(换装、场景切换)这几套模型,显存根本不够分。我试过在一张 12G 的卡上硬塞,结果是模型加载到一半就 OOM,反复重启,体验极差。把大模型推理挪到远端之后,本机只需要承担轻量级的语音和图像任务,显存压力瞬间降下来。
第二个痛点是响应延迟的分配。很多人以为延迟越低越好,其实要分场景看。对话的"思考"部分(大模型推理)用户是能容忍一两秒的,但语音的"开口"必须快——你按下说话键,如果半秒内没有反馈,用户就会觉得卡。所以把 TTS 放在本机、把大模型放远端,恰好符合"慢的可以等、快的必须快"这个原则。
第三个痛点是迭代成本。大模型更新换代太快了,今天这个版本明天那个版本,如果全部本地部署,每次换模型都要重新下载几十 GB 的权重、重新调环境。远端部署的话,换模型只是改一个 API 地址的事,本机代码几乎不用动。
这套架构适合谁?我觉得有三类人值得参考:一是想在个人电脑上做 AI 陪伴类应用、但硬件不够的开发者;二是想研究"端云协同"这种混合推理模式的工程师;三是单纯想搞明白语音链路和图像链路怎么串起来的技术爱好者。哪怕你最后不用远端大模型,这套"本机做实时交互、远端做重推理"的分工思路也是通用的。
下面我按实际搭建的顺序,把每个环节拆开讲。先讲整体链路怎么设计,再讲远端大模型怎么接,然后是本机语音这条线,最后是场景换装这条线,中间穿插我踩过的坑。
2. 端云分工的整体链路设计
2.1 一次完整对话要经过哪几个节点
在动手写代码之前,我习惯先把数据流画清楚。这套应用的完整链路是这样的:
- 用户按下录音键,本机开始采集音频
- 音频经过 VAD(语音活动检测)切分出有效语音段
- 语音段送进 ASR(语音识别)转成文本
- 文本加上对话历史、人设提示词,打包成请求发给远端大模型
- 远端大模型流式返回文本
- 文本按句子切分,逐句送进本机 TTS 合成语音
- 语音边合成边播放,同时根据文本内容触发场景换装
- 换装请求送进本机图像生成模块,生成新背景或新形象
这里面有个关键设计:第 5 步到第 7 步必须是流水线并行的。如果等大模型把整段话全部返回再开始合成语音,用户要等好几秒才能听到第一句话。正确做法是流式接收,收到一个完整句子就立刻送去合成。我实测下来,这样能把"首字延迟"从 3 秒压到 800 毫秒左右。
2.2 为什么大模型放远端、语音图像留本机
这个分工不是随便定的,背后有三个硬约束。
约束一:显存占用。语音识别(如 Whisper 系列的中等模型)大概占 1-2G,语音合成(如 VITS 类)占 1G 左右,图像生成(如 SD 系列)占 4-6G。加起来 8G 左右,一张消费级显卡还能扛。但如果再加上大模型,直接爆掉。所以大模型必须挪走。
约束二:网络依赖的容忍度。语音和图像是"实时交互"环节,一旦网络抖动,用户立刻能感知到卡顿。而大模型推理本身就有延迟,用户对它的容忍度更高。把对网络敏感的部分放本机,是降低体验风险的关键。
约束三:隐私与成本的平衡。语音数据里往往包含用户最私密的信息,放本机处理能减少上传。而大模型推理是算力大头,放远端可以用更便宜的算力。这个平衡点因项目而异,但大方向是"敏感数据本地化、重算力云端化"。
2.3 通信协议怎么选
远端大模型和本机之间用什么协议通信?我对比过三种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HTTP 短连接 | 实现简单,调试方便 | 每次请求都要握手,流式支持一般 | 低频调用、原型验证 |
| HTTP 流式(SSE) | 支持流式返回,实现不复杂 | 单向,本机不能主动推消息 | 对话类应用首选 |
| WebSocket | 全双工,延迟低 | 实现复杂,需要心跳保活 | 需要双向实时通信 |
我最后选了HTTP 流式(SSE)。原因是对话场景本质上是"本机发一次请求、远端持续返回"的单向流,SSE 刚好匹配,而且调试的时候用 curl 就能看到返回,非常方便。WebSocket 虽然更灵活,但对这个场景来说是过度设计。
提示:SSE 的返回格式要约定好。我用的格式是每行一个 JSON,包含
type(text/done/error)和content字段。这样本机解析起来简单,也方便后续扩展。
2.4 对话历史怎么管理
对话历史是这类应用的灵魂,但也是最容易出问题的地方。我的做法是:
- 本机维护一个滑动窗口,只保留最近 N 轮对话(我设的是 10 轮)
- 每轮对话包含用户输入和 AI 回复
- 超出窗口的对话做摘要压缩,而不是直接丢弃
- 人设提示词单独放在最前面,不参与窗口滑动
为什么要做摘要压缩?因为直接丢弃会导致 AI"失忆",用户提到之前聊过的内容时 AI 一脸茫然,体验很差。摘要压缩虽然会损失细节,但至少保留了主线。我用的是让远端大模型自己总结的方式——把要丢弃的几轮对话发过去,让它压缩成一句话。
3. 远端大模型的接入与流式处理
3.1 接口选型:别一上来就追求最强模型
很多人一上来就想接最强的模型,结果发现延迟高、成本贵,反而不适合陪伴类应用。我的经验是:陪伴场景要的是"快"和"稳",不是"最聪明"。
我实测过几类模型在陪伴场景下的表现:
- 超大参数模型:回答质量高,但首字延迟经常超过 2 秒,用户等得着急
- 中等参数模型:延迟 800 毫秒到 1.5 秒,质量够用,性价比最高
- 小参数模型:延迟低,但经常答非所问,人设容易崩
最后我选的是中等参数的模型,配合精心设计的提示词,效果比直接用大模型还好。因为陪伴场景的对话其实不复杂,关键是"人设稳定"和"响应快"。
3.2 提示词工程:人设稳定的关键
提示词写得好不好,直接决定 AI 像不像一个"人"。我踩过的坑是:一开始只写了"你是一个温柔的助手",结果 AI 回复非常模板化,像客服机器人。
后来我改成结构化提示词,包含这几个部分:
【身份】你是一个 25 岁的插画师,性格温和但有点小傲娇 【说话风格】口语化,偶尔用语气词,不说教,不列条目 【背景故事】你住在海边小城,养了一只叫团子的猫 【行为准则】不主动提自己是 AI,遇到不知道的事就说"这个我不太清楚诶" 【当前场景】{scene},根据场景调整你的语气关键是背景故事和行为准则这两块。有了背景故事,AI 的回复就有了"根",不会飘;有了行为准则,AI 就不会动不动暴露自己是程序。
注意:提示词里不要写"不要做 XX"这种否定句,模型对否定句的理解经常出问题。要写"遇到 XX 情况时,你应该做 YY",用正向描述。
3.3 流式接收的代码实现
流式接收的核心是"边收边处理"。我用 Python 写了一个简单的客户端:
import requests import json def stream_chat(messages, api_url, api_key): headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "messages": messages, "stream": True, "temperature": 0.8, "max_tokens": 512 } buffer = "" with requests.post(api_url, headers=headers, json=payload, stream=True) as resp: for line in resp.iter_lines(): if not line: continue line = line.decode("utf-8") if line.startswith("data: "): data = line[6:] if data == "[DONE]": break try: chunk = json.loads(data) delta = chunk["choices"][0]["delta"].get("content", "") buffer += delta # 按标点切句,凑够一句就送去 TTS if any(p in delta for p in "。!?\n"): yield buffer.strip() buffer = "" except json.JSONDecodeError: continue if buffer.strip(): yield buffer.strip()这段代码的关键在buffer和切句逻辑。为什么要按标点切句?因为 TTS 是按句子合成的,如果按字符切,合成出来的语音会断断续续,非常难听。按标点切句能保证每段语音是完整的语义单元。
3.4 断线重连与降级策略
远端大模型最怕的就是网络抖动。我遇到过好几次聊到一半突然断流,用户那边就是"AI 突然不说话了",体验极差。
我的处理策略是三层:
- 超时重试:单次请求超过 15 秒没返回,自动重试一次
- 断流续接:如果已经收到部分文本就断了,把已收到的部分作为上下文,重新发起请求让它接着写
- 降级回复:如果连续两次失败,本机直接返回一句预设的兜底话术,比如"我这边信号好像有点问题,你再说一遍?"
第三层特别重要。很多开发者只做重试,不做降级,结果网络彻底断了之后应用就卡死在那里。有了兜底话术,至少用户知道发生了什么,不会以为程序崩了。
4. 本机语音链路的搭建细节
4.1 语音识别:VAD 是绕不开的第一关
语音识别本身不难,难的是什么时候开始识别、什么时候结束识别。如果一直开着麦克风识别,会有大量无效音频,既浪费算力又容易误触发。
VAD(语音活动检测)就是解决这个问题的。它的作用是判断"当前这段音频里有没有人说话"。我用的是基于能量的简单 VAD,原理是计算音频帧的短时能量,超过阈值就认为是语音。
但纯能量 VAD 有个问题:环境噪音大的时候会误判。我的改进是加了一个"双阈值"机制:
- 高阈值:能量超过这个值,确定是语音
- 低阈值:能量低于这个值,确定是静音
- 中间区域:保持上一帧的状态,避免频繁切换
这样在嘈杂环境下也能稳定工作。实测下来,误触发率从原来的 20% 降到了 5% 以下。
4.2 语音合成:为什么我放弃了云端 TTS
一开始我用的是云端 TTS,因为音质确实好。但用了几天就放弃了,原因有三个:
- 延迟不可控:云端 TTS 的响应时间波动很大,有时候 200 毫秒,有时候 2 秒,用户能明显感觉到"时快时慢"
- 按量计费:陪伴类应用对话量大,云端 TTS 的成本很快就上去了
- 隐私顾虑:所有对话内容都要上传到云端合成,用户隐私没法保证
换成本机 TTS 之后,延迟稳定在 100-300 毫秒,成本为零,隐私也有保障。音质确实比云端差一点,但对陪伴场景来说完全够用。
本机 TTS 我推荐用 VITS 类的模型,它的特点是音质自然、推理快、支持情感控制。我用的模型大概 1G 左右,在消费级显卡上跑实时合成毫无压力。
4.3 语音链路的流水线设计
语音这条线最容易犯的错误是"串行处理"——等 ASR 全部完成再送大模型,等大模型全部返回再送 TTS。这样每一环都要等上一环,总延迟是各环节之和。
正确的做法是流水线并行:
ASR 输出第 1 句 → 立刻送大模型 大模型返回第 1 句 → 立刻送 TTS TTS 合成第 1 句 → 立刻播放 同时,ASR 继续识别第 2 句...这样总延迟约等于"最慢的那一环",而不是"所有环节之和"。我实测下来,流水线设计能把整体响应时间从 4 秒压到 1.5 秒左右。
实现流水线的关键是用队列解耦各环节。每个环节是一个独立的线程或协程,从上游队列取数据,处理后放进下游队列。这样各环节可以并行工作,互不阻塞。
4.4 打断处理:让对话更自然
真实的对话里,人是会打断对方的。如果 AI 说话的时候用户插话,AI 应该立刻停下来听。这个功能看起来简单,实现起来有几个坑:
- 回声消除:AI 自己的声音会被麦克风采集到,如果不处理,AI 会"听到自己说话"然后误以为用户在说话
- 播放中断:TTS 正在播放的音频要能立刻停止,而不是等当前句子播完
- 状态同步:打断之后,各环节的状态要正确重置,不能残留上一轮的上下文
我的做法是:播放音频的时候,把当前播放的音频数据缓存下来,同时开启回声消除。检测到用户说话时,立刻停止播放、清空下游队列、重置状态机。这套逻辑我调了挺久才稳定,建议一开始就把打断功能设计进去,不要后期再加。
5. 场景换装的图像生成链路
5.1 换装到底换的是什么
"场景换装"这个词听起来很玄,拆开看其实就两件事:
- 换场景:根据对话内容,切换背景图(比如聊到海边就换成海滩背景)
- 换形象:根据对话内容,切换角色的外观(比如聊到睡觉就换成睡衣形象)
这两件事的技术实现不一样。换场景相对简单,本质是"文本到图像"的生成,或者从预设图库里选一张。换形象复杂一些,需要保持角色的一致性——不能聊着聊着角色脸都变了。
5.2 场景切换的触发机制
场景不能乱换,得跟对话内容相关。我的做法是让远端大模型在回复的时候,顺便输出一个场景标签:
[SCENE:beach] 今天海边的风好舒服啊,你要不要一起来?本机解析到这个标签后,触发场景切换。这样做的好处是场景切换和对话内容是强关联的,不会出现"聊着聊着突然换背景"的突兀感。
场景标签的粒度要控制好。太细会导致频繁切换,太粗又体现不出变化。我最后定了 8 个场景:日常、海边、森林、夜晚、雨天、咖啡厅、卧室、节日。每个场景对应一套背景图和一套角色形象。
5.3 图像生成的性能优化
本机跑图像生成,最大的问题是慢。一张 512x512 的图,用 SD 系列模型跑,大概要 3-5 秒。这个延迟对陪伴场景来说太长了。
我的优化方案是预生成 + 缓存:
- 应用启动时,预生成所有场景的背景图,缓存到本地
- 角色形象也预生成几套常用的,缓存起来
- 只有用户触发了"自定义场景"这种低频操作,才实时生成
这样 90% 的场景切换都是"读缓存",延迟降到 100 毫秒以内。实时生成只用在少数场景,用户也能接受那几秒的等待。
提示:预生成的图要压缩存储,不然几十张图很快就占满硬盘。我用的是 WebP 格式,压缩率比 PNG 高很多,画质损失也小。
5.4 角色一致性的保持
如果要做"换形象"而不是"换背景",角色一致性就是核心难题。同一个角色,穿不同衣服、在不同场景下,脸必须是一样的。
我的做法是用LoRA 微调。先准备 20-30 张同一个角色的不同角度、不同表情的图,训练一个 LoRA 模型。之后生成新形象时,加载这个 LoRA,就能保证脸的一致性。
LoRA 训练的门槛其实不高,一张消费级显卡跑几个小时就能出结果。关键是训练素材的质量——素材要清晰、角度要多样、背景要干净。我一开始用了很多带复杂背景的图,训练出来的 LoRA 总是把背景也学进去,后来换成纯色背景的素材才解决。
6. 踩过的坑与排查实录
6.1 音频采集的采样率不匹配
这个问题折磨了我整整一个下午。现象是:ASR 识别出来的文本全是乱码,偶尔能识别出一两个字。
排查过程是这样的:
- 先怀疑是 ASR 模型的问题,换了一个模型,还是乱码
- 然后怀疑是音频格式问题,把采集的音频存成 wav 文件用播放器打开,发现声音是正常的
- 最后对比 ASR 要求的采样率和实际采集的采样率,发现一个是 16000Hz,一个是 44100Hz
采样率不匹配会导致音频的"音调"变了,ASR 模型听到的是被拉伸或压缩的声音,自然识别不出来。解决办法很简单:在采集端做重采样,统一到 16000Hz。
注意:重采样要用高质量的重采样算法,简单的线性插值会有明显失真。我用的是
librosa的resample,效果不错。
6.2 TTS 合成的音频有杂音
TTS 合成出来的音频有"滋滋"的杂音,一开始以为是模型问题,换了好几个模型都有。后来发现是音频缓冲区的问题。
具体来说,TTS 输出的音频是分块生成的,如果直接把每块音频拼起来播放,块与块之间的衔接处会有不连续,听起来就是杂音。解决办法是在拼接的时候做淡入淡出,让块与块之间平滑过渡。
这个坑很隐蔽,因为杂音不是一直有,而是在句子中间偶尔出现,很容易被误认为是模型质量问题。
6.3 大模型返回的文本带格式标记
远端大模型返回的文本经常带 Markdown 标记,比如**加粗**、- 列表项。这些标记直接送进 TTS 会被读出来,变成"星号星号加粗星号星号",非常出戏。
解决办法是在送 TTS 之前做文本清洗:
- 去掉 Markdown 标记
- 去掉括号里的补充说明(除非是语气词)
- 把数字、英文缩写转成中文读法
这个清洗逻辑要写得细一点。我一开始只去掉了**,结果#标题符号又被读出来了。后来干脆写了一个正则规则集,把所有常见标记都覆盖了。
6.4 场景切换的时机不对
场景切换太早或太晚都会破坏体验。太早的话,AI 还没说到相关内容,背景就变了,用户一脸懵;太晚的话,AI 都说完了背景才变,感觉脱节。
我的解决办法是延迟触发:解析到场景标签后,不立刻切换,而是等 TTS 播放到对应的那句话时再切换。这样场景变化和语音内容是同步的,体验自然很多。
实现上,我在 TTS 的播放队列里给每个句子打上场景标签,播放到带标签的句子时触发切换。这个逻辑稍微复杂一点,但效果值得。
6.5 长时间运行的显存泄漏
应用跑几个小时之后会越来越卡,最后 OOM 崩溃。排查发现是显存泄漏——每次图像生成都会分配新的显存,但旧的没有释放。
PyTorch 的显存管理有个特点:它不会立刻把不用的显存还给系统,而是缓存起来备用。这在短时间运行没问题,但长时间运行会越积越多。
解决办法是定期调用torch.cuda.empty_cache(),强制释放缓存。但要注意,这个操作会暂停当前的 GPU 任务,所以不能频繁调用。我的做法是每生成 10 张图调用一次,平衡性能和显存占用。
7. 一些让体验更顺滑的细节
7.1 首字延迟的极致优化
首字延迟是陪伴类应用的生命线。用户按下说话键到听到 AI 第一句话,这个时间越短越好。我做了几件事来压缩它:
- 预热模型:应用启动时就把 ASR、TTS 模型加载好,不要等到第一次使用时才加载
- 流式 ASR:不要等用户说完再识别,边说边识别,说完立刻出结果
- 短句优先:提示词里让大模型"先说短句回应,再展开",这样第一句话能很快返回
- TTS 预热:第一次合成前先跑一次空合成,把模型预热
这几招下来,首字延迟从最初的 3 秒多压到了 800 毫秒左右。
7.2 语音的自然停顿
TTS 合成出来的语音如果一句接一句没有停顿,听起来很机械。真实的对话里,句子之间是有停顿的,长句中间也有换气。
我的做法是在 TTS 合成时,根据标点插入不同长度的静音:
- 逗号:150 毫秒
- 句号:300 毫秒
- 问号、感叹号:350 毫秒
- 段落之间:500 毫秒
这样合成出来的语音就有了自然的节奏感,听起来舒服很多。
7.3 情绪与语音的匹配
陪伴类应用里,AI 的情绪要和语音匹配。开心的时候语速快一点、音调高一点,难过的时候语速慢一点、音调低一点。
实现上,我在提示词里让大模型输出情绪标签,然后 TTS 根据情绪标签调整参数:
| 情绪 | 语速 | 音调 | 音量 |
|---|---|---|---|
| 开心 | 1.1x | +10% | 正常 |
| 平静 | 1.0x | 正常 | 正常 |
| 难过 | 0.9x | -10% | 略低 |
| 惊讶 | 1.2x | +15% | 略高 |
这个调整幅度不能太大,不然会显得很假。我调了好几轮才找到合适的参数。
7.4 对话节奏的控制
AI 不能一直说个不停,也不能一直沉默。好的对话节奏是"你说一句,我说一句",有来有回。
我的做法是给 AI 的回复长度设一个上限,超过就截断。同时,如果用户连续几次都是短回复,AI 也相应缩短回复,避免"用户说一个字,AI 说一段话"的尴尬。
这个逻辑听起来简单,但实际调起来需要根据用户的行为动态调整。我最后用的是一个简单的规则:AI 回复长度 = 用户最近三次回复的平均长度 × 2,上下限分别是 20 字和 200 字。
8. 关于这套架构的一些个人体会
搭完这套东西,我最大的感受是:端云协同不是妥协,而是一种更聪明的设计。很多人觉得"全部本地化"才是终极方案,但实际做下来会发现,本地和云端各有各的优势,把它们放在合适的位置,效果比全部塞在一起好得多。
另一个体会是延迟的感知比延迟的绝对值更重要。用户能感知到的延迟,往往不是总时长,而是"有没有反馈"。只要在用户操作的瞬间给出反馈(比如一个"正在思考"的动画、一声轻响),用户对后续延迟的容忍度会高很多。所以我在每个环节都加了即时反馈,哪怕只是一个小小的视觉提示。
还有就是别追求一步到位。我一开始想把这套系统做得完美,结果卡在细节上很久。后来改成"先跑通主链路,再逐个优化",反而进展快了很多。主链路跑通之后,你会发现很多之前担心的细节其实没那么重要,而真正影响体验的问题会自己冒出来。
最后说一个我觉得最有价值的设计:把每个环节都做成可替换的。ASR、TTS、大模型、图像生成,每个模块都定义好输入输出接口,具体用哪个实现可以随时换。这样当有更好的模型出现时,换上去就行,不用改整个系统。我现在的系统里,TTS 已经换过三次,大模型换过两次,每次都是改几行配置的事。这种灵活性,在技术迭代这么快的领域里,比任何单点优化都重要。