乐于分享
好东西不私藏

C++实现Whisper+Kimi端到端AI智能语音助手

C++实现Whisper+Kimi端到端AI智能语音助手

https://space.bilibili.com/3494351095204205

第1章 项目概览与运行配置

1.1 项目背景与定位

为什么需要这个项目?

voice_ai_chat 是一个端到端语音对话系统,它将两大核心能力串联:

模块 技术 运行位置 职责
ASR (语音识别) Whisper.cpp 本地运行 麦克风采集 → 实时转文本
LLM (大语言模型) Kimi API 云端调用 文本 → 流式生成回复

核心价值

  • 隐私优先:语音数据本地处理,不上传云端

  • 低延迟:本地 ASR 避免网络传输延迟

  • 低成本:Whisper 本地免费运行,只消耗 LLM API 费用

典型应用场景

  • 语音助手/智能客服原型

  • 实时会议记录与 AI 摘要

  • 无障碍辅助工具开发

  • 本地隐私敏感的语音交互应用


1.2 项目结构一览

ai-sdk-cpp-laoliao/applications/voice_ai_chat/
├── main.cpp              # 程序入口:参数解析、初始化、主循环
├── voice_ai_chat.h       # 核心类定义与配置结构
├── voice_ai_chat.cpp     # 核心实现:LLM 工作线程、队列管理
├── CMakeLists.txt        # 编译配置
└── README.md             # 项目文档

依赖关系

main.cpp
  ├── voice_ai_chat.h/cpp      (AI 对话层)
  ├── realtime_coordinator.h   (ASR 协调层)
  ├── audio_capture.h          (音频采集)
  ├── endpoint_detector.h      (VAD 端点检测)
  └── asr_worker.h             (Whisper 识别工作线程)

项目源码领取:https://www.bilibili.com/video/BV1xVQmB6Eti/

1.3 编译与运行

编译步骤

# 1. 确保依赖已安装
sudo apt-get install -y libsdl2-dev pkg-config  # Ubuntu/Debian
# 或
brew install sdl2 pkg-config                      # macOS

# 2. 进入项目目录
cd ai-sdk-cpp-laoliao

# 3. 创建构建目录
mkdir-p build && cd build

# 4. 编译
cmake ..
make-j$(nproc) voice_ai_chat

运行前的必要配置

必须设置环境变量

exportMOONSHOT_API_KEY="your-moonshot-api-key-here"

获取方式:访问 Moonshot AI 开放平台 注册并创建 API Key。

可选环境变量

exportWHISPER_MODEL_PATH="/path/to/your/model.bin"# 指定模型路径
exportVOICE_AI_LOG_LEVEL=debug                      # 设置日志级别

1.4 运行参数详解

基础运行命令

# 方式1:纯文本模式(调试 LLM 链路)
./applications/bin/voice_ai_chat --stdin

# 方式2:语音模式(指定模型路径)
./applications/bin/voice_ai_chat ../../../models/ggml-small.bin

# 方式3:语音模式 + 完整参数
./applications/bin/voice_ai_chat \
  ../../../models/ggml-small.bin \
--mode balanced \
--vad-threshold0.06 \
--threads8

参数分类速查表

A. 模式选择参数

参数 说明 可选值 默认值
--stdin 文本输入模式(不走语音) 关闭
--mode ASR 速度/质量模式 fast/balanced/quality balanced

B. 模型与路径参数

参数 说明 示例
model_path Whisper 模型文件路径(位置参数) ../../../models/ggml-small.bin
--ai-model 大模型名称 kimi-k2.5kimi-k1.5

C. VAD (语音检测) 参数

参数 说明 建议范围 默认值
--vad-threshold 语音检测阈值(越小越灵敏) 0.03 ~ 0.15 0.06
--poll-interval-ms VAD 检测间隔(毫秒) 100 ~ 500 250
--max-segment-ms 单次送识别最大音频长度 1000 ~ 5000 2200

D. 性能参数

参数 说明 建议范围 默认值
--threads ASR 推理线程数 4 ~ 16 8
--llm-queue-size LLM 请求队列长度 4 ~ 16 8

E. 文本优化参数

参数 说明 示例
--prompt-file 加载自定义提示词文件 --prompt-file ../prompts/tech.txt
--hotwords-file 加载热词列表 --hotwords-file ../hotwords/tech.txt
--lexicon-file 加载词库纠错 --lexicon-file ../lexicons/common.txt
--replacements-file 加载术语替换规则 --replacements-file ../replacements/tech.txt

F. Partial 预览参数(建议关闭)

参数 说明 建议范围 默认值
--enable-partial 开启说话过程中的临时预览 关闭
--partial-interval-ms partial 刷新周期 400 ~ 700 700
--partial-min-segment-ms partial 最短音频 500 ~ 800 900
--partial-max-segment-ms partial 最长音频 1200 ~ 2200 1600

G. 设备与日志参数

参数 说明
--list-input-devices 列出可用输入设备并退出
--input-device 指定输入设备(名称或索引)
--log-level 日志级别:debug/info/warn/error

1.5 模型选择与推理速度

Whisper 模型对比

Whisper.cpp 支持多种模型,模型越大准确率越高,但速度越慢

模型 参数量 文件大小 实时因子 (RTF) 适用场景 推荐指数
tiny 39M ~75MB ~0.1x 极简设备、高实时要求 ⭐⭐⭐
base 74M ~142MB ~0.3x 平衡选择、移动端 ⭐⭐⭐⭐
small 244M ~466MB ~1.0x 推荐默认、PC端实时 ⭐⭐⭐⭐⭐
medium 769M ~1.5GB ~3-5x 质量优先、短句 ⭐⭐⭐⭐
large-v3 1.5B ~2.9GB ~8-15x 非实时、高质量需求 ⭐⭐

RTF (Real-Time Factor): 处理1秒音频需要的实际秒数。RTF < 1 才能实时。

硬件配置建议

CPU 要求

模型 最低 CPU 推荐 CPU 内存需求
tiny 2核 4核 2GB
base 2核 4核 4GB
small 4核 8核+ 6GB
medium 8核 16核+ 12GB
large-v3 16核 32核+ 16GB

模式选择指南

根据你的 CPU 性能和实时性要求选择 --mode

# 场景1:强实时、短句命令(如语音输入框)
--mode fast --threads8--max-segment-ms1600
# 特点:延迟最低,准确率略低

# 场景2:普通对话、平衡选择(推荐)
--mode balanced --threads8--max-segment-ms2200
# 特点:速度与质量兼顾

# 场景3:长句、质量优先(会议记录)
--mode quality --threads8--max-segment-ms2600
# 特点:准确率最高,延迟较大

模式内部参数详解

模式 max_len max_tokens audio_ctx beam/best_of 适用
fast 32/40 24/32 256/384 best_of=1 实时命令
balanced 32/72 24/0 256/512 best_of=2 日常对话
quality 32/96 24/0 256/640 beam=4 精确转录

格式说明:partial参数/final参数max_tokens=0 表示不限制。


1.6 推荐配置组合

配置 A:极速实时(短句命令)

./voice_ai_chat ../../../models/ggml-small.bin \
--mode fast \
--vad-threshold0.05 \
--poll-interval-ms200 \
--threads8 \
--max-segment-ms1600

适用:语音输入框、快速命令、即时响应场景

配置 B:平衡模式(推荐日常使用)

./voice_ai_chat ../../../models/ggml-small.bin \
--mode balanced \
--vad-threshold0.06 \
--poll-interval-ms250 \
--threads8 \
--max-segment-ms2200 \
--prompt-file ../prompts/general.txt \
--lexicon-file ../lexicons/common.txt

适用:日常对话、办公场景

配置 C:质量优先(会议记录)

./voice_ai_chat ../../../models/ggml-medium.bin \
--mode quality \
--vad-threshold0.06 \
--poll-interval-ms300 \
--threads8 \
--max-segment-ms2600 \
--prompt-file ../prompts/general.txt \
--replacements-file ../replacements/tech.txt

适用:会议记录、访谈转录、质量优先场景

配置 D:低资源设备

./voice_ai_chat ../../../models/ggml-base.bin \
--mode fast \
--vad-threshold0.08 \
--poll-interval-ms300 \
--threads4 \
--max-segment-ms1800

适用:嵌入式设备、旧电脑、树莓派等


1.7 常见问题诊断

Q1: 说话但没有文本输出?

排查步骤:

  1. 检查日志级别:--log-level debug

  2. 确认 VAD 是否检测到语音:看 [VAD-DEBUG] 日志

  3. 检查音频设备:--list-input-devices 确认使用正确设备

  4. 调整 VAD 阈值:--vad-threshold 0.05(更灵敏)

Q2: 推理速度太慢?

优化建议(按优先级):

  1. 换小模型:large → medium → small → base

  2. 用 fast 模式:--mode fast

  3. 缩短片段:--max-segment-ms 1600

  4. 增加线程:--threads 8(不超过物理核心数)

  5. 检查 CPU:是否在运行其他占用 CPU 的程序?

Q3: 出现很多繁体字?

解决方案:

  1. 使用 --prompt-file 加载简体提示词

  2. 使用 --lexicon-file 加载词库纠错

  3. 后续可考虑集成 OpenCC 进行繁转简

Q4: 输出中出现”常见术语包括”等 prompt 内容?

这是 prompt 泄漏,说明提示词太长。解决方法:

  1. 改用短提示词:../prompts/general.txt

  2. 停用 --hotwords-file,改用 --replacements-file


第2章 架构设计与数据流

2.1 整体架构概览

四层架构设计

┌────────────────────────────────────────┐
│                        Voice AI Chat                                                  │
│                    (应用层 / 协调层)                                                 │
├────────────────────────────────────────┤
│  ┌──────────┐      ┌────────┐        ┌────────┐    │
│  │   ASR层          │──▶│  Final文本   │──▶│   LLM层    │     │
│  │ (本地Whisper)│       │   队列         │        │ (云端Kimi) │    │
│  └──────────┘      └────────┘        └────────┘    │
├────────────────────────────────────────┤
│  ┌────────┐         ┌─────────┐                                  │
│  │   VAD层     │───▶│ 端点检测器   │                                  │
│  │ (语音检测)  │          │                    │                                  │
│  └────────┘         └─────────┘                                  │
├────────────────────────────────────────┤
│  ┌─────────┐                                                                   │
│  │  采集层         │  SDL2 + 环形缓冲区                                    │
│  │ (麦克风)        │                                                                   │
│  └─────────┘                                                                   │
└────────────────────────────────────────┘

设计哲学:解耦与异步

为什么这样分层?

旧版问题 当前解决方案
VAD、分段、ASR 都在主循环 各层独立,通过队列通信
ASR 阻塞导致音频丢失 ASR 在独立 Worker 线程
无法扩展 partial/final 双通道 回调机制便于扩展
难接入 LLM(网络阻塞) 独立 LLM Worker 线程

2.2 数据流详解

阶段1:音频采集层

// AudioCapture 核心职责
classAudioCapture {
// 1. SDL2 初始化音频设备
// 2. 音频回调写入环形缓冲区
// 3. 提供 GetAudioRange() 供 VAD 读取
};

关键设计

  • Ring Buffer:避免内存分配,支持实时读取

  • 绝对时间戳:每个采样都有全局时间,便于切片

阶段2:VAD 端点检测层

// EndpointDetector 核心职责
classEndpointDetector {
// 1. 定时从 AudioCapture 读取音频窗口
// 2. 调用 whisper_vad_segments() 检测语音概率
// 3. 判断语音开始/结束,生成 utterance 区间
};

状态流转

静音状态 ──(vad_prob > threshold)──▶ 说话中 ──(vad_prob < threshold 持续 N ms)──▶ 静音状态
              │                                                             │
              ▼                                                             ▼
        触发 "speech started"                                    触发 "speech ended"
                                                                   生成 utterance

阶段3:ASR 识别层

// AsrWorker 核心职责(独立线程)
classAsrWorker {
// 1. 从队列取出 utterance job
// 2. 调用 whisper_full_with_state() 识别
// 3. 输出识别结果(partial / final)
};

背压控制

utterance queue (有界队列)
  ┌────────────────────────────┐
  │ [job1] [job2] [job3] ...                               │
  └────────────────────────────┘
          ▲                                ▼
    EndpointDetector      AsrWorker
    (生产者)                          (消费者)

队列满时的策略:丢弃最旧的 job,保证实时性

阶段4:LLM 对话层

// VoiceAIChat 核心职责(独立线程)
classVoiceAIChat {
// 1. 通过回调接收 final 文本
// 2. LLM Worker 线程消费文本队列
// 3. 流式调用 Kimi API
// 4. 管理对话历史
};

数据流向

ASR final output
       │
       ▼
┌──────────────────┐
│ final_text_queue               │ (双端队列,mutex 保护)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ LLM Worker Loop             │ (condition_variable 等待)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ Kimi stream_text               │ (HTTP SSE 流式)
└──────────────────┘
       │
       ▼
   控制台输出

2.3 时序图

完整交互时序


2.4 线程模型

线程分布图

主线程 (main)
  ├── AudioCapture 线程 (SDL2 音频回调线程)
  ├── AsrWorker 线程 (Whisper 推理)
  ├── LLM Worker 线程 (VoiceAIChat)
  └── 主循环 (coordinator.Run())

线程职责与同步

线程 职责 同步机制
主线程 初始化、信号处理、协调器主循环
SDL 音频线程 采集音频写入 ring buffer 无锁 ring buffer
AsrWorker 消费 utterance 队列,执行识别 std::deque + std::mutex
LLM Worker 消费 final 文本队列,调用 API std::deque + std::mutex + std::condition_variable

关键同步代码

// LLM Worker 等待队列(voice_ai_chat.cpp)
voidVoiceAIChat::LLMWorkerLoop() {
while (true) {
std::stringuser_text;
    {
std::unique_lock<std::mutex>lock(queue_mutex_);
queue_cv_.wait(lock, [this]() {
return!running_.load() ||!final_text_queue_.empty();
      });

if (!running_.load() &&final_text_queue_.empty()) {
return;  // 退出信号
      }

user_text=std::move(final_text_queue_.front());
final_text_queue_.pop_front();
    }  // 解锁

ProcessWithAI(user_text);  // 处理请求
  }
}

// 生产者入队
voidVoiceAIChat::EnqueueFinalText(conststd::string&text) {
  {
std::lock_guard<std::mutex>lock(queue_mutex_);
if (final_text_queue_.size() >=config_.max_pending_final_texts) {
final_text_queue_.pop_front();  // 背压:丢弃最旧
    }
final_text_queue_.push_back(text);
  }
queue_cv_.notify_one();  // 通知消费者
}

2.5 为什么 Partial 不进 LLM?

Partial vs Final

特性 Partial Final
触发时机 说话过程中定期输出 一句话说完后输出
稳定性 频繁变化、可能回滚 稳定、确定
用途 屏幕预览给用户看 正式送 LLM 处理

如果把 Partial 送进 LLM 会怎样?

用户说话:"今天天气怎么样"

时间线:
T1: partial -> "今天天"     ──┐
T2: partial -> "今天天气怎"   ──┼── 都送进 LLM?
T3: partial -> "今天天气怎么样" ──┘
T4: final   -> "今天天气怎么样"

问题:
1. 上下文污染:LLM 看到 3 次重复请求
2. Token 浪费:多次请求消耗 API 额度
3. 过早触发:话没说完就生成回复

推荐策略

┌───────────────────────────┐
│         ASR 层 (Realtime)                          │
│  ┌────────┐       ┌─────────┐    │
│  │    Partial     │──▶│ 屏幕显示      │     │  (不送 LLM)
│  └────────┘       └─────────┘     │
│  ┌────────┐       ┌──────────┐  │
│  │    Final        │──▶│ LLM Worker  │   │  (正式处理)
│  └────────┘       └──────────┘  │
└───────────────────────────┘

2.6 扩展设计:未来方向

阶段 2 规划

当前架构                      目标架构
────────                 ──────────
┌──────┐                ┌──────┐
│  ASR      │                │  ASR     │
│ small     │                │ tiny/      │──▶ Partial (预览)
└───┬──┘                │ small     │
         │                        └───┬──┘
        ▼                                  │
┌──────┐                     ▼
│  LLM      │                ┌───────┐
│ (Kimi)    │                 │  LLM       │
└──────┘                 │ (Kimi)     │
                                    └───────┘

新增:                           
• 双模型策略 (tiny + small)      
• TTS 回播                       
• Barge-in (打断检测)            

可能的架构演进

// 未来可能的多级架构
classVoiceAIChat {
// 层1:ASR (本地)
// 层2:理解/意图 (本地轻量模型或规则)
// 层3:LLM (云端或本地大模型)
// 层4:TTS (本地或云端)
};

2.7 本章小结

核心要点

  1. 四层架构:采集 → VAD → ASR → LLM,各层解耦

  2. 异步设计:ASR 和 LLM 都有独立 Worker 线程

  3. 队列通信:生产者-消费者模式,有界队列背压

  4. Partial 策略:仅用于预览,不进 LLM

关键代码路径

  • 数据流:AudioCapture → EndpointDetector → AsrWorker → VoiceAIChat → Kimi

  • 同步点:std::mutex 保护队列,std::condition_variable 唤醒 Worker

思考题

  1. 为什么要用双端队列 (std::deque) 而不是普通队列?

  2. 如果 LLM 响应很慢,如何设计才能不阻塞新的 ASR 结果?

  3. 如何实现 “Barge-in”(用户打断 AI 说话)功能?

第3章 核心模块源码解析

3.1 VoiceAIChat 类概览

类定义(voice_ai_chat.h)

namespacevoice_ai {

structVoiceAIConfig {
std::stringmoonshot_base_url="https://api.moonshot.cn";
std::stringai_model="kimi-k2.5";
std::stringai_system_prompt="你是 Kimi,一个中文优先...";
std::stringmoonshot_api_key;
boolenable_streaming=true;
size_tmax_pending_final_texts=8;
realtime::LogLevellog_level=realtime::LogLevel::kInfo;
};

classVoiceAIChat {
public:
explicitVoiceAIChat(constVoiceAIConfig&config);
~VoiceAIChat();

// 禁止拷贝(资源管理语义)
VoiceAIChat(constVoiceAIChat&=delete;
VoiceAIChat&operator=(constVoiceAIChat&=delete;

// 生命周期
boolInit();
voidStop();

// 回调设置(用于外部集成)
voidSetTranscriptionCallback(TranscriptionCallbackcallback);
voidSetAIResponseCallback(AIResponseCallbackcallback);

// 输入接口
voidSendTextMessage(conststd::string&text);      // 文本模式
voidHandleFinalTranscript(conststd::string&text); // ASR 回调

private:
// 内部实现...
};

}  // namespace voice_ai

设计模式分析

设计要点 实现方式 目的
配置集中 VoiceAIConfig 结构体 参数统一管理
资源安全 = delete 拷贝构造 防止资源重复释放
延迟初始化 std::optional<ai::Client> 延迟创建 API 客户端
回调机制 std::function 支持外部扩展

3.2 初始化流程

Init() 方法

boolVoiceAIChat::Init() {
if (!InitAIClient()) {  // 步骤1:初始化 AI 客户端
returnfalse;
  }
StartLLMWorker();       // 步骤2:启动工作线程
returntrue;
}

InitAIClient() 详解

boolVoiceAIChat::InitAIClient() {
// 设置日志(只显示警告和错误)
ai::logger::install_logger(
std::make_shared<ai::logger::ConsoleLogger>(
ai::logger::LogLevel::kLogLevelWarn));

// 获取 API Key(环境变量优先)
constautoapi_key=config_.moonshot_api_key.empty()
?GetEnvOrDefault("MOONSHOT_API_KEY""")
      : config_.moonshot_api_key;

if (api_key.empty()) {
realtime::AppLogger::Instance().Error(
"MOONSHOT_API_KEY environment variable not set");
returnfalse;
  }

// 创建客户端(使用 std::optional 延迟初始化)
ai_client_.emplace(
ai::openai::create_client(api_keyconfig_.moonshot_base_url));

if (!ai_client_->is_valid()) {
realtime::AppLogger::Instance().Error("AI client init failed");
returnfalse;
  }

// 初始化对话历史(系统提示词)
conversation_history_= {
ai::Message::system(config_.ai_system_prompt),
  };

returntrue;
}

关键点

  • 环境变量 MOONSHOT_API_KEY 必须设置

  • std::optional 实现延迟初始化,避免构造时出错

  • 系统提示词在初始化时加入对话历史


3.3 LLM Worker 线程

线程生命周期

voidVoiceAIChat::StartLLMWorker() {
boolexpected=false;
// compare_exchange_strong: 原子 CAS 操作
// 确保只有一个线程能成功启动
if (!running_.compare_exchange_strong(expectedtrue)) {
return;  // 已经在运行
  }

llm_thread_=std::thread(&VoiceAIChat::LLMWorkerLoopthis);
}

voidVoiceAIChat::StopLLMWorker() {
constboolwas_running=running_.exchange(false);
queue_cv_.notify_all();  // 唤醒所有等待的线程

if (was_running&&llm_thread_.joinable()) {
llm_thread_.join();  // 等待线程结束
  }
}

Worker 主循环(生产者-消费者模式)

voidVoiceAIChat::LLMWorkerLoop() {
while (true) {
std::stringuser_text;
size_tpending_after_pop=0;

    {
// 1. 获取锁
std::unique_lock<std::mutex>lock(queue_mutex_);

// 2. 条件等待(原子释放锁并等待)
queue_cv_.wait(lock, [this]() {
// 唤醒条件:停止信号 或 队列非空
return!running_.load() ||!final_text_queue_.empty();
      });

// 3. 检查退出条件
if (!running_.load() &&final_text_queue_.empty()) {
return;  // 优雅退出
      }

// 4. 消费消息
user_text=std::move(final_text_queue_.front());
final_text_queue_.pop_front();
pending_after_pop=final_text_queue_.size();
    }  // 5. 自动解锁

// 6. 处理(在锁外执行,避免阻塞入队)
ai_processing_=true;
ProcessWithAI(user_text);
ai_processing_=false;
  }
}

入队方法(生产者)

voidVoiceAIChat::EnqueueFinalText(conststd::string&text) {
  {
std::lock_guard<std::mutex>lock(queue_mutex_);

// 背压控制:队列满时丢弃最旧的消息
if (final_text_queue_.size() >=config_.max_pending_final_texts) {
realtime::AppLogger::Instance().Warn(
"final 文本队列已满,丢弃最旧的一条");
final_text_queue_.pop_front();
    }

final_text_queue_.push_back(text);
  }  // 锁释放

queue_cv_.notify_one();  // 通知一个等待的消费者
}

为什么用 notify_one 而不是 notify_all

  • 只有一个 LLM Worker 线程

  • notify_one 更高效(只唤醒一个线程)


3.4 调用 Kimi API

ProcessWithAI() 主流程

voidVoiceAIChat::ProcessWithAI(conststd::string&user_text) {
// 1. 检查客户端状态
if (!ai_client_.has_value()) {
return;
  }

// 2. 添加到对话历史(User 消息)
  {
std::lock_guard<std::mutex>lock(mutex_);
conversation_history_.push_back(ai::Message::user(user_text));
  }

// 3. 准备请求参数
ai::GenerateOptionsgenerate_options;
  {
std::lock_guard<std::mutex>lock(mutex_);
generate_options.model=config_.ai_model;
generate_options.messages=conversation_history_;
  }

// 4. 打印 AI 前缀(带颜色)
  {
std::lock_guard<std::mutex>lock(output_mutex_);
std::cout<<kAnsiAssistantColor<<"Kimi> "<<std::flush;
  }

// 5. 流式调用
if (config_.enable_streaming) {
ProcessStreaming(generate_options);
  } else {
ProcessNonStreaming(generate_options);
  }
}

流式处理详解

voidVoiceAIChat::ProcessStreaming(ai::GenerateOptions&options) {
ai::StreamOptionsstream_options(std::move(options));
autostream=ai_client_->stream_text(stream_options);

std::stringassistant_reply;
boolstream_failed=false;

for (constauto&event : stream) {
if (event.is_text_delta()) {
// 收到文本片段
assistant_reply+=event.text_delta;

// 外部回调
if (ai_response_callback_) {
ai_response_callback_(event.text_deltatrue);
      }

// 实时输出
std::lock_guard<std::mutex>lock(output_mutex_);
std::cout<<event.text_delta<<std::flush;

    } elseif (event.is_error()) {
// 流错误处理
stream_failed=true;
realtime::AppLogger::Instance().Error(
"Kimi stream error: "+event.error.value_or("unknown"));
break;
    }
  }

// 完成处理
std::lock_guard<std::mutex>lock(mutex_);
if (stream_failed||assistant_reply.empty()) {
// 失败时回退 User 消息(对话不保存)
conversation_history_.pop_back();
  } else {
// 成功,添加 Assistant 回复到历史
conversation_history_.push_back(
ai::Message::assistant(assistant_reply));
  }
}

流式响应的优势

  • 用户体验好:边生成边显示

  • 感知延迟低:不需要等全部生成完

  • 与 ChatGPT 等产品体验一致


3.5 主程序入口

main() 函数结构(main.cpp)

intmain(intargcchar**argv) {
try {
// 1. 配置解析
voice_ai::VoiceAIConfigconfig;
realtime::RuntimeOptionsrealtime_options;
ParseArguments(argcargvconfigrealtime_options);

// 2. 环境变量覆盖
ApplyEnvironmentVariables(configrealtime_options);

// 3. 加载外部文件(提示词、词库等)
LoadExternalFiles(realtime_options);

// 4. 初始化日志
realtime::AppLogger::Instance().SetLevel(config.log_level);

// 5. 列出设备(如果指定)
if (realtime_options.list_input_devices) {
returnrealtime::AudioCapture::ListInputDevices() ?0 : 1;
    }

// 6. 创建应用实例
voice_ai::VoiceAIChatapp(config);

// 7. 信号处理(Ctrl+C 优雅退出)
std::signal(SIGINTHandleSignal);

if (!app.Init()) {
return1;
    }

// 8. 模式选择:文本模式 或 语音模式
if (stdin_mode) {
RunStdinMode(app);
    } else {
RunVoiceMode(apprealtime_options);
    }

  } catch (conststd::exception&e) {
std::cerr<<"Exception: "<<e.what() <<"\n";
return1;
  }
}

语音模式初始化

voidRunVoiceMode(voice_ai::VoiceAIChat&app
constrealtime::RuntimeOptions&options) {
// 1. 创建协调器
realtime::RealtimeCoordinatorcoordinator(options);

// 2. 设置 ASR final 回调(核心集成点)
coordinator.SetFinalTextCallback(
    [&app](conststd::string&text) { 
app.HandleFinalTranscript(text); 
    });

// 3. 初始化并运行
if (!coordinator.Init()) {
std::cerr<<"[MAIN] realtime coordinator init failed\n";
return1;
  }

coordinator.Run();  // 阻塞直到停止
}

核心集成点SetFinalTextCallback 将 ASR 层和 LLM 层连接起来。


3.6 参数解析详解

命令行解析逻辑

for (inti=1i<argc++i) {
conststd::stringarg=argv[i];

if (arg=="--stdin") {
stdin_mode=true;
  } 
elseif (arg=="--ai-model"&&i+1<argc) {
config.ai_model=argv[++i];  // 后置递增,先取值再移动
  }
elseif (arg=="--llm-queue-size"&&i+1<argc) {
config.max_pending_final_texts=
static_cast<size_t>(std::stoul(argv[++i]));
  }
// ... 更多参数
elseif (!arg.empty() &&arg[0!='-') {
cli_model_path=arg;  // 位置参数:模型路径
  }
}

环境变量优先级

// 环境变量优先级高于命令行
if (constchar*env=std::getenv("WHISPER_MODEL_PATH"); 
env!=nullptr&&!std::string(env).empty()) {
realtime_options.model_path=env;
elseif (!cli_model_path.empty()) {
realtime_options.model_path=cli_model_path;
}

// 日志级别
if (constchar*env=std::getenv("VOICE_AI_LOG_LEVEL"); env!=nullptr) {
config.log_level=realtime::AppLogger::ParseLevel(env);
}

优先级规则

  1. 环境变量(最高优先级)

  2. 命令行参数

  3. 默认值


3.7 信号处理与优雅退出

全局指针(用于信号处理)

namespace {
voice_ai::VoiceAIChat*g_app=nullptr;
realtime::RealtimeCoordinator*g_coordinator=nullptr;
}

信号处理器

voidHandleSignal(intsig) {
if (sig==SIGINT) {  // Ctrl+C
std::cout<<"\n[MAIN] stopping...\n";

// 按依赖顺序停止
if (g_coordinator!=nullptr) {
g_coordinator->Stop();  // 先停采集和 ASR
    }
if (g_app!=nullptr) {
g_app->Stop();  // 再停 LLM(完成正在进行的请求)
    }
  }
}

注册信号处理

intmain(...) {
// ...
voice_ai::VoiceAIChatapp(config);
g_app=&app;

std::signal(SIGINTHandleSignal);  // 注册处理器
// ...
}

为什么先停 Coordinator 再停 VoiceAIChat?

  • Coordinator 停止后不再产生新的 final 文本

  • VoiceAIChat 可以继续处理队列中的剩余请求

  • 避免请求丢失


3.8 本章小结

核心代码路径回顾

main()
  ├── ParseArguments()        # 参数解析
  ├── VoiceAIChat::Init()
  │     ├── InitAIClient()    # API 客户端初始化
  │     └── StartLLMWorker()  # 启动工作线程
  ├── SetFinalTextCallback()  # 注册 ASR 回调
  └── coordinator.Run()         # 主循环

LLM Worker 线程
  └── LLMWorkerLoop()
        ├── wait()            # 等待队列消息
        ├── pop()             # 消费消息
        └── ProcessWithAI()   # 调用 Kimi
              ├── stream_text()   # 流式请求
              └── update history  # 更新对话

关键设计模式

  1. RAIIstd::optionalstd::lock_guard 自动管理资源

  2. 生产者-消费者std::deque + std::mutex + std::condition_variable

  3. 回调机制std::function 实现层间解耦

  4. 原子操作std::atomic 控制线程状态

思考题

  1. 为什么要用 std::move 从队列取出文本?

  2. conversation_history_ 为什么要用 mutex 保护?

  3. 如何给项目添加 “清空对话历史” 的功能?

第4章 关键技术点剖析

4.1 线程安全设计

多线程访问的共享资源

classVoiceAIChat {
// 需要同步保护的成员
std::deque<std::string>final_text_queue_;  // LLM 队列
ai::Messagesconversation_history_;            // 对话历史
std::optional<ai::Client>ai_client_;        // API 客户端

// 同步原语
std::mutexmutex_;           // 保护 conversation_history_
std::mutexqueue_mutex_;     // 保护 final_text_queue_
std::mutexoutput_mutex_;    // 保护控制台输出顺序
std::condition_variablequeue_cv_;  // 队列非空通知
};

锁粒度设计

细粒度锁策略

// Good: 不同资源用不同锁
std::mutexqueue_mutex_;      // 只保护队列
std::mutexhistory_mutex_;    // 只保护对话历史
std::mutexoutput_mutex_;     // 只保护输出

// Bad: 一把大锁保护所有
std::mutexbig_lock_;  // 避免!会造成不必要的阻塞

代码示例

voidVoiceAIChat::ProcessWithAI(conststd::string&user_text) {
// 操作1:修改对话历史(需要锁)
  {
std::lock_guard<std::mutex>lock(mutex_);
conversation_history_.push_back(ai::Message::user(user_text));
  }  // 锁立即释放

// 操作2:准备请求参数(不需要锁,因为 conversation_history_ 已复制)
ai::GenerateOptionsgenerate_options;
  {
std::lock_guard<std::mutex>lock(mutex_);
generate_options.messages=conversation_history_;
  }  // 锁立即释放

// 操作3:网络请求(不需要锁,避免阻塞其他线程)
autostream=ai_client_->stream_text(stream_options);

// 操作4:输出响应(需要 output_mutex_ 保证输出顺序)
for (constauto&event : stream) {
std::lock_guard<std::mutex>lock(output_mutex_);
std::cout<<event.text_delta<<std::flush;
  }
}

死锁避免

原则

  1. 锁顺序一致:多个锁时,始终以相同顺序获取

  2. 避免锁嵌套:尽量使用独立锁,减少嵌套

  3. 使用 std::lock_guard:RAII 自动释放,避免忘记解锁

// 安全的嵌套锁(如果需要)
voidSomeFunction() {
std::lock_guard<std::mutex>lock_a(mutex_a_);
  {
std::lock_guard<std::mutex>lock_b(mutex_b_);
// 操作
  }  // lock_b 先释放
}  // lock_a 后释放

// 危险:不同函数锁顺序不一致会导致死锁
voidFunctionA() {
std::lock_guard<std::mutex>a(mutex_a_);
std::lock_guard<std::mutex>b(mutex_b_);  // 顺序:a -> b
}

voidFunctionB() {
std::lock_guard<std::mutex>b(mutex_b_);  // 顺序:b -> a ❌
std::lock_guard<std::mutex>a(mutex_a_);
}

4.2 背压控制 (Backpressure)

问题场景

用户快速说话      ASR 识别        LLM 处理
    │                       │                       │
    ▼                      ▼                      ▼
句子1 ──────▶ 文本1 ─────▶ 请求1 (慢)
句子2 ──────▶ 文本2 ───▶ 请求2 (等待中)
句子3 ──────▶ 文本3 ──▶ 请求3 (堆积...)
句子4 ──────▶ 文本4
                  ...

问题:LLM 处理慢,队列无限增长 → 内存溢出、延迟累积

解决方案:有界队列 + 丢弃策略

voidVoiceAIChat::EnqueueFinalText(conststd::string&text) {
std::lock_guard<std::mutex>lock(queue_mutex_);

// 背压控制:队列满时丢弃最旧的消息
if (final_text_queue_.size() >=config_.max_pending_final_texts) {
realtime::AppLogger::Instance().Warn(
"final 文本队列已满,丢弃最旧的一条");
final_text_queue_.pop_front();  // 丢弃最旧
  }

final_text_queue_.push_back(text);  // 加入最新
}

背压策略对比

策略 实现 适用场景
丢弃最旧 pop_front() + push_back() 实时性优先(本项目使用)
丢弃最新 拒绝入队 完整性优先
阻塞入队 队列满时阻塞生产者 吞吐优先
动态扩容 队列自动增长 内存充足场景

为什么选”丢弃最旧”?

  • 语音对话场景:用户更关心最新说的话

  • 旧消息可能已经”过时”(上下文已变)

  • 避免延迟累积,保证实时响应


4.3 回调机制设计

回调类型定义

// ASR 结果回调
usingTranscriptionCallback=
std::function<void(conststd::string&textboolis_final)>;

// AI 响应回调  
usingAIResponseCallback=
std::function<void(conststd::string&textboolis_streaming_chunk)>;

回调注册与调用

classVoiceAIChat {
public:
// 设置回调(支持外部扩展)
voidSetTranscriptionCallback(TranscriptionCallbackcallback) {
transcription_callback_=std::move(callback);
  }

voidSetAIResponseCallback(AIResponseCallbackcallback) {
ai_response_callback_=std::move(callback);
  }

private:
voidHandleTranscription(conststd::string&textboolis_final) {
// 调用外部注册的回调
if (transcription_callback_) {
transcription_callback_(textis_final);
    }
  }

TranscriptionCallbacktranscription_callback_;
AIResponseCallbackai_response_callback_;
};

使用示例

// 主程序中注册回调
voice_ai::VoiceAIChatapp(config);

// 回调1:收到 ASR 结果时打印日志
app.SetTranscriptionCallback(
    [](conststd::string&textboolis_final) {
if (is_final) {
LOG_INFO<<"Final transcription: "<<text;
        }
    });

// 回调2:收到 AI 响应时发送到 WebSocket
app.SetAIResponseCallback(
    [&websocket](conststd::string&chunkboolis_streaming) {
websocket.Send(chunk);  // 实时推送到前端
    });

Lambda 捕获注意事项

// 危险:捕获局部变量的引用
voidDangerous() {
voice_ai::VoiceAIChatapp(config);
std::stringprefix="User: ";

app.SetTranscriptionCallback(
    [&prefix](conststd::string&textbool) {  // ❌ 引用捕获
std::cout<<prefix<<text<<"\n";  // Dangerous 返回后 prefix 失效
    });
}  // prefix 被销毁,回调变成悬空引用

// 安全:值捕获 或 捕获指针
voidSafe() {
autoapp=std::make_shared<voice_ai::VoiceAIChat>(config);
std::stringprefix="User: ";

app->SetTranscriptionCallback(
    [prefixapp](conststd::string&textbool) {  // ✅ 值捕获
std::cout<<prefix<<text<<"\n";  // 安全,有独立的拷贝
    });
}

4.4 条件变量使用模式

生产者-消费者完整模式

template<typenameT>
classBoundedQueue {
public:
explicitBoundedQueue(size_tcapacity) : capacity_(capacity) {}

// 生产者
voidPush(Titem) {
std::unique_lock<std::mutex>lock(mutex_);

// 等待队列有空位
not_full_.wait(lock, [this] { 
returnqueue_.size() <capacity_
    });

queue_.push_back(std::move(item));
not_empty_.notify_one();  // 通知消费者
  }

// 消费者
TPop() {
std::unique_lock<std::mutex>lock(mutex_);

// 等待队列有数据
not_empty_.wait(lock, [this] { 
return!queue_.empty(); 
    });

Titem=std::move(queue_.front());
queue_.pop_front();
not_full_.notify_one();  // 通知生产者
returnitem;
  }

private:
std::deque<T>queue_;
size_tcapacity_;
std::mutexmutex_;
std::condition_variablenot_empty_;
std::condition_variablenot_full_;
};

本项目中的变体(无界队列 + 丢弃)

voidVoiceAIChat::LLMWorkerLoop() {
while (true) {
std::stringuser_text;

    {
std::unique_lock<std::mutex>lock(queue_mutex_);

// 条件等待:停止信号 或 队列非空
queue_cv_.wait(lock, [this] {
return!running_.load() ||!final_text_queue_.empty();
      });

// 检查退出条件
if (!running_.load() &&final_text_queue_.empty()) {
return;  // 优雅退出
      }

// 消费
user_text=std::move(final_text_queue_.front());
final_text_queue_.pop_front();
    }  // 解锁

ProcessWithAI(user_text);  // 处理(在锁外)
  }
}

关键点

  • wait() 会自动释放锁并阻塞,被唤醒时重新获取锁

  • 使用 lambda 谓词防止虚假唤醒 (spurious wakeup)

  • 处理逻辑在锁外执行,减少临界区


4.5 流式 HTTP 处理

SSE (Server-Sent Events) 基础

HTTP 请求
  │
  ▼
POST /v1/chat/completions
Headers:
  Authorization: Bearer {api_key}
  Accept: text/event-stream  ← 关键:请求流式响应

响应(流式):
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" world"}}]}
data: {"choices":[{"delta":{"content":"!"}}]}
data: [DONE]

流式处理代码

voidVoiceAIChat::ProcessStreaming(ai::GenerateOptions&options) {
// 1. 创建流式选项
ai::StreamOptionsstream_options(std::move(options));

// 2. 发起流式请求
autostream=ai_client_->stream_text(stream_options);

// 3. 逐块处理
for (constauto&event : stream) {
if (event.is_text_delta()) {
// 收到文本片段
conststd::string&chunk=event.text_delta;

// 实时输出
std::cout<<chunk<<std::flush;

// 累加完整回复
assistant_reply+=chunk;

    } elseif (event.is_error()) {
// 错误处理
HandleError(event.error);
stream_failed=true;
break;
    }
  }

// 4. 流结束,更新对话历史
if (!stream_failed) {
conversation_history_.push_back(
ai::Message::assistant(assistant_reply));
  }
}

流式 vs 非流式对比

特性 流式 (streaming) 非流式 (blocking)
首字延迟 低(毫秒级) 高(等待全部生成)
用户体验 边生成边看 等待后一次性显示
实现复杂度 高(需处理分片) 低(一次请求响应)
取消支持 可中断 需等待完成
适用场景 对话、长文本 短文本、简单查询

4.6 配置加载优先级

三级配置体系

┌──────────────────────────────┐
│  Level 1: 代码硬编码默认值                              │
│  (最低优先级)                                                   │
├──────────────────────────────┤
│  Level 2: 命令行参数                                        │
│  (./voice_ai_chat --mode fast ...)                     │
├──────────────────────────────┤
│  Level 3: 环境变量                                           │
│  (export WHISPER_MODEL_PATH=...)            │
│  (最高优先级)                                                  │
└──────────────────────────────┘

实现代码

// 优先级:环境变量 > 命令行 > 默认值
std::stringGetModelPath(conststd::string&cli_path) {
// 1. 检查环境变量(最高优先级)
if (constchar*env=std::getenv("WHISPER_MODEL_PATH")) {
if (!std::string(env).empty()) {
returnenv;
    }
  }

// 2. 使用命令行参数
if (!cli_path.empty()) {
returncli_path;
  }

// 3. 使用默认值
return"../../../models/ggml-small.bin";
}

// 应用示例
realtime_options.model_path=GetModelPath(cli_model_path);

为什么要这样设计?

配置来源 适用场景 优势
环境变量 敏感信息(API Key)、全局默认 不暴露在命令行历史,便于容器化部署
命令行 临时调整、脚本调用 灵活、可见、易于文档化
默认值 快速开始、示例 零配置启动

4.7 日志系统设计

日志级别

enumclassLogLevel {
kDebug,  // 详细调试信息(开发用)
kInfo,   // 一般信息(运行状态)
kWarn,   // 警告(需要注意但非致命)
kError// 错误(功能受影响)
};

日志使用示例

// 不同级别的日志
AppLogger::Instance().Debug("queue size="+std::to_string(size));
AppLogger::Instance().Info("AI client initialized");
AppLogger::Instance().Warn("队列已满,丢弃旧消息");
AppLogger::Instance().Error("API request failed: "+error);

运行时调整日志级别

# 方式1:命令行
./voice_ai_chat --log-level debug

# 方式2:环境变量
exportVOICE_AI_LOG_LEVEL=debug
./voice_ai_chat

生产环境建议

环境 推荐级别 原因
开发调试 debug 查看详细数据流
测试环境 info 关注运行状态
生产环境 warn 减少日志量,只关注问题

4.8 本章小结

关键技术点回顾

技术点 核心机制 应用场景
线程安全 std::mutex + 细粒度锁 多线程共享资源保护
背压控制 有界队列 + 丢弃最旧 防止慢消费者堆积
回调机制 std::function + Lambda 层间解耦、外部扩展
条件变量 wait() + notify_one() 生产者-消费者同步
流式处理 SSE 分片处理 低延迟 AI 响应
配置优先级 环境变量 > 命令行 > 默认 灵活配置管理

最佳实践

  1. 锁的粒度要细:不同资源用不同锁,临界区尽量小

  2. 使用 RAIIstd::lock_guardstd::unique_ptr 自动管理资源

  3. Lambda 捕获要谨慎:避免悬空引用,优先值捕获

  4. 条件变量要配谓词:防止虚假唤醒

  5. 配置分层设计:环境变量管敏感/全局,命令行管临时调整

思考题

  1. 如果要用本项目做 Web 服务,回调机制如何配合 WebSocket?

  2. 如何添加一个 “暂停/恢复” 功能?需要修改哪些同步逻辑?

  3. 如果要支持多轮对话的 “撤回” 功能,对话历史该如何设计?

第5章 实验演示

5.1 环境准备

检查清单

项目 检查命令 预期结果
API Key echo $MOONSHOT_API_KEY 显示有效 key
编译产物 ls build/applications/bin/voice_ai_chat 文件存在
模型文件 ls models/ggml-small.bin 文件存在
音频设备 arecord -l 或 ./voice_ai_chat --list-input-devices 显示设备列表

快速编译脚本

#!/bin/bash
# build.sh - 一键编译脚本

cd ai-sdk-cpp-laoliao
mkdir-p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make-j$(nproc) voice_ai_chat

echo"编译完成,二进制位置:"
echo"  ./applications/bin/voice_ai_chat"

5.2 实验1:文本模式验证 LLM 链路

目的

在不涉及语音的情况下,先验证 AI 对话功能正常。

步骤

# 1. 设置 API Key
exportMOONSHOT_API_KEY="your-api-key"

# 2. 启动文本模式
./applications/bin/voice_ai_chat --stdin

# 3. 输入测试
=== Voice AI Chat (stdin mode) ===
输入文本后回车,输入 /exit 退出。

你好,请介绍一下你自己
Kimi> 你好!我是 Kimi,一个 AI 助手...

今天的天气怎么样?
Kimi> 我无法获取实时天气信息...

/exit

预期输出

你(ASR)> 你好,请介绍一下你自己
Kimi> 你好!我是 Kimi,一个 AI 助手。我可以帮助你解答问题、写作...

你(ASR)> 今天的天气怎么样?
Kimi> 我无法获取实时天气信息...

故障排查

现象 可能原因 解决方案
MOONSHOT_API_KEY not set 环境变量未设置 export MOONSHOT_API_KEY=xxx
AI client init failed API Key 无效 检查 Key 是否正确
连接超时 网络问题 检查网络,或尝试 ping api.moonshot.cn
返回乱码 终端编码 export LANG=en_US.UTF-8

5.3 实验2:基础语音模式

目的

验证完整的语音输入 → ASR → LLM 流程。

步骤

# 1. 列出设备(确认麦克风可用)
./applications/bin/voice_ai_chat --list-input-devices

# 预期输出:
# Input Device 0: HDA Intel PCH: ALC257 Analog (hw:0,0)
# Input Device 1: USB Audio Device: USB Audio (hw:1,0)

# 2. 基础语音模式(使用默认配置)
./applications/bin/voice_ai_chat \
  ../../../models/ggml-small.bin

# 3. 说话测试(建议中文短句)
# 示例:"你好"、"今天星期几"、"讲个笑话"

观察要点

  1. VAD 检测:看到 [ENDPOINT] speech started 表示检测到语音开始

  2. 识别输出:看到 [TEXT] #1 xxx 表示 ASR 成功

  3. LLM 响应:看到 Kimi> xxx 表示 AI 正在回复

预期完整输出

[VAD-DEBUG] max_prob=0.821, segments=1, speech=yes
[ENDPOINT] speech started
...
[ENDPOINT] speech ended
[QUEUE] enqueue utterance #1 start_ms=... end_ms=...
[ASR-WORKER] processing utterance #1 samples=...
[TEXT] #1 你好

你(ASR)> 你好
Kimi> 你好!很高兴见到你...

5.4 实验3:模型速度对比

目的

对比不同模型的推理速度,选择合适的模型。

测试脚本

#!/bin/bash
# benchmark.sh - 模型速度对比

MODELS=("tiny" "base" "small")
TEST_AUDIO="test.wav"  # 准备一段 5 秒测试音频

echo "=== 模型速度对比测试 ==="

for model in "${MODELS[@]}"; do
  echo ""
  echo "测试模型: $model"
  echo "模型大小: $(ls -lh models/ggml-$model.bin | awk '{print $5}')"

  # 使用 time 统计时间
  time ./applications/bin/voice_ai_chat \
    models/ggml-$model.bin \
    --mode fast \
    --threads 8 \
    2>&1 | grep -E "(real|user|sys|RTF)"
done

预期结果参考

模型 大小 理论 RTF 实测延迟
tiny 75MB ~0.1x 0.3-0.5s
base 142MB ~0.3x 0.8-1.2s
small 466MB ~1.0x 1.5-2.5s
medium 1.5GB ~3-5x 4-8s

选择建议

# 低配设备(4核以下)
./voice_ai_chat models/ggml-base.bin --mode fast --threads 4

# 标准配置(8核,推荐)
./voice_ai_chat models/ggml-small.bin --mode balanced --threads 8

# 高配设备(追求质量)
./voice_ai_chat models/ggml-medium.bin --mode quality --threads 16

5.5 实验4:VAD 参数调优

目的

找到适合你麦克风和环境的 VAD 参数。

测试命令

# 测试1:高灵敏度(适合安静环境、远场语音)
./applications/bin/voice_ai_chat \
  ../../../models/ggml-small.bin \
  --mode fast \
  --vad-threshold 0.03 \
  --poll-interval-ms 150

# 测试2:默认灵敏度
default: --vad-threshold 0.06 --poll-interval-ms 250

# 测试3:低灵敏度(适合嘈杂环境、近场语音)
./applications/bin/voice_ai_chat \
  ../../../models/ggml-small.bin \
  --mode fast \
  --vad-threshold 0.12 \
  --poll-interval-ms 300

调优指南

场景 VAD 阈值 轮询间隔 原因
安静办公室 0.03-0.05 150-200ms 灵敏检测,快速响应
普通家庭 0.06-0.08 250ms 默认平衡
嘈杂环境 0.10-0.15 300-400ms 减少误触发
近场麦克风 0.08-0.10 200-250ms 避免过度灵敏

观察指标

# 好的 VAD 表现
[VAD-DEBUG] max_prob=0.921, speech=yes  # 说话时有高概率
[ENDPOINT] speech started               # 快速检测到开始
...
[VAD-DEBUG] max_prob=0.012, speech=no   # 结束后低概率
[ENDPOINT] speech ended                 # 准确检测结束

# 不好的 VAD 表现(需要调参)
[VAD-DEBUG] max_prob=0.051, speech=no   # 说话但检测为静音(阈值太高)
[VAD-DEBUG] max_prob=0.211, speech=yes  # 环境噪音误触发(阈值太低)

5.6 实验5:模式参数对比

目的

理解 fastbalancedquality 三种模式的区别。

测试方法

# 准备一段测试音频(建议 5-10 秒中文)
# 或者使用相同的一句话重复测试

# 测试 fast 模式
echo"=== Fast Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode fast \
--threads82>&1 | grep-E"(real|TEXT)"

# 测试 balanced 模式  
echo"=== Balanced Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode balanced \
--threads82>&1 | grep-E"(real|TEXT)"

# 测试 quality 模式
echo"=== Quality Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode quality \
--threads82>&1 | grep-E"(real|TEXT)"

参数对比实验

# 实验:max-segment-ms 对延迟的影响
for ms in 1600 2200 3000; do
  echo "max-segment-ms = $ms"
  time ./voice_ai_chat models/ggml-small.bin \
    --mode balanced \
    --max-segment-ms $ms \
    --threads 8
done

结果分析

模式 准确率 延迟 适用场景
fast ⭐⭐⭐ 最低 命令词、短句
balanced ⭐⭐⭐⭐ 中等 日常对话
quality ⭐⭐⭐⭐⭐ 较高 长句、专业内容

5.7 实验6:文本优化功能

目的

测试热词、词库、替换规则对识别准确率的提升。

实验6.1:热词提示词

# 1. 创建热词文件
cat > hotwords_tech.txt << EOF
实时性
延迟
吞吐
模型推理
量化
神经网络
EOF

# 2. 使用热词运行
./voice_ai_chat models/ggml-small.bin \
  --mode balanced \
  --hotwords-file hotwords_tech.txt

# 3. 测试:说出这些技术术语,观察识别准确率

实验6.2:术语替换

# 1. 创建替换规则文件
cat > replacements_tech.txt << EOF
实实性 => 实时性
推力 => 推理
模形 => 模型
量化 => 量化
EOF

# 2. 使用替换规则运行
./voice_ai_chat models/ggml-small.bin \
  --mode balanced \
  --replacements-file replacements_tech.txt

# 3. 测试:故意说错词,观察是否被纠正

实验6.3:词库纠错

# 1. 创建词库文件
cat > lexicon_office.txt << EOF
# 标准词 <TAB> 别名1,别名2,...
带饭 带翻,代饭
报销 报消
工牌 工牌子
邮箱 油香
EOF

# 2. 使用词库运行
./voice_ai_chat models/ggml-small.bin \
  --mode balanced \
  --lexicon-file lexicon_office.txt

# 3. 测试:说出"带翻",观察输出是否为"带饭"

5.8 实验7:调试与日志

目的

学习使用日志排查问题。

开启详细日志

# debug 级别会显示所有内部状态
./voice_ai_chat models/ggml-small.bin \
  --mode balanced \
  --log-level debug \
  2>&1 | tee voice_ai_debug.log

关键日志解读

# 过滤关键日志
# 1. VAD 检测
grep "VAD-DEBUG" voice_ai_debug.log

# 2. 端点事件
grep "ENDPOINT" voice_ai_debug.log

# 3. 队列操作
grep "QUEUE" voice_ai_debug.log

# 4. ASR 处理
grep "ASR-WORKER" voice_ai_debug.log

# 5. 最终文本
grep "TEXT" voice_ai_debug.log

常见问题日志模式

问题1:VAD 检测不到语音

# 现象:没有 speech started
[VAD-DEBUG] max_prob=0.021, segments=0, speech=no
# 解决:降低阈值 --vad-threshold 0.03

问题2:队列堆积

[QUEUE] enqueue utterance #10 ...
[QUEUE] skip utterance: too short ...
# 或
[QUEUE] skip utterance: empty audio range
# 解决:检查音频设备,或降低 max-segment-ms

问题3:LLM 队列满

[warn] final 文本队列已满,丢弃最旧的一条
# 解决:增加 --llm-queue-size,或检查网络

5.9 综合实验:配置你的最佳参数

实验目标

通过系统测试,找到适合你的硬件和场景的最佳参数组合。

测试矩阵

#!/bin/bash
# optimization.sh - 参数优化测试

MODELS=("base" "small")
MODES=("fast" "balanced")
THRESHOLDS=(0.04 0.06 0.08)

for model in "${MODELS[@]}"; do
  for mode in "${MODES[@]}"; do
    for threshold in "${THRESHOLDS[@]}"; do
      echo ""
      echo "=========================================="
      echo "Model: $model, Mode: $mode, Threshold: $threshold"
      echo "=========================================="

      # 运行测试(建议录制一段标准测试语音)
      timeout 30 ./voice_ai_chat \
        models/ggml-$model.bin \
        --mode $mode \
        --vad-threshold $threshold \
        --threads 8 \
        --log-level info
    done
  done
done

评估维度

维度 评估方法 目标值
延迟 说完话到看到 Kimi> 的时间 < 3s
准确率 正确识别字数 / 总字数 > 90%
误触发 环境噪音触发次数 < 1次/分钟
丢字率 未检测到的说话次数 < 5%
CPU 占用 top/htop 观察 < 70%

5.10 本章小结

实验技能总结

实验 掌握技能 关键参数
文本模式 LLM 链路验证 --stdin
基础语音 端到端流程 模型路径
模型对比 速度/质量权衡 --mode
VAD 调优 环境适配 --vad-threshold
模式对比 参数理解 --max-segment-ms
文本优化 准确率提升 --replacements-file
调试日志 问题排查 --log-level debug

推荐配置速查表

# 极速实时(低配设备)
./voice_ai_chat models/ggml-base.bin --mode fast --threads 4

# 平衡推荐(大多数用户)
./voice_ai_chat models/ggml-small.bin --mode balanced --threads 8

# 质量优先(高配设备)
./voice_ai_chat models/ggml-medium.bin --mode quality --threads 16

学习资源推荐

Whisper.cpp 相关

  • whisper.cpp GitHub

  • ggml 张量库文档

  • Whisper 论文

TTS 相关

  • Piper TTS

  • ChatTTS

  • Bert-VITS2

LLM 本地部署

  • llama.cpp

  • Ollama(更简单的本地 LLM 管理)

  • vLLM(高性能推理)

C++ 并发编程

  • 《C++ Concurrency in Action》(Anthony Williams)

  • cppreference.com 的并发部分