
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包含typetext/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: fBearer {api_key}, Content-Type: application/json } payload { messages: messages, stream: True, temperature: 0.8, max_tokens: 512 } buffer with requests.post(api_url, headersheaders, jsonpayload, streamTrue) 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.1x10%正常平静1.0x正常正常难过0.9x-10%略低惊讶1.2x15%略高这个调整幅度不能太大不然会显得很假。我调了好几轮才找到合适的参数。7.4 对话节奏的控制AI 不能一直说个不停也不能一直沉默。好的对话节奏是你说一句我说一句有来有回。我的做法是给 AI 的回复长度设一个上限超过就截断。同时如果用户连续几次都是短回复AI 也相应缩短回复避免用户说一个字AI 说一段话的尴尬。这个逻辑听起来简单但实际调起来需要根据用户的行为动态调整。我最后用的是一个简单的规则AI 回复长度 用户最近三次回复的平均长度 × 2上下限分别是 20 字和 200 字。8. 关于这套架构的一些个人体会搭完这套东西我最大的感受是端云协同不是妥协而是一种更聪明的设计。很多人觉得全部本地化才是终极方案但实际做下来会发现本地和云端各有各的优势把它们放在合适的位置效果比全部塞在一起好得多。另一个体会是延迟的感知比延迟的绝对值更重要。用户能感知到的延迟往往不是总时长而是有没有反馈。只要在用户操作的瞬间给出反馈比如一个正在思考的动画、一声轻响用户对后续延迟的容忍度会高很多。所以我在每个环节都加了即时反馈哪怕只是一个小小的视觉提示。还有就是别追求一步到位。我一开始想把这套系统做得完美结果卡在细节上很久。后来改成先跑通主链路再逐个优化反而进展快了很多。主链路跑通之后你会发现很多之前担心的细节其实没那么重要而真正影响体验的问题会自己冒出来。最后说一个我觉得最有价值的设计把每个环节都做成可替换的。ASR、TTS、大模型、图像生成每个模块都定义好输入输出接口具体用哪个实现可以随时换。这样当有更好的模型出现时换上去就行不用改整个系统。我现在的系统里TTS 已经换过三次大模型换过两次每次都是改几行配置的事。这种灵活性在技术迭代这么快的领域里比任何单点优化都重要。