Agent架构

语音优先 AI 助手项目 — AI 完整复现指南

本文档旨在让 AI 基于此文档从零复现整个项目。包含所有架构决策、代码结构、配置、依赖、踩坑记录和验证步骤。


1. 项目概述

一个本地运行的语音优先 AI 助手系统,基于 nanobot 框架深度扩展,支持:

  • 实时全双工语音对话:按住说话 → ASR → LLM → TTS 流式播放
  • 双脑路由:4B 小模型处理简单问题(~1.2s),35B 大模型处理复杂问题
  • 声纹识别:CAM++ 192 维 embedding,区分说话人,陌生人拦截
  • 双向流式 TTS:CosyVoice3,LLM 边输出文本 TTS 边合成语音,端到端首音 ~4-5s
  • FunASR 引擎:SenseVoiceSmall ASR + CAM++ 声纹(HTTP 路径)
  • WebUI:nanobot React SPA,通过 Vite 开发 / FastAPI 托管
  • 三层记忆:短期 Session + 中期 SQLite + 长期 Kuzu 图谱

2. 架构与端口布局

┌─────────────────────────────────────────────────────────────────┐
│  浏览器 (localhost:5173 Vite dev 或 :8000 生产)                │
│  React SPA + AudioWorklet + PcmStreamPlayer + speaker_monitor   │
└──────┬──────────────┬──────────────┬──────────────┬─────────────┘
       │ WS /realtime  │ WS /ws        │ HTTP         │ WS bistream
       │ (PCM 上行)    │ (聊天)        │ /speaker /asr│
       ▼               ▼               ▼              │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐   │
│ Nanobot      │ │ Backend      │ │ Backend      │   │
│ Gateway      │ │ :8000/ws     │ │ :8000 REST   │   │
│ :8765        │ │ DualBrain    │ │ FunASR+CAM++ │   │
│ faster-      │ │ → Ollama     │ │ → DualBrain  │   │
│ whisper+CAM++│ │              │ │              │   │
│ AgentLoop    │ │              │ │              │   │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘   │
       │                │                │            │
       │  HTTP /api/chat│                │ HTTP       │
       ▼                ▼                ▼            ▼
┌──────────────────────────────────────────────────────────┐
│  Ollama :11434                                            │
│  qwen2.5:1.5b (分诊/快脑) / nexusriot...:4b (快脑)       │
│  fredrezones55/Qwen3.6-35B (慢脑)                        │
└──────────────────────────────────────────────────────────┘
       ▲                ▲
       │ WS bistream    │ HTTP /v1/audio/speech
       │                │
┌──────┴────────────────┴──────────────────────────────────┐
│  TTS CosyVoice3 :9880 (conda cosyvoice, MPS 加速)        │
│  /v1/audio/speech (一次性) /stream (NDJSON) /bistream(WS)│
└──────────────────────────────────────────────────────────┘

端口分配

端口 服务 进程 Python 环境
5173 Vite Dev Server (前端 HMR) node
8000 Backend FastAPI (双脑+FunASR+SPA托管) backend/main.py venv (Python 3.14)
8765 Nanobot Gateway (WS /realtime + /ws) nanobot gateway venv
8766 Memory UI nanobot gateway venv
8767 Token UI nanobot gateway venv
18790 Gateway 健康检查 nanobot gateway venv
9880 CosyVoice3 TTS uvicorn conda cosyvoice (Python 3.10)
11434 Ollama LLM ollama serve

两条独立数据路径

路径 A — 实时语音(访问 :8765 / Vite :5173)

  1. 浏览器 AudioWorklet 采集 16kHz PCM → WS :8765/realtime
  2. Gateway VAD 分段 → faster-whisper ASR + CAM++ 声纹 并行(asyncio.gather)
  3. transcript.final 下行 → 前端自动发 WS :8765/ws 聊天
  4. Gateway AgentLoop → Ollama 35B → delta 流式下行
  5. 前端 tts.bistream_text → Gateway BistreamTTS → TTS :9880 WS → PCM 回传 → PcmStreamPlayer 播放

路径 B — HTTP 语音回合(访问 :8000)

  1. 浏览器 POST :8000/voice/turn(multipart WAV)
  2. Backend 串行:CAM++ 声纹 → FunASR SenseVoiceSmall ASR → DualBrainRouter
  3. DualBrain:规则粗筛 + 4B 分诊 → SIMPLE 走 4B 快脑 / COMPLEX 走 nanobot AgentLoop 慢脑
  4. Backend HTTP POST TTS :9880/v1/audio/speech → WAV
  5. JSON 响应

3. 系统要求

硬件

  • Apple Silicon Mac(M4 Pro 实测,M1/M2/M3 均可)
  • 内存 ≥ 32GB(35B 模型需 ~22GB)
  • 磁盘 ≥ 60GB(模型文件)

软件

  • macOS 14+
  • Homebrew
  • Python 3.11+(主 venv 用 3.14 实测可行;注意 nanobot 要求 ≥3.11,TTS conda 用 3.10)
  • Node.js 18+
  • Ollama
  • ffmpeg(brew install ffmpeg
  • conda/miniconda(用于隔离 TTS 环境)

4. 三层代码边界(极其重要)

项目根目录/
├── backend/           ← 第 1 层:监控后端(可自由修改)
│   ├── main.py            FastAPI 主应用
│   ├── dual_brain.py      双脑路由器
│   ├── webui_gateway.py   SPA 托管 + WS 聊天协议子集
│   ├── voice.html         独立语音回合页面
│   └── static/            speaker_widget.js / speaker_monitor.js / architecture.html
│
├── nanobot-assistant/ ← 第 2 层:多模态扩展插件(可自由修改)
│   └── nanobot_assistant/
│       ├── audio/
│       │   ├── funasr_engine.py    FunASR ASR + CAM++ 声纹(核心)
│       │   ├── speaker_gate.py     Resemblyzer 声纹(旧版,已被 CAM++ 替代)
│       │   ├── vad.py              Silero VAD 状态机
│       │   ├── mic_capture.py      麦克风采集
│       │   ├── local_asr.py        faster-whisper 封装
│       │   ├── tts_engine.py       TTS 封装
│       │   └── player.py           音频播放
│       ├── channels/
│       │   └── desktop_voice.py    桌面语音通道(entry_point 插件)
│       ├── tools/                  screenshot/speak/remember/recall
│       └── memory/                 向量记忆(ChromaDB + CLIP)
│
├── nanobot/           ← 第 3 层:Agent 核心(原则上严禁直接修改)
│   └── nanobot/             (本项目授权修改了少量文件,见第 8 节)
│       ├── realtime/websocket.py    ★ 改动:ASR+声纹并行、bistream TTS
│       ├── tts/client.py            ★ 改动:新增 BistreamTTS 类
│       ├── channels/websocket.py    ★ 改动:speaker_verifier 注入
│       ├── webui/gateway_services.py ★ 改动:CAM++ verifier 构建
│       └── webui/...                ★ 改动:设置 API 等
│
└── venv/              Python 虚拟环境(非代码)

边界规则

  • backend/nanobot-assistant/ 可自由修改
  • nanobot/ 核心原则上不改,必须通过插件/entry_point 扩展
  • 本项目因需要集成声纹和双向流 TTS,授权修改了 nanobot/ 中少量文件(第 8 节详述)

5. 安装步骤

5.1 基础工具

brew install ffmpeg git node
# 安装 Ollama: https://ollama.ai/download

5.2 Ollama 模型

# 拉取模型
ollama pull qwen2.5:1.5b                    # 分诊/快脑(1.0GB)
ollama pull nexusriot/Qwen3.5-Uncensored-HauhauCS-Aggressive:4b  # 快脑(3.4GB)
ollama pull fredrezones55/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:latest  # 慢脑(22GB)

# 配置 Ollama 多模型并行(防止切换抖动)
# 编辑 ~/.zshrc 或启动脚本:
export OLLAMA_MAX_LOADED_MODELS=4
export OLLAMA_KEEP_ALIVE=-1
export OLLAMA_NUM_PARALLEL=2

5.3 CosyVoice3 TTS 服务

# 1. 克隆 CosyVoice 仓库
cd ~
git clone https://github.com/FunAudioLLM/CosyVoice.git
cd CosyVoice
git submodule update --init --recursive

# 2. 创建独立 conda 环境(必须 Python 3.10)
conda create -n cosyvoice python=3.10 -y
conda activate cosyvoice

# 3. 安装 Conda 依赖
conda install -y -c conda-forge pynini==2.1.5
pip install -r requirements.txt

# 4. 安装 Matcha-TTS(子模块)
cd third_party/Matcha-TTS
pip install -e .
cd ../..

# 5. 安装 openai-whisper(CosyVoice front-end 依赖)
# 注意:setuptools 必须 <81,旧版 setup.py 用 pkg_resources
pip install 'setuptools<81'
pip install openai-whisper --no-build-isolation

# 6. 安装运行时依赖
pip install 'torch==2.3.1' 'torchaudio==2.3.1'
pip install fastapi uvicorn soundfile librosa conformer hydra-core omegaconf gdown matplotlib rich

# 7. 下载 CosyVoice3 0.5B 模型(从 ModelScope)
# 模型路径: ~/.cache/modelscope/hub/iic/CosyVoice2-0.5B/
# 或使用 modelscope CLI:
python -c "from modelscope import snapshot_download; snapshot_download('iic/CosyVoice2-0.5B')"

# 8. 创建参考音频目录和文件
mkdir -p ~/cosyvoice/voices
# 准备 default.wav(3-10秒清晰人声,16kHz+)和 default.txt
# default.txt 内容必须以 "You are a helpful assistant.<|endofprompt|>" 开头
echo -n "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。" > ~/cosyvoice/voices/default.txt

# 9. 创建启动脚本 ~/cosyvoice/start_tts.sh
cat > ~/cosyvoice/start_tts.sh << 'EOF'
#!/bin/bash
export PYTHONPATH=~/cosyvoice/CosyVoice
export TTS_COSYVOICE_MODEL_PATH=~/.cache/modelscope/hub/iic/CosyVoice2-0.5B
export TTS_VOICES_DIR=~/cosyvoice/voices
export TTS_DEVICE=mps
exec ~/miniconda3/envs/cosyvoice/bin/python -m uvicorn \
    nanobot_tts_service.app:app \
    --host 127.0.0.1 --port 9880
EOF
chmod +x ~/cosyvoice/start_tts.sh

关键 MPS 修复(在 cosyvoice.py 适配器中):

  • CosyVoice load_model 在非 CUDA 环境硬编码 frontend.device='cpu'
  • bi-streaming 路径不像非流式那样自动 .to(model.device)
  • 必须在 load_model 返回后显式设置:
    • model.frontend.device = torch.device("mps")
    • model.model.device = torch.device("mps")
    • llm + flow 跑 MPS,hift 声码器留 CPU(因为 f0_predictor 需 float64,MPS 不支持)
    • patch hift.inference 将 mel 张量移到 CPU

5.4 Nanobot 核心

cd /path/to/project
git clone <nanobot-repo> nanobot   # 或使用已有的 nanobot checkout

# 创建主虚拟环境(Python 3.11+,3.14 实测可行)
python3 -m venv venv
source venv/bin/activate

# 安装 nanobot(开发模式)
cd nanobot
pip install -e ".[api,voice]"
pip install langgraph              # 默认依赖但有时漏装
cd ..

# 安装 nanobot-assistant 插件(开发模式)
cd nanobot-assistant
pip install -e ".[audio,screenshot,memory]"
cd ..

# 安装 FunASR(ASR + 声纹)
pip install funasr modelscope

# 安装其他依赖
pip install kuzu>=0.11.3           # 长期记忆图谱

5.5 前端构建

cd nanobot/webui
npm install
# 开发模式(HMR):
npm run dev    # 监听 :5173
# 生产构建(输出到 nanobot/nanobot/web/dist/):
npm run build

5.6 Backend 依赖

backend 的 requirements.txt 很精简:

fastapi>=0.110
uvicorn[standard]>=0.27
httpx>=0.27
pydantic>=2.5

其余依赖(nanobot SDK、funasr 等)通过 sys.path 注入从父目录导入。


6. 配置文件

6.1 Nanobot 配置 ~/.nanobot/config.json

关键配置段:

{
  "model": {
    "provider": "ollama",
    "model": "fredrezones55/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:latest",
    "baseUrl": "http://localhost:11434"
  },
  "tts": {
    "enabled": true,
    "provider": "cosyvoice_local",
    "api_base": "http://127.0.0.1:9880",
    "model": "cosyvoice3-0.5b",
    "voice": "default",
    "language": "zh",
    "speed": 1.0,
    "fallback_provider": "browser",
    "auto_read": true
  },
  "transcription": {
    "enabled": true,
    "provider": null,
    "model": null,
    "language": "zh"
  },
  "realtime_voice": { "enabled": true },
  "gateway": {
    "host": "127.0.0.1",
    "port": 18790
  },
  "longTermMemory": {
    "enabled": true,
    "backend": "kuzu",
    "kuzuDbPath": "~/.nanobot/workspace/memory/memory_graph.kuzu"
  },
  "channels": {
    "transcriptionProvider": "local",
    "transcriptionLanguage": "zh"
  }
}

注意

  • transcription.provider: null → 代码 fallback 到 "local" → faster-whisper
  • channels.transcriptionProvider: "local" → 确保使用本地 faster-whisper
  • TTS provider cosyvoice_local 是自定义的,在 nanobot TTS 客户端中处理

6.2 声纹配置 ~/.nanobot-assistant/speaker_settings.json

{
  "threshold": 0.55,
  "enabled": true
}
  • 阈值 0.55 = 余弦相似度 ≥0.55 判定匹配
  • 声纹数据存储在 ~/.nanobot-assistant/campp_speakers/
  • 索引文件 ~/.nanobot-assistant/campp_index.json

6.3 环境变量

# Ollama 调优
export OLLAMA_MAX_LOADED_MODELS=4
export OLLAMA_KEEP_ALIVE=-1
export OLLAMA_NUM_PARALLEL=2

# ASR 调优(可选)
export NANOBOT_ASR_DEVICE=cpu
export NANOBOT_ASR_COMPUTE_TYPE=int8
export NANOBOT_ASR_BEAM_SIZE=1
export NANOBOT_ASR_CONDITION_PREVIOUS=0

# DualBrain(可选覆盖)
export DUAL_BRAIN_ENABLED=true
export TRIAGE_MODEL=nexusriot/Qwen3.5-Uncensored-HauhauCS-Aggressive:4b
export FAST_MODEL=nexusriot/Qwen3.5-Uncensored-HauhauCS-Aggressive:4b
export OLLAMA_BASE=http://localhost:11434

7. 自定义模块详解

7.1 Backend (backend/)

main.py — FastAPI 主应用

核心功能

  • 通过 sys.path.insert 导入 nanobot 和 nanobot-assistant
  • 生命周期管理:加载 nanobot bot、FunASR 引擎、声纹门控
  • 16 个路由端点

路由清单

方法 路径 功能
GET /health 健康检查
GET /api/info 系统信息
POST /chat 聊天(经 DualBrain 路由)
POST /asr ASR 转写(FunASR SenseVoiceSmall)
GET /speaker/status 声纹状态
GET /speaker/speakers 已录入说话人列表
POST /speaker/enroll 录入声纹(multipart audio+name)
POST /speaker/verify 声纹验证(multipart audio)
POST /speaker/enabled 启用/禁用声纹
POST /speaker/threshold 设置阈值
DELETE /speaker/{id} 删除指定说话人
DELETE /speaker 删除所有声纹
POST /voice/turn 完整语音回合(声纹→ASR→双脑→TTS)
POST /memory/retrieve 记忆检索
GET /memory/peek 记忆预览
GET /skills 技能列表

关键代码模式

# 路径注入
BACKEND_DIR = Path(__file__).resolve().parent
NANOBOT_REPO = BACKEND_DIR.parent / "nanobot"
NANOBOT_SRC = NANOBOT_REPO / "nanobot"
if NANOBOT_SRC.exists():
    os.sys.path.insert(0, str(NANOBOT_SRC.parent))
NANOBOT_ASSISTANT = BACKEND_DIR.parent / "nanobot-assistant"
if NANOBOT_ASSISTANT.exists():
    os.sys.path.insert(0, str(NANOBOT_ASSISTANT))

# 声纹初始化(FunASR CAM++ 优先,Resemblyzer 回退)
from nanobot_assistant.audio.funasr_engine import get_default_funasr_gate
state.speaker_gate = get_default_funasr_gate()

# ASR 初始化(FunASR SenseVoiceSmall)
from nanobot_assistant.audio.funasr_engine import FunASRWhisper
state.asr = FunASRWhisper(device="cpu", language="zh")

# 推理用 asyncio.to_thread 避免阻塞事件循环
result = await asyncio.to_thread(gate.verify, wav_path)
text = await asyncio.to_thread(asr_engine.transcribe, tmp)

/voice/turn 端点逻辑

  1. 保存上传音频,转 WAV
  2. CAM++ 声纹验证(即使被拒绝也继续 ASR,用于显示识别文本)
  3. 声纹匹配 → FunASR ASR → DualBrain 路由 → TTS 合成
  4. 声纹不匹配 → 返回 rejected=true(但仍包含 asr_text)
  5. 返回

dual_brain.py — 双脑路由器

架构

用户消息 → 规则粗筛(URL/代码/文件/关键词)
              ↓ 命中复杂模式
         COMPLEX → 慢脑(nanobot bot.run,含工具/记忆)
              ↓ 未命中
         4B 分诊 LLM(~400ms,think=false)
              ↓
    ┌── SIMPLE → 快脑 4B(~1.2s,无工具/记忆,6轮历史)
    └── COMPLEX → 慢脑

关键设计

  • 所有 Ollama 调用走原生 /api/chat(不是 /v1/chat/completions),必须传 think:false
  • thinking 模型走 /v1 端点会把回答藏在 reasoning 字段导致 content 为空
  • 全链路降级:任何阶段失败 → 慢脑("宁可慢,不可错")
  • 快脑系统提示:口语化、简洁、不假装调用工具
  • DualBrainConfig.from_env() 支持环境变量覆盖

规则粗筛模式

_COMPLEX_PATTERNS = (
    re.compile(r"https?://", re.I),               # URL
    re.compile(r"```"),                            # 代码块
    re.compile(r"(^|\s)[~./][\w./-]*/[\w./-]+"),  # 文件路径
    re.compile(r"\.(py|js|ts|json|md|sh|...)\b"), # 文件扩展名
    re.compile(r"[中文]*(文件|目录|运行|执行|代码|...)", re.I),
    re.compile(r"\b(run|execute|search|fetch|...)\b", re.I),
)

webui_gateway.py — SPA 托管 + WS 聊天

功能

  • 托管 nanobot 构建好的 React SPA(nanobot/nanobot/web/dist/
  • 注入 <script src="/speaker-widget.js"> 到 index.html
  • 实现 WS /ws 聊天协议子集(ready/attached/delta/stream_end/turn_end)
  • 所有聊天经 DualBrainRouter
  • brain 字段通过 tool_hint 气泡回传 UI
  • 显式路由 /speaker-widget.js/speaker-monitor.js/architecture.html(在 SPA catch-all 之前注册)

SPA fallback 排除列表

if full_path.startswith((
    "api/", "webui/", "ws", "assets/", "brand/",
    "speaker/", "voice/", "chat", "asr", "memory", "skills", "health",
)):
    return JSONResponse({"detail": "not found"}, status_code=404)

static/speaker_monitor.js — 实时声纹浮窗

  • 独立 getUserMedia + ScriptProcessor 能量 VAD
  • 启动 0.5s 环境噪声校准(自适应阈值)
  • 降采样 16k Int16 → WAV → POST /speaker/verify + /asr
  • fetch 15s 超时(AbortController)
  • 连续 3 次失败 → 清空队列 + 5s 退避
  • 队列上限 3 条,超出丢弃最旧
  • 当实时语音活跃时,不独立采音,监听 nanobot:speaker-identified CustomEvent
  • Vite dev 模式通过 transformIndexHtml 注入

static/speaker_widget.js — 主页声纹挂件

  • 右下角 FAB 按钮(push-to-talk)
  • 声纹管理面板(录入/删除/开关)
  • 通过 /voice/turn 端点做完整语音回合

7.2 Nanobot Assistant (nanobot-assistant/)

audio/funasr_engine.py — FunASR ASR + CAM++ 声纹

FunASRWhisper 类

  • 模型:iic/SenseVoiceSmall(中文 ASR,~12s 加载,~0.7s 推理)
  • transcribe(wav_path) -> str:返回纯文本,自动去除 <|zh|><|NEUTRAL|> 等特殊标签
  • _infer_lock(threading.Lock):保护 model.generate() 并发调用

FunASRSpeakerGate 类

  • 模型:iic/speech_campplus_sv_zh-cn_16k-common(192 维 L2 归一化 embedding)
  • 声纹存储:~/.nanobot-assistant/campp_speakers/*.npy + campp_index.json
  • verify(audio_path) -> VerifyResult:文件路径输入,内部用 librosa/ffmpeg 加载
  • verify_pcm(pcm_bytes, sample_rate, channels) -> dict:原始 PCM 输入(实时路径用)
  • enroll(audio_path, name) -> dict:录入新说话人(需 ≥3s 音频)
  • _infer_lock:保护 model.generate() 并发调用
  • 点积 = 余弦相似度(embedding 已 L2 归一化)

关键代码模式

class FunASRSpeakerGate:
    def __init__(self, ...):
        self._lock = threading.RLock()      # 保护 speaker 列表
        self._infer_lock = threading.Lock() # 保护模型推理

    def _embed(self, wav):
        enc = self._ensure_model()
        with tempfile.NamedTemporaryFile(suffix=".wav") as tmp:
            sf.write(tmp.name, wav, SAMPLE_RATE)
            with self._infer_lock:
                result = enc.generate(input=tmp.name)
        emb = np.asarray(result[0]["spk_embedding"], dtype=np.float32).flatten()
        norm = float(np.linalg.norm(emb))
        if norm > 0: emb = emb / norm
        return emb

    def verify_pcm(self, pcm, sample_rate=16000, channels=1):
        wav = np.frombuffer(pcm, dtype=np.int16).astype(np.float32) / 32768.0
        if channels > 1: wav = wav.reshape(-1, channels).mean(axis=1)
        if sample_rate != SAMPLE_RATE:
            wav = librosa.resample(wav, orig_sr=sample_rate, target_sr=SAMPLE_RATE)
        emb = self._embed(wav)
        with self._lock: speakers = list(self._speakers)
        scored = sorted(((float(np.dot(emb, s["embedding"])), s) for s in speakers),
                       key=lambda x: x[0], reverse=True)
        top_score, top = scored[0]
        return {"match": top_score >= self.threshold, "score": round(top_score, 4), ...}

线程安全要点

  • FunASR 的 AutoModel.generate() 不是线程安全的
  • 必须用 _infer_lock 串行化推理
  • ASR 模型和声纹模型各有独立的锁(不同模型实例可并行)

audio/vad.py — Silero VAD 状态机(独立语音通道用)

  • 弃用 VADIterator,直接调 Silero 模型逐帧取概率
  • 三层抗噪:启动校准 → 连续帧确认 → 自适应阈值
  • 参数:threshold 0.65、min_speech_ms 400、speech_pad_ms 80、silence_ms 300

Entry Points 注册 (pyproject.toml)

[project.entry-points."nanobot.channels"]
desktop_voice = "nanobot_assistant.channels.desktop_voice:DesktopVoiceChannel"

[project.entry-points."nanobot.tools"]
screenshot = "nanobot_assistant.tools.screenshot:ScreenshotTool"
speak = "nanobot_assistant.tools.tts:SpeakTool"
remember = "nanobot_assistant.tools.remember:RememberTool"
vector_recall = "nanobot_assistant.tools.recall:VectorRecallTool"

7.3 TTS 服务 (nanobot/services/tts/)

独立 Python 包,在 conda cosyvoice 环境中运行。

端点

方法 路径 功能
POST /v1/audio/speech 一次性合成 WAV(OpenAI 兼容)
GET /v1/audio/speech/stream NDJSON 流式 PCM(按句切分)
WS /v1/audio/speech/bistream 双向流式:文本增量进 → PCM 增量出

Bi-Stream 协议

Client → Server:
  {"type":"start","model":"cosyvoice3-0.5b","voice":"default","language":"zh","speed":1.0}
  {"type":"text","text":"你好,"}
  {"type":"text","text":"世界。"}
  {"type":"finish"}

Server → Client:
  {"type":"format","sample_rate":24000,"content_type":"audio/pcm"}
  {"type":"chunk","seq":0,"sample_rate":24000,"data":"<base64 PCM16>"}
  {"type":"chunk","seq":1,...}
  {"type":"done","chunks":N}

实现:用 queue.Queue 桥接 asyncio 与 worker 线程。CosyVoice 的 inference_zero_shot(generator, ..., stream=True) 接受文本生成器,在独立线程中边消费文本边产出音频 token,经 out_q 传回 asyncio,经 WS 发送。

关键约束:首段文本必须 ≥10-15 字或到标点,否则模型空等导致首块延迟极高(2 字片段 14.6s vs 15 字片段 8.2s)。


8. Nanobot 核心修改(授权文件清单)

以下文件在 nanobot/nanobot/ 中被修改。每个修改的目的和关键变更:

8.1 nanobot/realtime/websocket.py

修改目的:ASR+声纹并行、双向流 TTS 桥接、ASR/TTS 耗时观测

变更

  1. __init__ 新增 speaker_verifier 参数
  2. _transcribe_final 重写:asyncio.gather(_do_asr(), _do_verify()) 并行执行
  3. ASR 结果和 speaker 信息合并到 transcript.final 事件
  4. 新增 tts.bistream_start/text/finish 消息处理
  5. _bistream_start 创建 BistreamTTS 会话,_drain_bistream 把 PCM chunk 转 tts.audio 发回客户端
  6. tts.cancel 同时取消 bistream 会话
  7. _cancel_tasks 清理所有 bistream 会话
  8. 发射 asr_completed/tts_completed 观测事件(含 first_chunk_msbistream:true

8.2 nanobot/tts/client.py

修改目的:新增 BistreamTTS 客户端类

变更

  • 新增 BistreamTTS 类(约 160 行)
  • async with await session.start() 建立 WS 连接
  • push_text(str) 喂文本片段
  • finish() 结束输入
  • async for chunk in session 迭代 TTSStreamChunk(pcm/sample_rate)
  • _ws_url_from_http() 把 http:// 转 ws://
  • _recv_loop 后台任务持续接收 chunk/done/error 入 asyncio.Queue

8.3 nanobot/channels/websocket.py

修改目的:注入 speaker_verifier 到 RealtimeVoiceConnection

变更

  • 创建 RealtimeVoiceConnection 时传 speaker_verifier=getattr(self.gateway, "speaker_verifier", None)
  • 同时透传 memory_event_sink(用于 ASR/TTS 观测事件)

8.4 nanobot/webui/gateway_services.py

修改目的:在 Gateway 进程中构建 CAM++ 声纹验证器

变更

  • GatewayServices dataclass 新增 speaker_verifier: Any | None = None 字段
  • build_gateway_services() 调用 _build_speaker_verifier()
  • _build_speaker_verifier() 懒加载 get_default_funasr_gate(),包装为 async callable:
    async def _verify(pcm, sample_rate, channels):
        return await asyncio.to_thread(gate.verify_pcm, pcm, sample_rate, channels)
    
  • 无已录入声纹时返回 None(不禁用功能,只是不验证)

8.5 nanobot/providers/local_transcription.py

修改目的:ASR 性能优化

变更

  • 默认 greedy 解码(beam_size 5→1,快 3-5x)
  • condition_on_previous_text=False
  • word_timestamps=False
  • 新增 transcribe_pcm() 方法:PCM bytes → numpy → 直接喂模型(省 WAV 临时文件)
  • 所有参数支持环境变量覆盖
  • 模型缓存 _MODEL_CACHE 按 (model, device, compute_type) 复用

8.6 nanobot/webui/agent_graph_observer.py

修改目的:修复观测盲区

变更

  • 新增 _legacy_turn_runs:从 agent_loop_duration + agent_iteration/agent_tool_calls 投影出 legacy 引擎的 turn graph
  • turn 列表合并 langgraph 与 legacy 两路分页
  • 新增 _attach_audio_timings:按 session_key+时间窗关联 ASR/TTS 事件到 turn
  • turn 对象带 asr_ms/tts_ms/asr_engine/tts_engine
  • AgentOverview 新增 asr/tts 节点

8.7 前端文件

文件 变更
webui/vite.config.ts 注入 speaker-monitor.js;代理 /speaker /asr → :8000
webui/src/lib/realtime-voice-client.ts speaker 字段类型;startBistreamTTS() 方法
webui/src/hooks/useRealtimeVoice.ts currentSpeaker 状态;ttsSettings;speaker 事件处理;CustomEvent 派发
webui/src/hooks/useVoicePlayback.ts 按句/分隔符切分经 neuralQueue 流水线(边播边合成)
webui/src/components/thread/ThreadShell.tsx realtimeActive 状态;bistream bridge effect;传递 realtimeSpeaker
webui/src/components/thread/ThreadComposer.tsx realtimeSpeaker prop;状态栏显示说话人标签
webui/src/lib/types.ts SpeakerInfo 类型定义
webui/src/lib/api.ts 声纹/实时语音相关 API 函数

9. 启动流程

9.1 启动顺序

# 1. 确保 Ollama 运行
ollama serve  # 或已作为服务运行

# 2. 启动 TTS 服务(独立 conda 环境,端口 9880)
~/cosyvoice/start_tts.sh &

# 3. 等待 TTS 模型加载(~25s 冷启动),验证:
curl -s http://127.0.0.1:9880/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"input":"测试","model":"cosyvoice3-0.5b","voice":"default"}' \
  -o /tmp/test_tts.wav

# 4. 启动 Nanobot Gateway(端口 8765/8766/8767/18790)
source venv/bin/activate
nanobot gateway &

# 5. 启动 Backend(端口 8000)
cd backend
python main.py &

# 6. (可选)启动 Vite Dev Server(端口 5173,前端 HMR)
cd nanobot/webui
npm run dev

9.2 一键启动脚本参考

#!/bin/bash
# start_all.sh
set -e

# TTS
if ! lsof -ti:9880 >/dev/null 2>&1; then
  echo "Starting TTS..."
  ~/cosyvoice/start_tts.sh &
  sleep 25
fi

# Gateway
if ! lsof -ti:8765 >/dev/null 2>&1; then
  echo "Starting Gateway..."
  cd /path/to/project
  source venv/bin/activate
  nanobot gateway &
  sleep 8
fi

# Backend
if ! lsof -ti:8000 >/dev/null 2>&1; then
  echo "Starting Backend..."
  cd /path/to/project/backend
  /path/to/project/venv/bin/python main.py &
  sleep 6
fi

# Vite (optional)
if ! lsof -ti:5173 >/dev/null 2>&1; then
  echo "Starting Vite..."
  cd /path/to/project/nanobot/webui
  npm run dev &
fi

echo "All services started."
echo "  Frontend:  http://localhost:5173 (dev) or http://localhost:8000 (prod)"
echo "  Gateway:   http://localhost:8765"
echo "  Backend:   http://localhost:8000"
echo "  TTS:       http://localhost:9880"

9.3 验证清单

# 所有服务健康
curl -s http://localhost:8000/health
curl -s http://localhost:8765/ >/dev/null && echo "Gateway OK"
curl -s http://localhost:18790/health
curl -s http://localhost:9880/ >/dev/null 2>&1 && echo "TTS OK"
curl -s http://localhost:11434/api/tags | python3 -c "import sys,json; print(f'Ollama: {len(json.load(sys.stdin)[\"models\"])} models')"

# 声纹状态
curl -s http://localhost:8000/speaker/status | python3 -m json.tool

# ASR 测试
curl -s http://localhost:8000/asr -F "audio=@test.wav" | python3 -m json.tool

# 聊天测试
curl -s http://localhost:8000/chat -H "Content-Type: application/json" \
  -d '{"message":"你好","session_key":"test"}' | python3 -m json.tool

10. 踩坑记录(必须避免)

10.1 Ollama thinking 模型坑

问题:Qwen3 thinking 模型走 /v1/chat/completions 端点时,回答藏在 reasoning 字段,content 为空。

解决:必须走 Ollama 原生 /api/chat 端点,并传 "think": false

async with client.stream("POST", "/api/chat", json={
    "model": model, "messages": messages,
    "stream": True, "think": False,  # 关键!
    "options": {"temperature": 0.7}
}) as r:
    async for line in r.aiter_lines():
        data = json.loads(line)
        content = data.get("message", {}).get("content", "")

10.2 CosyVoice MPS 设备坑

问题Placeholder storage has not been allocated on MPS device!

原因:CosyVoice3 frontend 在非 CUDA 环境硬编码 self.device='cpu',bi-streaming 路径不像非流式那样自动 .to(model.device)

解决:在 load_model 返回后显式设置:

model.frontend.device = torch.device("mps")
model.model.device = torch.device("mps")
model.model.llm.to(mps)
model.model.flow.to(mps)
# hift 留 CPU(f0_predictor 需 float64)

10.3 CosyVoice hift 声码器 MPS 不兼容

问题:MPS 上 f0_predictor 需要 float64 但 MPS 不支持。

解决:hift 留 CPU,patch hift.inference 将 mel 张量移到 CPU:

original_hift = inner.hift.inference
def _hift_on_cpu(speech_feat, finalize=True):
    return original_hift(speech_feat.detach().cpu(), finalize=finalize)
inner.hift.inference = _hift_on_cpu

10.4 CosyVoice inference_zero_shot 参数坑

问题prompt_wav 参数传 load_wav() 返回的 tensor 会报错。

解决:必须传文件路径字符串,前端内部会自己 load_wav。

10.5 CosyVoice 线性重采样 MPS bug

问题:MPS 上线性重采样器产出不正确的输出长度(fft shape mismatch)。

解决:MPS 模式下设 self.hifp_resampler = None 跳过重采样器。

10.6 FunASR 并发推理崩溃

问题:多个请求同时调 model.generate() 导致段错误/死锁。

解决:每个模型实例加 threading.Lock(),串行化推理。ASR 模型和声纹模型各有独立锁。

10.7 langgraph 缺失

问题NANOBOT_GRAPH_ENGINE=langgraph 是默认值,但 langgraph 包未安装时所有聊天返回 "Sorry, I encountered an error."

解决pip install langgraph

10.8 setuptools 版本

问题:webrtcvad/openai-whisper 旧版 setup.py 用 pkg_resources,setuptools ≥81 已移除。

解决pip install 'setuptools<81' + --no-build-isolation

10.9 浏览器录音格式

问题:浏览器录音是 webm/opus,soundfile 读不了,audioread 未装。

解决_load_wav 先试 librosa,失败再用 ffmpeg 转 16k PCM。

10.10 网关与后端是独立进程

问题:Backend 的 bot._loop.channels 为空,无法从后端注入 speaker_verifier 到 Gateway 的 WebSocket。

解决:speaker_verifier 在 build_gateway_services() 中构建(Gateway 进程内),不通过后端注入。

10.11 实时语音 ASR 用的是 faster-whisper 不是 FunASR

关键区别

  • Gateway (:8765) 实时语音路径用 nanobot 内置的 faster-whisper small(local provider)
  • Backend (:8000) HTTP 路径用 FunASR SenseVoiceSmall
  • 两者都加载 CAM++ 声纹模型(各自进程独立实例)
  • 不要假设实时语音走 FunASR

10.12 Vite 代理配置

Vite dev server 必须正确代理:

proxy: {
  "/webui": { target: "http://127.0.0.1:8765", changeOrigin: true },
  "/api":   { target: "http://127.0.0.1:8765", changeOrigin: true },
  "/auth":  { target: "http://127.0.0.1:8765", changeOrigin: true },
  "/speaker": { target: "http://127.0.0.1:8000", changeOrigin: true },
  "/asr":     { target: "http://127.0.0.1:8000", changeOrigin: true },
}

11. 关键文件清单

Backend 自定义文件(全部需要创建)

  • backend/main.py — FastAPI 主应用(~750 行)
  • backend/dual_brain.py — 双脑路由器(~350 行)
  • backend/webui_gateway.py — SPA 托管 + WS 聊天(~350 行)
  • backend/voice.html — 独立语音页面
  • backend/static/speaker_widget.js — 主页声纹挂件
  • backend/static/speaker_monitor.js — 实时声纹浮窗
  • backend/static/architecture.html — SVG 数据流向图
  • backend/requirements.txt

Nanobot Assistant 文件

  • nanobot-assistant/nanobot_assistant/audio/funasr_engine.py — FunASR ASR + CAM++(~480 行)
  • nanobot-assistant/nanobot_assistant/audio/speaker_gate.py — Resemblyzer 声纹(旧版)
  • nanobot-assistant/nanobot_assistant/audio/vad.py — Silero VAD
  • nanobot-assistant/nanobot_assistant/audio/mic_capture.py — 麦克风采集
  • nanobot-assistant/nanobot_assistant/audio/local_asr.py — faster-whisper 封装
  • nanobot-assistant/nanobot_assistant/audio/tts_engine.py — TTS 封装
  • nanobot-assistant/nanobot_assistant/audio/player.py — 音频播放
  • nanobot-assistant/nanobot_assistant/channels/desktop_voice.py — 桌面语音通道
  • nanobot-assistant/nanobot_assistant/tools/*.py — screenshot/speak/remember/recall
  • nanobot-assistant/pyproject.toml — entry_points 注册

TTS 服务文件

  • nanobot/services/tts/nanobot_tts_service/__init__.py
  • nanobot/services/tts/nanobot_tts_service/app.py — FastAPI TTS 服务(~410 行)
  • nanobot/services/tts/nanobot_tts_service/cosyvoice.py — CosyVoice 适配器(~300 行)
  • nanobot/services/tts/nanobot_tts_service/qwen.py — Qwen TTS 回退
  • nanobot/services/tts/nanobot_tts_service/audio_util.py
  • nanobot/services/tts/pyproject.toml

Nanobot 核心修改文件

  • nanobot/nanobot/realtime/websocket.py — ASR+声纹并行、bistream TTS
  • nanobot/nanobot/tts/client.py — BistreamTTS 类
  • nanobot/nanobot/channels/websocket.py — speaker_verifier 注入
  • nanobot/nanobot/webui/gateway_services.py — CAM++ verifier 构建
  • nanobot/nanobot/providers/local_transcription.py — ASR 优化
  • nanobot/nanobot/webui/agent_graph_observer.py — 观测盲区修复

前端修改文件

  • nanobot/webui/vite.config.ts — 代理 + 注入
  • nanobot/webui/src/lib/realtime-voice-client.ts — bistream + speaker
  • nanobot/webui/src/hooks/useRealtimeVoice.ts — speaker 状态
  • nanobot/webui/src/hooks/useVoicePlayback.ts — 流水线播放
  • nanobot/webui/src/components/thread/ThreadShell.tsx — bistream bridge
  • nanobot/webui/src/components/thread/ThreadComposer.tsx — speaker badge
  • nanobot/webui/src/lib/types.ts — 类型定义

12. 模型文件清单

模型 用途 位置 大小
SenseVoiceSmall ASR (Backend) ~/.cache/modelscope/models/iic--SenseVoiceSmall/ ~900MB
speech_campplus_sv_zh-cn_16k-common 声纹 ~/.cache/modelscope/models/iic--speech_campplus.../ ~100MB
CosyVoice2-0.5B / Fun-CosyVoice3-0.5B TTS ~/.cache/modelscope/hub/iic/CosyVoice2-0.5B/ ~4.7GB
faster-whisper small ASR (Gateway) ~/.cache/huggingface/hub/ (自动下载) ~488MB
qwen2.5:1.5b Ollama 分诊 Ollama 管理 1.0GB
nexusriot/...:4b Ollama 快脑 Ollama 管理 3.4GB
fredrezones55/...:35B Ollama 慢脑 Ollama 管理 22.1GB

13. 性能基准(M4 Pro, MPS)

操作 耗时
CAM++ 声纹验证(稳态) ~50-100ms
CAM++ 冷启动 ~280ms
SenseVoiceSmall ASR 推理 ~0.7s
faster-whisper small ASR ~0.7s
CosyVoice 冷加载 ~25s
CosyVoice 短文本稳态合成 ~3.9-4.3s
CosyVoice 流式首块(预热后) ~3.5s
CosyVoice bi-stream 首块(≥15字) ~5.6s
4B 快脑简单问题 ~1.2s
4B 分诊 ~400ms
端到端语音首音(bi-stream 并行) ~4-5s

14. 数据流总结

实时语音路径 A (:8765):
  Mic PCM → [VAD] → [faster-whisper ASR ∥ CAM++ 声纹] → transcript.final
    → WS /ws 聊天 → AgentLoop → Ollama 35B → delta 流式
    → tts.bistream_text → Gateway BistreamTTS → TTS :9880
    → PCM chunks → PcmStreamPlayer 播放

HTTP 语音路径 B (:8000):
  WAV POST → [CAM++ 声纹 → FunASR ASR] → DualBrainRouter
    → 规则粗筛 → 4B 分诊 → 4B 快脑 / 35B 慢脑
    → TTS HTTP :9880 → WAV → JSON 响应

15. 复现后自检清单


附录 A:双脑分诊路由设计文档 v1.1

来源:backend/docs/dual-brain-design-v1.1.md

双脑分诊路由 设计文档 v1.1

目标:在不修改 nanobot 任何代码的前提下,于 backend 层引入「快脑/慢脑」双脑分诊路由,大幅降低简单请求的响应延迟;北极星是做到接近真人的中文语音对话

状态:已实测验证(核心链路可用) · 落点:backend/ · 作者:架构协调
日期:2026-08-14

v1.1 变更(相对 v1.0):

  • 加入 1.5B / 4B / 9.7B 三方实测数据,选型由"拍脑袋"改为"数据驱动"。
  • 记录并根治 thinking 坑(必须走 Ollama 原生 /api/chat + think:false)。
  • 架构精简:4B 一脑两用(分诊 + 对话),1.5B 降为可选"反射脑"。
  • 目标升级:从"降延迟" → "类真人语音对话"。
  • 新增「基础设施 / Ollama 部署调参」与「语音链路」两节。
  • ASR 本地化落地:云端 Groq → 本地 faster-whisper(§11.1)。

1. 背景与问题

当前 /chat 是"一条道走到黑":无论请求简单与否,都跑完整 nanobot agent loop。

POST /chat → bot.run() → AgentLoop → ContextBuilder(阻塞式记忆检索+技能加载)
           → AgentRunner(最多 200 轮 LLM+工具循环) → Qwen3.6-35B(本地 Ollama, 22GB)

即便只说"你好",也要先读盘做记忆检索/技能加载,再喂 35B 跑一遍 → 简单请求被拖到几十秒级。

本机模型(实测 /api/show 参数)

模型 实际参数 大小 能力 拟定角色
qwen2.5:1.5b 1.5B 1.0GB 对话 ⚡ 反射脑/接话音(可选)
nexusriot/Qwen3.5-...HauhauCS:4b 4.5B 3.4GB vision+tools+think 对话脑 + 分诊(主力)
qwen3.5:latest 9.7B 6.6GB vision+tools+think 中脑(可选)
qwen3.6:latest (35B) 36B 22GB 全能 🧠 慢脑(工具/记忆/深度推理)

2. 目标与非目标

目标

  • G1:简单请求(闲聊/常识/短问答)由快脑亚秒级返回,绕过记忆检索与工具循环。
  • G2:复杂请求仍走完整慢脑 bot.run(),行为与现状一致。
  • G3:分诊快(实测 ~400ms),不成为新瓶颈。
  • G4:全部改动落在 backend/,零改动 nanobot
  • G5:全链路降级——任何一环失败都回退纯慢脑,保证不劣化。
  • G6(北极星):对话脑输出短、口语、有情绪,为"类真人语音对话"打底。

非目标

  • 不做并行推测/流式两段式返回(留 v2.0)。
  • 不修改 nanobot 的 runner.py / loop.py / context.py
  • 语音端到端(STT/TTS/打断)本版只出方向,不实现(见 §11)。

3. 边界声明(强约束)

范畴 目录 本方案是否改动
监控/后端系统 backend/ ✅ 新增 + 修改
语音扩展层 nanobot-assistant/ ⚠️ 仅语音链路阶段涉及(见 §11)
nanobot 核心 nanobot/ 否(严禁)

集成只走公共复用点:慢脑用 Nanobot.run();会话用 SessionManager;快脑/分诊直连 Ollama 原生 API(与 nanobot 完全解耦,可独立单测)。


4. 架构(v1.1:4B 一脑两用)

实测表明 4B 分诊准确率 100%(§9.2),故取消"1.5B 专职分诊"的中间层,由 4B 兼任分诊与对话:

graph TD A[POST /chat] --> R{DualBrainRouter} R --> P[规则前置粗筛<br/>URL/代码/文件/工具词 → 直接 COMPLEX] P -->|命中| S P -->|未命中| T[4B 分诊<br/>think=false, 只输出一词<br/>~400ms] T -->|SIMPLE| F[⚡ 4B 对话脑<br/>直接答, 无工具/记忆<br/>读最近6轮历史<br/>~1.2s] T -->|COMPLEX| S[🧠 35B 慢脑<br/>bot.run 完整 agent loop] T -.分诊异常/超时.-> S F -.快脑异常.-> S F --> O[ChatResponse + 写回 Session] S --> O RB[1.5B 反射脑(可选)<br/>说话瞬间先接一声 嗯~] -.感知延迟遮蔽.-> F

决策原则:存疑一律判 COMPLEX(宁可慢,不可错)。复杂走慢脑最多回到现状,绝不劣化。


5. 模块与文件

backend/
├── main.py                 # 改:lifespan 装配 Router;/chat 分流 + 开关 + brain 字段
├── dual_brain.py           # ✅ 已实现:DualBrainRouter(规则前置+分诊+快脑+降级+写回)
├── bench_fast_brain.py     # ✅ 跑分:1.5B/4B/9.7B 首字延迟+自然度横评
├── probe_4b_triage.py      # ✅ 探针:4B 指令/意图识别准确率
└── docs/
    ├── dual-brain-design-v1.0.md
    └── dual-brain-design-v1.1.md   # 本文档

5.1 关键实现点(dual_brain.py)

  • _chat() 走 Ollama 原生 POST /api/chat,固定 think:false,读 message.content(§8 坑)。
  • rule_based_is_complex():URL / ``` 代码块 / 文件路径 / 工具关键词 → 直接 COMPLEX,省一次 LLM。
  • route():规则前置 → 4B 分诊 → SIMPLE 走快脑 / COMPLEX 走慢脑;逐层 try/except 降级。
  • 快脑答完 session.add_message() + save() 写回同一 session_key,快慢脑共享历史。

6. Session 一致性

  • 慢脑 bot.run(session_key=K) 内部已用 SessionManager 读写 sessions/<K>.jsonl
  • 快脑手动把 user+assistant 追加进同一 Ksave()
  • 读:快脑 get_history(max_messages=6) 取最近几轮做上下文(支持有上下文闲聊)。
  • ⚠️ backend /chatreq.session_key(默认 web:default),快慢脑必须一致。

7. /chat 接口改动(T2,待实现)

对外协议不变,仅内部分流;ChatResponse 增可选 brain 字段(fast/slow)供观测与对照。
环境变量:DUAL_BRAIN_ENABLED(默认 true)、TRIAGE_MODELFAST_MODELOLLAMA_BASE


8. ⚠️ 关键坑:thinking 模式(实测踩到并已根治)

现象:首版快脑走 OpenAI 兼容 /v1 端点,4B 全返回空、稳定 5 秒超时。

根因:4B/9.7B/35B 都是 thinking(思考型)模型——回答前先产出一段"内心推理草稿",默认把内容写进 reasoning 字段,content 为空:

"message": { "content": "", "reasoning": "Thinking Process: 1. Analyze..." }  // finish_reason: length

/v1 端点用 /no_think 前缀无效

解法(已落地):改用 Ollama 原生 POST /api/chat 并传 "think": false → 内容回到 content,done_reason: stop,回答又快又自然。

准则:

  • 对话脑 / 分诊 → 必须 think:false(要脱口而出,不要沉思)。
  • 慢脑(35B)复杂任务 → 可保留 thinking(要想清楚)。

9. 实测数据(控制变量)

环境:Apple M4 Pro / 48GB / Ollama 0.32.6,全部 think:false,已预热剔除冷启动。

9.1 对话质量 & 延迟横评(中位数,bench_fast_brain.py)

模型 首字延迟 TTFT 总耗时 生成速度 自然度(人工)
1.5B 124ms 319ms 137 c/s 中,偏客服腔
4B 358ms 1141ms 51 c/s 高:有情绪/比喻/网感
9.7B 449ms 2527ms 43 c/s 高但啰嗦、爱反问

样例(Q: 安慰我一下):

  • 1.5B:"累是正常的嘛,休息一会儿再聊吧" — 通顺但机械。
  • 4B:"抱抱你~泡杯热茶,好好睡一觉🍵💤" — 有温度、有动作、像朋友
  • 9.7B:同样温暖但更长、更书面。

结论:9.7B(准 7B 档)不如 4B 适合语音对话脑——延迟翻倍(2.5s vs 1.1s)、太啰嗦(念出来长),真人感无质的提升。9.7B 更适合"中脑"(稍复杂问答、不需工具时)。

9.2 指令/意图识别(probe_4b_triage.py)

能力 结果
意图识别(简单 vs 复杂,12 例) 12/12 = 100%
格式遵守("只输出一个词") 12/12 = 100%
结构化输出(强制 JSON) 合法 JSON,且能追问缺失信息
单次分诊耗时 ~400ms(稳定)

4B 可一脑兼任"分诊 + 对话",故 v1.1 取消 1.5B 专职分诊层。

注:测例边界较清晰;真实语音输入更模糊(口语/错字/半句)。故保留"规则前置 + 存疑判 COMPLEX"兜底,误判偏向安全侧(走慢脑)。

9.3 端到端冒烟(dual_brain.py,直连 Ollama)

简单请求走 fast(分诊 ~420ms / 总 ~1.2s,回答自然),复杂请求正确转 slow,快脑回答正确写回 session。核心链路验证通过。


10. 基础设施 / Ollama 部署调参(重要)

沿用 Ollama(已装、模型齐、nanobot 已指向,切换成本为零)。但双脑需常驻多模型,必须防"切换抖动":

内存账:4B(3.4)+ 35B(22)+ 可选 1.5B(1)+ 9.7B(6.6)≈ 33GB < 48GB,可全常驻。

export OLLAMA_MAX_LOADED_MODELS=4   # 多模型同时常驻,杜绝互相挤掉
export OLLAMA_KEEP_ALIVE=-1         # 常驻不卸载(或 "2h")
export OLLAMA_NUM_PARALLEL=2        # 单模型并发
# 设完重启 ollama 服务生效

否则:35B 闲置被卸载 → 下次复杂请求先花 ~10-20s 重新加载 22GB,延迟不降反升。


11. 语音链路(北极星,展望 v2.0)

"类真人"= 对话脑质量(已达标)+ 语音链路。语音代码在 nanobot-assistant/(耳机语音+VAD+ASR+Piper TTS),属扩展层,可改;nanobot 核心不碰。

11.1 已完成:ASR 本地化 ✅

ASR 从云端 Groq 换成本地 faster-whisper,与"本地优先"方向一致:

  • 新增 local_asr.py(faster-whisper 封装,单例懒加载,线程安全)。
  • config.py 新增 ASRConfig(模型尺寸/设备/语言可配)。
  • desktop_voice.py 重写 transcribe_audio 优先走本地,失败自动回退 nanobot 原管线。
  • 实测:small/int8/CPU,中文准确 100%(置信度 1.00),转写 ~1.3s,模型仅首次加载 ~14s。
  • 边界:全部落 nanobot-assistant/,零改动 nanobot

11.2 后续事项(比换更大模型收益更高)

  1. 语音↔双脑对接(⭐ 最高优先):现状语音把文字丢进 nanobot MessageBus → 被 35B 慢脑消费,语音回复全是慢脑。需把出口改走 DualBrainRouter,简单闲聊才能 4B 秒回。
  2. TTS 音色(像不像真人的最大杠杆):Piper zh_CN-huayan-medium 一耳朵是合成音;换更自然、带情绪韵律的 TTS。
  3. 端到端流式 + 打断(barge-in):流式 STT→LLM→TTS,允许用户说话打断 AI。
  4. 口语人设:语音回答强制短句、口语、有情绪(system prompt 层,已在快脑体现)。
  5. 1.5B 反射脑/接话音:说话瞬间先"嗯~/让我想想",遮蔽 4B 的 ~1.2s 延迟,听感更像真人。

12. 降级策略(不劣化)

触发 行为
DUAL_BRAIN_ENABLED=false 全走现状 bot.run(= V0 基线)
分诊超时/报错 判 COMPLEX → 慢脑
判 SIMPLE 但快脑超时/报错 落慢脑重答
Ollama 小模型未就绪 启动探测失败 → 自动禁用双脑

13. 任务进度

任务 状态
T1 · dual_brain.py 双脑核心(规则前置+分诊+快脑+降级+写回) ✅ 完成并冒烟验证
T2 · 接入 main.py/chat(装配+分流+开关+brain 字段) ⏳ 待做
T3 · V0 vs V1 端到端跑分脚本 ⏳ 待做
T4 · 全链路测试 + 回填结果 ⏳ 待做
(旁证)bench_fast_brain.py / probe_4b_triage.py ✅ 已产出数据

14. 已定稿决策(v1.1)

  • ✅ 对话脑 = 4B;分诊 = 4B(一脑两用);1.5B = 可选反射脑;9.7B = 可选中脑;慢脑 = 35B。
  • ✅ 快脑/分诊 = Ollama 原生 /api/chat + think:false(不用 /v1)。
  • ✅ 规则前置粗筛 + 存疑判 COMPLEX 兜底。
  • brain 字段返回(实验对照需要)。
  • ✅ 快脑读最近 6 轮历史(有上下文闲聊)。
  • ✅ 沿用 Ollama + OLLAMA_MAX_LOADED_MODELS/KEEP_ALIVE 防抖动。

---

# 附录 B:实时语音与神经 TTS 兼容改造设计

> 来源:nanobot/docs/realtime-voice-compatibility-design.md

# 实时语音与神经 TTS 兼容改造设计

> 日期:2026-08-21  
> 状态:技术评审稿,不代表已经实现  
> 关联计划:[`realtime-voice-tts-migration-plan.md`](./realtime-voice-tts-migration-plan.md)  
> 目标:明确实时语音和 TTS 模型切换与当前 nanobot 架构的冲突、影响及兼容方案

## 1. 结论

实时语音和神经 TTS 与当前架构**没有不可解决的根本冲突**,但不能直接覆盖现有
协议和组件。

正确实施方式是:

```text
保留现有文本聊天主链
  + 保留完整文件 ASR
  + 保留 browser TTS
  + 新增独立 realtime 会话层
  + 新增可切换 TTS Provider
  + 使用 feature flag 渐进启用

按本文方案实施后:

  • 现有文字输入、图片、会话历史、工具调用和 Agent UI 不应改变。
  • 当前麦克风手动录音继续作为降级路径。
  • 当前浏览器 TTS 继续作为最终降级 Provider。
  • 实时语音只在用户主动开启时工作。
  • 新协议或 TTS 服务失败时不影响普通聊天。

如果直接把 PCM、VAD、流式 ASR 和 TTS 状态塞进当前聊天 WebSocket 和
AgentLoop,则会明显增加现有界面的回归风险。

2. 当前架构基线

2.1 当前输入链路

ThreadComposer
  -> useVoiceRecorder
  -> MediaRecorder
  -> 完整 WebM Data URL
  -> NanobotClient.transcribeAudio
  -> WebSocket JSON: transcribe_audio
  -> webui_transcription_event
  -> LocalWhisperTranscriptionProvider
  -> transcription_result
  -> 自动发送或追加草稿

当前特点:

  • 用户必须显式开始和结束录音。
  • 服务端收到完整音频后才开始识别。
  • 没有临时字幕。
  • 没有服务端语音 Session。
  • ASR 请求与 Agent Turn 是两个独立动作。

2.2 当前 Agent 链路

WebSocket JSON message
  -> InboundMessage
  -> AgentLoop
  -> Runner / LangGraph
  -> delta / tool / reasoning
  -> stream_end
  -> turn_end

已有标识:

  • chat_id
  • turn_id
  • stream_id

已有取消:

WebUI stop()
  -> 发送文本命令 /stop
  -> AgentLoop._cancel_active_tasks(session_key)

2.3 当前输出链路

Assistant message/delta
  -> useVoicePlayback
  -> Sentence boundary detection
  -> SpeechSynthesisUtterance
  -> window.speechSynthesis

现有行为:

  • 新回复可自动朗读。
  • 历史消息建立静默基线,不自动播放。
  • 每条完成回复支持手动播放和停止。
  • 支持系统音色和 0.8x/1x/1.2x/1.5x
  • 开始录音会停止朗读并取消当前 Agent Turn。

2.4 当前 WebSocket约束

服务端当前读取逻辑:

async for raw in connection:
    if isinstance(raw, bytes):
        raw = raw.decode("utf-8")

这意味着:

  • binary frame 被假定为 UTF-8 文本。
  • 原始 PCM 通常不能解码成 UTF-8,会被忽略。
  • 当前协议实际上是 JSON 文本协议。

前端同样假设服务端数据是 JSON:

JSON.parse(typeof ev.data === "string" ? ev.data : "")

因此双向二进制音频不能直接加入当前收发逻辑。

3. 冲突总表

编号 当前设计 目标设计 风险等级 解决方向
C1 单条 JSON WebSocket 连续 PCM/Binary 独立 realtime 通道
C2 完整文件 ASR Streaming ASR 新增 Session 接口,旧接口保留
C3 /stop 按 session 取消 generation 精确中断 新协议映射现有取消
C4 浏览器 TTS 服务端神经 TTS Provider 单选与降级
C5 localStorage 保存音色 服务端保存模型配置 配置所有权拆分
C6 文本消息状态 音频播放状态 独立 realtime state
C7 Agent delta 面向 UI delta 同时驱动 TTS 服务端句子切分器
C8 Gateway 单进程 TTS 大模型常驻 独立 TTS 服务
C9 Python 3.14 TTS 依赖偏 3.10/3.11 独立虚拟环境
C10 会话历史异步水合 新消息自动朗读 历史静默基线
C11 多 chat 共用连接 多实时音频会话 session/generation 隔离
C12 UI 只有录音状态 全双工状态机 可选状态条,不替换输入框

4. C1:聊天 WebSocket 与二进制 PCM 冲突

4.1 冲突原因

当前 nanobot/channels/websocket.py

  • 接收 string 或 bytes。
  • bytes 会尝试 UTF-8 解码。
  • 非 UTF-8 bytes 被忽略。
  • _dispatch_envelope 只处理 JSON envelope。

当前 NanobotClient

  • onmessage 只接受 JSON string。
  • binary response 会进入 JSON parse failure。
  • send queue 类型为 Outbound JSON。

直接在原连接发送 PCM 会产生:

  • 服务端丢弃音频。
  • 前端无法解析 TTS 音频。
  • JSON 控制消息和高频音频争抢发送队列。
  • 大量音频可能延迟 turn_end、工具事件和 /stop
  • 现有单元测试和第三方 WebSocket 客户端契约被破坏。

4.2 推荐方案

第一版新增独立端点:

现有: ws://host:8765/              文本聊天
新增: ws://host:8765/realtime     实时语音

控制面与数据面:

/                  JSON 文本、工具、会话、历史
/realtime          JSON control + binary PCM/audio

Realtime 连接通过短期 token 鉴权,并显式关联:

{
  "type": "realtime.bind",
  "chat_id": "...",
  "realtime_session_id": "...",
  "client_id": "..."
}

4.3 为什么不立即复用原连接

独立连接的优势:

  • 不修改现有 JSON parser。
  • 不影响文字聊天重连。
  • 音频背压不会阻塞工具事件。
  • 可单独配置消息大小、ping、超时和并发。
  • 可独立回收实时会话。
  • 后续迁移 WebRTC 时不影响聊天协议。

4.4 后续优化

稳定后可评估将音频合并进单连接,但必须:

  • 定义 binary header。
  • 修改前后端 frame dispatcher。
  • 分离 chat queue 与 audio queue。
  • 建立协议版本协商。

本项目第一阶段不建议这样做。

5. C2:完整文件 ASR 与 Streaming ASR 冲突

5.1 冲突原因

当前接口:

async def transcribe(file_path) -> str

当前 WebUI 请求:

{
  "type": "transcribe_audio",
  "data_url": "data:audio/webm;base64,..."
}

它无法表达:

  • 连续 append。
  • partial transcript。
  • utterance commit。
  • rolling context。
  • sequence。
  • reset 和 cancel。

5.2 兼容方案

保留现有接口,新增:

class StreamingASRSession(Protocol):
    async def append(self, pcm: bytes, sequence: int) -> None: ...
    async def get_partial(self) -> TranscriptUpdate | None: ...
    async def finalize(self) -> TranscriptFinal: ...
    async def cancel(self) -> None: ...

路由规则:

手动录音模式
  -> transcribe_audio
  -> 完整文件 ASR

实时模式
  -> audio.append
  -> StreamingASRSession
  -> transcript.partial/final

5.3 faster-whisper 的限制

当前 faster-whisper 是文件/片段推理,不是真正的流式状态模型。第一阶段采用:

  • 服务端 PCM Buffer。
  • 每 300~500 ms 对滚动窗口解码。
  • 稳定前缀算法生成 partial。
  • VAD 结束后完整 final。

后续可替换 sherpa-onnx 或 FunASR streaming,但不改变协议。

5.4 对现有界面的影响

默认关闭实时模式时:

  • 现有麦克风行为完全不变。
  • 当前录音波形继续使用。
  • 当前 ASR 错误提示继续使用。

开启实时模式时:

  • 输入框上方增加 partial transcript。
  • 原输入框仍可编辑。
  • final transcript 才进入 Agent。

6. C3:/stop 与 generation 精确中断冲突

6.1 冲突原因

当前 /stop

  • 以 session key 找 active tasks。
  • 取消该会话中的 Agent task 和 subagent。
  • 不知道具体 TTS request。
  • 不知道旧 ASR utterance。
  • 无 generation fence。

实时语音中,用户插话需要同时取消:

  1. 浏览器播放。
  2. TTS 服务生成。
  3. Gateway 音频转发。
  4. Sentence Segmenter。
  5. Agent Turn。
  6. 旧 ASR partial。

6.2 兼容方案

新增控制事件:

{
  "type": "turn.interrupt",
  "chat_id": "...",
  "realtime_session_id": "...",
  "turn_id": "...",
  "generation_id": "..."
}

服务端处理:

turn.interrupt
  -> 标记 generation cancelled
  -> cancel TTS request
  -> clear sentence queue
  -> stop forwarding audio
  -> AgentLoop._cancel_active_tasks(session_key)
  -> emit turn.interrupted

/stop 保留,并映射为:

cancel 当前 session 的 active generation + Agent task

6.3 Generation fence

所有异步结果检查:

if event.generation_id != session.active_generation_id:
    drop(event)

解决:

  • 被中断的 TTS 迟到 chunk。
  • 旧 ASR partial。
  • 旧 LLM delta。
  • 重连前的缓存事件。

7. C4:浏览器 TTS 与神经 TTS 重复播放冲突

7.1 冲突原因

如果服务端神经 TTS 与当前 useVoicePlayback 同时启用:

  • 同一回复会播放两遍。
  • 两种声音重叠。
  • 停止按钮只会停止其中一条路径。
  • 自动播放状态不可预测。

7.2 兼容方案

TTS Provider 必须单选:

browser
cosyvoice_local
qwen3_tts_local
cloud_realtime

统一策略:

switch (tts.provider) {
  case "browser":
    use SpeechSynthesis
    break
  default:
    use streaming audio player
}

禁止:

browser auto-read = true
server TTS auto-read = true

7.3 降级

神经 TTS 满足任一条件时降级:

  • health 不通过。
  • 首包超时。
  • 模型未加载。
  • 请求失败。
  • 音频格式不支持。

降级顺序:

configured neural provider
  -> browser SpeechSynthesis
  -> text only

每个 generation 只能降级一次,防止重复播报。

8. C5:localStorage 与服务端配置所有权冲突

8.1 当前状态

浏览器保存:

  • 自动发送。
  • 自动朗读。
  • 语速。
  • 系统 voice URI。

8.2 目标状态

服务端保存:

  • TTS Provider。
  • 模型 ID。
  • TTS 服务地址。
  • 默认语言。
  • 默认音色 ID。
  • 超时和 fallback。

浏览器保存:

  • 当前设备是否自动朗读。
  • 当前设备音量。
  • 用户临时语速覆盖。
  • browser Provider 的 system voice URI。

8.3 合并规则

effective provider = server config
effective model = server config
effective voice =
  browser provider ? localStorage voiceURI
                   : server voice ID + optional local override
effective auto-read = localStorage
effective speed = local override ?? server default

8.4 设置页影响

“设置 → Voice”拆成两个组:

Speech Recognition
  - ASR Provider
  - ASR Model
  - Language
  - Limits

Speech Output
  - TTS Provider
  - TTS Model
  - Service Health
  - Voice
  - Preview
  - Speed
  - Auto-read
  - Browser fallback

9. C6:消息状态与音频状态冲突

9.1 冲突原因

UIMessage.isStreaming 表示文字是否仍在生成,不能同时表示:

  • TTS 正在排队。
  • TTS 正在生成。
  • 音频正在缓冲。
  • 音频正在播放。
  • 已中断。

复用该字段会导致:

  • 文本停止按钮和语音停止按钮混淆。
  • Turn 已结束但音频仍播放时状态错误。
  • 消息历史被写入临时播放状态。

9.2 兼容方案

Realtime 状态独立保存:

interface RealtimeVoiceState {
  sessionState: RealtimeSessionState;
  activeUtteranceId: string | null;
  activeTurnId: string | null;
  activeGenerationId: string | null;
  transcriptPartial: string;
  ttsState: "idle" | "queued" | "synthesizing" | "playing";
  playingMessageId: string | null;
}

UIMessage 只保留持久化文本和 Turn 元数据,不保存音频 buffer。

10. C7:Agent delta 与 TTS 切分冲突

10.1 冲突原因

逐 token 送 TTS:

  • 音频碎片化。
  • 韵律差。
  • 请求数量过高。

等完整回复再送:

  • 首响延迟过高。
  • 不符合实时交互。

10.2 兼容方案

服务端新增 SentenceSegmenter

  • 遇到 。!?.!?\n 提交。
  • 中文累计 18~40 字允许软切分。
  • 代码块、表格和工具轨迹不朗读。
  • Markdown 链接只读 label。
  • turn_end flush 剩余文本。

必须在服务端执行,原因:

  • 所有客户端行为一致。
  • 可与 generation cancel 同步。
  • 不依赖浏览器重渲染。
  • 可以直接驱动神经 TTS。

当前浏览器分句逻辑只服务 browser Provider。

11. C8:Gateway 与 TTS 模型资源冲突

11.1 冲突原因

CosyVoice/Qwen3-TTS 与 Gateway 同进程会:

  • 与 ASR、Kuzu、Agent 争抢内存。
  • 模型加载阻塞启动。
  • 推理阻塞事件循环的风险更高。
  • TTS 崩溃导致聊天服务退出。
  • 依赖版本互相约束。

11.2 兼容方案

独立服务:

nanobot gateway             Python 3.14 / current venv
TTS inference service       Python 3.10 or 3.11 / isolated venv

本地端口示例:

127.0.0.1:9880

Gateway 只持有:

  • health client。
  • capability cache。
  • voice/model cache。
  • synthesis stream client。
  • cancellation handle。

11.3 进程故障

TTS 服务退出时:

  • Gateway 保持运行。
  • 文本回复继续。
  • 当前 generation 降级或静默。
  • WebUI 显示 TTS unavailable。

12. C9:Python 版本与依赖冲突

当前 nanobot 使用 Python 3.14。神经 TTS 项目常固定:

  • Python 3.10/3.11。
  • 特定 torch/torchaudio。
  • CUDA 或 MPS 版本。
  • 特定 protobuf、numpy 或 transformers。

不得把完整 TTS 依赖直接加入当前 .venv

推荐:

nanobot/.venv                 主程序
services/tts/.venv            TTS 服务

服务间只通过 HTTP/WebSocket 协议耦合。

13. C10:历史水合与自动播放冲突

13.1 已有问题

进入页面时:

初始 messages=[]
  -> useSessionHistory 异步返回历史
  -> messages 突然增加

如果只比较 messages 差异,会把历史误判为新回复并自动播放。

13.2 当前已实现保护

useVoicePlayback 接收:

  • 当前可见 messages。
  • 服务端 historicalMessages。

历史记录先写入 spoken offset 基线,不进入自动播放。

13.3 神经 TTS 必须保持相同契约

Realtime TTS 只能由实时事件触发:

assistant.delta with current generation_id

禁止从 React 历史消息列表反推服务端 TTS。

手动按钮可以读取任意历史消息并发起新的独立 TTS request。

14. C11:多会话与音频资源隔离冲突

当前一个 WebSocket 客户端可以 attach 多个 chat。实时音频必须避免:

  • Chat A 的声音在切换到 Chat B 后继续播放。
  • Chat A 的 VAD final 被提交到 Chat B。
  • 两个会话共享一个 generation ID。

映射关系:

client_id
  -> realtime_session_id
      -> chat_id
      -> active_utterance_id
      -> active_turn_id
      -> active_generation_id

规则:

  • 一个浏览器 Tab 同时最多一个 active realtime session。
  • 切换 chat 先停止采集、ASR 和 TTS。
  • realtime session 不跟随普通 chat attach 自动创建。
  • 必须由用户主动开启。

15. C12:实时状态与现有对话界面冲突

15.1 不能替换的现有控件

必须保留:

  • 文本输入框。
  • 发送按钮。
  • 图片附件。
  • 工作区选择。
  • 模型徽标。
  • /stop
  • 手动麦克风。
  • 回复操作栏。

15.2 新增 UI

建议最小增量:

  • 麦克风旁增加实时模式 Toggle。
  • 输入框内显示 partial transcript。
  • 输入框上方显示一条紧凑状态:
正在听...
正在识别...
正在思考...
正在播报...
已中断
  • Voice 设置新增 Provider、模型、健康和试听。

15.3 不允许的 UI 行为

  • 不创建独立语音页面。
  • 不遮盖对话历史。
  • 不把实时状态做成大型卡片。
  • 不隐藏原发送按钮。
  • 不让音频波形改变输入框尺寸。
  • 不因 TTS 服务离线禁用文字聊天。

16. 兼容目标架构

                             ┌─ Legacy text message ──> AgentLoop
Browser ── Chat WebSocket ───┤
                             └─ Existing transcribe_audio

Browser ─ Realtime WebSocket ─> RealtimeSessionManager
                                  │
                                  ├─ AudioBuffer
                                  ├─ VAD
                                  ├─ StreamingASRSession
                                  ├─ TurnManager
                                  └─ generation cancellation
                                           │ final transcript
                                           ▼
                                       AgentLoop
                                           │ delta
                                           ▼
                                  SentenceSegmenter
                                           │
                                           ▼
                                      TTSClient
                                           │
                ┌──────────────────────────┴────────────────────┐
                ▼                                               ▼
        CosyVoice/Qwen service                         Browser fallback
                │ PCM/Opus                                      │ text
                ▼                                               ▼
        AudioContext player                            SpeechSynthesis

17. Feature Flag

新增:

{
  "realtimeVoice": {
    "enabled": false,
    "transport": "websocket",
    "autoCommitSilenceMs": 600,
    "partialIntervalMs": 400,
    "bargeIn": true
  },
  "tts": {
    "enabled": true,
    "provider": "browser",
    "fallbackProvider": "browser"
  }
}

默认:

realtimeVoice.enabled = false
tts.provider = browser

这保证升级部署后现有界面行为不变。

Runtime capabilities 增加:

{
  "realtime_voice": false,
  "streaming_asr": false,
  "streaming_tts": false,
  "tts_provider_switching": false
}

WebUI 只在 capability 为 true 时展示对应入口。

18. 协议兼容

18.1 现有协议保持不变

以下事件和 envelope 不修改语义:

new_chat
attach
set_workspace_scope
message
transcribe_audio
delta
stream_end
turn_end
goal_status
goal_state
session_updated

18.2 新实时协议使用版本号

{
  "type": "realtime.start",
  "protocol_version": 1
}

服务端不支持时返回:

{
  "event": "realtime.error",
  "code": "unsupported_protocol"
}

WebUI 收到后自动退出实时模式,不影响 chat WebSocket。

18.3 音频帧

第一阶段独立 realtime WebSocket 可先使用:

JSON header + base64 PCM

用于验证状态机。之后切换 binary:

byte 0       protocol version
byte 1       frame type
byte 2..17   session UUID
byte 18..25  sequence uint64
remaining    PCM payload

不要在状态机和协议尚未稳定前同时引入 binary。

19. TTS Provider 切换事务

模型切换必须是事务化的:

1. 用户选择 Provider/模型
2. WebUI 请求 health/capabilities
3. 服务端验证模型存在
4. 服务端执行 warmup
5. 返回 ready
6. 保存配置
7. 停止旧 generation
8. 新请求使用新 Provider

失败时:

  • 不写配置。
  • 当前 Provider 继续工作。
  • 当前文字对话不受影响。

切换中:

  • 禁止同时发起两次切换。
  • 不迁移正在播放的句子。
  • 可以继续发送文字消息。

20. 对当前对话界面的影响

20.1 默认关闭实时模式

界面影响:

无行为变化

只有 Voice 设置会多出尚未启用或不可用的能力状态。

20.2 开启神经 TTS、关闭实时输入

界面影响:

  • 回复朗读按钮不变。
  • 底层由 browser TTS 改为音频流。
  • 播放按钮状态仍为播放/停止。
  • 设置中增加模型、服务和音色。
  • 文字与图片聊天不变。

20.3 开启实时语音

界面新增:

  • listening 状态。
  • partial transcript。
  • 实时模式开关。
  • speaking/interrupted 状态。

不改变:

  • 消息气泡结构。
  • 会话列表。
  • Memory/Knowledge/Token 页面。
  • 工具轨迹和 Reasoning 展示。
  • 手动输入方式。

20.4 风险最高的共享组件

文件 风险 控制措施
nanobot-client.ts 实时连接独立 Client
useNanobotStream.ts 不处理音频,仅补 generation 元数据
ThreadShell.tsx realtime hook 独立注入
ThreadComposer.tsx feature flag 控制新增状态
MessageBubble.tsx 保持统一 play/stop callback
SettingsView.tsx Provider 表单独立于 ASR 表单

21. 推荐迁移顺序

Stage A:不碰现有聊天协议

  1. 建立 TTS Provider 接口。
  2. 建立独立神经 TTS 服务。
  3. 只替换回复手动播放。
  4. browser TTS 保持默认和 fallback。

退出条件:

  • TTS 服务健康、取消和超时可用。
  • 文字聊天零回归。

Stage B:流式 TTS

  1. 增加服务端 SentenceSegmenter。
  2. 增加 PCM 播放器。
  3. 增加 generation fence。
  4. 新回复可边生成边播报。

退出条件:

  • 无重复、漏读和乱序。
  • 中断后旧 chunk 不恢复。

Stage C:独立实时输入通道

  1. 新建 /realtime
  2. 新建 AudioWorklet。
  3. 新建 AudioBuffer 和 VAD。
  4. 先只输出 final transcript。

退出条件:

  • 自动开始、结束识别稳定。
  • 旧手动录音仍可用。

Stage D:Streaming ASR

  1. rolling faster-whisper。
  2. partial transcript。
  3. 稳定前缀与 final 修正。

退出条件:

  • partial 不进入 Agent。
  • final 只提交一次。

Stage E:Barge-in

  1. TurnManager。
  2. turn.interrupt
  3. Agent/TTS/ASR 全链取消。
  4. generation 过滤。

退出条件:

  • 插话 200 ms 内停止。
  • 多会话无串音。

Stage F:WebRTC

只在 WebSocket PCM 路径稳定后实施。

22. 回退策略

22.1 配置回退

{
  "realtimeVoice": {
    "enabled": false
  },
  "tts": {
    "provider": "browser"
  }
}

22.2 运行时回退

Realtime WS 失败
  -> 手动 MediaRecorder

Streaming ASR 失败
  -> 完整文件 faster-whisper

Neural TTS 失败
  -> browser SpeechSynthesis

Browser TTS 失败
  -> text only

22.3 Git 回退

当前稳定封存点:

architecture-snapshot-20260821
406c08a029a50564fa1c3fff702272f9047b69cf

实时语音每个 Stage 应创建独立提交和标签,不应在一个提交中完成全部替换。

建议标签:

voice-stage-a-tts-provider
voice-stage-b-streaming-tts
voice-stage-c-realtime-input
voice-stage-d-streaming-asr
voice-stage-e-barge-in

23. 测试门禁

每个 Stage 必须通过:

23.1 现有功能

  • 文字发送。
  • 图片发送。
  • 新会话。
  • 会话切换。
  • 历史加载。
  • /stop
  • 工具调用。
  • Goal。
  • Memory/Knowledge/Token 页面。

23.2 语音兼容

  • 手动录音。
  • 完整文件 ASR。
  • 新消息自动朗读。
  • 历史消息不自动朗读。
  • 手动播放历史回复。
  • 播放/停止按钮。
  • 语速和音色。
  • browser fallback。

23.3 实时专项

  • partial/final 顺序。
  • final 只提交一次。
  • generation 过滤。
  • 插话中断。
  • 多 chat 隔离。
  • 重连。
  • TTS 首包超时。
  • 音频 underrun。

23.4 性能

指标 目标
ASR 首个 partial 300~500 ms
停顿到 final < 800 ms
Agent 首 token < 500 ms,快脑路径
TTS 首音频 < 800 ms
端到端首响 800~1500 ms
插话停止 < 200 ms
连续运行 30 分钟无泄漏

24. 监控与诊断

新增事件只记录元数据,不记录原始 PCM:

realtime_session_started
realtime_session_stopped
audio_chunk_received
audio_chunk_dropped
vad_speech_started
vad_speech_stopped
asr_partial_emitted
asr_final_emitted
turn_committed
turn_interrupted
tts_request_started
tts_first_chunk
tts_request_completed
tts_request_cancelled
audio_playback_underrun

关联字段:

chat_id
client_id
realtime_session_id
utterance_id
turn_id
generation_id
sequence
duration_ms
latency_ms
provider
model

25. 最终决策

25.1 可以直接复用

  • AgentLoop。
  • 当前文本 WebSocket。
  • turn_idstream_id
  • /stop 底层取消能力。
  • faster-whisper 模型。
  • 当前消息 UI。
  • 当前浏览器 TTS fallback。
  • 当前语音偏好和回复播放按钮。

25.2 必须新增

  • 独立 realtime WebSocket。
  • RealtimeSessionManager。
  • AudioWorklet PCM 采集。
  • AudioBuffer 与服务端 VAD。
  • StreamingASRSession。
  • generation ID。
  • SentenceSegmenter。
  • TTS Provider 和独立推理服务。
  • AudioContext 流式播放器。

25.3 不应修改

  • 记忆系统的数据模型。
  • 知识库的数据模型。
  • 普通消息持久化格式。
  • 工具调用协议。
  • Memory、Knowledge、Token UI。

26. 评审结论

该计划可实施,但必须满足以下前置修正:

  1. Realtime 音频使用独立通道,不直接侵入当前 JSON Chat WebSocket。
  2. Streaming ASR 新增接口,不替换完整文件 ASR。
  3. 神经 TTS 与 browser TTS 同一时刻只能启用一个。
  4. Provider/模型由服务端配置,设备偏好由 localStorage 保存。
  5. 所有实时异步结果使用 generation ID 隔离。
  6. 实时模式默认关闭,并由 runtime capability 控制 UI。
  7. TTS 模型使用独立 Python 环境和进程。
  8. 每个迁移 Stage 都保留旧路径和独立回退点。

按这些约束实施,当前对话界面的主体功能不会受到结构性破坏;界面变化仅限可选的
语音状态、临时字幕和 Voice 设置扩展。


附录 C:Agent 链路观测方案 v1.0

来源:backend/docs/agent-graph-observability-v1.0.md

记忆观测中心 Agent 链路观测方案 v1.0

版本:v1.0
日期:2026-08-18
状态:设计方案,尚未开始实现
边界约束:在用户明确说“开始实现”前,不修改代码、不启动服务、不删除接口。实现时优先工作在 Memory UI / 观测层,避免改动 nanobot 核心业务逻辑。


1. 背景与目标

当前 nanobot 已完成三层 LangGraph 化改造:

层级 位置 说明
A TurnGraph nanobot/nanobot/agent/loop.py 主流程 9 态:RESTORE / COMPACT / COMMAND / TRIAGE / BUILD / RUN / SAVE / RESPOND / DONE
B MemoryFlow Graph nanobot/nanobot/agent/memory_flow.py 记忆管线编排,现阶段主要用于长期/中期检索及 Dream/consolidation 管线
C RunSubgraph nanobot/nanobot/agent/runner.py 内层工具循环 6 节点:guard / llm_call / tool_execution / finalize_response / increment / max_iterations_finalize

用户目标:

  1. 在记忆观测中心内看到当前 agent 的节点链路。
  2. 当 LangGraph 结构发生变化时,UI 能自动同步。
  3. 节点开始/结束、条件路径、耗时信息尽量复用 LangGraph 原生能力。
  4. 删除或隐藏之前自实现的重复接口和页面入口,避免多套观测 UI 并存。
  5. 不依赖 LangGraph-GUI、LangGraph Studio 或 LangSmith 登录作为主观测入口。

2. 设计原则

2.1 LangGraph 原生优先

拓扑和耗时的主数据源必须优先来自 LangGraph:

  • 图结构:从 compiled graph / graph introspection 读取。
  • 运行事件:优先使用 astream_events(..., version="v2") 或等价 LangGraph event stream。
  • 节点耗时:由 LangGraph 节点 start/end 事件归一化计算。
  • 条件路径:根据 event stream 中实际触发节点序列和 state 输出推导。

nanobot 自己的 MemoryEventSink 只作为持久化投影和业务事件补充,不再作为图耗时的首要来源。

2.2 观测层与业务层分离

新增逻辑应收敛到:

  • nanobot/nanobot/webui/*
  • nanobot/webui/src/memory-ui.tsx
  • 必要时新增独立观测 adapter 文件

尽量不改:

  • LLM provider 业务逻辑
  • tool 执行逻辑
  • memory write / Dream write 语义
  • session save 语义

如必须接入 loop.py / runner.py / memory_flow.py,只允许做最小观测接线,不能改变执行结果。

2.3 统一入口

记忆观测中心成为唯一主观测入口:

  • 不再依赖外置 LangGraph-GUI。
  • 不再维护手写 nanobot-agent.json
  • 不再长期保留多个功能重复的耗时页面。

3. 目标用户体验

Memory UI 新增一个页面:Agent 链路

页面布局:

顶部:自动刷新开关 / 当前拓扑版本 / 最近更新时间 / 图引擎开关状态

左侧:
  - Graph 选择:TurnGraph / RunSubgraph / MemoryFlow
  - Turn 列表:最近 N 个 turn

中间:
  - 当前图拓扑
  - 选中 turn 后高亮实际执行路径
  - 节点显示耗时、状态、错误标识

右侧:
  - 节点详情
  - 最近运行样本
  - P50/P95/最大耗时
  - 关联 MemoryEvent

最小可用版本:

  1. 展示三张图的静态拓扑。
  2. 自动刷新拓扑版本。
  3. 选择最近 turn 后高亮实际走过的节点。
  4. 显示每个节点本次耗时。

增强版本:

  1. 聚合耗时统计。
  2. 条件边实际命中率。
  3. 错误节点筛选。
  4. run/tree 级别展开:TurnGraph 的 RUN 节点可展开 RunSubgraph,BUILD 节点可展开 MemoryFlow。

4. 数据流设计

4.1 拓扑数据流

AgentLoop._compile_turn_graph()
AgentRunner._compile_run_graph()
LangGraphMemoryRunner._compile_plan()
        ↓
CompiledStateGraph / graph introspection
        ↓
AgentGraphTopologyExporter
        ↓
/api/memory/agent-graph/topology
        ↓
Memory UI AgentGraphPage

拓扑版本:

version = hash(
  graph node list +
  graph edge list +
  source file mtimes +
  graph engine flags
)

前端通过 /api/memory/agent-graph/version 轮询,版本变化后重新拉取 topology。

4.2 运行事件数据流

compiled_graph.astream_events(..., version="v2")
        ↓
LangGraphEventNormalizer
        ↓
node_started / node_completed / node_failed / edge_routed / graph_completed
        ↓
MemoryEventSink.emit(...)
        ↓
memory_observation.db
        ↓
/api/memory/agent-graph/turns/{turn_id}
        ↓
Memory UI overlay

关键点:

  • LangGraph event stream 是耗时计算主来源。
  • MemoryEventSink 只存 normalized projection。
  • 事件中不保存完整 prompt / tool payload,避免 memory_observation.db 膨胀。
  • 大字段使用摘要、hash 或引用 cursor。

5. 后端接口设计

新增 API 前缀:

/api/memory/agent-graph/*

5.1 GET /api/memory/agent-graph/version

返回:

{
  "enabled": true,
  "version": "sha256:...",
  "generated_at": "2026-08-18T00:00:00Z",
  "engines": {
    "memory": "langgraph",
    "turn": "langgraph",
    "run": "langgraph"
  }
}

用途:

  • 前端低成本轮询。
  • 拓扑变化时触发自动刷新。

5.2 GET /api/memory/agent-graph/topology

返回:

{
  "enabled": true,
  "version": "sha256:...",
  "graphs": [
    {
      "id": "turn_graph",
      "label": "TurnGraph",
      "entry": "restore",
      "nodes": [
        {
          "id": "restore",
          "label": "RESTORE",
          "kind": "state",
          "source": "AgentLoop._state_restore"
        }
      ],
      "edges": [
        {
          "source": "restore",
          "target": "compact",
          "kind": "normal"
        },
        {
          "source": "command",
          "target": "triage",
          "kind": "conditional",
          "condition": "dispatch"
        }
      ]
    }
  ]
}

5.3 GET /api/memory/agent-graph/turns

查询最近 turn 的图执行摘要。

参数:

  • limit
  • before
  • session_key
  • graph_id

返回:

{
  "enabled": true,
  "turns": [
    {
      "turn_id": "...",
      "session_key": "...",
      "timestamp": "...",
      "graphs": ["turn_graph", "run_subgraph"],
      "total_duration_ms": 1234.5,
      "stop_reason": "completed"
    }
  ],
  "next_before": 123
}

5.4 GET /api/memory/agent-graph/turns/{turn_id}

返回某个 turn 的实际执行路径。

{
  "enabled": true,
  "turn_id": "...",
  "runs": [
    {
      "graph_id": "turn_graph",
      "run_id": "...",
      "nodes": [
        {
          "node": "restore",
          "status": "completed",
          "started_at": "...",
          "ended_at": "...",
          "duration_ms": 10.5,
          "event": "ok"
        }
      ],
      "edges_taken": [
        {
          "source": "command",
          "target": "triage",
          "condition": "dispatch"
        }
      ]
    }
  ],
  "events": []
}

5.5 GET /api/memory/agent-graph/stats

返回聚合耗时。

维度:

  • node
  • graph
  • session
  • stop_reason

6. 事件模型

新增 normalized 事件类型建议:

event_type 来源 用途
langgraph_node_started LangGraph event stream 节点开始
langgraph_node_completed LangGraph event stream 节点完成,含 duration
langgraph_node_failed LangGraph event stream 节点失败
langgraph_graph_completed LangGraph event stream 图运行完成

事件 details 示例:

{
  "graph_id": "run_subgraph",
  "run_id": "...",
  "parent_run_id": "...",
  "node": "llm_call",
  "iteration": 1,
  "duration_ms": 812.4,
  "status": "completed",
  "input_summary": {
    "message_count": 12
  },
  "output_summary": {
    "route": "tools",
    "tool_call_count": 1
  }
}

注意:

  • 不把完整 prompt 写入这些事件。
  • 不把完整 tool result 写入这些事件。
  • 需要与现有 llm_prompt_submitted 的膨胀问题隔离。

7. 与现有页面/接口的关系

7.1 先保留的现有能力

短期保留:

  • /api/memory/events
  • /api/memory/categories/*
  • /api/memory/graph/*
  • /api/memory/files/*
  • /api/memory/medium-term
  • /api/memory/system/queues

这些不与新 Agent 链路页直接重复。

7.2 待合并的页面

新页面覆盖后,可从侧边栏移除:

  • 流程监控
  • 单 Turn 耗时
  • 链路明细
  • 按状态统计耗时
  • 按会话统计耗时
  • 按结束原因统计耗时

对应前端组件:

  • FlowMonitorPage
  • DurationTurnsPage
  • DurationLinksPage
  • DurationAggregatePage

7.3 待删除或降级的接口

新接口稳定后,以下接口可删除或转为兼容别名:

  • /api/memory/durations/turns
  • /api/memory/durations/turns/{turn_id}
  • /api/memory/durations/links
  • /api/memory/durations/stages
  • /api/memory/durations/sessions
  • /api/memory/durations/stop-reasons

删除条件:

  1. 前端没有引用。
  2. 测试没有引用。
  3. 用户确认旧入口可以下线。
  4. 至少一次完整回归通过。

8. 需要调整的现有实现

8.1 RunSubgraph 自定义耗时

当前 AgentRunner._make_run_node 已用 perf_counter 自算 run trace。

处理策略:

  • 短期保留为 fallback/debug。
  • 新观测页不以它为主数据源。
  • 当 LangGraph event stream 覆盖 RunSubgraph 后,可删除或仅在 legacy/non-LangGraph 模式下启用。

8.2 AgentLoop 自定义 trace

当前 agent_loop_duration.details.trace 是外层 TurnGraph 耗时来源。

处理策略:

  • 短期保留兼容旧 durations 页面。
  • 新页面优先读取 LangGraph normalized events。
  • 如果 legacy 引擎仍可用,则该 trace 可作为 legacy fallback。

8.3 MemoryFlow 事件

当前 memory_node_started/completed/failed 已经比较完整。

处理策略:

  • 对 LangGraph 模式增加 graph_id/run_id/parent_run_id 等字段。
  • 原 event_type 保持兼容。
  • 新页面可把它们归一化为 MemoryFlow 节点运行事件。

9. 实施阶段

Phase A:拓扑只读 API

目标:

  • 新增 topology/version API。
  • 不接入运行事件。
  • 前端能看到当前三张图结构。

验收:

  • 修改图节点/边后 version 变化。
  • 前端自动刷新。
  • 不影响现有 Memory UI 页面。

Phase B:LangGraph event stream 采集

目标:

  • 对 TurnGraph / RunSubgraph / MemoryFlow 运行过程接入 LangGraph events。
  • 归一化为 langgraph_* 观测事件。
  • 不改变业务输出。

验收:

  • 能看到节点 start/end。
  • 能计算 duration_ms。
  • 与现有 agent_loop_duration 对比偏差可解释。

Phase C:AgentGraphPage

目标:

  • 新增统一页面。
  • 展示 topology + turn overlay。
  • 支持节点详情和耗时聚合。

验收:

  • 可替代 流程监控单 Turn 耗时链路明细
  • 页面上不再需要跳多个重复入口。

Phase D:旧入口移除

目标:

  • 从侧边栏删除重复页面。
  • 删除不再使用的 durations 接口或改为兼容别名。

验收:

  • 前端无引用。
  • 后端测试通过。
  • 用户确认移除范围。

10. 测试方案

10.1 后端测试

  • topology API 返回三张图。
  • version 在图结构变化时变化。
  • LangGraph event stream 能生成 node started/completed。
  • event details 不包含完整 prompt/tool result 大字段。
  • legacy fallback 不破坏现有 durations 测试。

10.2 前端测试

  • AgentGraphPage 能加载 topology。
  • version 变化后自动刷新。
  • 选择 turn 后高亮实际路径。
  • 节点详情展示 duration/error/event。
  • 删除旧入口后侧边栏无死链。

10.3 回归测试

至少运行:

pytest tests/agent/test_run_graph.py
pytest tests/agent/test_turn_graph.py
pytest tests/agent/test_shadow.py
pytest tests/agent/test_memory_observation_medium_term.py

前端:

npm run check

11. 风险与决策点

风险 影响 应对
LangGraph event schema 随版本变化 事件采集断裂 建立 adapter 层和契约测试
event stream 引入额外开销 turn 延迟增加 只保存摘要,采样或开关控制
MemoryEventSink 数据膨胀 SQLite 继续膨胀 禁止完整 prompt/tool result 进入 langgraph_* 事件
legacy / langgraph 双路径差异 UI 显示不一致 legacy 用现有 trace fallback,langgraph 用原生 events
过早删除旧接口 影响已有页面/测试 新页面覆盖后再删,先做引用扫描

12. 待用户确认

实现前需要确认:

  1. 新页面命名是否使用 Agent 链路
  2. 自动刷新周期是否默认 2 秒。
  3. 是否允许新增 langgraph_* 事件写入 memory_observation.db
  4. 是否接受旧 durations 页面先隐藏、后删除的两阶段策略。
  5. 是否保留 studio_graphs.py / langgraph.json 作为官方 Studio 备用入口,还是在本方案实现后一起移除。

13. 当前结论

推荐方案:

LangGraph compiled graph
  -> topology introspection
  -> LangGraph astream_events runtime trace
  -> normalized MemoryEvent projection
  -> Memory UI AgentGraphPage

这条路径满足:

  • 本地可用。
  • 不依赖 LangGraph-GUI。
  • 不依赖 LangGraph Studio 登录。
  • 结构变化自动同步。
  • 耗时优先复用 LangGraph 原生事件。
  • 记忆观测中心成为统一观测入口。

附录 D:项目优化记录

来源:.trae/documents/项目优化记录.md

项目优化记录

整理日期:2026-08-24
项目目录:/Users/bytedance/project/测试
说明:本文档记录项目中所有已实施的架构优化、性能改进和功能增强,按模块分类归档。


一、架构边界与原则

1.1 三层代码边界

层级 目录 职责 修改原则
监控/后端系统 backend/ REST 适配、双脑路由、WebUI 网关 ✅ 可自由修改
多模态扩展层 nanobot-assistant/ 语音、截屏、向量记忆、本地 ASR ✅ 可自由修改
Agent 核心 nanobot/ Agent Loop、记忆、工具、Provider ❌ 严禁直接修改(通过插件/entry point 扩展)

1.2 核心架构原则

  • 零改动 nanobot:所有定制化能力通过 backend/ 适配层或 nanobot-assistant/ 插件扩展实现
  • 全链路降级:任何优化环节失败都必须回退到基线行为,保证"不劣化"
  • 单一职责:每个模块只负责一个领域,通过清晰接口交互
  • 本地优先:ASR、LLM、TTS、向量数据库等优先本地部署,降低延迟和隐私风险

二、记忆系统优化

2.1 三层记忆架构

优化前:nanobot 仅有短期会话记忆和简单的文件型长期记忆。

优化后

层级 存储 能力 关键文件
短期记忆 Session JSONL 当前对话上下文 nanobot 原生
中期记忆 SQLite 语义检索、scope boost、importance、命中次数、TTL、异步总结 [medium_term_memory.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/medium_term_memory.py)
长期记忆 Kuzu 图数据库 实体、事实、关系、来源、批次、别名、图谱检索 [memory_graph_store.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_graph_store.py)、[memory_kuzu_backend.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_kuzu_backend.py)

2.2 Dream 记忆流转流水线

将 Dream 从简单的文件总结扩展为完整的长期记忆沉淀流水线:

  1. 候选提取:从中期记忆和历史记录中提取候选内容
  2. LLM 结构化抽取:输出实体、事实、关系等结构化结果
  3. 规则过滤:规范化和过滤低质量内容
  4. 图谱批次写入:批量写入长期 Kuzu 图谱

关键文件:[memory_flow.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_flow.py)、[memory_nodes.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_nodes.py)、[memory_llm.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_llm.py)、[memory_rules.py](file:///Users/bytedance/project/测试/nanobot/nanobot/agent/memory_rules.py)

2.3 图谱语义去重

新增 /memory-dedupe [--dry-run|--apply] 命令:

  • LLM 生成去重计划,存储层严格按 ID 执行
  • 支持四类操作:重复实体合并、重复事实合并、重复关系合并、误建模实体降级为事实属性
  • 支持预览、执行、结果统计、批次记录和观测事件
  • 待执行计划内存缓存,页面刷新后可继续确认

关键文件:[builtin.py](file:///Users/bytedance/project/测试/nanobot/nanobot/command/builtin.py)、memory_graph_dedupe.md Prompt 模板

2.4 Kuzu 图谱读写增强

  • 事实显示统一为 实体 -属性-> 值,关系显示统一为 主语 -谓词-> 宾语
  • 图谱边增加语义标签,不再只显示 HAS_FACTRELATES_TO
  • 读取时折叠已声明别名实体,避免 UI 重复节点
  • 查询同时覆盖实体名、事实属性、事实值、关系谓词和目标实体
  • 节点详情增加入边、出边、事实属性和值
  • Dream 写入前注入现有属性和谓词词表,减少同义字段膨胀
  • 精确重复事实和关系可确定性识别,不完全依赖 LLM
  • 合并时迁移来源边、批次边、所有者边和实体关系,并清理重复节点

2.5 中期记忆时效治理

  • 对"近期安排、后续安排、接下来安排"等模糊文本进行重写
  • 为日程、计划、待办、截止时间等时效内容默认设置 1-7 天 TTL
  • 总结 Prompt 中注入当前对话时间锚点
  • 上下文输出增加 recorded_atexpires_at
  • 增加 time_sensitive evidence guard
  • 增加专用中期记忆总结模型的配置和 Provider 构建逻辑

三、知识库系统优化

3.1 通用插件架构

新增通用插件注册机制,支持以下扩展点:

  • Server 和 Agent 生命周期
  • System Prompt 上下文段
  • Slash Command
  • Cron Job
  • Memory UI Web Route
  • 主 WebUI 导航项
  • Bootstrap Payload

支持内建插件注册及 nanobot.plugins entry point 外部发现,单个插件异常被隔离。

关键文件:[plugin.py](file:///Users/bytedance/project/测试/nanobot/nanobot/plugin.py)

3.2 知识库插件化

将知识库从核心逻辑中解耦为独立插件:

  • 知识库按插件方式注册 /kb-dream、定时任务、Web 路由和 UI 入口
  • 增加会话结束后的即时知识捕获 Hook
  • 只有具备可验证 source_urls 的内容才允许自动沉淀
  • 支持 LLM 自动推荐稳定分类
  • 支持"智能整理目录"和"重建目录结构"

关键文件:[knowledge/plugin.py](file:///Users/bytedance/project/测试/nanobot/nanobot/knowledge/plugin.py)

3.3 知识库数据模型增强

  • Markdown 正文保持 source of truth
  • 物理 Markdown 文件改为扁平存储,不再按年月目录组织
  • 新增 KnowledgeDirectoryIndex,使用 SQLite 保存虚拟目录映射
  • 一篇文章可出现在多个虚拟主题目录,不复制物理文件
  • 支持 YAML/TOML front matter、普通 Markdown、MDX 和多种元数据字段别名
  • 对无 front matter 文件自动提取标题、摘要、来源链接,生成稳定文章 ID
  • 同一会话可合并文章,跨会话可按标题和内容相似度匹配
  • KB Pipeline 使用内容指纹游标,降低历史压缩后重复沉淀风险

3.4 知识库 MCP Server

新增独立 MCP Server,支持外部 MCP 客户端访问:

  • 命令入口:nanobot-kb-mcp
  • 工具:kb_searchkb_readkb_writekb_listkb_graphkb_rebuild
  • 使用 stdio transport,可供 Claude Desktop、OpenClaw 等调用

关键文件:[knowledge/mcp_server.py](file:///Users/bytedance/project/测试/nanobot/nanobot/knowledge/mcp_server.py)


四、双脑分诊路由优化(backend 层)

4.1 架构概述

优化前/chat 一条道走到黑,无论简单/复杂请求都跑完整 nanobot agent loop + 35B 大模型,简单请求被拖到几十秒级。

优化后:在 backend 层引入"快脑/慢脑"双脑分诊路由,零改动 nanobot

用户请求 → 规则前置粗筛(URL/代码/文件/工具词 → COMPLEX)
         → 4B 分诊(think=false,~400ms)
            ├─ SIMPLE → 4B 快脑直接答(无工具/记忆,~1.2s)
            └─ COMPLEX → 35B 慢脑完整 agent loop

4.2 关键优化点

优化项 说明
4B 一脑两用 实测 4B 分诊准确率 100%,取消专职分诊 1.5B 层,减少模型切换
规则前置粗筛 URL、代码块、文件路径、工具关键词等直接判 COMPLEX,省一次 LLM 调用
thinking 模式根治 必须走 Ollama 原生 /api/chat + think:false,否则回答藏在 reasoning 字段导致空响应
Session 一致性 快脑答完手动写回同一 session_key,快慢脑共享历史
全链路降级 分诊失败/超时 → COMPLEX;快脑失败/超时 → 慢脑重答;启动探测失败 → 自动禁用
brain 字段回传 ChatResponse 增加 brain 字段(fast/slow),WebSocket 通过 tool_hint 气泡显示,便于观测

4.3 实测性能数据

环境:Apple M4 Pro / 48GB / Ollama 0.32.6,全部 think:false,已预热:

模型 首字延迟 TTFT 总耗时 生成速度 自然度
1.5B 124ms 319ms 137 c/s 中,偏客服腔
4B 358ms 1141ms 51 c/s 高:有情绪/比喻/网感
9.7B 449ms 2527ms 43 c/s 高但啰嗦、爱反问
35B(慢脑基线) - 几十秒级 - 全能但慢

4.4 Ollama 部署调优

防止多模型切换抖动:

export OLLAMA_MAX_LOADED_MODELS=4   # 多模型同时常驻
export OLLAMA_KEEP_ALIVE=-1         # 常驻不卸载
export OLLAMA_NUM_PARALLEL=2        # 单模型并发

内存账:4B(3.4GB) + 35B(22GB) + 可选 1.5B(1GB) + 9.7B(6.6GB) ≈ 33GB < 48GB。

关键文件:

  • [dual_brain.py](file:///Users/bytedance/project/测试/backend/dual_brain.py) - 双脑核心路由
  • [main.py](file:///Users/bytedance/project/测试/backend/main.py) - FastAPI 主服务
  • [dual-brain-design-v1.1.md](file:///Users/bytedance/project/测试/backend/docs/dual-brain-design-v1.1.md) - 设计文档

五、WebUI 网关优化

5.1 复用 nanobot SPA

在 backend 层直接挂载 nanobot 已构建的 React SPA,无需重新开发前端:

  • 服务 nanobot web/dist 静态资源
  • 重新实现 nanobot WebUI 的 WebSocket wire 协议子集
  • 所有聊天消息经过双脑路由器

5.2 协议适配

端点 功能
GET /webui/bootstrap 握手,返回 token 和 WS 地址
WS /ws 聊天:ready/attached/delta/stream_end/turn_end
GET /api/sessions 最小化只读 REST,防止侧边栏报错
GET /voice 独立语音聊天页面

5.3 可观测性增强

  • 通过 tool_hint 气泡显示"⚡ 快脑 4B"或"🐢 慢脑 35B"
  • 慢脑回答附带使用的工具列表
  • turn_end 帧回传 latency_msbrain 字段

关键文件:[webui_gateway.py](file:///Users/bytedance/project/测试/backend/webui_gateway.py)


六、多模态助手优化(nanobot-assistant)

6.1 耳机语音

  • MicCapture 使用 sounddevice 采集 16 kHz PCM
  • Silero VAD 自动识别语音开始和结束
  • 调用 ASR 完成语音转文字
  • DesktopVoiceChannel 将语音输入接入 MessageBus
  • Piper TTS 本地生成中文语音并播放
  • RingBuffer、播放器、TTS 引擎和 VAD 职责拆分

6.2 本地 ASR 优化

从云端 Groq 切换为本地 faster-whisper:

  • 单例懒加载,线程安全
  • 模型尺寸/设备/语言可配
  • 失败自动回退 nanobot 原管线
  • 实测:small/int8/CPU,中文准确 100%,转写 ~1.3s

关键文件:[audio/local_asr.py](file:///Users/bytedance/project/测试/nanobot-assistant/nanobot_assistant/audio/local_asr.py)

6.3 截屏视觉

  • screenshot 工具使用 mss 捕获屏幕
  • 支持主屏、全屏和选区配置
  • 截图保存到用户目录,作为视觉输入返回给多模态 LLM
  • 截图可异步写入向量记忆
  • 默认关闭截屏,降低隐私风险

6.4 多模态向量记忆

  • 使用 ChromaDB 保存文字、图片、视频帧和截图
  • 使用 CLIP 将文本和图像映射到统一向量空间
  • 提供 remembervector_recall 工具
  • 视频索引器通过 ffmpeg/OpenCV 抽帧,可提取音频转写
  • 支持四类集合:text_notes、images、video_frames、screenshots

6.5 插件注册

通过 Python entry_points 零侵入扩展 nanobot:

  • Channel:desktop_voice
  • Tools:screenshotspeakremembervector_recall
  • Skills:multimodal-memoryscreenshot-visionvoice-companion
  • CLI:setup、doctor、start-voice、index-video、recall

关键目录:[nanobot-assistant/](file:///Users/bytedance/project/测试/nanobot-assistant/)


七、观测系统优化

7.1 Memory UI

  • 结构化事件总线和 SQLite 事件存储
  • 记录检索、注入、总结、Dream、图谱写入及执行阶段信息
  • 使用 Cytoscape.js 替换手写 SVG 图谱
  • 支持拖拽、缩放、平移、自动布局、节点聚焦、邻居高亮和详情联动
  • 支持图谱扫描重复节点、预览计划、执行合并流程

7.2 Token 观测

  • 记录构建上下文、模型调用、重试、Finalize、Dream 等阶段的 Token 消耗
  • 独立 Token UI 和 Vite 构建入口

7.3 隐私收敛

  • 从部分 Token/事件详情中移除用户原文,降低敏感信息暴露
  • 精简重复事件逻辑

八、后端服务优化

8.1 FastAPI 后端

  • 使用 FastAPI 封装本地 nanobot SDK
  • lifespan 初始化 Nanobot、MemoryStore、MemoryRetriever、SkillsLoader
  • 初始化失败进入 degraded 状态,而非终止服务
  • 使用异步锁串行化同一 Bot 实例上的聊天请求
  • CORS 配置,为 Web 和 Android 客户端预留接入

8.2 API 端点

方法 路径 功能
GET /api/info 服务信息和端点列表
GET /health 配置、workspace、memory、skill、Bot 状态
POST /chat 双脑路由对话
POST /asr 本地 faster-whisper 语音转写
POST /memory/retrieve 独立记忆检索验证
GET /memory/peek MEMORY 内容预览
GET /skills 已加载 Skill 列表
GET /voice 语音聊天页面

8.3 配置化

所有关键参数支持环境变量覆盖:

  • DUAL_BRAIN_ENABLED - 双脑开关(默认 true)
  • TRIAGE_MODEL / FAST_MODEL - 分诊/快脑模型
  • OLLAMA_BASE - Ollama 地址
  • NANOBOT_CONFIG / NANOBOT_WORKSPACE - nanobot 配置和工作区
  • HOST / PORT - 监听地址

九、语音抗噪优化(P0,2026-08-24 实施)

目标:降低开放办公区同事说话声与环境音对耳机语音听写的误触发。本阶段为零新依赖调参,全部落在 nanobot-assistant/audio/

9.1 三层抗噪手段

手段 机制 作用
启动环境噪声校准 启动后前 0.5s 测量 Silero 语音概率/RMS 基底,自适应抬高阈值 持续的背景人声/空调噪声不触发
连续帧确认 需连续 3 帧(≈96ms)高于有效阈值才进入说话状态 键盘敲击、咳嗽、拍桌、翻页等脉冲噪声被拒
阈值/语长/尾音收紧 vad_threshold 0.5→0.65;min_speech_ms 250→400;speech_pad_ms 120→80;silence_ms 350→300 减少短促闲聊误触发,砍掉尾音混入的同事声

9.2 实现要点

  • [vad.py](file:///Users/bytedance/project/测试/nanobot-assistant/nanobot_assistant/audio/vad.py) 不再使用 VADIterator,改为直接调用 Silero 模型逐帧取概率(model(tensor_1x512, sr).item()),以便做平滑/确认/校准。
  • 状态机:IDLE(校准→pre_buffer 预卷→连续确认)→ SPEAKING(缓冲→静音阈值结束→尾部静音裁剪保留 speech_pad)。
  • RMS 回退路径同步加入校准与连续帧确认,行为一致。
  • 新增 force_energy_vad 开关用于测试隔离 Silero。
  • 修复 [mic_capture.py](file:///Users/bytedance/project/测试/nanobot-assistant/nanobot_assistant/audio/mic_capture.py) 最大语长强制截断只喂 100ms 静音、在 silence_ms=300 下无法真正终止的问题,改为喂足 silence_ms+60ms 静音。
  • [config.py](file:///Users/bytedance/project/测试/nanobot-assistant/nanobot_assistant/config.py) 的 MicConfig 新增 min_speech_msspeech_pad_msstart_confirm_windowsstartup_calibration_sadaptive_threshold_margin 字段。

9.3 配置项(~/.nanobot-assistant/config.yamlmic: 段)

mic:
  vad_threshold: 0.65
  silence_ms: 300
  min_speech_ms: 400
  speech_pad_ms: 80
  start_confirm_windows: 3
  startup_calibration_s: 0.5
  adaptive_threshold_margin: 0.12

9.4 边界与局限

  • P0 能压稳态/脉冲噪声和短促人声,但无法区分"你的人声"与"同事的连续人声"——两者都是真人语音。要真正挡住同事持续说话,需 P2 声纹门控(Resemblyzer)或 P3 唤醒词。
  • 已选定 DeepFilterNet3 作为 P1 实时降噪引擎(用户确认),下阶段引入。

9.5 验证

  • tests/test_vad.py:6 项全通过(含新增的脉冲拒绝、持续语音接受、校准抬阈值、最短语长丢弃)。
  • nanobot-assistant 全量测试:21 passed, 4 skipped,无回归。
  • 修复了一个既有测试缺陷:test_vad_rms_fallback_detects_speech 名称为 RMS 回退却未强制走 RMS,在装有 silero 的环境下静默走 Silero 路径且对白噪声不触发(改动前同样失败),已通过 force_energy_vad=True 使其真正测试目标路径。

十、声纹识别(SpeakerGate,2026-08-24 初版 / 2026-08-25 多说话人升级)

目标:在语音对话链路中加入独立的"声纹门控"节点,识别每段人声属于谁——只放行已录入说话人的语音并在界面显示其名字,从根本上拦截同事说话(P0 抗噪只能压环境音,无法区分人声归属)。

v2 升级(2026-08-25):从"单机主开关"升级为多命名说话人识别。可录入多人,每次监听到人声后在界面展示识别到的说话人(名字 + 颜色 + 相似度),未录入者显示最接近的候选并被拦截。详见 §10.3.1。

10.1 架构:作为独立节点计入语音 loop

语音页面 /voice/turn 的处理链路由三个独立节点串联,每个节点独立计时并回传 nodes

录音 audio/webm
   │
   ├─ Node 1: SpeakerGate(Resemblyzer 多说话人声纹余弦匹配)
   │     ├─ 无任何录入 → rejected: not_enrolled(短路,不进 ASR/LLM)
   │     ├─ 最高相似度 < 阈值 → rejected: voice_mismatch,附最接近候选(短路)
   │     └─ 匹配 → 带 speaker_id/name/score ↓
   ├─ Node 2: ASR(faster-whisper 本地转写)
   └─ Node 3: DualBrain(4B 快脑 / 35B 慢脑)→ 回复

声纹节点在 ASR/LLM 之前短路,未通过校验的人声完全不消耗转写和模型算力;通过校验的语音会把识别到的说话人名字透传到界面展示。

10.2 核心实现

文件 职责
[speaker_gate.py](file:///Users/bytedance/project/测试/nanobot-assistant/nanobot_assistant/audio/speaker_gate.py) SpeakerGate 类:Resemblyzer VoiceEncoder 懒加载单例、多说话人命名录入/最佳匹配/列表/删除、持久化与旧数据迁移;256 维 L2 归一化 embedding,点积即余弦相似度
[main.py](file:///Users/bytedance/project/测试/backend/main.py) 声纹状态 + 接口 + /voice/turn 循环节点编排(回传 speaker_name 与 top3 候选)
[voice.html](file:///Users/bytedance/project/测试/backend/voice.html) 顶栏录入按钮 + 说话人芯片面板;底栏"声纹"开关;录音走 /voice/turn;每条语音显示识别到的说话人;节点耗时可视化

10.3 后端接口

方法 路径 功能
GET /speaker/status 录入状态、启用状态、阈值、说话人列表与人数
GET /speaker/speakers 列出所有已录入的命名说话人
POST /speaker/enroll multipart:audio + name,录入一个命名说话人(可多人)
POST /speaker/verify 上传音频,返回最佳匹配说话人(id/name/score/top3 候选)
POST /speaker/enabled {enabled: bool} 开关
POST /speaker/threshold {threshold: 0~1} 调阈值
DELETE /speaker/{speaker_id} 删除单个说话人
DELETE /speaker 删除全部
POST /voice/turn 三节点串联:声纹→ASR→双脑;speaker 字段回传 speaker_id/speaker_name/score/candidatesnodes 计时

10.3.1 多说话人与界面展示

  • SpeakerGate 从"单机主"扩展为多命名说话人:每人一条 256 维 embedding,存 voiceprints/<id>.npy,索引存 voiceprints.json;设置存 speaker_settings.json
  • verify 对所有已录入说话人算余弦相似度,取最高分,回传 top3 候选;/voice/turnspeaker_name 透传到前端。
  • [voice.html](file:///Users/bytedance/project/测试/backend/voice.html) 顶栏下新增说话人面板:每个说话人是一个带颜色圆点的芯片(颜色按 id 哈希稳定分配),点 × 可删除;录入时先 prompt 询问名字。
  • 每条用户语音气泡显示识别到的说话人标签(🗣️ <名字> + 对应颜色)与相似度;未匹配的语音以警告气泡展示"检测到人声但声纹不匹配(相似度 x,最接近:某某 y)"并附 top3 候选,不进入 ASR/LLM
  • 自动迁移:旧版单文件 voiceprint.npy + voiceprint.json 首次加载时迁移为名为"机主"的说话人(legacy: true)。

10.3.2 Agent 主页悬浮挂件(2026-08-25)

把声纹录入入口从独立 /voice 页搬到了 nanobot agent 主页/ 的 React SPA),无需切换页面即可录入/管理声纹。

  • 由于不能改 nanobot 源码,采用后端网关注入:[webui_gateway.py](file:///Users/bytedance/project/测试/backend/webui_gateway.py) 在返回 SPA index.html 前,向 </body> 前注入 <script src="/speaker-widget.js" defer></script>;SPA 子路由(catch-all)同样注入。
  • 挂件 [speaker_widget.js](file:///Users/bytedance/project/测试/backend/static/speaker_widget.js)(纯原生 JS,无框架,自带 scoped 样式,z-index 极高)在主页右下角渲染一个紫色悬浮麦克风按钮:
    • 点击展开面板:已录入说话人芯片(彩色圆点 + × 删除)、"录入新说话人"按钮(弹框问名字 → 浏览器录音 → POST /speaker/enroll)、"拦截未录入陌生人"开关(/speaker/enabled)、状态提示。
    • 悬浮按钮右上角显示已录入人数徽标;录入中按钮变红脉冲,再次点击停止。
    • 自动适配深/浅色主题;点击面板外自动关闭。
  • 挂件与 SPA 完全解耦(不碰 React 树),只调用同源 /speaker/* 接口;这些接口已在 [main.py](file:///Users/bytedance/project/测试/backend/main.py) 注册,注册顺序早于网关 catch-all,不会被劫持。
  • /voice 独立页保留完整的"声纹→ASR→双脑"语音回合与说话人展示;主页挂件负责录入/管理,两者共用同一份 ~/.nanobot-assistant/voiceprints/ 数据。

10.3.3 主页语音输入(说话直接显示识别结果,2026-08-25)

把语音输入集成进 agent 主页:点挂件麦克风说话 → 识别文字直接填入聊天框并发送,AI 回复在主页原生渲染。

  • 挂件 [speaker_widget.js](file:///Users/bytedance/project/测试/backend/static/speaker_widget.js) 重写为双按钮:青色麦克风 FAB(按住式点击说话)+ 齿轮(声纹管理面板)。
  • 流程:getUserMedia 录音 → 停止后并行调用 /asr(faster-whisper 转写)和 /speaker/verify(声纹识别)→ 取转写文本:
    • 若开启"拦截陌生人"且声纹不匹配 → toast 提示"未识别说话人,已拦截",不发送。
    • 否则 → 把文字注入 SPA 的 textarea(用原生 value setter + input 事件触发 React onChange),再派发 Enter 键事件,由 SPA 自己经 /ws 发送 → 双脑 → 回复原生渲染。只走一次 LLM,不重复调用。
  • 右下角 toast 实时反馈:🗣️ <名字> + 识别文字 + "已填入输入框并发送";找不到输入框时降级复制到剪贴板。
  • 关键实现:findComposer() 按 placeholder("输入消息…"/"Type your message…"/"輸入訊息…")定位可见 textarea;setComposerText()Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set 设值以兼容 React 受控组件。
  • 声纹管理面板保留:录入/列表/删除/拦截开关。

10.3.4 5173 开发界面实时声纹监听(2026-08-25)

在 nanobot WebUI 开发服务器(http://localhost:5173,Vite dev,连 8765 gateway)上实时显示监听到的说话人。

  • 架构:5173(Vite)→ 代理 /speaker/asr 到 8000(声纹后端);8765 仍为 nanobot 原生 gateway。三层端口各司其职。
  • 注入方式:在 nanobot/webui/vite.config.ts 加一个 Vite 插件 speaker-monitor-inject,用 transformIndexHtml</head> 前注入 <script src="http://127.0.0.1:8000/speaker-monitor.js" defer>;同时在 server.proxy 增加 /speaker/asrhttp://127.0.0.1:8000(可用 SPEAKER_API_URL 环境变量覆盖)。不改 React 源码。
  • 监听脚本 [speaker_monitor.js](file:///Users/bytedance/project/测试/backend/static/speaker_monitor.js):独立 getUserMedia 采集麦克风(与 WebUI 自己的实时语音通道并存,浏览器允许多消费者),用 ScriptProcessor + 能量 VAD(RMS 阈值 0.015、静音 700ms 断句、最长 6s 强制 flush、最短 600ms 过滤脉冲)自动分段,降采样到 16kHz Int16 打包 WAV,POST /speaker/verify,悬浮卡片实时显示:
    • 当前说话人名字(彩色,匹配已录入者)或"未知说话人"(附最接近候选与相似度)
    • 音量条、相似度、语音时长
    • 最近 8 条识别记录滚动日志
    • 开始/停止监听按钮、清空按钮
  • 后端端点webui_gateway.py 新增 /speaker-monitor.js(返回 backend/static/speaker_monitor.js);修复 catch-all 排除规则:"speaker""speaker/",避免 /speaker-monitor.js/speaker-widget.js 被误拦截为 404。
  • 验证:5173 页面 HTML 含注入脚本;curl 5173/speaker/status 经代理返回 8000 的声纹数据;JS 语法检查通过;已录入说话人"徐庆杰"可被实时识别。

10.3.4.1 与实时对话按钮联动 + 会话说话人统计(2026-08-25)

监听脚本升级:不再需要手动点"开始监听",点击 WebUI 的实时语音按钮(Radio 图标)即自动启停声纹识别,并在卡片上展示本次会话识别到的说话人数量与名单。

  • 自动联动:用 MutationObserver 监听 document.bodyaria-pressed 属性变化,定位实时语音按钮(button[aria-pressed] 且 aria-label 含"实时/realtime/即時")。按下 → 自动 start() 开麦克风+VAD+验证;弹起 → 自动 stop() 释放。手动"开始/停止"按钮仍保留作兜底。
  • 会话统计:卡片新增"本次会话识别到 N 人"区域,用彩色标签列出每个说话人名字 + 出现次数(×n),未知说话人单独记为灰色"未知"标签。每次开始新会话自动清空,"重置"按钮可手动清空。
  • 顶部徽标显示已录入声纹人数;当前说话人大字显示 + 相似度/时长;音量条;最近 6 条识别记录。
  • 关键选择器:button[aria-pressed](Radix Button),标签匹配 i18n 中的"开启实时语音/Start realtime voice"等。

10.4 关键设计点

  • 本地优先:Resemblyzer(CPU,模型 ~17MB,0.02s 加载),零云端。
  • 懒加载单例get_default_gate() 进程级复用,构造时不加载模型,首次录入/验证才加载。
  • 持久化(多说话人):每人一条 embedding 存 ~/.nanobot-assistant/voiceprints/<id>.npy;索引 voiceprints.json(id/name/created_at/duration_s/legacy);阈值与 enabled 存 speaker_settings.json。旧版单文件 voiceprint.npy 自动迁移为"机主"。
  • 识别策略:对所有已录入说话人算余弦相似度取最高分,回传 top3 候选;不依赖单个机主。
  • 音频解码:librosa 优先,失败回退 ffmpeg 转 16k 单声道 PCM,兼容浏览器 audio/webm;codecs=opus
  • 阈值:默认 0.70(同人通常 >0.75,异人通常 <0.65),可在 UI/接口调。
  • 录入质量门槛:预处理(webrtcvad 裁静音)后 <3s 拒绝录入;验证 <0.8s 直接判不匹配。
  • 全链路降级:声纹验证异常时放行(fallback: true),不因声纹故障阻断对话;未装 resemblyzer 时 /speaker/status 返回 available:false,开关与录入禁用。
  • 零改动 nanobot:核心类放 nanobot-assistant/audio/,编排在 backend/

10.5 环境依赖

  • 新增可选依赖:resemblyzerlibrosasoundfile(已加入 pyproject.tomlaudio extras)。
  • Python 3.14 兼容:webrtcvad 2.0.10 顶部 import pkg_resources,需 setuptools<81(setuptools ≥81 移除了 pkg_resources)。

10.6 验证

  • tests/test_speaker_gate.py:10 项全通过(状态、未录入拒绝、短音频拒绝、命名录入+同人匹配、多人最佳匹配、开关/阈值持久化、单人删除、全删、重载、旧版迁移)。
  • nanobot-assistant 全量:31 passed, 4 skipped
  • HTTP 端到端(TestClient):多说话人录入/列表/按名验证(score)/删除全部 200;/speaker/verify 正确返回 speaker_name 与 top3 候选。

十一、待后续优化方向

优先级 方向 说明
⭐ 最高 语音↔双脑对接 语音出口改走 DualBrainRouter,简单闲聊 4B 秒回
TTS 音色升级 Piper 机械感强,换更自然、带情绪韵律的 TTS
端到端流式 + 打断 流式 STT→LLM→TTS,支持 barge-in
P1 实时降噪 引入 DeepFilterNet3(已选型),压制非稳态环境噪声
P2 声纹门控 ✅ 已完成(见第十节,Resemblyzer 声纹门控节点)
1.5B 反射脑 说话瞬间先"嗯~"遮蔽 4B 延迟,听感更真人
V0 vs V1 跑分脚本 量化双脑优化的端到端延迟改善
中脑(9.7B) 稍复杂问答、不需工具时的中间层

十二、相关设计文档索引

文档 位置
项目修改汇总(历史) [.trae/documents/项目修改汇总.md](file:///Users/bytedance/project/测试/.trae/documents/项目修改汇总.md)
双脑设计 v1.1 [backend/docs/dual-brain-design-v1.1.md](file:///Users/bytedance/project/测试/backend/docs/dual-brain-design-v1.1.md)
多模态助手架构 [nanobot-assistant/docs/architecture.md](file:///Users/bytedance/project/测试/nanobot-assistant/docs/architecture.md)
记忆可视化计划 [.trae/documents/nanobot记忆可视化与行为记录系统计划.md](file:///Users/bytedance/project/测试/.trae/documents/nanobot记忆可视化与行为记录系统计划.md)
Token 观测方案 [.trae/documents/nanobot_token统计观测页面方案.md](file:///Users/bytedance/project/测试/.trae/documents/nanobot_token统计观测页面方案.md)
长期记忆图谱设计 [.trae/documents/nanobot长期记忆知识图谱系统设计方案.md](file:///Users/bytedance/project/测试/.trae/documents/nanobot长期记忆知识图谱系统设计方案.md)
阶段三与四计划 [.trae/documents/基于nanobot的个人AI助手_阶段三与四.md](file:///Users/bytedance/project/测试/.trae/documents/基于nanobot的个人AI助手_阶段三与四.md)
语音截图多模态 [.trae/documents/基于nanobot的语音截图多模态AI助手.md](file:///Users/bytedance/project/测试/.trae/documents/基于nanobot的语音截图多模态AI助手.md)

附录 E:nanobot-assistant 架构与使用

来源:nanobot-assistant/docs/architecture.md + nanobot-assistant/docs/usage.md

Architecture

┌────────────────────────────────────────────────────────────────────┐
│                          User (Earphones)                          │
│                       🎙️  mic      🔊 speaker                      │
└────────────┬──────────────────────────────────┬────────────────────┘
             │ PCM 16kHz                        │ PCM 22kHz
             ▼                                  ▲
┌────────────────────────────────────────────────────────────────────┐
│  nanobot-assistant.channels.desktop_voice                          │
│   - MicCapture (sounddevice + silero-VAD)                         │
│   - PiperTTS (local)                                              │
│   - Player (sounddevice OutputStream)                             │
└────────────┬──────────────────────────────────┬────────────────────┘
             │ InboundMessage(text)              │ TTS request
             ▼                                  ▲
┌────────────────────────────────────────────────────────────────────┐
│  nanobot.bus.MessageBus                                            │
│   - inbound queue  →  AgentLoop  →  outbound queue                │
└────────────┬──────────────────────────────────┬────────────────────┘
             │ InboundMessage                    │ OutboundMessage
             ▼                                  ▲
┌────────────────────────────────────────────────────────────────────┐
│  nanobot.agent (loop / runner / context / tools)                   │
│   - LLM providers (Anthropic, OpenAI, Bedrock, ...)               │
│   - Tools (auto-discovered from entry_points):                    │
│       • screenshot      (mss → save PNG → vision input)          │
│       • speak           (Piper TTS → speaker)                     │
│       • remember        (vector DB write)                         │
│       • vector_recall   (vector DB read)                          │
│       • + 10+ built-in (cron, web, file, image-gen, mcp, ...)    │
│   - Skills (auto-loaded from workspace/skills/):                 │
│       • multimodal-memory  /  screenshot-vision  /  voice-companion│
│   - Dream memory (long-term text consolidation)                   │
└────────────┬──────────────────────────────────┬────────────────────┘
             │ query / add                      │
             ▼                                  ▲
┌────────────────────────────────────────────────────────────────────┐
│  nanobot_assistant.memory (ChromaDB + CLIP)                        │
│   - text_notes / images / video_frames / screenshots              │
│   - Embedder: CLIP-ViT-B-32 (local)  | OpenAI | Voyage           │
│   - VideoIndexer: ffmpeg + opencv + nanobot ASR                   │
└────────────────────────────────────────────────────────────────────┘

Data Flow: Voice Turn

  1. User speaks → sounddevice captures 30 ms frames
  2. silero-VAD detects speech start / end
  3. On end, the captured int16 PCM is written to a temp WAV
  4. nanobot's existing transcribe_audio_file (Whisper / Groq / etc.) returns text
  5. InboundMessage(channel="desktop_voice", chat_id="desktop_voice", content=text) → bus
  6. AgentLoop consumes, builds context (with multimodal memory hits), calls LLM
  7. OutboundMessage flows back; desktop_voice channel speaks it via Piper

Data Flow: Screenshot

  1. Agent decides to look at the screen
  2. Calls screenshot(region="main") tool
  3. mss captures PNG; saved to ~/.nanobot-assistant/screenshots/YYYY-MM-DD/
  4. nanobot attaches the path as a vision input to the tool result
  5. LLM sees the image on the next turn
  6. (Async) the screenshot is also indexed into the screenshots collection

Data Flow: Remember / Recall

  • remember(content, type) → embedder → ChromaDB add
  • vector_recall(query) → embedder → ChromaDB query → top-k hits with metadata
  • The multimodal-memory skill tells the agent when to use them

Plugin Registration

nanobot-assistant registers itself via Python entry_points:

[project.entry-points."nanobot.channels"]
desktop_voice = "nanobot_assistant.channels.desktop_voice:DesktopVoiceChannel"

[project.entry-points."nanobot.tools"]
screenshot = "nanobot_assistant.tools.screenshot:ScreenshotTool"
speak      = "nanobot_assistant.tools.tts:SpeakTool"
remember   = "nanobot_assistant.tools.remember:RememberTool"
vector_recall = "nanobot_assistant.tools.recall:VectorRecallTool"

After pip install -e ., nanobot's discover_all() and ToolLoader.scan()
will pick these up automatically — no nanobot source modifications required.


使用指南

来源:nanobot-assistant/docs/usage.md

Usage

0. Prerequisites

  • macOS / Linux / Windows
  • Python ≥ 3.11
  • nanobot onboarded (i.e. ~/.nanobot/config.json exists with at least one LLM provider)
  • System: ffmpeg (brew install ffmpeg / apt install ffmpeg)

1. Install

cd /Users/bytedance/project/测试/nanobot-assistant
pip install -e ".[audio,screenshot,memory]"

For the minimal install (TTS only, no screenshots/memory):

pip install -e ".[audio]"

2. Initialize

nanobot-assistant setup
# 1) 写入默认配置到 ~/.nanobot-assistant/config.yaml
# 2) 提示后续步骤

3. Health Check

nanobot-assistant doctor

If any item shows FAIL, install the missing optional dependency.

4. Enable the desktop voice channel

Edit ~/.nanobot/config.json to enable the channel:

{
  "channels": {
    "desktop_voice": { "enabled": true }
  }
}

Start nanobot:

nanobot-assistant start-voice

(Or just nanobot gateway — the plugin is auto-discovered.)

The first time the channel speaks, Piper will download
zh_CN-huayan-medium (~60 MB) to ~/.nanobot-assistant/models/piper/.

5. Try it

  • Speak Chinese: "你好" → should hear a Chinese greeting back
  • Speak English: "hello" → should hear an English reply
  • Say: "截屏" → screenshot is taken, vision context attached
  • Say: "记住这张照片" → the screenshot is also indexed in vector memory

6. Index a video

nanobot-assistant index-video /path/to/movie.mp4

This extracts one frame every 2 s, embeds each, and stores them in the
video_frames collection. If ffmpeg is available and transcribe=true,
the audio track is also transcribed and stored as a text_notes entry.

7. Recall a memory

nanobot-assistant recall "海边的照片"
# 或:nanobot-assistant recall "上次我跟你说的那个 API bug" --modality text

Or, from any nanobot session, just say "记不记得..." — the agent has the
vector_recall tool and the multimodal-memory skill will guide it.

8. Voice commands cheatsheet

中文 English Action
截屏 screenshot / look at my screen capture screen
记住这个 remember this index last media into vector DB
你记得吗 do you remember trigger vector_recall
念一下 read it speak text via Piper

9. Files & data locations

Purpose Path
Config ~/.nanobot-assistant/config.yaml
TTS models ~/.nanobot-assistant/models/piper/
Embedding cache ~/.nanobot-assistant/embedding_cache/
Vector DB ~/.nanobot-assistant/vector_db/
Screenshots ~/.nanobot-assistant/screenshots/YYYY-MM-DD/
TTS cache ~/.nanobot-assistant/tts_cache/
Extracted video frames ~/.nanobot-assistant/video_frames/<video>/
nanobot config ~/.nanobot/config.json (managed by nanobot)

10. Troubleshooting

  • No sound output: check macOS Sound settings; grant microphone permission to Terminal/iTerm
  • ASR fails: ensure OPENAI_API_KEY (or GROQ_API_KEY) is set; see nanobot docs
  • ChromaDB error on import: pip install "chromadb>=0.5"; on Apple Silicon you may need pip install chromadb --no-binary :all:
  • CLIP model download slow: pre-download with huggingface-cli download sentence-transformers/clip-ViT-B-32
  • ffmpeg not found: brew install ffmpeg / apt install -y ffmpeg
  • VAD too sensitive / not sensitive enough: tune mic.vad_threshold and mic.silence_ms

附录 F:TTS 服务说明

来源:nanobot/services/tts/README.md

nanobot Local TTS Service

This process isolates CosyVoice and its Python/PyTorch dependencies from the
nanobot gateway.

Environment

Use Python 3.10, 3.11, or 3.12 in an isolated environment. Install this
service and the engine extras you need:

python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[qwen]"

For CosyVoice, install its upstream repository (including
third_party/Matcha-TTS) into the same environment, then use:

export TTS_COSYVOICE_MODEL_PATH=/absolute/path/to/Fun-CosyVoice3-0.5B
export TTS_VOICES_DIR=/absolute/path/to/voices
uvicorn nanobot_tts_service.app:app --host 127.0.0.1 --port 9880

Qwen3-TTS uses its official qwen-tts package. The service lazily downloads
the selected Hugging Face model unless an explicit path is configured:

export TTS_QWEN3_TTS_0_6B_PATH=/absolute/path/to/Qwen3-TTS-12Hz-0.6B-CustomVoice
uvicorn nanobot_tts_service.app:app --host 127.0.0.1 --port 9880

A reference voice consists of matching files:

voices/
├── assistant.wav
└── assistant.txt

The text file must contain the exact transcript of the reference audio. Select
assistant as the voice in nanobot. If the selected voice matches a built-in
CosyVoice speaker ID, the service uses inference_sft; otherwise it uses the
reference WAV and transcript with inference_zero_shot.

API

GET  /health
GET  /v1/tts/models
GET  /v1/tts/voices
POST /v1/audio/speech

The speech endpoint accepts an OpenAI-style JSON body and returns WAV audio.
Its model field selects cosyvoice3-0.5b, qwen3-tts-0.6b, or
qwen3-tts-1.7b.

posted on 2026-08-05 20:08  落子无悔96  阅读(18)  评论(0)    收藏  举报

导航