K230 智能助手对话循环架构实战
项目背景
K230 是嘉楠堪智的边缘 AI 芯片(6 TOPS NPU),搭载 CanMV MicroPython 固件。本项目用 K230 作为前端传感器(摄像头+麦克风),连接本地 PC 后端(FastAPI + OpenClaw AI),构建一个能主动打招呼、语音对话的智能助手。
核心挑战:K230 是资源受限的嵌入式设备(1GB RAM,MicroPython),不能运行 LLM/TTS,需要通过 HTTP 与 PC 后端协同完成完整对话链路。
系统架构
K230 (MicroPython) PC Backend (FastAPI :8080)
┌─────────────────┐ ┌──────────────────────────┐
│ 摄像头 → 人脸检测 │──POST──►│ /api/event → LLM → TTS │
│ 麦克风 → 录音 │──POST──►│ /api/voice → ASR → LLM │
│ 轮询指令 │◄──GET──│ /api/command ← 录音指令 │
└─────────────────┘ └──────────────────────────┘
关键约束:
- K230 端是 单线程状态机(DETECTING → RECORDING → UPLOADING),任何时刻只能做一件事
- 录音需要暂停摄像头(DMA 资源冲突),录完需要完整恢复传感器
- K230 WiFi 仅 2.4GHz,局域网传输有丢包/ECONNRESET 风险
今天解决的问题
1. 对话循环设计:后端驱动录音
问题:最初设计是人脸检测直接触发录音,但这样无法实现"TTS 播完 → 录音 → 回复 → 再录音"的多轮对话。
方案:改为后端驱动模式:
- 后端 TTS 播放完毕后,设置
_pending_command = "record" - K230 每隔 3 秒轮询
GET /api/command,取走指令后进入录音状态 - 录音 → 上传 → ASR → LLM → TTS → 又设置 record 指令 → 循环
- 用户不说话(ASR 为空)→ 循环自然终止
核心流程:
人脸到达 → 后端打招呼(TTS) → K230收到record → 录音5s → 上传WAV
→ ASR识别 → LLM回复 → TTS播放 → K230收到record → 录音 → ...(直到用户不回应)
2. 会话管理
问题:没有历史记忆,每次对话都是独立的。
方案:ConversationSession 类管理单次会话:
- 10 分钟会话超时,过期自动重置
greeted标志防止重复打招呼- 保留最近 10 分钟的对话历史传给 LLM
- 新会话清空
_pending_command,防止残留指令
3. Socket 泄漏导致 ENOMEM
问题:K230 频繁报 errno 12 (ENOMEM),因为 urequests 的响应没有正确关闭。
方案:所有 HTTP 请求加 try/finally 确保 resp.close():
resp = None
try:
resp = requests.get(url, timeout=timeout)
# ...
except Exception:
pass
finally:
if resp:
try:
resp.close()
except Exception:
pass
4. 语音上传可靠性优化
问题:160KB WAV 文件上传偶尔 ECONNRESET。
方案:
- 超时从 10s → 60s(与后端一致)
- 最多重试 2 次,指数退避(2s)
- DNS 预解析,重试时复用地址
- 数据全部发出即视为成功(弱网下响应可能丢失)
- 小 chunk (4096) + 短间隔 (1ms) 减少路由器丢包
- 每次重试前
gc.collect()释放内存
5. 录音采样率优化
问题:44100Hz 录音产生 ~441KB WAV 文件,上传负担大。
方案:采样率从 44100 → 16000Hz:
- 文件从 ~441KB → ~160KB(减少 63%)
- ASR (faster-whisper) 内部就是 16kHz 处理,完全够用
- 经实测验证 K230 硬件支持 16000Hz
6. TTS 输出清理
问题:LLM 回复包含 markdown 符号(加粗、标题、列表等),TTS 会读出来。
方案:_strip_markdown() 函数在 TTS 前过滤:
text = re.sub(r'\*{1,3}([^*]+)\*{1,3}', r'\1', text) # 去加粗/斜体
text = re.sub(r'^#{1,6}\s*', '', text, flags=re.MULTILINE) # 去标题
# ... 去列表、代码块、链接、分割线
7. MicroPython urequests 的坑
resp.json()在某些情况下会失败,改用resp.text+json.loads()更可靠resp.read()和resp.text互斥,调用后数据会被消耗- socket 创建后必须在 finally 中关闭,否则内存泄漏
技术要点总结
| 问题 | 方案 |
|---|---|
| 嵌入式设备无法运行 LLM | 前后端分离,K230 做感知,PC 做 AI |
| 单线程状态机限制 | 后端轮询指令驱动状态转换 |
| DMA 资源冲突 | 录音前暂停传感器,录完后完整恢复 |
| Socket 内存泄漏 | try/finally 强制关闭,gc.collect() |
| 语音传输不可靠 | 60s 超时 + 重试 + 小 chunk |
| ASR 不需要高采样率 | 16kHz 足够,文件减小 63% |
| LLM 输出含 markdown | TTS 前正则过滤 |
| 无会话记忆 | ConversationSession 管理 10 分钟会话 |
当前状态
- 基础对话循环已跑通:人脸到达 → 打招呼 → 语音对话 → 多轮循环
- 人脸检测 → 上报事件 → 后端打招呼 → TTS 播放 → K230 录音 → 上传 → ASR → LLM → TTS → 循环
- 语音上传可靠性大幅改善(60s 超时 + 重试机制)
- 采样率降至 16kHz,文件体积减少 63%
待优化方向
- 语音唤醒词(避免一直轮询录音)
- 人脸识别身份(当前只检测是否有人)
- 多事件并发处理
- 人脸库持久化
- 离线 TTS 质量