1. 这套架构到底在解决什么问题
先把场景说清楚。我想做一个 AI 陪伴类应用,核心体验是:用户打开 App 就能跟一个虚拟角色聊天,角色有自己的人设、语气、记忆,聊到兴起还能“换装”——比如从日常休闲切换成赛博朋克风格,同时语音回复也要跟上,不能永远是干巴巴的文字。
听起来像是把几个现成 API 拼一拼就完事了?我一开始也这么想,真动手才发现坑在哪儿。
第一个坑是大模型的部署位置。你当然可以调云端 API,但陪伴类应用有个特点:对话轮次多、上下文长、角色设定复杂,token 消耗非常快。如果全部走云端,成本会随着用户量线性上涨,而且延迟不可控。更关键的是,角色的“人格”需要长期记忆和微调,云端 API 很难让你深度定制。
第二个坑是语音链路。陪伴应用如果没有语音,体验直接砍半。但语音涉及 TTS(文字转语音)和 ASR(语音转文字)两个方向,还要考虑音色、情感、延迟。全放本地?手机扛不住。全放云端?延迟和成本又上来了。
第三个坑是场景换装。用户说“换一身衣服”,背后其实是图像生成——要根据角色当前形象和用户描述,生成一张新的角色图。这个如果也走云端,每次生成都要等好几秒,体验很割裂。
所以我的核心思路是:把重计算放在远程服务器,把轻交互和实时响应放在本机。具体来说,大模型推理跑在远程(可以是自己的服务器,也可以是租用的 GPU 实例),本机负责语音处理、图像后处理和 UI 交互。这样既保证了大模型的能力和可定制性,又让用户端保持轻量和流畅。
这套架构适合谁参考?如果你正在做 AI 陪伴、虚拟角色、语音助手类应用,或者单纯想搞清楚“大模型 + 语音 + 图像生成”怎么串起来,下面的内容应该能帮你少走不少弯路。
2. 整体架构设计与选型逻辑
2.1 为什么是“远程大模型 + 本机语音图像”这个组合
先画个简单的数据流:用户说话 → 本机 ASR 转文字 → 发送到远程大模型 → 大模型返回文字回复 → 本机 TTS 转语音播放 → 如果触发换装,本机发起图像生成请求 → 远程生成后返回图片 → 本机展示。
这个链路里,大模型推理是最重的,7B 参数量的模型至少需要 8GB 显存,13B 需要 16GB 以上,手机端根本跑不动。所以必须放远程。而ASR 和 TTS虽然也有计算量,但现在的轻量级模型已经能在手机端实时运行,比如 Piper TTS 的中文模型只有几十 MB,Whisper 的小模型也能在手机上跑出可接受的延迟。放本机的好处是:语音数据不用上传,隐私性好,而且省去了网络往返的延迟。
图像生成比较特殊。Stable Diffusion 这类模型在手机上跑不现实,但我们可以把生成放在远程,本机只做后处理和展示。不过这里有个优化点:如果用户只是换装,不需要重新生成整张图,可以用本机的图像处理库做局部替换或风格迁移,这样响应更快。
注意:远程大模型和远程图像生成可以放在同一台 GPU 服务器上,也可以分开部署。如果预算有限,建议先合并,用队列管理请求,避免显存争抢。
2.2 大模型选型:7B 还是 13B,量化到什么程度
选模型这件事,我的经验是先看显存,再看效果,最后看速度。
如果你用的是单张 24GB 显存的卡(比如 4090),7B 模型用 FP16 精度大概占 14GB,剩下 10GB 可以留给图像生成。13B 模型 FP16 需要 26GB,放不下,必须量化。4-bit 量化后 13B 大概占 7-8GB,效果损失在可接受范围内。
我实测下来,7B 模型 + 4-bit 量化是性价比最高的组合。显存占用降到 4-5GB,推理速度也快,首 token 延迟能控制在 500ms 以内。对于陪伴类应用,角色对话不需要模型有太强的推理能力,更重要的是语气、人设和记忆一致性,7B 完全够用。
具体选哪个模型?我试过几个主流的中文对话模型,各有特点。有的角色扮演能力强,但容易“出戏”;有的指令遵循好,但语气偏正式。建议你根据自己的角色设定去测,重点看模型在长对话中的表现——有些模型聊到十几轮就开始重复或者忘记设定,这种就不适合陪伴场景。
2.3 语音链路:ASR 和 TTS 的本机部署方案
语音这块我踩的坑最多。先说 ASR。
ASR 方案对比:
| 方案 | 模型大小 | 中文准确率 | 延迟 | 适合场景 |
|---|---|---|---|---|
| Whisper tiny | 39M | 一般 | 低 | 快速原型 |
| Whisper base | 74M | 较好 | 中 | 日常对话 |
| Whisper small | 244M | 好 | 较高 | 高质量转写 |
| 专用中文 ASR | 50-100M | 好 | 低 | 中文陪伴场景 |
我的建议是:如果主要做中文陪伴,优先选专门针对中文优化的轻量 ASR 模型,准确率比 Whisper tiny 好,延迟也低。Whisper 的优势是多语言,但中文场景下不一定最优。
TTS 方案对比:
| 方案 | 音色自然度 | 情感支持 | 本机运行 | 中文发音 |
|---|---|---|---|---|
| Piper TTS | 中等 | 弱 | 是 | 需调参 |
| 系统 TTS | 一般 | 无 | 是 | 好 |
| 神经网络 TTS | 高 | 强 | 需优化 | 好 |
| 云端 TTS API | 很高 | 很强 | 否 | 很好 |
Piper TTS 是我用得比较多的,优点是轻量、离线、免费。但中文发音确实需要调——默认模型有些字发音不准,需要自己训练或者找社区优化过的中文语音包。如果你对音色要求高,可以考虑用神经网络 TTS,但要注意模型大小和推理速度的平衡。
实操心得:TTS 的延迟比音色更重要。陪伴场景下,用户说完话后如果等 2 秒才听到回复,体验会很差。建议把 TTS 的推理放在独立线程,边生成边播放,首字延迟控制在 300ms 以内。
2.4 图像生成:换装功能的实现路径
换装功能有两种实现路径:
路径一:全量生成。用户描述新服装,远程 Stable Diffusion 生成一张全新的角色图。优点是灵活,什么服装都能画;缺点是慢,一张图至少 3-5 秒,而且角色一致性难保证。
路径二:局部替换 + 风格迁移。先让用户选择预设的服装模板,本机用图像处理库把角色头部和身体拼接,再做风格统一。优点是快,几乎实时;缺点是服装种类受限于预设。
我的做法是两者结合:预设几套常用服装走路径二,用户自定义描述走路径一。这样大部分换装请求都能快速响应,只有少数复杂需求才走远程生成。
3. 核心细节解析与实操要点
3.1 远程大模型部署:从环境配置到接口封装
远程服务器的环境配置是第一步。我用的方案是Python + FastAPI + vLLM。vLLM 是目前推理效率比较高的框架,支持连续批处理和 PagedAttention,吞吐量比原生 HuggingFace 高不少。
安装步骤大致如下:
# 创建虚拟环境 python -m venv llm_env source llm_env/bin/activate # 安装 vLLM pip install vllm # 安装 FastAPI 和依赖 pip install fastapi uvicorn pydantic模型下载建议从国内镜像站拉,速度会快很多。下载完成后,用 vLLM 启动服务:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --dtype auto \ --max-model-len 4096 \ --gpu-memory-utilization 0.85 \ --port 8000这里有几个参数需要解释:
--dtype auto:自动选择精度,有 GPU 就用 FP16,没有就降级。--max-model-len 4096:最大上下文长度。陪伴场景下 4096 够用了,太长会吃显存。--gpu-memory-utilization 0.85:GPU 显存使用率上限。留 15% 给图像生成和其他任务。
启动后,你会得到一个兼容 OpenAI 接口的 API 端点。本机 App 直接用 HTTP 请求调用就行,不需要额外的 SDK。
注意:如果你的服务器有多个 GPU,可以用
--tensor-parallel-size参数做张量并行。但 7B 模型单卡就够了,多卡反而增加通信开销。
3.2 本机语音处理:ASR 和 TTS 的集成细节
本机语音处理我推荐用Python + ONNX Runtime的方案。ONNX Runtime 在移动端的推理效率不错,而且支持多种硬件加速。
ASR 集成:
import onnxruntime as ort import numpy as np # 加载 ASR 模型 session = ort.InferenceSession("asr_model.onnx") def transcribe(audio_data): # 预处理:重采样到 16kHz,归一化 audio = preprocess(audio_data) # 推理 inputs = {session.get_inputs()[0].name: audio} outputs = session.run(None, inputs) # 后处理:解码为文字 text = decode(outputs) return text关键点是预处理。手机录制的音频采样率可能是 44.1kHz 或 48kHz,ASR 模型通常要求 16kHz,必须重采样。另外要做降噪和静音检测,不然会把环境噪音也转成文字。
TTS 集成:
def synthesize(text): # 文本预处理:分词、数字转写、多音字处理 processed_text = preprocess_text(text) # 推理 audio = tts_session.run(None, {input_name: processed_text}) # 后处理:淡入淡出、音量归一化 audio = postprocess(audio) return audioTTS 的文本预处理很重要。中文里有大量多音字,比如“行”在“银行”和“行走”里发音不同。Piper TTS 的中文模型对多音字处理不够好,需要自己加一层规则或者用分词工具辅助判断。
实操心得:TTS 输出建议加 50ms 的淡入和 100ms 的淡出,避免播放时的爆音。音量归一化到 -3dB 左右,既不会削波,也不会太轻。
3.3 场景换装:图像生成与角色一致性
换装功能的核心难点是角色一致性。用户希望换装后还是同一个角色,只是衣服变了。如果每次生成的脸都不一样,体验会很出戏。
我的解决方案是IP-Adapter + ControlNet。IP-Adapter 负责保持角色面部特征,ControlNet 负责控制姿势和构图。具体流程:
- 用户上传或选择角色基准图。
- 提取面部特征向量,存入角色档案。
- 换装时,用 IP-Adapter 注入面部特征,用 ControlNet 保持姿势。
- 生成新图后,本机做面部融合,进一步保证一致性。
如果不想搞这么复杂,还有一个简化方案:局部重绘。只把服装区域遮罩出来,让模型重绘这部分,其他区域保持不变。这样角色一致性天然有保障,而且生成速度更快。
# 局部重绘的遮罩生成 def create_clothing_mask(image, clothing_region): mask = np.zeros(image.shape[:2], dtype=np.uint8) # 根据预设区域或用户涂抹生成遮罩 mask[clothing_region] = 255 return mask注意:局部重绘的边缘容易有接缝,建议在遮罩边缘做羽化处理,让新旧区域过渡自然。
3.4 数据流与状态管理:对话上下文和角色记忆
陪伴应用的状态管理比普通聊天复杂。你需要维护:
- 对话历史:最近 N 轮对话,用于模型上下文。
- 角色设定:人设、语气、背景故事,每次请求都要带上。
- 长期记忆:用户提到的重要信息,比如“我喜欢猫”“我生日是 3 月 5 日”。
- 场景状态:当前服装、当前场景、当前情绪。
我的做法是用一个JSON 结构统一管理,存在本机数据库里。每次请求大模型时,把角色设定和长期记忆拼接到 system prompt 里,对话历史放在 messages 里。
{ "character": { "name": "小夜", "personality": "温柔、有点傲娇、喜欢音乐", "background": "虚拟歌手,住在赛博城市" }, "memory": [ {"key": "user_likes", "value": "猫、爵士乐"}, {"key": "user_birthday", "value": "3月5日"} ], "scene": { "outfit": "cyberpunk", "mood": "happy" } }这个结构的好处是可扩展。以后想加新功能,比如“角色好感度”“解锁新场景”,直接往 JSON 里加字段就行。
4. 实操过程与核心环节实现
4.1 远程服务器从零搭建:环境、模型、接口
我以一台 24GB 显存的 GPU 服务器为例,走一遍完整流程。
第一步:系统环境准备。Ubuntu 22.04,安装 NVIDIA 驱动和 CUDA。驱动版本建议 535 以上,CUDA 用 12.1 或 12.2。
# 检查驱动 nvidia-smi # 安装 CUDA Toolkit sudo apt install nvidia-cuda-toolkit第二步:Python 环境。用 conda 或 venv 都行,我习惯用 conda,方便管理不同项目的依赖。
conda create -n ai_companion python=3.10 conda activate ai_companion pip install vllm fastapi uvicorn第三步:下载模型。以 7B 中文对话模型为例,从镜像站下载:
# 假设用 huggingface-cli huggingface-cli download model_name --local-dir ./models/chat_model第四步:启动大模型服务。用 vLLM 启动,注意显存分配:
python -m vllm.entrypoints.openai.api_server \ --model ./models/chat_model \ --dtype auto \ --max-model-len 4096 \ --gpu-memory-utilization 0.6 \ --port 8000这里gpu-memory-utilization设为 0.6,留 40% 给图像生成。如果只跑大模型,可以设到 0.9。
第五步:部署图像生成服务。我用的是 Stable Diffusion WebUI 的 API 模式,或者自己用 diffusers 库封装一个 FastAPI 服务。
from diffusers import StableDiffusionPipeline import torch pipe = StableDiffusionPipeline.from_pretrained( "model_path", torch_dtype=torch.float16 ).to("cuda") @app.post("/generate") def generate(prompt: str, negative_prompt: str = ""): image = pipe(prompt, negative_prompt=negative_prompt).images[0] # 保存或返回 base64 return {"image": image_to_base64(image)}第六步:接口联调。本机 App 通过 HTTP 请求调用这两个服务。建议加一个简单的 API 网关,统一鉴权和限流。
实操心得:远程服务的网络延迟很关键。如果服务器在国内,延迟能控制在 20-50ms;如果走公网到海外,可能 100ms 以上。建议选离用户近的机房,或者用 CDN 加速静态资源。
4.2 本机 App 的语音模块集成
本机 App 我用的是 Python + Kivy 做原型,实际产品可以考虑 Flutter 或原生开发。语音模块的集成步骤如下:
第一步:录音。用 sounddevice 或 pyaudio 采集音频,采样率 16kHz,单声道,16bit。
import sounddevice as sd def record_audio(duration=5): audio = sd.rec( int(duration * 16000), samplerate=16000, channels=1, dtype='int16' ) sd.wait() return audio第二步:VAD(语音活动检测)。不要一直录音,用 VAD 检测用户什么时候开始说话、什么时候结束。Silero VAD 是个不错的选择,模型小,准确率高。
# 伪代码 vad = SileroVAD() while True: chunk = get_audio_chunk() if vad.is_speech(chunk): buffer.append(chunk) elif len(buffer) > 0: # 检测到静音,结束录音 break第三步:ASR 转写。把录音数据送入 ASR 模型,得到文字。
第四步:发送到大模型。把文字和上下文打包,POST 到远程 API。
第五步:TTS 合成。收到回复文字后,送入 TTS 模型,得到音频数据。
第六步:播放。用 sounddevice 或系统音频接口播放。
整个链路的延迟拆解:
| 环节 | 延迟 |
|---|---|
| 录音 + VAD | 100-300ms |
| ASR | 200-500ms |
| 网络传输 | 50-200ms |
| 大模型推理 | 500-2000ms |
| TTS | 200-500ms |
| 播放缓冲 | 100ms |
| 总计 | 1.2-3.6s |
这个延迟对于陪伴场景是可以接受的,但还有优化空间。比如大模型推理可以用流式输出,边生成边 TTS,首字延迟能降到 1 秒以内。
4.3 换装功能的完整实现流程
换装功能的交互流程设计如下:
- 用户点击“换装”按钮。
- 弹出服装选择界面,预设几套服装 + 自定义输入框。
- 用户选择后,本机先检查是否有缓存。
- 如果有缓存,直接展示;如果没有,发起远程生成请求。
- 生成完成后,本机做后处理(裁剪、调色、融合)。
- 更新角色状态,展示新形象。
预设服装的实现:我提前用图像生成模型生成了几套基准服装图,存为本机资源。用户选择时,直接用图像处理库做合成。
from PIL import Image def apply_outfit(character_img, outfit_img, mask): # 根据遮罩合成 result = Image.composite(outfit_img, character_img, mask) # 边缘羽化 result = feather_edges(result, mask) return result自定义服装的实现:用户输入描述,比如“红色连衣裙,带蕾丝边”,拼接到 prompt 里发给远程生成服务。
prompt = f"character wearing {user_input}, same face, same pose, high quality"生成回来后,本机做面部对齐和融合,保证角色一致性。
注意:自定义生成的等待时间较长,建议加一个加载动画,并且允许用户取消。如果生成失败,要有降级方案,比如展示预设服装。
4.4 角色记忆与上下文管理的代码实现
记忆管理我用的是SQLite + JSON的方案。SQLite 存结构化数据,JSON 存灵活字段。
import sqlite3 import json class MemoryManager: def __init__(self, db_path): self.conn = sqlite3.connect(db_path) self._init_tables() def _init_tables(self): self.conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY, key TEXT UNIQUE, value TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) def add_memory(self, key, value): self.conn.execute( "INSERT OR REPLACE INTO memories (key, value) VALUES (?, ?)", (key, json.dumps(value)) ) self.conn.commit() def get_all_memories(self): cursor = self.conn.execute("SELECT key, value FROM memories") return {row[0]: json.loads(row[1]) for row in cursor}每次请求大模型时,把记忆拼接到 system prompt:
def build_system_prompt(character, memories): prompt = f"你是{character['name']},{character['personality']}。" prompt += f"背景:{character['background']}。" if memories: prompt += "你知道以下关于用户的信息:" for key, value in memories.items(): prompt += f"{key}: {value}。" return prompt对话历史的管理要注意截断策略。不能无限增长,否则会超出模型的上下文长度。我的做法是保留最近 10 轮对话,更早的对话做摘要压缩。
def truncate_history(history, max_turns=10): if len(history) <= max_turns * 2: return history # 保留最近 N 轮 recent = history[-max_turns*2:] # 更早的做摘要 older = history[:-max_turns*2] summary = summarize(older) return [{"role": "system", "content": f"之前对话摘要:{summary}"}] + recent5. 常见问题与排查技巧实录
5.1 大模型服务常见问题速查
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 启动时报显存不足 | 模型太大或显存被占用 | 降低 gpu-memory-utilization,或用量化模型 |
| 推理速度慢 | 批处理设置不当 | 调整 max-num-seqs,开启连续批处理 |
| 回复重复 | 温度参数太低 | 提高 temperature 到 0.7-0.9 |
| 角色出戏 | system prompt 不够强 | 加强人设描述,加 few-shot 示例 |
| 上下文丢失 | max-model-len 太小 | 增大上下文长度,或优化截断策略 |
| API 超时 | 网络问题或请求过大 | 加超时重试,压缩请求体 |
排查思路:先看日志,vLLM 会输出详细的推理信息。如果显存不足,用nvidia-smi看占用。如果速度慢,看是不是 batch size 太小,或者模型没加载到 GPU 上。
实操心得:vLLM 的
--enforce-eager参数可以关闭 CUDA graph,虽然会降低一点速度,但能避免一些奇怪的报错。调试阶段建议开启。
5.2 语音链路延迟优化技巧
语音延迟是陪伴应用体验的关键。我总结了几条优化经验:
ASR 优化:
- 用流式 ASR,边说边转写,不要等用户说完再开始。
- 模型选小的,tiny 或 base 就够用,准确率差距在陪伴场景下不明显。
- 预处理用 GPU 加速,如果手机支持。
TTS 优化:
- 流式 TTS,边生成边播放,首字延迟能降到 200ms 以内。
- 缓存常用回复的音频,比如“嗯”“好的”“我在听”。
- 用轻量模型,Piper TTS 的中文模型只有几十 MB。
网络优化:
- 大模型请求用 HTTP/2,支持多路复用。
- 加请求压缩,减少传输数据量。
- 如果延迟敏感,考虑用 WebSocket 长连接。
5.3 图像生成失败与角色不一致的排查
生成失败:
- 检查 prompt 是否有敏感词,有些模型会过滤。
- 检查显存是否足够,图像生成比大模型更吃显存。
- 检查模型是否加载正确,有时候会静默失败。
角色不一致:
- 检查 IP-Adapter 是否生效,权重是否合适。
- 检查 ControlNet 的姿势控制是否过强,导致面部被扭曲。
- 检查随机种子,固定种子能提高一致性。
换装边缘不自然:
- 遮罩羽化半径调大,建议 10-20 像素。
- 生成后做色彩匹配,让新旧区域色调一致。
- 用泊松融合代替简单合成,过渡更自然。
实操心得:角色一致性是个玄学问题,没有百分百完美的方案。我的经验是,预设服装走局部重绘,自定义服装走全量生成 + 面部融合,这样平衡效果和速度。
5.4 本机资源占用与性能平衡
本机跑 ASR 和 TTS 会占 CPU 和内存。如果同时跑图像后处理,低端手机可能会卡。
优化策略:
- ASR 和 TTS 不要同时跑,串行执行。
- 用 ONNX Runtime 的 GPU 加速,如果手机支持。
- 图像后处理用轻量库,比如 Pillow-SIMD。
- 加内存缓存,避免频繁加载模型。
性能监控:
import psutil import time def monitor_performance(): while True: cpu = psutil.cpu_percent() mem = psutil.virtual_memory().percent print(f"CPU: {cpu}%, MEM: {mem}%") time.sleep(1)如果 CPU 持续超过 80%,就要考虑降级方案,比如用系统 TTS 代替神经网络 TTS。
6. 后续扩展与个人经验分享
这套架构跑通之后,扩展空间很大。比如加情感识别,通过语音语调判断用户情绪,让角色做出相应反应。或者加多角色切换,用户可以在不同角色之间切换,每个角色有自己的记忆和场景。
我还试过把大模型换成多模态模型,让角色能“看到”用户发的图片,并做出评论。这个功能实现起来不难,把图片编码后拼接到 prompt 里就行,但要注意图片大小和推理延迟的平衡。
最后分享一个我踩过的坑:不要一开始就追求完美。我最初想一次性把大模型、语音、图像全做好,结果每个模块都卡住,进度很慢。后来改成先跑通文字对话,再加语音,最后加换装,每一步都验证后再往下走,效率高很多。
如果你也在做类似的项目,建议先从最简单的链路开始:本机发文字 → 远程大模型 → 本机显示回复。跑通之后再逐步加语音和图像。这样每一步都有正反馈,也不容易迷失在细节里。