之前一直想在手头的小型边缘设备上做一个完全不依赖公网的语音助手:说一句话,先转成文字,交给本地大模型回答,最后再把回复合成语音播放出来。智能音箱的玩法听起来很成熟,但一旦落到离线 + 边缘设备场景,需要串联的东西比想象中多得多:音频采集、语音活动检测、本地 ASR、本地 LLM、本地 TTS,每一环都有“能跑”和“跑得顺”的差距。
本文就把 ASR → LLM → TTS 这条完整链路在 reComputer Mini 上的搭建过程记录下来,既是一套边缘端离线语音助手的参考实现,也是一份可以直接照着复现的部署笔记。文章会先讲清各模块的作用和选型思路,再给出完整的代码、配置和运行验证步骤,最后补充性能调优与高频问题排查。
本文适合以下读者:
- 手里有 Jetson 系列设备,想做离线语音交互原型;
- 正在学习边缘 AI 部署,想了解 ASR、LLM、TTS 如何配合;
- 已经跑过单一模型,但想把整条语音链路工程化。
1. 项目背景与核心概念
1.1 reComputer Mini 是什么
reComputer Mini 是 Seeed 基于 NVIDIA Jetson 平台推出的紧凑型边缘 AI 开发设备,通常由 Jetson 核心模块加扩展载板组成。它的特点是尺寸小、功耗低、自带 GPU 算力,非常适合作端侧 AI 推理节点。
这类设备和普通树莓派的区别在于:
- 拥有 NVIDIA GPU,可以加速深度学习推理;
- 支持 CUDA,可以运行比单片机方案大得多的模型;
- 官方有 JetPack SDK,预装系统镜像、驱动和 CUDA 环境;
- 适合做人脸识别、目标检测、语音识别、本地大模型等场景。
在语音助理项目中,GPU 主要用来加速本地 LLM 的推理,同时 ASR 和 TTS 这类相对轻量的模型也可以选择 GPU 或 CPU 执行。
1.2 ASR、LLM、TTS 在语音助手中的分工
一个完整的语音助手通常由三个核心模型组成:
| 模块 | 全称 | 职责 |
|---|---|---|
| ASR | Automatic Speech Recognition | 把麦克风采集到的语音转成文字 |
| LLM | Large Language Model | 理解用户意图,生成自然语言回复 |
| TTS | Text to Speech | 把文字回复合成语音,通过扬声器播放 |
三者组合在一起,就构成了语音交互的闭环:
text复制麦克风录音
↓
语音活动检测(VAD)
↓
ASR(语音 → 文本)
↓
LLM(文本 → 文本回复)
↓
TTS(文本 → 语音)
↓
扬声器播放
项目标题中的“离线语音助手”,指的是 ASR、LLM、TTS 全部跑在 reComputer Mini 本地,不调用云端 API,不需要公网上行网络。这也是边缘智能设备最常见的落地形态之一。
1.3 为什么要做离线语音助手
离线方案最直接的价值是隐私和数据安全。
联网语音助手的录音通常要上传到服务端做识别和理解,即使厂商声明数据加密,对很多企业、实验室、个人开发者来说,始终是一个心理和合规层面都不太舒服的方案。本地离线处理可以从源头避免音频和文本数据离开设备。
其次是可控性和成本:
- 不依赖公网,可用在无外网的内网环境;
- 不需要按调用量付费;
- 模型、逻辑全部在本地,方便定制 Prompt 和功能边界;
- 网络抖动、服务停服不会影响基础体验。
代价是设备算力有限,不能直接跑超大模型。因此需要根据设备内存、算力和任务复杂度,选择合适的模型规模和量化方式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与整体方案设计
2.1 ASR 引擎选型
在 Jetson 上做语音识别,常见选择有:
| 方案 | 优点 | 注意点 |
|---|---|---|
| sherpa-onnx | 支持多种模型,跨平台,CPU/GPU 均可运行 | 需要按模型类别选择加载 API |
| Whisper 系列 | 准确率高,多语言支持好 | 模型较大,实时性稍差 |
| PaddleSpeech | 中文效果好 | 依赖较多,ARM 上安装成本偏高 |
本文选择 sherpa-onnx,主要原因是它在 Jetson 上有较好的兼容性,而且同时提供 ASR 和 TTS 能力,可以用同一套推理框架承载语音链路的两端。它的离线识别支持 Paraformer、Zipformer、Whisper 等模型,中文场景推荐 Paraformer 或 SenseVoice 系列。
2.2 LLM 选型
Jetson 平台适合运行参数量较小的开源模型,常见组合:
- Qwen2.5 系列 1.5B / 3B;
- Llama 3.2 1B / 3B;
- Phi-3 Mini;
- Gemma 2 2B。
这些模型都可以用 Ollama 管理,Ollama 在 x86 和 ARM64 Linux 上都有较成熟的运行方式,会自动尝试使用 GPU 加速。对于边缘设备,模型量化参数建议选择 Q4_K_M 级别的版本,兼顾显存占用和回复质量。
2.3 TTS 引擎选型
TTS 有两个路线:
- sherpa-onnx 内置 VITS 模型推理,支持中文;
- Piper TTS 是另一个轻量方案,但中文模型资源较少。
由于我们已经引入 sherpa-onnx 做 ASR,所以 TTS 也优先使用 sherpa-onnx。这样推理库统一,Python 包只需要一个,部署依赖更简洁。
2.4 整体链路设计
最终技术栈:
text复制语音输入:sounddevice + 本地 VAD
ASR:sherpa-onnx + Paraformer 中文模型
LLM:Ollama + Qwen2.5 1.5B
TTS:sherpa-onnx + VITS 中文模型
扬声器输出:sounddevice 或 aplay
项目文件结构规划如下:
text复制voice-assistant/
├── config.py # 统一配置:采样率、模型路径、Prompt 等
├── vad.py # 录音与语音活动检测
├── asr_engine.py # sherpa-onnx 语音识别封装
├── llm_engine.py # Ollama 对话封装
├── tts_engine.py # sherpa-onnx 语音合成封装
└── main.py # 主循环:串联 ASR → LLM → TTS
这样可以最大程度解耦各模块。后续如果你想把 ASR 换成流式识别,或把 LLM 换成 llama.cpp,只需要改对应模块文件。
3. 环境准备与系统配置
3.1 硬件与系统环境说明
本文示例以 NVIDIA Jetson 平台、JetPack 5 或更高版本环境为主。reComputer Mini 的具体核心模块配置需要结合你手上的型号确认,不同型号在内存、GPU 算力上有差异,但安装流程基本一致。
版本建议:
| 组件 | 建议环境 |
|---|---|
| 系统 | JetPack 5.x / 6.x |
| Python | 3.8 及以上 |
| pip | 最新版本 |
| CUDA | 随 JetPack 预装即可 |
| 麦克风 | USB 麦克风或板载音频输入 |
| 扬声器 | USB 音箱或 3.5mm 音频输出 |
如果使用的是 Jetson Nano 这类较老的平台,需要根据可用内存调整模型大小,建议选择 1B 以下参数级别的 LLM。
3.2 安装 Python 依赖
建议先创建一个虚拟环境,避免污染系统 Python:
bash复制python3 -m venv ~/venv/voice_assistant
source ~/venv/voice_assistant/bin/activate
然后安装基础依赖:
bash复制pip install pip --upgrade
pip install numpy sounddevice requests
sounddevice 用于读取麦克风音频流,内部依赖 PortAudio。在 Jetson 上如果安装过程中缺少系统依赖,可以先用 apt 安装:
bash复制sudo apt update
sudo apt install libportaudio2 libsndfile1
安装 sherpa-onnx:
bash复制pip install sherpa-onnx
如果你需要 GPU 版本的 sherpa-onnx,需要根据官方指引确认当前包是否自带 CUDA 支持,这里建议先用 CPU 版本跑通链路,再优化性能。
3.3 安装 Ollama 并下载模型
在 Jetson 上安装 Ollama,主要方式是通过官方脚本:
bash复制curl -fsSL https://ollama.com/install.sh | sh
安装完成后,启动服务并下载模型:
bash复制ollama serve &
ollama pull qwen2.5:1.5b
ollama serve 会默认监听 localhost:11434,后续 Python 代码通过 HTTP 接口调用。首次拉取模型需要联网,但模型下载完成后,推理过程完全本地执行。
如果 Ollama 官方脚本不适合你的设备,也可以用 llama.cpp 直接加载 GGUF 模型。本文代码按 Ollama 方式组织,方便替换。
3.4 下载 ASR 和 TTS 模型
sherpa-onnx 官方提供了多个预训练模型,访问项目的 GitHub Releases 页面,下载对应模型并解压到项目 models 目录:
- ASR 模型:建议选择
sherpa-onnx-paraformer-zh-2023-09-14或支持 SenseVoice 的模型; - TTS 模型:建议选择
sherpa-onnx-vits-zh-ll等中文 VITS 模型。
模型下载后,目录结构类似:
text复制models/
├── sherpa-onnx-paraformer-zh-2023-09-14/
│ ├── config.yaml
│ ├── model.onnx
│ ├── tokens.txt
└── sherpa-onnx-vits-zh-ll/
├── model.onnx
├── tokens.txt
├── lexicon.txt
└── dict/
注意 ASR 和 TTS 模型的 tokens.txt 不能混用,后面会专门讲这个问题。
3.5 测试音频设备
在动手写代码之前,先确认麦克风和扬声器都能正常工作。
测试录音:
bash复制arecord -d 5 -f S16_LE -r 16000 -c 1 test.wav
测试播放:
bash复制aplay test.wav
如果没有 arecord 和 aplay,说明 ALSA 工具未安装:
bash复制sudo apt install alsa-utils
这一步非常重要。如果设备上没有检测到麦克风,后面录音代码运行时会直接报错。
4. 编写核心模块代码
4.1 统一配置 config.py
为了让所有模块使用一致的参数,先把配置统一放在 config.py 中。
python复制# 文件路径:voice-assistant/config.py
SAMPLE_RATE = 16000 # 16kHz 单声道,适合语音识别
CHANNELS = 1
DTYPE = "int16"
# VAD 参数:音量阈值和静音判定时长
VAD_THRESHOLD = 500 # 可通过环境测试调整
SILENCE_DURATION = 1.0 # 静音达 1 秒,认为一句话结束
MAX_RECORD_SECONDS = 10 # 单次最长录音时间,防止无限录音
# ASR 模型路径
ASR_MODEL_DIR = "models/sherpa-onnx-paraformer-zh-2023-09-14"
# TTS 模型路径
TTS_MODEL_DIR = "models/sherpa-onnx-vits-zh-ll"
# LLM 配置
OLLAMA_URL = "http://localhost:11434"
LLM_MODEL_NAME = "qwen2.5:1.5b"
# 对话上下文轮数,防止历史记录过长占用显存
MAX_HISTORY_TURNS = 6
# 系统提示词,定义助手的角色与行为边界
SYSTEM_PROMPT = "你是一个运行在边缘设备上的语音助手,请用简洁的中文回答问题。"
这里把模型路径和 Prompt 都抽到配置里,后续不需要改核心代码,只需调整配置就能切换模型。
4.2 录音与语音活动检测 vad.py
语音助手的难点之一,是如何判断用户开始说话和结束说话。常见做法是实时读取麦克风音频块,计算 RMS(均方根能量),当能量超过阈值时认定为语音开始;当静音持续超过设定时间时,认定一句话结束。
python复制# 文件路径:voice-assistant/vad.py
import time
import numpy as np
import sounddevice as sd
import config
def audio_rms(frame):
samples = np.frombuffer(frame, dtype=np.int16)
if len(samples) == 0:
return 0.0
return float(np.sqrt(np.mean(samples.astype(np.float32) ** 2)))
def record_until_silence():
print(">>> 正在聆听,请说话...")
frames = []
speech_started = False
silence_start = None
def callback(indata, frames_count, time_info, status):
frames.append(indata.copy())
with sd.InputStream(
samplerate=config.SAMPLE_RATE,
channels=config.CHANNELS,
dtype=config.DTYPE,
blocksize=int(config.SAMPLE_RATE * 0.1), # 100ms 一个音频块
callback=callback,
):
start_time = time.time()
while True:
# 如果已有超过最大录音时长,强制结束
if time.time() - start_time > config.MAX_RECORD_SECONDS:
break
# 取最近一个音频块做能量判断
if frames:
frame = frames[-1].tobytes()
rms = audio_rms(frame)
else:
rms = 0.0
if rms > config.VAD_THRESHOLD:
speech_started = True
silence_start = None
print(".", end="", flush=True)
else:
if speech_started:
if silence_start is None:
silence_start = time.time()
elif time.time() - silence_start >= config.SILENCE_DURATION:
break
time.sleep(0.05)
print()
if not speech_started:
return None, 0.0
audio = b"".join([f.tobytes() for f in frames])
return audio, config.SAMPLE_RATE
这段代码有几个关键点:
- 通过
sd.InputStream采集麦克风数据,回调函数会把每个音频块保存下来; - 能量判断使用 RMS,而不是简单看最大值,因为 RMS 对噪声更稳定;
speech_started用于忽略开头的一段静音,避免误触发;- 一旦检测到静音超过 1 秒,就结束录音并返回音频数据。
如果录音返回为 None,主程序需要进入下一轮监听,而不是继续处理。
4.3 ASR 语音识别封装 asr_engine.py
sherpa-onnx 对不同模型提供了不同的加载方式。Paraformer 模型使用 from_paraformer 工厂方法,加载后通过 create_stream 和 decode_stream 完成识别。
python复制# 文件路径:voice-assistant/asr_engine.py
import sherpa_onnx
import config
class ASREngine:
def __init__(self, model_dir):
self.recognizer = sherpa_onnx.OfflineRecognizer.from_paraformer(
encoder=f"{model_dir}/encoder.onnx",
decoder=f"{model_dir}/decoder.onnx",
tokens=f"{model_dir}/tokens.txt",
num_threads=2,
sample_rate=config.SAMPLE_RATE,
feature_dim=80,
decoding_method="greedy_search",
provider="cpu",
)
def transcribe(self, audio_bytes):
stream = self.recognizer.create_stream()
stream.accept_waveform(config.SAMPLE_RATE, audio_bytes)
self.recognizer.decode_stream(stream)
return stream.result.text.strip()
如果你下载的是 Whisper 系列模型,需要换成 from_whisper,配置字段也会不同。开发时建议先运行 sherpa-onnx 官方 examples 里的对应脚本,确认模型能够被正确加载,再整合到自己工程里。
在 main.py 中,把这个模块实例化一次,避免每轮语音都重新加载模型。
4.4 LLM 对话封装 llm_engine.py
LLM 模块通过 Ollama 提供的 HTTP API 完成对话。调用 /api/chat 接口时,需要传入模型名称和消息列表。消息列表是 OpenAI 兼容格式,便于维护多轮上下文。
python复制# 文件路径:voice-assistant/llm_engine.py
import requests
import config
class LLMEngine:
def __init__(self, model_name=config.LLM_MODEL_NAME):
self.model_name = model_name
self.history = [{"role": "system", "content": config.SYSTEM_PROMPT}]
def ask(self, user_text):
self.history.append({"role": "user", "content": user_text})
resp = requests.post(
f"{config.OLLAMA_URL}/api/chat",
json={
"model": self.model_name,
"messages": self.history,
"stream": False,
},
timeout=120,
)
resp.raise_for_status()
data = resp.json()
reply = data["message"]["content"].strip()
self.history.append({"role": "assistant", "content": reply})
# 控制上下文长度,超出后丢弃最早的非系统消息
if len(self.history) > config.MAX_HISTORY_TURNS * 2 + 1:
self.history = [self.history[0]] + self.history[-(config.MAX_HISTORY_TURNS * 2):]
return reply
def reset(self):
self.history = [{"role": "system", "content": config.SYSTEM_PROMPT}]
这里有一个容易忽略的问题:如果不限制历史长度,多轮对话后 token 数会持续增长,在 Jetson 上很快就把显存占满,导致推理速度雪崩式下降。上面的裁剪逻辑把历史限制在系统提示 + 最近 N 轮用户/助手消息。
如果你想让生成的回复更稳定,可以在 Prompt 里强调不要输出表情符号和过长内容,因为 TTS 朗读大量特殊符号时效果并不好。
4.5 TTS 语音合成封装 tts_engine.py
sherpa-onnx 的 TTS 模块加载 VITS 模型,输入文本,输出音频采样数据,然后通过 sounddevice 播放。
不同版本的 sherpa-onnx Python API 字段可能略有差异,下面以较通用的 VITS 配置为例:
python复制# 文件路径:voice-assistant/tts_engine.py
import numpy as np
import sherpa_onnx
import sounddevice as sd
import config
class TTSEngine:
def __init__(self, model_dir):
tts_config = sherpa_onnx.OfflineTtsConfig(
model=sherpa_onnx.OfflineTtsModelConfig(
vits=sherpa_onnx.OfflineTtsVitsModelConfig(
model=f"{model_dir}/model.onnx",
tokens=f"{model_dir}/tokens.txt",
lexicon=f"{model_dir}/lexicon.txt",
dict_dir=f"{model_dir}/dict",
),
provider="cpu",
num_threads=2,
),
rule_fsts=f"{model_dir}/date.fst,{model_dir}/phone.fst",
)
self.tts = sherpa_onnx.OfflineTts(tts_config)
self.sample_rate = self.tts.sample_rate
def speak(self, text):
if not text:
return
audio = self.tts.generate(text, sid=10, speed=1.0)
samples = np.array(audio.samples, dtype=np.float32)
sd.play(samples, samplerate=self.sample_rate)
sd.wait()
关于 sid,也就是说话人 ID:
- 不同 VITS 模型支持的音色数量不一样;
- 有些模型只有 0 号音色,此时传
sid=0即可; - 部分中文模型内置了多个音色,可以通过传不同 ID 试验。
如果 API 字段报错,建议先对照本地 Python 包里 examples/offline-tts 代码确认当前版本写法,因为 sherpa-onnx 更新比较频繁。
4.6 主程序 main.py
把前三步串起来,形成完整对话循环。
python复制# 文件路径:voice-assistant/main.py
import config
from vad import record_until_silence
from asr_engine import ASREngine
from llm_engine import LLMEngine
from tts_engine import TTSEngine
def main():
print("正在加载模型,请稍候...")
asr = ASREngine(config.ASR_MODEL_DIR)
llm = LLMEngine(config.LLM_MODEL_NAME)
tts = TTSEngine(config.TTS_MODEL_DIR)
print("语音助手已就绪。按 Ctrl+C 退出。")
try:
while True:
audio, sample_rate = record_until_silence()
if audio is None:
continue
# 1. ASR:语音转文字
text = asr.transcribe(audio)
if not text:
print("没有识别到有效内容,请重试。")
continue
print(f"\n[识别] {text}")
# 2. LLM:文字转回复
reply = llm.ask(text)
print(f"[助手] {reply}")
# 3. TTS:文字转语音
tts.speak(reply)
except KeyboardInterrupt:
print("\n退出语音助手。")
if __name__ == "__main__":
main()
主循环的设计逻辑是:
- 先监听用户说话;
- 没有语音则继续等待;
- 识别为空则跳过该轮;
- 识别到文本后,交给 LLM;
- LLM 返回文本,交给 TTS 合成并播放。
整个过程是串行执行的。实际产品如果要支持“打断”或“边生成边播放”,需要改成多线程体系,后面工程化建议部分会提到。
5. 运行与验证
5.1 启动 Ollama 服务
如果 Ollama 没有启动,先启动:
bash复制ollama serve &
确认 API 可访问:
bash复制curl http://localhost:11434/api/tags
返回 JSON 中包含已下载的模型列表,说明服务正常。
5.2 运行语音助手
进入项目目录,激活虚拟环境:
bash复制source ~/venv/voice_assistant/bin/activate
python main.py
正常情况下,会出现:
text复制正在加载模型,请稍候...
语音助手已就绪。按 Ctrl+C 退出。
>>> 正在聆听,请说话...
此时对着麦克风说一句话,例如“帮我介绍一下你自己”。识别到文字后,程序会依次打印识别结果和助手回复,并播放合成语音。
5.3 预期日志示例
text复制>>> 正在聆听,请说话...
...
[识别] 帮我介绍一下你自己
[助手] 我是一个运行在边缘设备上的语音助手,基于本地大语言模型运行。我可以离线回答你的问题。
如果 TTS 正常,扬声器会播放这句话的语音。
5.4 常见失败现象
| 现象 | 大概率原因 |
|---|---|
| 录音一直没有反应 | VAD 阈值过高,或麦克风设备未正确识别 |
| 识别出来是乱码 | tokens.txt 与模型不匹配,或采样率配置不一致 |
| Ollama 报连接失败 | 服务未启动,或端口被占用 |
| TTS 没有声音 | 扬声器未正确选择,或 sid 超出模型音色范围 |
遇到这些问题,建议一个问题一个问题排查:先分别运行 arecord、aplay、ASR 单测、LLM 单测、TTS 单测,最后再跑主程序,而不是直接调试整条链路。
6. 性能优化与工程调优
6.1 降低录音数据量
录音格式固定为 16kHz、单声道、16bit,每秒数据量只有约 32KB。这比 WAV 默认的 44.1kHz stereo 小很多,同时已经满足 ASR 模型的输入要求。不要为了“音质更好”提高采样率,对语音识别反而没有明显收益。
6.2 模型加载策略
模型加载是启动阶段最耗时的部分。主程序目前是启动时一次性加载全部模型,这是推荐做法。不要在每个循环里重复加载模型或创建 recognizer 实例。
如果启动时发现内存不够,可以考虑:
- 把 ASR 和 TTS 都换成更小的量化模型;
- 把 LLM 换成 0.5B 参数级别;
- 减少 Ollama 的 context 大小。
6.3 GPU 与 CPU 的取舍
在 Jetson 上:
- LLM 优先使用 GPU 推理,默认 Ollama 会尝试使用 CUDA;
- ASR 和 TTS 模型相对较小,CPU 推理通常已经够用;
- 如果同时运行多个模型导致显存不足,反而会拖慢整机速度,需要权衡。
nvidia-smi 可以查看 GPU 占用情况。观察对话过程中的显存变化,如果接近上限,应降低上下文轮数或换更小模型。
6.4 并发与打断
当前实现是“一句话一个回合”的串行流程,适合作为原型验证。如果要做到更自然的交互,可以考虑:
- 录音线程持续采集音频;
- ASR 线程负责转写;
- LLM 推理线程负责生成;
- TTS 播放线程负责语音输出;
- 主线程维护状态机,支持“说话中打断播放”。
这种改造复杂度和 bug 概率都会上升,建议在串行版跑通后再逐步加入。
7. 常见问题排查清单
以下是实际部署中最容易踩的坑,按问题现象、原因、解决思路整理成表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 程序启动时提示找不到模型文件 | 模型没有解压到正确目录 | 检查 config.py 中的路径和 models 目录结构 |
| 录音开始后一直不结束 | 环境噪声大,静音段能量仍高于阈值 | 适当提高 VAD_THRESHOLD,或对音频做降噪 |
| 识别结果空字符串 | 说话声音过小,或录音字节被 0 填充 | 调大麦克风音量,检查录音数据 RMS 值 |
| 识别文本出现乱码 | 模型 tokens.txt 与模型不匹配 | 重新从模型压缩包解压配套 tokens.txt |
| ASR 速度很慢 | CPU 线程数太少,或模型偏大 | 提高 num_threads,或换小模型 |
| Ollama 接口请求超时 | 模型首次加载,或生成内容过长 | 增加 timeout,限制 num_predict |
| LLM 回复包含特殊符号 | 未在 Prompt 中限制格式 | 在系统提示词中明确“不要输出特殊字符和表情” |
| TTS 合成后播放无声音 | 输出设备未正确选择 | 检查 aplay -l,并确认 sounddevice 输出设备索引 |
| TTS 音色不对或报错 | sid 超出模型支持范围 | 查看模型说明,尝试 sid=0 或其他合法值 |
| 长时间运行后内存持续增长 | 上下文历史过长或录音缓存未释放 | 检查 MAX_HISTORY_TURNS,确认每轮后清空临时录音缓存 |
部署时建议按照这个顺序排查:先确保 ASR 模块单独能识别出文字,再确认 LLM 单独能回复,再确认 TTS 单独能播放,最后合并主程序。这样定位问题会快很多。
8. 应用于实际项目的最佳实践
8.1 配置与代码分离
模型路径、采样率、Prompt、最大上下文轮数等都放在 config.py 中,不要散落在业务代码里。切换模型或调整设备参数时,只需修改配置文件和下载新模型,不会影响主逻辑。
8.2 日志与状态输出
目前代码使用 print 输出运行日志。如果要长期运行,建议改用 logging 模块,并把日志同时输出到文件和终端。日志至少需要记录:
- 每轮识别的文本;
- LLM 回复文本;
- 各模块耗时;
- 异常堆栈;
- 模型加载时间。
这些数据对性能调优非常有价值。
8.3 使用 systemd 作为后台服务
如果把语音助手部署成开机自启服务,可以编写 systemd unit 文件。建议在 /etc/systemd/system/voice-assistant.service 中配置:
ini复制[Unit]
Description=Offline Voice Assistant
After=network.target
[Service]
Type=simple
User=your_user
WorkingDirectory=/home/your_user/voice-assistant
ExecStart=/home/your_user/venv/voice_assistant/bin/python main.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
使用 systemd 管理的好处是可以自动重启、集中查看日志:
bash复制sudo systemctl daemon-reload
sudo systemctl enable voice-assistant
sudo systemctl start voice-assistant
sudo journalctl -u voice-assistant -f
8.4 模型许可与数据安全
使用开源模型前,要确认模型许可证是否允许商用或修改。不同模型许可证不一致,尤其是 Whisper、Qwen、Llama 等,在使用前查看对应模型的 license 文档。
此外,这条链路会把用户语音和文本保存在设备本地。如果要落地到生产环境,建议:
- 不对录音文件长期保留,处理完成后即丢弃;
- 在 TTS 播放前对 LLM 输出做文本内容过滤;
- 不在日志中记录完整的用户语音特征;
- 对麦克风权限按最小权限原则配置,避免无关进程采集音频。
8.5 上下文安全边界
系统提示词是控制助手行为边界最直接的手段。在边缘设备上,可以用它限制回复长度、话题范围、输出格式。对于可能的敏感话题,本地模型并不具备完整的安全对齐能力,因此任何面向用户开放的离线语音助手,都需要在 LLM 输出后增加一层独立的内容过滤。
9. 扩展方向与后续优化
本文实现的链路是离线语音助手的最小闭环。后续可以继续扩展的方向如下。
9.1 流式语音识别
当前使用离线识别,用户说完一句话后才开始识别。如果希望边说边出结果,可以使用 sherpa-onnx 的在线识别模型(online ASR)。它延迟更低,但工程复杂度更高,需要维护流式状态。
9.2 语音唤醒词
在语音助手长期静默监听的场景下,麦克风一直运行会消耗电量和算力。可以加入唤醒词检测,只在检测到特定唤醒词后启动 VAD 和 ASR。常见方案包括 sherpa-onnx 的 keyword spotter 或专用唤醒模型。
9.3 本地知识库 RAG
如果想让离线助手回答私有文档问题,可以在 LLM 前增加一个本地向量检索模块。用户提问先转化为向量,在本地向量库中检索相关片段,再拼入 Prompt 让 LLM 回答。这样不需要微调模型就能扩展知识范围。
9.4 多语言支持
当前围绕中文语音链路搭建。如果设备使用环境是多语言混合,可以替换为 Whisper 模型,TTS 则选择对应语言模型。切换时注意 tokens.txt 和 lexicon 都要同步更换。
10. 学习路线建议
如果你是从零开始接触边缘语音助手,建议按下面的顺序推进:
- 先跑通麦克风录音和扬声器播放,确认硬件链路正常;
- 单独测试 ASR 模型,用一段 WAV 文件验证识别效果;
- 单独测试 Ollama API,确认模型加载和回复正常;
- 单独测试 TTS 模型,确认可以合成并播放语音;
- 将三个模块串成完整闭环;
- 加入 VAD、上下文管理、日志、systemd 等工程能力。
每一步都用一个最小脚本验证,不要一次写完所有代码再统一调试。这样即使出问题,也能快速定位到具体环节。
如果你想真正深入这套方向,下一步值得研究的是流式 ASR 和本地 RAG。它们分别决定“语音助手能不能及时响应”和“能不能回答专业问题”,是离线语音助手从玩具走向工具的两道关键门槛。
最后想说的是:先别急着追求复杂的并发架构,也不要在模型选择上过度纠结。把录音、识别、对话、合成这四段最小子链路跑通,你就能直观感知到设备性能的瓶颈在哪里。基于真实瓶颈再做针对性优化,比照着网上各种“最优方案”折腾设备更有价值。
如果这篇文章对你有用,建议收藏备用,动手搭一套属于自己的离线语音助手。
