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):
- 浏览器 AudioWorklet 采集 16kHz PCM → WS :8765/realtime
- Gateway VAD 分段 → faster-whisper ASR + CAM++ 声纹 并行(asyncio.gather)
- transcript.final 下行 → 前端自动发 WS :8765/ws 聊天
- Gateway AgentLoop → Ollama 35B → delta 流式下行
- 前端 tts.bistream_text → Gateway BistreamTTS → TTS :9880 WS → PCM 回传 → PcmStreamPlayer 播放
路径 B — HTTP 语音回合(访问 :8000):
- 浏览器 POST :8000/voice/turn(multipart WAV)
- Backend 串行:CAM++ 声纹 → FunASR SenseVoiceSmall ASR → DualBrainRouter
- DualBrain:规则粗筛 + 4B 分诊 → SIMPLE 走 4B 快脑 / COMPLEX 走 nanobot AgentLoop 慢脑
- Backend HTTP POST TTS :9880/v1/audio/speech → WAV
- 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-whisperchannels.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 端点逻辑:
- 保存上传音频,转 WAV
- CAM++ 声纹验证(即使被拒绝也继续 ASR,用于显示识别文本)
- 声纹匹配 → FunASR ASR → DualBrain 路由 → TTS 合成
- 声纹不匹配 → 返回 rejected=true(但仍包含 asr_text)
- 返回
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-identifiedCustomEvent - 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 耗时观测
变更:
__init__新增speaker_verifier参数_transcribe_final重写:asyncio.gather(_do_asr(), _do_verify())并行执行- ASR 结果和 speaker 信息合并到
transcript.final事件 - 新增
tts.bistream_start/text/finish消息处理 _bistream_start创建 BistreamTTS 会话,_drain_bistream把 PCM chunk 转tts.audio发回客户端tts.cancel同时取消 bistream 会话_cancel_tasks清理所有 bistream 会话- 发射
asr_completed/tts_completed观测事件(含first_chunk_ms、bistream: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++ 声纹验证器
变更:
GatewayServicesdataclass 新增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=Falseword_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 VADnanobot-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/recallnanobot-assistant/pyproject.toml— entry_points 注册
TTS 服务文件
nanobot/services/tts/nanobot_tts_service/__init__.pynanobot/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.pynanobot/services/tts/pyproject.toml
Nanobot 核心修改文件
nanobot/nanobot/realtime/websocket.py— ASR+声纹并行、bistream TTSnanobot/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 + speakernanobot/webui/src/hooks/useRealtimeVoice.ts— speaker 状态nanobot/webui/src/hooks/useVoicePlayback.ts— 流水线播放nanobot/webui/src/components/thread/ThreadShell.tsx— bistream bridgenanobot/webui/src/components/thread/ThreadComposer.tsx— speaker badgenanobot/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-14v1.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 兼任分诊与对话:
决策原则:存疑一律判 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 追加进同一
K并save()。 - 读:快脑
get_history(max_messages=6)取最近几轮做上下文(支持有上下文闲聊)。 - ⚠️ backend
/chat用req.session_key(默认web:default),快慢脑必须一致。
7. /chat 接口改动(T2,待实现)
对外协议不变,仅内部分流;ChatResponse 增可选 brain 字段(fast/slow)供观测与对照。
环境变量:DUAL_BRAIN_ENABLED(默认 true)、TRIAGE_MODEL、FAST_MODEL、OLLAMA_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 后续事项(比换更大模型收益更高)
- 语音↔双脑对接(⭐ 最高优先):现状语音把文字丢进 nanobot MessageBus → 被 35B 慢脑消费,语音回复全是慢脑。需把出口改走
DualBrainRouter,简单闲聊才能 4B 秒回。 - TTS 音色(像不像真人的最大杠杆):Piper
zh_CN-huayan-medium一耳朵是合成音;换更自然、带情绪韵律的 TTS。 - 端到端流式 + 打断(barge-in):流式 STT→LLM→TTS,允许用户说话打断 AI。
- 口语人设:语音回答强制短句、口语、有情绪(system prompt 层,已在快脑体现)。
- 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_idturn_idstream_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 类型为
OutboundJSON。
直接在原连接发送 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。
实时语音中,用户插话需要同时取消:
- 浏览器播放。
- TTS 服务生成。
- Gateway 音频转发。
- Sentence Segmenter。
- Agent Turn。
- 旧 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_endflush 剩余文本。
必须在服务端执行,原因:
- 所有客户端行为一致。
- 可与 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:不碰现有聊天协议
- 建立 TTS Provider 接口。
- 建立独立神经 TTS 服务。
- 只替换回复手动播放。
- browser TTS 保持默认和 fallback。
退出条件:
- TTS 服务健康、取消和超时可用。
- 文字聊天零回归。
Stage B:流式 TTS
- 增加服务端 SentenceSegmenter。
- 增加 PCM 播放器。
- 增加 generation fence。
- 新回复可边生成边播报。
退出条件:
- 无重复、漏读和乱序。
- 中断后旧 chunk 不恢复。
Stage C:独立实时输入通道
- 新建
/realtime。 - 新建 AudioWorklet。
- 新建 AudioBuffer 和 VAD。
- 先只输出 final transcript。
退出条件:
- 自动开始、结束识别稳定。
- 旧手动录音仍可用。
Stage D:Streaming ASR
- rolling faster-whisper。
- partial transcript。
- 稳定前缀与 final 修正。
退出条件:
- partial 不进入 Agent。
- final 只提交一次。
Stage E:Barge-in
- TurnManager。
turn.interrupt。- Agent/TTS/ASR 全链取消。
- 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_id和stream_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. 评审结论
该计划可实施,但必须满足以下前置修正:
- Realtime 音频使用独立通道,不直接侵入当前 JSON Chat WebSocket。
- Streaming ASR 新增接口,不替换完整文件 ASR。
- 神经 TTS 与 browser TTS 同一时刻只能启用一个。
- Provider/模型由服务端配置,设备偏好由 localStorage 保存。
- 所有实时异步结果使用 generation ID 隔离。
- 实时模式默认关闭,并由 runtime capability 控制 UI。
- TTS 模型使用独立 Python 环境和进程。
- 每个迁移 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 |
用户目标:
- 在记忆观测中心内看到当前 agent 的节点链路。
- 当 LangGraph 结构发生变化时,UI 能自动同步。
- 节点开始/结束、条件路径、耗时信息尽量复用 LangGraph 原生能力。
- 删除或隐藏之前自实现的重复接口和页面入口,避免多套观测 UI 并存。
- 不依赖 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
最小可用版本:
- 展示三张图的静态拓扑。
- 自动刷新拓扑版本。
- 选择最近 turn 后高亮实际走过的节点。
- 显示每个节点本次耗时。
增强版本:
- 聚合耗时统计。
- 条件边实际命中率。
- 错误节点筛选。
- 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 的图执行摘要。
参数:
limitbeforesession_keygraph_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
返回聚合耗时。
维度:
nodegraphsessionstop_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 耗时链路明细按状态统计耗时按会话统计耗时按结束原因统计耗时
对应前端组件:
FlowMonitorPageDurationTurnsPageDurationLinksPageDurationAggregatePage
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
删除条件:
- 前端没有引用。
- 测试没有引用。
- 用户确认旧入口可以下线。
- 至少一次完整回归通过。
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. 待用户确认
实现前需要确认:
- 新页面命名是否使用
Agent 链路。 - 自动刷新周期是否默认 2 秒。
- 是否允许新增
langgraph_*事件写入memory_observation.db。 - 是否接受旧 durations 页面先隐藏、后删除的两阶段策略。
- 是否保留
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 从简单的文件总结扩展为完整的长期记忆沉淀流水线:
- 候选提取:从中期记忆和历史记录中提取候选内容
- LLM 结构化抽取:输出实体、事实、关系等结构化结果
- 规则过滤:规范化和过滤低质量内容
- 图谱批次写入:批量写入长期 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_FACT或RELATES_TO - 读取时折叠已声明别名实体,避免 UI 重复节点
- 查询同时覆盖实体名、事实属性、事实值、关系谓词和目标实体
- 节点详情增加入边、出边、事实属性和值
- Dream 写入前注入现有属性和谓词词表,减少同义字段膨胀
- 精确重复事实和关系可确定性识别,不完全依赖 LLM
- 合并时迁移来源边、批次边、所有者边和实体关系,并清理重复节点
2.5 中期记忆时效治理
- 对"近期安排、后续安排、接下来安排"等模糊文本进行重写
- 为日程、计划、待办、截止时间等时效内容默认设置 1-7 天 TTL
- 总结 Prompt 中注入当前对话时间锚点
- 上下文输出增加
recorded_at和expires_at - 增加
time_sensitiveevidence 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_search、kb_read、kb_write、kb_list、kb_graph、kb_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_ms和brain字段
关键文件:[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 将文本和图像映射到统一向量空间
- 提供
remember和vector_recall工具 - 视频索引器通过 ffmpeg/OpenCV 抽帧,可提取音频转写
- 支持四类集合:text_notes、images、video_frames、screenshots
6.5 插件注册
通过 Python entry_points 零侵入扩展 nanobot:
- Channel:
desktop_voice - Tools:
screenshot、speak、remember、vector_recall - Skills:
multimodal-memory、screenshot-vision、voice-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_ms、speech_pad_ms、start_confirm_windows、startup_calibration_s、adaptive_threshold_margin字段。
9.3 配置项(~/.nanobot-assistant/config.yaml 的 mic: 段)
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/candidates,nodes 计时 |
10.3.1 多说话人与界面展示
- SpeakerGate 从"单机主"扩展为多命名说话人:每人一条 256 维 embedding,存
voiceprints/<id>.npy,索引存voiceprints.json;设置存speaker_settings.json。 verify对所有已录入说话人算余弦相似度,取最高分,回传 top3 候选;/voice/turn把speaker_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)、状态提示。 - 悬浮按钮右上角显示已录入人数徽标;录入中按钮变红脉冲,再次点击停止。
- 自动适配深/浅色主题;点击面板外自动关闭。
- 点击展开面板:已录入说话人芯片(彩色圆点 + × 删除)、"录入新说话人"按钮(弹框问名字 → 浏览器录音 → POST
- 挂件与 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、/asr→http://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.body上aria-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 环境依赖
- 新增可选依赖:
resemblyzer、librosa、soundfile(已加入pyproject.toml的audioextras)。 - 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
- User speaks → sounddevice captures 30 ms frames
- silero-VAD detects speech start / end
- On end, the captured int16 PCM is written to a temp WAV
- nanobot's existing
transcribe_audio_file(Whisper / Groq / etc.) returns text InboundMessage(channel="desktop_voice", chat_id="desktop_voice", content=text)→ busAgentLoopconsumes, builds context (with multimodal memory hits), calls LLMOutboundMessageflows back;desktop_voicechannel speaks it via Piper
Data Flow: Screenshot
- Agent decides to look at the screen
- Calls
screenshot(region="main")tool - mss captures PNG; saved to
~/.nanobot-assistant/screenshots/YYYY-MM-DD/ - nanobot attaches the path as a vision input to the tool result
- LLM sees the image on the next turn
- (Async) the screenshot is also indexed into the
screenshotscollection
Data Flow: Remember / Recall
remember(content, type)→ embedder → ChromaDBaddvector_recall(query)→ embedder → ChromaDBquery→ 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.jsonexists 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(orGROQ_API_KEY) is set; see nanobot docs - ChromaDB error on import:
pip install "chromadb>=0.5"; on Apple Silicon you may needpip 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_thresholdandmic.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.
浙公网安备 33010602011771号