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 输出含 markdownTTS 前正则过滤
无会话记忆ConversationSession 管理 10 分钟会话

当前状态

  • 基础对话循环已跑通:人脸到达 → 打招呼 → 语音对话 → 多轮循环
  • 人脸检测 → 上报事件 → 后端打招呼 → TTS 播放 → K230 录音 → 上传 → ASR → LLM → TTS → 循环
  • 语音上传可靠性大幅改善(60s 超时 + 重试机制)
  • 采样率降至 16kHz,文件体积减少 63%

待优化方向

  • 语音唤醒词(避免一直轮询录音)
  • 人脸识别身份(当前只检测是否有人)
  • 多事件并发处理
  • 人脸库持久化
  • 离线 TTS 质量