本地语音对话机器人 --- 开发实践
从零构建多引擎可切换的 Python 语音对话机器人,完整记录技术选型、架构设计、问题排查与修复、配置说明。
项目周期 :2026-05-31 至 2026-06-02 | 迭代轮次 :30+ 次 | 自动化测试:85 项
目录
- 问题解决 --- 9 个核心 Bug 的排查思路、根因分析与修复步骤
- 配置说明 --- 8 大配置模块、58 个参数的全量清单与推荐值
- 架构设计 --- 模块拓扑、设计原则、数据流
- 测试策略 --- 85 项自动化测试与环境检测
- 运维管理 --- 启动方式、服务管理脚本
一、问题解决
1.1 混元 LLM 反复报 AuthFailure.SecretIdNotFound
现象
切换到混元后端后,所有对话请求返回 HTTP 500,错误信息:[混元] API 错误 [AuthFailure.SecretIdNotFound]: The SecretId is not found。但同一份 secret_id/secret_key 在腾讯云 ASR 中正常工作,且独立 Python 脚本直接调用混元 API 也正常。
排查过程
| 步骤 | 检查项 | 发现 |
|---|---|---|
| 1 | 直接测试混元 API(独立脚本) | API 正常返回,HTTP 200,TC3 签名通过 |
| 2 | 检查 Web 进程内的 config._data |
secret_id 长度 11(应为 36),secret_key 长度 3(应为 32) |
| 3 | 定位 /api/config 脱敏逻辑 |
发现 t["secret_key"] = "***" 直接修改了原始 dict |
| 4 | 确认污染路径 | 每次调用 /api/config 都把实际密钥替换为脱敏占位符 |
| 5 | 排查 _call_api 密钥读取 |
__init__ 时缓存的密钥正确,但 config._data 已被污染;之前修复为实时读取后仍依赖 config._data |
根因
web_console.py 的 api_config() 脱敏时使用浅引用而非深拷贝:
python
# 修复前 --- 直接修改原始 config._data
cfg = config.get_all() # 返回原始 dict 引用
t = cfg["tencent_asr"]
t["secret_key"] = "***" # 污染了全局配置!
调用链:前端页面加载 → 请求 /api/config → 密钥被替换为 "***" → 后续所有混元 API 调用使用 "***" 做 TC3 签名 → AuthFailure.SecretIdNotFound。
修复
python
# 修复后 --- 深拷贝,不修改原始数据
import copy
cfg = copy.deepcopy(config.get_all())
同时将 _call_api 的密钥读取从依赖 __init__ 缓存的实例变量改为每次请求时从 config 实时读取:
python
secret_id = config.get("tencent_asr.secret_id", "")
secret_key = config.get("tencent_asr.secret_key", "")
authorization = self._sign_static(secret_id, secret_key, payload, timestamp)
影响文件 :web_console.py L716-727、llm_hunyuan.py L196-210
1.2 git push 被 GitHub Secret Scanning 拦截
现象
git push origin master 报错:
remote: error: GH013: Repository rule violations found for refs/heads/master.
remote: - Tencent Cloud Secret ID
remote: path: config.yaml:59
根因
config.yaml 第 59-61 行明文包含腾讯云 secret_id 和 secret_key,被 GitHub Push Protection 扫描拦截。该密钥已在多个历史 commit 中存在。
修复步骤
| 步骤 | 命令/操作 | 说明 |
|---|---|---|
| 1 | 替换 config.yaml 中密钥为 YOUR_SECRET_ID / YOUR_SECRET_KEY |
脱敏当前文件 |
| 2 | 创建 .gitignore,添加 config.yaml |
防止未来再次提交 |
| 3 | 创建 config.example.yaml |
为新开发者提供模板 |
| 4 | git reset --soft <first-commit> |
重建为单个干净提交,彻底清除历史中的密钥 |
| 5 | git commit -m "Initial clean" |
提交全部文件(不含 config.yaml) |
| 6 | git push --force origin master |
强制推送清理后的仓库 |
| 7 | 恢复本地 config.yaml 真实密钥 |
本地服务继续正常运行 |
防护 :.gitignore 确保 config.yaml 永远不会再被提交。
1.3 前端 UI 与后端状态不同步
现象
Web 控制台显示"状态就绪"且启动提示"已在运行",但停止按钮灰色不可用,语音交互无响应。
排查过程
| 步骤 | 检查项 | 发现 |
|---|---|---|
| 1 | 直接调用 /api/status |
running: True,但 llm_ready: False |
| 2 | 检查 _bot_running 与 _llm_client 一致性 |
后端 running 为 True 但引擎实例为 None |
| 3 | 检查并发调用 | init_engines() 耗时较长(加载 STT 模型),期间前端多次点击启动 |
| 4 | 分析锁机制 | 原代码无互斥锁,多次启动调用产生竞态 |
根因
三层原因叠加:
- 无并发锁 :
api_start执行init_engines()需 10-30 秒,期间前端可重复点击 - 启动失败不重置 :
init_engines抛异常后_bot_running残留为 True - 前端不主动同步:初始加载后仅单向操作,不轮询后端状态
修复
- 后端 :
threading.Lock互斥锁 + 启动失败强制重置全部状态 - 前端 :3 秒轮询
/api/status全状态同步 +/api/health死锁检测 - 容错:停止按钮始终可用(强制停止),不依赖状态判断
影响文件 :web_console.py api_start/api_stop/api_health,templates/index.html updateSTTStatus
1.4 音频播放失败(无声音)
现象
TTS 合成正常(MP3 文件存在且非空),但播放无声音。日志中无错误记录。
排查过程
| 步骤 | 检查项 | 发现 |
|---|---|---|
| 1 | 检查 TTS 输出文件 | temp_response.mp3 存在,16-22 KB,文件正常 |
| 2 | 手动用系统播放器打开 MP3 | 可以正常播放 |
| 3 | 检查 player.py 播放逻辑 |
os.startfile 非阻塞,finally 块立即删除文件 |
| 4 | 验证竞态 | os.startfile 启动播放器后立即执行 _cleanup(),文件在播放前被删除 |
根因
Windows os.startfile() 是异步非阻塞 调用------它只是告诉系统"打开这个文件",不等待播放完成。原代码在 try...finally 的 finally 块中立即执行 _cleanup(audio_path),导致 MP3 文件在播放器加载之前就被删除。
修复
python
# 修复前
os.startfile(audio_path) # 非阻塞
# finally 立即删除文件 → 竞态
# 修复后
# 方案一:PowerShell MediaPlayer 阻塞播放
ps_cmd = (
"$player = New-Object System.Windows.Media.MediaPlayer; "
"$player.Open('{path}'); $player.Play(); "
"while($player.Position -lt $player.NaturalDuration.TimeSpan.TotalMilliseconds){Start-Sleep -Milliseconds 100}; "
"$player.Close()"
)
subprocess.run(["powershell", "-Command", ps_cmd], timeout=60)
# 方案二:播放完成后延迟清理
time.sleep(0.5) # 给播放器足够的缓冲时间
self._cleanup(audio_path)
影响文件 :player.py _play_win_default、play
1.5 Python 代码中 </think> 标签导致 SyntaxError
现象
llm_client.py 在 Python 解释器加载时报 SyntaxError: invalid syntax,指向包含 </think> 标记的行。
根因
源代码中 </think> 标记内部的双引号 " 与 Python 字符串定界符冲突,导致字符串提前闭合,后续内容被当作代码解析:
python
# 错误写法 --- 引号冲突
if "</think>" in text: # Python 将第一个 " 视为字符串结束
...
修复
使用字符拼接构建标记字符串,完全避免引号冲突:
python
end_tag = '<' + '/think' + '>' # 即 "</think>"
if end_tag in text:
last_end = text.rfind(end_tag)
result = text[last_end + len(end_tag):].strip()
影响文件 :llm_client.py _filter_thinking
1.6 Windows 编译依赖导致安装失败
现象
pip install pyaudio 报错:error: portaudio.h: No such file or directory
根因
pyaudio 需要编译 C 扩展,依赖 portaudio 开发库。Windows 默认无此库,需手动安装 MSVC 和 portaudio 头文件。同理,pywhispercpp 也需 CMake + C++ 编译器。
修复
全部替换为预编译库:
pyaudio→sounddevice(预编译 wheel,底层同样基于 portaudio)pywhispercpp→faster-whisper(CTranslate2 后端,纯 Python + 预编译二进制)
1.7 HuggingFace 模型下载超时
现象
faster-whisper 初始化时报 ConnectTimeout 或 ReadTimeout,无法从 huggingface.co 下载模型。
排查与修复
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | 测试 huggingface.co 连通性 |
不可达(网络限制) |
| 2 | 测试 hf-mirror.com 国内镜像 |
可达 |
| 3 | 设置 HF_ENDPOINT=https://hf-mirror.com |
下载成功但文件 0 字节 |
| 4 | 发现 HuggingFace 符号链接在 Windows 不兼容 | 文件为 0 字节占位 |
| 5 | 使用 huggingface_hub.snapshot_download(local_dir_use_symlinks=False) |
下载到真实文件(72MB) |
| 6 | 配置 stt.local_cache 指向本地目录 |
后续启动直接加载,0.4s 完成 |
影响文件 :stt_engine.py、config.yaml stt.local_cache
1.8 Python 3.14 移除 audioop 导致 pydub 不可用
现象
import pydub 报 ModuleNotFoundError: No module named 'audioop'
根因
Python 3.13+ 移除了 audioop 标准库模块(PEP 594),pydub 依赖此模块进行音频格式转换。
修复
播放器完全绕过 pydub:
- 首选 :PowerShell
System.Windows.Media.MediaPlayer.NET 原生播放 - 备选 :
os.startfile调用系统关联播放器 - 检测 :
shutil.which("ffplay")存在时优先使用 ffplay
影响文件 :player.py
1.9 腾讯云 SDK 无法安装
现象
pip install tencentcloud-sdk-python 报 ConnectTimeout 或 ConnectionError(PyPI 不可达)。
修复
自实现 TC3-HMAC-SHA256 签名算法(参考腾讯云 API 3.0 签名文档),通过标准库 hmac + hashlib + httpx 直接发送 HTTP 请求。ASR(asr_tencent.py)和混元(llm_hunyuan.py)共享同一套签名逻辑。
签名流程:构建规范请求串 → 计算 SHA256 哈希 → HMAC 派生签名密钥 → 拼接 Authorization 头
影响文件 :asr_tencent.py、llm_hunyuan.py
二、配置说明
2.1 音频采集 (audio)
| 参数 | 类型 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|---|
sample_rate |
int | 16000 |
采样率 (Hz)。whisper 和 ASR 均要求 16000 | 16000(勿修改) |
sample_width |
int | 2 |
位深度 (字节)。2 = 16-bit | 2(勿修改) |
channels |
int | 1 |
声道数。1 = 单声道 | 1(勿修改) |
chunk_size |
int | 1024 |
每帧采样数。1024 ≈ 64ms @ 16kHz | 1024(勿修改) |
device_index |
int or null | null |
麦克风设备索引。null = 系统默认 | null;如需切换,运行 python main.py --list-devices 获取索引 |
2.2 语音活动检测 (vad)
| 参数 | 类型 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|---|
threshold |
int/float | 500 |
RMS 能量阈值。超过此值判定为"有人在说话"。环境越安静应设越低,环境嘈杂应提高 | 安静房间:200-500;嘈杂环境:800-1500 |
silence_duration |
float | 1.0 |
静音多少秒后判定说话结束 | 0.8 ~ 1.5 秒。太短切句、太长等待久 |
min_duration |
float | 0.5 |
最小有效录音时长 (秒)。短于此值的片段丢弃 | 0.3 ~ 0.5 秒 |
max_duration |
float | 30.0 |
最大录音时长 (秒)。防止环境噪音导致无限录制 | 15 ~ 30 秒 |
调优方法:
1. 运行 python main.py --debug,观察日志中的 RMS 值
2. 安静不说话时记录基线 RMS(通常 50-200)
3. 正常说话时记录峰值 RMS(通常 2000-8000)
4. threshold = 基线 RMS × 3 ~ 峰值 RMS × 0.3
2.3 语音识别 --- 本地 (stt)
| 参数 | 类型 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|---|
backend |
str | "whisper" |
STT 引擎选择。"whisper" = 本地 faster-whisper,"tencent" = 腾讯云 ASR |
网络不稳定用 whisper;追求精度用 tencent |
model_name |
str | "tiny" |
whisper 模型大小 | "tiny"(78MB,最快)→ "base"(145MB)→ "small"(488MB,精度更高) |
model_path |
str | "" |
自定义模型路径。留空自动下载 | 留空 |
hf_endpoint |
str | "https://hf-mirror.com" |
HuggingFace 镜像地址 | 国内必须设为镜像 |
local_cache |
str | 见备注 | whisper 模型本地缓存目录 | 首次下载后自动缓存 |
language |
str | "auto" |
识别语言 | 中文为主用 "zh";多语言用 "auto" |
device |
str | "cpu" |
推理设备 | 无 NVIDIA GPU 选 "cpu" |
compute_type |
str | "int8" |
计算精度 | CPU 推荐 "int8";GPU 推荐 "float16" |
n_threads |
int | 4 |
解码线程数 | CPU 核心数的一半 |
2.4 语音识别 --- 腾讯云 ASR (tencent_asr)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
bool | false |
是否启用。设为 true 且配置下方密钥即可 |
secret_id |
str | "" |
腾讯云 SecretId(获取地址) |
secret_key |
str | "" |
腾讯云 SecretKey |
appid |
int | 0 |
腾讯云 AppId(当前版本未使用,预留) |
engine_model_type |
str | "16k_zh" |
引擎模型。16k_zh 中文、16k_en 英文、8k_zh 电话 |
timeout |
int | 30 |
单次请求超时 (秒) |
max_retries |
int | 3 |
失败重试次数(不含永久错误如鉴权失败) |
2.5 大语言模型 (llm)
| 参数 | 类型 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|---|
backend |
str | "ollama" |
LLM 引擎选择。"ollama" 本地 / "hunyuan" 腾讯云混元 |
免费离线用 ollama;有腾讯云账号用 hunyuan |
base_url |
str | "http://localhost:11434" |
Ollama API 地址 | 默认即可 |
model |
str | "deepseek-r1:7b" |
Ollama 模型名称。运行 ollama list 查看 |
deepseek-r1:7b(推理强)/ qwen2:7b(更快) |
timeout |
int | 120 |
请求超时 (秒)。deepseek-r1 思考较慢需设高 | 120 ~ 180 |
system_prompt |
str | (见文件) | 系统提示词,定义机器人角色和回复风格 | 按需定制 |
auto_fallback |
bool | false |
混元失败时自动回退 Ollama | 默认 false(尊重用户选择);网络不稳定时可开 |
2.6 混元 LLM (hunyuan_llm)
前置条件 :在腾讯云控制台开通混元服务。密钥复用 tencent_asr.secret_id 和 tencent_asr.secret_key。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
model |
str | "hunyuan-lite" |
模型。hunyuan-lite 免费、hunyuan-standard、hunyuan-pro |
timeout |
int | 60 |
请求超时 (秒) |
max_retries |
int | 3 |
失败重试次数。(AuthFailure 类永久错误不重试) |
2.7 语音合成 (tts)
| 参数 | 类型 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|---|
voice |
str | "zh-CN-XiaoxiaoNeural" |
TTS 语音角色 | 女声 Xiaoxiao;男声 Yunxi;查看全部 edge-tts --list-voices |
rate |
str | "+0%" |
语速调整。"+20%" 加快 20%,"-10%" 减慢 10% |
"+0%" ~ "+10%" |
output_path |
str | "temp_response.mp3" |
临时输出路径(每次覆盖) | 默认即可 |
常用 TTS 角色:
| 角色 | 性别 | 风格 |
|---|---|---|
zh-CN-XiaoxiaoNeural |
女 | 活泼自然 |
zh-CN-XiaoyiNeural |
女 | 温柔 |
zh-CN-YunxiNeural |
男 | 新闻播报 |
zh-CN-YunyangNeural |
男 | 专业沉稳 |
2.8 日志 (logging)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
level |
str | "INFO" |
日志级别。DEBUG 输出所有调试信息,INFO 仅输出关键节点 |
file |
str | "chatbot.log" |
日志文件路径(同时输出到控制台) |
三、架构设计
模块拓扑
┌─ main.py (终端语音模式) ───── 录音→VAD→STT→LLM→TTS→播放
│
┌─ web_console.py ──┤ :5000 Flask + SSE + REST API
│ │
│ ┌─ index.html ──┤ 启停 / STT切换 / LLM切换 / 日志(智能滚动) / 对话 / 麦克风调试 / 播放校验
│ └─ llm_manager ─┤ 模型卡片 / 健康检测 / 一键切换 / 参数配置
│ .html
│
├─ config_manager.py ─ 单例 YAML 加载,热重载,点号路径访问
├─ recorder.py ─ sounddevice + RMS 能量 VAD
├─ stt_engine.py ─ create_stt_engine() 工厂路由
│ ├─ faster-whisper (本地 CPU)
│ └─ asr_tencent.py (TC3 签名 + 重试)
├─ llm_client.py ─ create_llm_client() 工厂路由
│ ├─ Ollama (流式 API)
│ └─ llm_hunyuan.py (TC3 签名 + 流式 + 自动回退)
├─ tts_engine.py ─ edge-tts 异步合成
├─ player.py ─ 多后端阻塞播放 (ffplay → MediaPlayer → os.startfile)
├─ check_env.py ─ 8 类环境检测
└─ test_all.py ─ 85 项自动化测试
设计原则
- 工厂模式解耦 :STT 和 LLM 均通过
create_xxx()工厂函数路由,配置切换时无需修改调用方代码 - 非阻塞容错:STT 初始化失败不影响 LLM/TTS(Web 控制台仍可文本对话);混元失败可自动回退 Ollama(需开关开启)
- 状态强一致 :启停互斥锁 + 3s 轮询同步 +
/api/health死锁检测与自动恢复 - 零外部 SDK :所有腾讯云服务通过自实现 TC3-HMAC-SHA256 签名访问,解除对
tencentcloud-sdk-python的依赖 - 降级链:每个技术栈至少有一个备用方案(pyaudio→sounddevice, pydub→MediaPlayer, pywhispercpp→faster-whisper, SDK→自实现签名)
四、测试策略
自动化测试 (test_all.py)
- 规模:85 项,覆盖 13 大类
- 分类:语法检查、导入验证、配置加载、录音器、播放器、LLM 客户端、TTS 引擎、STT 引擎、ASR 引擎、CLI 参数、配置完整性、VAD 状态机、思考过滤
- 运行 :
python test_all.py - 迭代模式:测试 → 发现缺陷 → 修复 → 重跑,直至全部通过
环境检测 (check_env.py)
- 8 类检查:Python 版本、依赖包、ffmpeg/ffplay、Ollama 服务、麦克风设备、config.yaml 完整性、Edge-TTS 连通性、C++ 编译器
- 运行 :
python check_env.py
五、运维管理
启动方式
bash
# 终端语音模式
python main.py # 语音对话(默认)
python main.py --text # 文本模式
python main.py --debug # 调试模式
python main.py --list-devices # 列出麦克风
# Web 控制台
python web_console.py # http://localhost:5000
python web_console.py --port 8080
# 一键启停(Windows CMD)
manage_services.bat start # 启动 Ollama + Web 控制台
manage_services.bat stop # 停止所有服务
项目文件清单
核心模块:main.py, web_console.py, config.yaml, requirements.txt
引擎层: config_manager.py, recorder.py, stt_engine.py, asr_tencent.py,
llm_client.py, llm_hunyuan.py, tts_engine.py, player.py
前端: templates/index.html, templates/llm_manager.html
工具: check_env.py, test_all.py, manage_services.bat, manage_services.ps1
文档: README.md, DEVELOPMENT_LOG.md, DEVELOPMENT_PRACTICE.md, AUTO_EXECUTION_LOG.md