很多开发者在第一次接触离线语音助手时,会误以为最难的环节是“找一个聪明的大模型”。但真正动手之后才发现,ASR、LLM、TTS 三套系统都不是新东西,真正折磨人的是它们如何在同一台边缘设备上串成一条完整、可复用、能排查的流水线。
本文以 reComputer Mini 这类基于 NVIDIA Jetson Orin Nano 的小型边缘计算设备为运行环境,完整演示一条 ASR -> LLM -> TTS 的本地离线语音链路。你不需要连接任何云端语音服务,所有音频采集、文本理解、语音合成都在设备本地完成。读完这篇文章后,你能跑通一个最小可用的离线语音助手,知道每一步在做什么,也知道哪些地方最容易踩坑。
先给一个明确判断:离线语音助手在边缘设备上完全可行,但它的核心约束不是“模型智商”,而是整条链路的延迟、内存占用和模型资源预算。如果你正在做一个语音控制类、隐私敏感类或弱网环境下的机器人项目,这篇文章会很有价值。
1. 这篇文章真正要解决的问题
为什么要在 reComputer Mini 上自建离线语音链路?这是由三个现实痛点驱动的问题。
第一是隐私。语音是最敏感的数据之一,它天然包含用户的口音、情绪、身份特征,甚至是在家里的位置关系。如果所有语音都上传到云端做识别和对话,数据一旦被截获或误用,风险远高于普通文本。离线语音助手可以把录音文件直接停留在设备内部,甚至处理完成就删除,从物理上规避了隐私外传。
第二是延迟与稳定性。本地链路的语音数据不需要经过公网转发,因此省去了上行传输、云端排队、下行回传的时间。更重要的是,在没有网络或者网络质量很差的环境里,离线方案依然可用。对智能家居、仓储物流、医疗护理等场景来说,网络抖动可能导致语音助手直接失效,这种情况是项目无法接受的。
第三是成本。云端语音 API 按照调用次数和时长计费,如果设备每天长时间运行,积少成多是笔不小的开支。而把 ASR、LLM、TTS 都部署在本地之后,单台设备的边际成本会低很多,团队也可以更自由地调参和定制模型,不必被供应商的接口能力绑住。
当然,离线方案也有很多限制。它不适合需要海量常识问答的通用秘书型助手,因为小型本地大模型的参数量远不如云端大模型。它也不适合在无 GPU、无神经加速器的普通单片机上运行,因为本地 LLM 的最低门槛是“能推理”。所以,本文的最适合读者是:
- 正在做边缘设备语音交互的嵌入式或 AI 应用开发者;
- 对隐私和成本敏感的产品开发者;
- 想跑通“本地大模型应用链路”的初学者;
- 正在调研离线语音落地方案的技术负责人。
在阅读过程中,你不需要提前具备语音算法知识,但最好熟悉 Python 基础语法,能使用 SSH 登录开发板,并对 Linux 命令有一定了解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础概念与核心原理
2.1 ASR、LLM、TTS 分别解决什么问题
语音助手链路可以类比成一个人的“耳朵、大脑、嘴巴”。
- ASR(Automatic Speech Recognition)负责把麦克风采集到的声波信号变成文本,相当于耳朵。它有“流式”和“非流式”之分,流式可以边说话边出字,延迟低;非流式要等用户说完一段话才开始识别,准确率通常更高。
- LLM(Large Language Model)负责把 ASR 输出的文本理解成自然回复,相当于大脑。它根据用户的问题,结合上下文生成一段回答文本。
- TTS(Text-to-Speech)负责把 LLM 生成的文本变成可以播放的语音,相当于嘴巴。它可以合成不同音色、语速和情感,但在嵌入式设备上要特别关注推理速度和音质平衡。
这三个模块单独看都有非常成熟的方案,但放在一起时,最容易被忽略的是它们之间的数据格式转换和时序控制。ASR 输出的是“一句话”的文本,LLM 需要的是“带系统提示词的 Prompt”,TTS 需要的是“不含换行和特殊符号的干净文本”。中间任何一步没做好,整个链路就会断掉。
2.2 为什么本地大模型要谈“量化”
reComputer Mini 的核心计算单元是 NVIDIA Jetson Orin Nano 模块,虽然不是数据中心级 GPU,但它自带 CUDA 核心,可以运行经过量化的小型大模型。这里经常出现一个误区:有人拿着 7B 甚至 13B 的 FP16 模型想直接塞进 8GB 内存的设备,结果刚加载就触发内存溢出。
量化是解决这个问题的关键手段。它的核心思路是将模型权重从高精度浮点数压缩为低精度整数,比如从 FP16 变成 INT8 或 INT4,从而大幅减少显存和内存占用。量化后的模型虽然会有一定精度损失,但对多数中文口语对话场景,损失并不明显。这也是为什么在 Jetson 这类边缘设备上,我们通常建议选择 Q4_K_M 或 Q4_0 等 4-bit 量化的小型模型。
2.3 本地链路和云端链路的本质区别
云端方案一般是:设备录音 -> 推流到服务端 -> 云端 ASR -> 云端 LLM -> 云端 TTS -> 回传音频。这个方案模型能力强,但每一步都要经过网络传输,而且接口按调用量付费。
本地方案则是:设备录音 -> 本地 ASR -> 本地 LLM -> 本地 TTS -> 本地播放。它的核心组件全都跑在同一台设备上,延迟可控、隐私可控、成本固定,代价是需要精心规划模型资源和推理时间。
| 对比维度 | 云端语音方案 | 本地离线方案 |
|---|---|---|
| 网络依赖 | 强 | 无 |
| 隐私性 | 语音数据外传 | 数据不出设备 |
| 模型能力 | 强,参数量大 | 中,适合垂直场景 |
| 运行成本 | 按调用量计费 | 电费和硬件成本 |
| 调试难度 | 依赖服务商文档 | 需要自己处理全链路 |
| 典型场景 | 客服机器人、办公助理 | 智能家居、机器人、隐私敏感场景 |
3. 硬件选型与运行环境准备
3.1 reComputer Mini 适合做语音助手吗
reComputer Mini 是 Seeed Studio 基于 NVIDIA Jetson Orin Nano 模组推出的边缘计算套件,它把 NVIDIA 平台的强大算力封装成了接近单板电脑的形态,适合做机器人、视觉检测和端侧 AI 推理。
从语音助手角度看,这套硬件有几个明显优势:
- 自带小型 GPU,能跑带 CUDA 加速的 ASR、LLM 和 TTS;
- 支持 JetPack SDK,NVIDIA 的驱动和 CUDA、TensorRT 等组件已经封装好;
- 接口丰富,可以外接 USB 麦克风、扬声器、摄像头,也支持 GPIO 控制外围设备;
- 整机体积小、功耗相对可控,适合嵌入到实际机器人或交互设备中。
真正的限制在于内存和存储。如果你手上的版本是 8GB 内存,那么 LLM 建议选择 3B 或 7B 的量化模型,同时要避免同时加载多个大模型。如果内存是 16GB,可以适当提升模型规模,但也不要无节制地加载模型。语音助手链路需要同时驻留 ASR 模型、LLM 和 TTS 模型,所以内存规划非常关键。
3.2 环境准备
开始部署之前,请先确认硬件已经安装好 JetPack 系统,并可以正常开机登录。如果你是第一次使用,建议先用 HDMI 显示器加键鼠操作,配置好网络和 SSH,之后再用 SSH 远程连接,这样修改配置更方便。
下面的命令只是基础操作,具体系统版本和路径请以设备实际情况为准。首先检查系统基本信息和内存情况:
bash复制sudo apt update && sudo apt upgrade -y
uname -a
nvidia-smi
free -h
df -h
nvidia-smi 能输出版本信息,就代表 CUDA 驱动已经就绪。然后我们需要安装 Python 虚拟环境,避免不同项目之间的依赖互相污染。这里以 venv 为例:
bash复制python3 -m venv ~/voice-assistant
source ~/voice-assistant/bin/activate
pip install --upgrade pip
3.3 安装核心依赖
这条语音链路涉及的软件比较多,最核心的依赖包括:
- 录音与播放:
sounddevice,用于读取麦克风和播放音频; - 音频处理:
numpy、webrtcvad可选,用于处理音频数据; - ASR/TTS 引擎:
sherpa-onnx或faster-whisper,按个人选择安装; - LLM 服务:Ollama,作为本地大模型推理服务;
- HTTP 请求:
requests,用于与 Ollama API 通信。
你可以先安装基础依赖:
bash复制pip install numpy sounddevice requests
如果使用 faster-whisper 做 ASR:
bash复制pip install faster-whisper
如果选择 sherpa-onnx,则安装:
bash复制pip install sherpa-onnx
Ollama 的安装方式需要单独处理。推荐直接从官方安装脚本安装,也可以下载离线包部署。安装完成后启动 Ollama 服务,我们再拉取适合边缘设备的模型。
bash复制curl -fsSL https://ollama.com/install.sh | sh
systemctl status ollama
这里要特别注意:如果设备有防火墙或代理限制,安装过程可能很慢。更稳妥的方式是在另一台电脑上用镜像源下载模型或安装包,再拷贝到 reComputer Mini 上。不要把大模型反复下载当作网络测试,那样非常浪费时间。
4. 核心流程拆解:ASR、LLM、TTS 如何串联
完整的语音助手链路可以拆成下面这些步骤:
- 麦克风采集用户语音;
- 音频队列传给 ASR 引擎,输出文本;
- 文本拼接成 Prompt,交给 LLM;
- LLM 生成回复文本;
- 回复文本经过去噪和清洗,交给 TTS 引擎;
- TTS 生成语音文件,播放器播放出来。
最简单的实现方式是“串行”。录音完成 -> 识别 -> 生成回复 -> 合成语音 -> 播放,整个过程同步执行。这种方案代码简单、容易排查,适合演示和第一版跑通,但用户体验是等待时间较长。
进阶一点的做法是“流水线并行”。比如在录音过程中,上一段音频已经在做 ASR;在 LLM 生成回复时,下一段用户语音的识别已经在跑。但是并行会显著增加线程复杂度,内存占用也会同步上升。对 reComputer Mini 这类设备来说,建议先用串行版本验证功能和稳定性,再根据瓶颈决定是否上并行。
发音设备也是容易被忽略的一环。很多开发板的风扇噪音比语音音量还大,或者麦克风离扬声器太近产生回授。调试时必须注意:对话测试时,扬声器和麦克风要拉开距离,音量设置不要太高。
另外,LLM 的回复文本并不是拿来就能直接合成语音。LLM 可能会输出换行、编号、“好的”等语气词,甚至带着 Markdown 符号。语音合成之前要做一次简单清洗,删掉特殊符号,再把长句按标点切分。
5. ASR 模块部署与实现
ASR 模块是本链路里门槛最低、但又最容易产生挫败感的部分。很多人第一反应是用云端识别接口,但我们要的是离线方案。这里介绍两种常用离线 ASR 路线。
5.1 轻量方案:sherpa-onnx
sherpa-onnx 是开源社区一个成熟的离线语音工具库,它支持流式和非流式识别,也支持中文模型,并且对 Jetson 这类带 CUDA 的芯片比较友好。它的优点是体积小、模型丰富、推理速度快;缺点是不同版本的 Python API 差异比较大,网上复制的代码经常跑不通。
因此,更稳妥的做法是先用命令行工具验证模型,再在业务代码里通过 subprocess 调用。
你可以先从官方模型仓库下载一个适合的中文识别模型,解压到本地目录。然后在模型目录中运行:
bash复制sherpa-onnx-offline \
--paraformer=./sherpa-onnx-paraformer-zh-2023-29-31/model.int8.onnx \
--tokens=./sherpa-onnx-paraformer-zh-2023-29-31/tokens.txt \
--wav=./test.wav
注意,不同模型的参数名不同,paraformer、wenet、zipformer 等都有自己的加载方式。实际使用时请以模型配套文档为准。
这种方式的优点是模型出了问题很容易单独排查,不需要把 Python API 的错误和业务代码混在一起。
5.2 社区更流行方案:faster-whisper
Whisper 是 OpenAI 开源的语音识别模型,faster-whisper 则是基于 CTranslate2 的高效实现。它的 Python API 更直观,很容易写进业务链路中。
在 reComputer Mini 上,我们既可以使用 CPU 模式,也可以尝试 CUDA 模式。CUDA 模式需要安装合适的 CTranslate2 和 cuDNN 环境,如果嫌麻烦,可以先从 CPU 模式开始。模型规模建议选 base 或 small,large 在 8GB 内存设备上会非常吃力。
python复制# file: asr.py
from faster_whisper import WhisperModel
model = WhisperModel(
"small",
device="cpu",
compute_type="int8",
cpu_threads=4,
num_workers=2,
)
def transcribe(wav_path: str) -> str:
segments, _info = model.transcribe(wav_path, language="zh")
text = "".join(segment.text for segment in segments).strip()
return text
这段代码逻辑很直白:加载模型,调用 transcribe,把分段结果拼接成完整文本。需要注意,language="zh" 表示强制中文识别,如果你需要中英混合识别,不建议锁死语言。
运行验证时,只需要两个文件:一个测试音频和一个调用脚本。下面这段代码读取 WAV 文件并打印识别文本:
bash复制python -c "import asr; print(asr.transcribe('./test.wav'))"
如果能够正确输出文本,说明 ASR 部分已经通了一半。常见失败情况包括:模型加载时内存不足、音频采样率与模型要求不一致、WAV 文件损坏等。这些问题都有对应的错误信息,按照提示处理即可。
5.3 实时录音的代码示例
真正的语音助手不能只识别固定 WAV,还需要从麦克风实时采样。这里用 sounddevice 录制一段固定长度的音频。为了演示方便,我们先把录音 5 秒或 8 秒后的数据写成一个临时 WAV 文件。
python复制# file: recorder.py
import sounddevice as sd
import numpy as np
import wave
def record_to_wav(path: str = "input.wav", duration: float = 5.0, samplerate: int = 16000):
print("请开始说话...")
frames = sd.rec(int(duration * samplerate), samplerate=samplerate, channels=1, dtype="int16")
sd.wait()
print("录音结束")
with wave.open(path, "wb") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(samplerate)
f.writeframes(frames.tobytes())
if __name__ == "__main__":
record_to_wav()
这段代码做了三件事:以 16kHz 采样率录音、等待录音完成、写入标准 WAV。16kHz 是语音识别常用的采样率,可以降低数据传输和处理压力。如果你的麦克风默认不支持 16kHz,sounddevice 也可能会自动重采样,但最好先确认设备支持。
在实际项目中,固定时长录音不是最佳做法。更好的方式是使用静音检测,检测到用户停止说话后自动结束录音。这需要用到 webrtcvad,但第一版链路先以“固定录音 + 串行处理”为主,后面再优化体验。
6. LLM 模块部署与实现
6.1 使用 Ollama 运行本地大模型
由于 reComputer Mini 不是传统的 x86 服务器,Ollama 在 Jetson 上运行需要满足几个条件。如果你在安装过程中遇到“找不到 CUDA”的问题,先确认 JetPack 是否完整安装,再检查用户权限。Ollama 默认以 11434 端口提供服务。
启动服务后,拉取适合设备的小型模型。这里有两个选择:如果内存是 8GB,推荐先从 qwen2.5:3b 或更小的模型开始;如果内存是 16GB,可以尝试 qwen2.5:7b 的 4-bit 量化版本。
bash复制ollama pull qwen2.5:3b
ollama run qwen2.5:3b "你好"
第一次执行 ollama run 时,模型需要从本地仓库加载,通常要等待十几秒到几十秒。如果这条命令能正常输出中文回复,说明模型和环境都没问题。
6.2 通过 HTTP API 调用 Ollama
在正式链路中,我们不会直接在 shell 里调用 Ollama,而是用 Python 请求其 HTTP 接口。Ollama 原生提供了 /api/generate 接口,我们可以发送 Prompt 并拿到完整回复。
python复制# file: llm.py
import requests
def chat(user_text: str, system_prompt: str = "你是一个友好的离线语音助手,请用简短自然的中文回答。") -> str:
url = "http://localhost:11434/api/generate"
payload = {
"model": "qwen2.5:3b",
"prompt": f"{system_prompt}\n用户:{user_text}\n助手:",
"stream": False,
"options": {
"temperature": 0.7,
"num_predict": 200,
},
}
resp = requests.post(url, json=payload, timeout=120)
data = resp.json()
return data["response"].strip()
这里需要注意的是 "stream": False,它让服务端一次性返回完整结果,代码更简单。如果你的设备性能很好,也可以改成流式方式,逐字逐词显示回复,但第一版没必要。
回调接口返回的是字符串,其中可能包含换行、Markdown 符号甚至英文引号。我们拿到结果后,要在 TTS 之前做清洗,不要直接把模型的原始输出喂给语音合成器。
6.3 LLM 模块的验证方式
在接入 TTS 之前,建议单独验证 LLM 是否正常工作:
bash复制python -c "import llm; print(llm.chat('你是谁'))"
正常情况下,你会看到一句中文回复。如果长时间没有输出,大概率是模型还在加载,或者内存不足导致进程被杀掉了。这时可以打开另一个终端,用 htop 或 free -h 观察内存使用。
7. TTS 模块部署与实现
7.1 选择离线 TTS 方案
本地 TTS 的选项不少,但适合边缘设备的并不多。之前比较流行的 espeak 支持多语言、占用极低,但音质机械感太强。相比之下,VITS 类模型的自然度更好,sherpa-onnx 集成了这类模型,也提供了命令行工具,很适合嵌入到 Python 流程中。
使用 sherpa-onnx TTS 时,流程是:准备一个 VITS 模型目录,再通过 sherpa-onnx-tts 命令生成 WAV 文件。
bash复制sherpa-onnx-tts \
--vits-model=./model.onnx \
--vits-tokens=./tokens.txt \
--output-filename=output.wav \
"你好,我是你的离线语音助手"
生成成功后,可以通过 aplay 或者 Python 播放验证:
bash复制aplay output.wav
如果 aplay 没有输出声音,先检查 USB 声卡和扬声器是否被系统识别,再看音量设置。开发板上经常出现“代码跑通了但没有声音”的乌龙,这类问题往往和模型没有任何关系。
7.2 在 Python 中调用 TTS
为了让整个链路统一,我们可以在 Python 里用 subprocess 调用 sherpa-onnx-tts 命令行,写一个封装函数:
python复制# file: tts.py
import subprocess
def synthesize(text: str, wav_path: str = "reply.wav") -> str:
cleaned = text.replace("\n", "").replace("#", "").replace("*", "")
if not cleaned:
cleaned = "抱歉,我没有理解你说的话。"
cmd = [
"sherpa-onnx-tts",
"--vits-model=./vits-model/model.onnx",
"--vits-tokens=./vits-model/tokens.txt",
"--output-filename=" + wav_path,
cleaned,
]
subprocess.run(cmd, check=True)
return wav_path
传给命令行的文本必须简洁,尽量不要包含特殊符号。因为命令行参数是按空格分割的,如果文本中有非常特殊的内容可能会出问题。更严谨的做法是把文本写入临时文件,再告诉 TTS 工具读取文件,但这会根据不同工具而变化,这里不再展开。
7.3 播放生成的语音
生成 WAV 之后,需要播放。最简单的播放方式是用 sounddevice 或者系统命令。用 sounddevice 可以避免依赖系统工具:
python复制import sounddevice as sd
import soundfile as sf
def play_wav(path: str, samplerate: int = 16000):
data, sr = sf.read(path)
sd.play(data, sr)
sd.wait()
这个函数会阻塞到播放结束,适合串行链路。如果想在播放的同时继续录音,就需要开启异步播放,但那样会增加回声干扰和线程管理复杂度。
8. 完整链路整合示例
到这里,ASR、LLM、TTS 三个模块都已经单独跑通了。下面把它们合并成主流程脚本。为了降低复杂度,主流程采用串行方式,录音、识别、对话、合成、播放依次执行。
python复制# file: assistant.py
import recorder
import asr
import llm
import tts
def run_once():
print("[1/5] 开始录音...")
recorder.record_to_wav("input.wav", duration=5.0)
print("[2/5] 正在进行语音识别...")
user_text = asr.transcribe("input.wav")
print(f"用户说:{user_text}")
if not user_text:
print("[!] 未识别到有效文字")
return
print("[3/5] 大模型正在生成回复...")
reply_text = llm.chat(user_text)
print(f"助手说:{reply_text}")
print("[4/5] 正在合成语音...")
tts.synthesize(reply_text, "reply.wav")
print("[5/5] 正在播放回复...")
tts.play_wav("reply.wav")
if __name__ == "__main__":
while True:
run_once()
try:
input("按 Enter 继续,Ctrl+C 退出")
except KeyboardInterrupt:
print("退出语音助手")
break
这个脚本完成了一次完整闭环。每次循环会录音 5 秒,然后依次执行识别、对话、合成、播放。你可能会觉得 5 秒录音太长,实际项目里应该用静音检测来代替固定时长,但作为最小演示已经足够清晰。
运行方式:
bash复制python assistant.py
脚本会先显示每个步骤的日志,遇到问题可以快速定位是录音、识别、LLM 还是 TTS 环节出了问题。这种“可观测性”在边缘 AI 项目里非常重要。
9. 运行结果与效果验证
9.1 如何判断链路是否成功
判断成功不能只看“程序没报错”,而是要看四个阶段是否都产生了有效结果:
input.wav是否包含用户语音,且能听到至少一句清晰的人类语音;asr.transcribe输出的文本与实际说话内容是否一致;llm.chat返回的回复是否符合语音助手的身份;reply.wav是否正常生成,并且能听到清晰的语音回复。
按这个顺序排查,可以快速缩小问题范围。假如 input.wav 里是静音,那么后续所有步骤都不会正常,因为 ASR 根本识别不出文本。
9.2 验证用例推荐
建议不要一上来就测试复杂指令,先跑几个最小用例:
- 用户说“你好”,ASR 输出“你好”,LLM 回复“你好,我是你的离线助手”,TTS 播放出来;
- 用户说“今天天气怎么样”,这时候 LLM 会因为没有联网而拒绝或说明自己查不到天气;
- 用户说一句很长的中文,验证 ASR 识别长句的能力和 LLM 生成回复的速度。
通过这些用例,你能直观感知到整条链路的延迟主要出现在哪个模块。通常来说,ASR 识别一两秒、LLM 生成几秒、TTS 合成半秒到一秒,总体延迟几秒到十几秒都很正常。真正要做的不是盲目优化速度,而是先让功能稳定。
9.3 失败时应该看哪里
如果是第一次运行,通常会在这几个位置卡住:
- 录音失败:检查麦克风设备权限,
arecord -l查看声卡列表; - ASR 模型加载失败:检查模型路径和依赖版本;
- Ollama 调用超时:检查模型是否正在加载,以及是否一次性拉取了超大模型;
- TTS 命令找不到:检查 Python 虚拟环境中是否安装了 sherpa-onnx,以及命令行工具是否在 PATH 中。
10. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 录音时没有声音 | 未选择正确的麦克风设备 | 检查 arecord -l,确认 USB 声卡被识别;测试 arecord -d 5 test.wav |
修改 sounddevice 输入设备编号,或外接带明确型号的免驱麦克风 |
| ASR 输出文字乱码 | 编码问题或模型采样率不匹配 | 查看模型文档要求采样率,检查 WAV 属性 | 统一使用 16kHz、16bit、单声道 WAV,必要时对音频重采样 |
| 识别结果为空 | 录音音量太低或静音检测提前结束 | 播放录音检查音量,或用 Audacity 查看波形 | 抬高麦克风增益,延长录音时间,增加自动增益处理 |
| LLM 加载时系统卡死 | 内存不足,模型规模超过物理内存 | free -h 查看剩余内存,dmesg 查看 OOM 日志 |
换更小模型或更激进的量化版本,关闭其他大进程 |
| Ollama 调用超时 | 模型仍在加载或 CPU/GPU 推理慢 | 先执行 ollama run 验证模型速度 |
增大 HTTP 超时时间,使用 stream: False,减少并发请求 |
| TTS 播放没有声音 | 扬声器未找到或音量过低 | 检查 aplay -l,播放系统测试音频 |
设置默认输出设备,调高 ALSA 音量,检查硬件连接 |
| 指令执行后整个脚本死在中间 | 模块子进程崩溃但父进程未捕获 | 查看 Python 异常输出,手动单独运行每个模块 | 在主流程中增加异常捕获和日志,模块之间做好状态输出 |
| 下载模型太慢 | 网络环境受限 | 查看下载速度,确认模型大小 | 替换模型源,或者用其他电脑下载后拷贝到设备 |
表格里的排查方式都遵循同一个原则:把大问题拆成独立小模块,先验证单个模块,再串联调试。很多开发者在边缘设备上折腾一整天,最后发现是麦克风声道设置错误,这其实都是可以避免的。
11. 最佳实践与工程建议
11.1 模型资源预算规划
在 reComputer Mini 上跑完整链路,最核心的工程问题就是内存管理。8GB 内存版本建议把 LLM 控制在 3B 左右,同时使用 INT4 量化;ASR 模型用 base 或 small,TTS 模型用单个 VITS 模型。也就是说,三个模型加在一起的总内存占用不要超过物理内存的 70%,否则系统会因为内存交换而频繁卡顿。
内存规划不是静态的。模型加载后,还要为运行时激活值、音频缓冲区和 Python 进程预留空间。建议在开发阶段使用 htop 实时观察内存,找到最坏情况下的内存峰值,然后按峰值加 20% 余量来做部署规划。
11.2 日志与中间产物保存
每一条语音链路都应该保留中间结果。录音文件、ASR 文本、LLM 回复、TTS 音频,这些都可以写入固定目录并加上时间戳。这样在线上环境出问题时,你可以回溯某一句用户请求在哪一步失败。
例如,可以把每次请求保存为:
text复制logs/2025-01-20/10_30_15_input.wav
logs/2025-01-20/10_30_15_asr.txt
logs/2025-01-20/10_30_15_llm.txt
logs/2025-01-20/10_30_15_reply.wav
这套日志结构非常简单,但非常有效。很多边缘 AI 项目在设备上无法随时接入显示器,能留痕、能回放、能对照,比任何监控面板都实在。
11.3 安全与权限边界
离线语音助手虽然不依赖外网,但本地 Ollama API 默认监听 11434 端口,只应在可信局域网内访问。建议修改 Ollama 环境变量,让它只监听 127.0.0.1,不要让设备把 API 暴露到公网,更不要用默认口令远程管理设备。
另一个安全点是用户语音数据。虽然音频是本地处理,但录音文件如果不做清理,长期累积会形成大量用户隐私数据。建议在日志看板中增加定期清理机制,例如只保留最近 7 天的录音,并对敏感片段进行脱敏删档。
11.4 使用 systemd 管理服务
如果希望语音助手开机自启或长期稳定运行,不建议直接在前台运行 python assistant.py,而是把整个链路封装成 systemd 服务。这样即使程序崩溃,系统也可以自动重启。
bash复制[Unit]
Description=Offline Voice Assistant
After=network.target ollama.service
[Service]
User=your_user
WorkingDirectory=/home/your_user/voice-assistant
ExecStart=/home/your_user/voice-assistant/bin/python assistant.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
如果后面需要加入唤醒词检测、自动录音开始停止、多轮对话上下文等能力,在这个 systemd 架构上扩展会非常方便,不会破坏原有的串行链路。
11.5 多轮对话与上下文管理
第一版 llm.chat 是单轮对话,没有上下文记忆。要让语音助手更像“助手”,可以把多轮对话记录保存到列表里,在每次请求时拼接成新的 Prompt。但注意,上下文越长,Token 消耗越高,LLM 推理时间也越长。建议限制历史轮数,比如保留最近 4 轮对话即可。
12. 总结与后续学习方向
这篇文章从零开始演示了在 reComputer Mini 上构建离线语音助手的完整链路。核心内容可以归纳为三句话:
- ASR、LLM、TTS 单独部署都不算难,真正的工程量在整条链路的串接和排障;
- 在边缘设备上,模型量化和内存规划决定了这条链路能不能跑得动;
- 先用串行、固定录音的方式跑通,再逐步优化流式识别、静音检测和上下文记忆,才是更推荐的项目节奏。
接下来,你可以继续深入的方向包括:用 sherpa-onnx 的流式 ASR 实现边说边出字;用 webrtcvad 做更智能的录音结束判断;接入向量数据库让 LLM 拥有本地知识库;使用 TensorRT 对 LLM 和 TTS 模型做推理加速;也可以为设备加装语音唤醒词模块,让语音助手更有“产品感”。
建议先把本文的串行链路完整跑通,再针对自己项目的具体情况做改造。离线语音助手看起来复杂,但把每一层拆开、验证、串联之后,它就不比一个普通的 Python 服务脚本更难维护。
