Files
ylhan f0dce05ac1 新增 Pipecat Flows 兼容性 spike 结论
交接文档规定"未经 spike 结论,不进入 Phase 3"。本次完成 Phase 1 spike,
7 项验证全部通过,并给出可进入 Phase 2 的结论。

新增 P1-Flows兼容性Spike结论.md

核心结论:
- **不装 pipecat-ai-flows**。Pipecat 1.10 内置 Flows(pipecat.flows),
  装独立包会报错提示卸载;独立包 1.4.0 会把 pipecat 降到 1.4.0
- **复用 DeepSeekLLMService**,不写 FallbackProvider 适配器。它自带
  DeepSeekLLMAdapter 处理 reasoning_content 回传(P0 踩过的坑),
  thinking 默认 disabled。代价是失去本地 vLLM 降级
- 实测:中文流式 14–22 块/回复、工具参数无重复拼接、回灌后出最终回答、
  worker.cancel() 立即终止、role_message 生效

定位了 5 个坑(4 个 Flows 相关 + 1 个环境):
1. FlowManager.initialize 是协程但签名显示 -> None,漏 await 静默失效,
   role_message/task_messages/工具全部不生效而 LLM 照样回复,极易误判
2. 必须用 PipelineRunner 跑 worker;直接 run() 会让工具执行失败,
   异常被转成通用错误串,handler 从不执行,默认日志看不到
3. respond_immediately 默认 True,initialize 会立刻触发一轮回复,
   做归因测试时不清空观察窗口会归因错
4. role_message 与 task_messages 各产生一条 system 消息,
   两者不要同时用(切回本地 vLLM 时会 400)
5. NLTK punkt_tab 本地也需预装,否则 setup 超时卡住

对现有功能的影响:分句 TTS、RTVI、记忆身份不受影响;
SearXNG 工具需迁移;本地 vLLM fallback 会失去。
第四节给出 Phase 3 的最小生产集成 diff 要点。
2026-09-16 13:13:52 +08:00

10 KiB
Raw Permalink Blame History

P0 实验报告 · Pipecat 传输层

日期:2026-09-15。状态:实时语音链路已闭环;A 方案验证通过。

代码:~/workspace/ai-companion/services/bot_server.py(服务端)、 client/test_client.py(自写 aiortc 客户端)

一、为什么做这个实验

方案文档写"Pipecat 作为唯一会话管线框架",但核实后发现它一行都没被用上 (只在 requirements.txt 里声明,代码里零引用)。助手引擎是独立 asyncio, 由测试脚本用文件音频驱动。

于是有个待定的架构问题:Pipecat 该用多深?

  • A 方案:Pipecat 只做传输与帧模型,会话语义归自有引擎(一个 FrameProcessor 吞掉整条管线)
  • B 方案:按 Pipecat 的 service 模型重构,STT/LLM/TTS/记忆都做成框架原生 service

实验目的:验证 A 方案的核心假设能否成立,并拿到真实传输下的延迟数字。

二、结论摘要

部分 结果
WebRTC 信令(offer/answer) ✅ 往返 47–280ms
ICE 连通 ✅ 走 172.16.1.x 直连
data channel + 心跳 ✅
双向媒体轨 ✅ 客户端音轨送达服务端,服务端音轨客户端收到
管线启动、处理器 setup() ✅,只产生一个自动 StartFrame
帧路由到自定义处理器 ✅ StartFrame 与 250+ 个音频帧实测到达
端到端出声 ✅ 文件音频 → STT → DeepSeek → Qwen3-TTS → 非静音回复
浏览器真人对话 ✅ SSH 隧道打开 Playground,麦克风输入与语音回复正常
单次真实链路延迟 源音频结束→客户端首个非静音约 5.8s;仍需优化

即:Pipecat 负责传输/帧/RTVI、自有引擎负责会话语义的 A 方案成立。

三、原问题根因与修复

原先看到 EngineProcessor.setup() 被调用、process_frame 却为 0,于是误判为 Pipeline 接线或模型不兼容。服务器完整日志揭示了真实顺序:Pipecat 在 setup 阶段 预热 NLTK 分句器,首次运行尝试下载 punkt_tab,20 秒后 setup timeout。worker 尚未走到自动发送 StartFrame 的阶段,因此所有业务帧都还在等待。

修复后把 punkt_tab 预装到权限 700 的 ~/.local/share/ai-companion/nltk_data,启动时 fail-fast 检查;同时改用 PipelineWorker(params=PIPELINE_PARAMS),删除客户端连接事件中手工发送的第二个 StartFrame。复测显示自动 StartFrame 正常抵达管线末端,音频持续进入处理器。

之后仍不出声是第二个独立问题:文件客户端播放完 WAV 后没有继续推进 PTS,服务端 收不到 VAD 所需的 700ms 尾静音。客户端现在固定发送 1.2s 尾静音且持续推进时钟。 接收端也改为只有检测到非静音音频才算成功,不再把 WebRTC 空闲静音当作回复。

四、挖出的 13 个集成坑

每个都会表现为"什么都没发生",极难排查。无论最终选 A 还是 B 都会踩。

# 坑 现象 怎么办
1 Pipecat 客户端 SDK 不覆盖 N5095 形态 浏览器/移动端/C++/ESP32 都有,没有"Linux x86 盒子接 USB 阵列" 自己写 aiortc 客户端(已有雏形)
2 客户端必须建 data channel 并定期发 "ping" 不建则服务端一帧音频都不推,日志只有一句 "Data channel not established within 10s" 建 channel + 1s 心跳
3 offer 里没有媒体时 aiortc 完全不收集 ICE 候选 信令成功、SDP 交换正常,但 ICE 永远停在 new 即使只测连通也要加一条(静音)音轨
4 ICE 收集完成前就发 offer SDP 里 0 个候选地址 等 iceGatheringState == "complete"
5 PipelineWorker.run() 要 WorkerParams(task_manager=...),不是 PipelineParams 不传报 "missing 1 required positional argument";传错报 "no attribute 'task_manager'" 两个参数分别给构造函数和 run()
6 TaskManager() 内部调 asyncio.get_running_loop() 模块级创建报 "no running event loop" 必须在异步上下文里建
7 后台任务里的异常被静默吞掉 日志里查不到任何线索,表现为"连接正常但没反应" 自己 try/except + log.exception
8 信令回调里 await task.run() 阻塞整个会话生命周期,管线初始化时序走歪 asyncio.create_task(task.run(...))
9 NLTK punkt_tab 在首个 worker 内临时下载 setup 20 秒超时,看起来像帧路由坏了 部署时预装到私有目录,启动前检查
10 PipelineWorker 本身会自动发送 StartFrame 手工再发会看到两个启动帧 删除连接回调里的手工 StartFrame
11 文件结束后静音帧 PTS 不增长 VAD 永远等不到说完 源位置与媒体时钟分离,额外发送 1.2s 静音
12 WebRTC 输出轨空闲时也有静音 “收到 34 秒音频/首音 206ms”的假成功 按非静音 RMS 判断业务回复
13 prebuilt Playground 连接前先请求 /start 页面正常但 Connect 在 authenticating 阶段报 Not Found 增加 /start,返回 /api/offer 的 webrtcRequestParams

第 5、6、7 条各花了好几轮才发现——因为异常在后台任务里,日志什么都不显示。

五、对 A/B 决策的影响

实验前的判断是"Pipecat 的独特价值收敛到传输层"(因为方案 §5 的音频边界决定 把 AEC 和播放放在客户端,绕开了 Pipecat 的打断机制)。

根因已经证明既不是 Pipeline 接线,也不是自定义处理器模型不兼容。因此确定采用 A 方案:保留已验证的 ConversationEngine,由薄 EngineProcessor 适配 Pipecat 的输入输出帧。网页客户端需要的 RTVI 继续开启;N5095 精简客户端可以忽略 暂不认识的 RTVI data-channel 消息。

不做 B 方案的全量重构。后续只在确有收益时渐进引入 Pipecat 的 VAD、turn processor、 metrics 与 interruption,不重写已有的记忆、身份、LLM/TTS 适配。

六、下一步

  1. 优化真实首音延迟:分别记录 VAD、STT、LLM 首 token、TTS 首段和客户端播放时间
  2. 继续用 prebuilt 网页客户端做真人语音、浏览器 AEC 与主观试听(基础对话已通过)
  3. Tailscale tailnet 开启 Serve 后,用 HTTPS 暴露 /client/
  4. 音频硬件到货后实现 N5095 声卡、AEC、播放参考与打断
  5. 录本人普通话语料,对 Zipformer / SenseVoice / faster-whisper 做 CER 对比

七、当前代码状态

服务端 services/bot_server.py:

  • 已实现:SmallWebRTC 信令端点 /api/offer、prebuilt 网页客户端挂载 /client/、 Playground 启动端点 /start、EngineProcessor(自写能量 VAD + 引擎调用)
  • 已改用 PipelineWorker,保留 RTVI;启动/超时/错误有显式日志
  • 已适配 RTVI send-text 产生的 LLMMessagesAppendFrame,网页文字输入与 Conversation 区回复走自有引擎,不依赖框架原生 LLM service;Playground 1.7 需补发完整 bot-transcription / bot-output,token 级事件不足以稳定渲染
  • 音频缓冲加入 1 秒预卷、60 秒上限和短噪声丢弃,连接结束释放会话资源
  • VAD 是极简能量判定,不是最终方案(方案 §5 的说完判定还没选型)
  • 当前没有唤醒门控:网页连接后每段有效语音都会触发回复。正式唤醒仍在 P2 的 N5095 本地实现,待机时不上传音频;本次按用户决定暂不处理
  • 已接入联网搜索:DeepSeek 通过 Function Calling 请求 web_search,执行层调用 仅绑定 127.0.0.1:18080 的私有 SearXNG,结果作为 tool 消息回灌后继续生成; 网页保留来源 URL,TTS 不朗读来源段
  • 2026-09-16 真实闭环验收通过:模型主动搜索,SearXNG 返回 5 条结果,最终回答带 两个来源;服务器网络下 360 搜索、搜狗可用,DuckDuckGo/Brave/Wikipedia 超时
  • SearXNG 的完整选型、部署、API、运维和排错记录见 SearXNG私有搜索服务部署与使用

客户端 client/test_client.py:

  • 已实现:非 trickle ICE、data channel + 心跳、文件音频实时发送、1.2s 尾静音、回复录音
  • 只有检测到非静音回复才成功;按 WebRTC 实际 48kHz 保存 WAV
  • 不做 AEC、不做播放参考、不做打断——那些要真实声卡和硬件
  • 它是 N5095 客户端的雏形,不是产品代码

八点五、2026-09-16 后续架构补充

媒体传输闭环不等于 Agent、长期记忆和实时打断已经闭环。真人网页测试后确认:

  • 姓名档案已经能跨连接恢复,但通用“记住/偏好”仍写 candidate,默认不会召回;
  • 当前 SimpleVAD 能判断 700ms 尾静音,助手说话时检测到用户声音却只记日志;
  • InterruptionFrame 和异步任务取消能力存在,但没有由 user-started-speaking 可靠触发,也没有保证清空浏览器已经缓冲的音频;
  • 当前只有 web_search 的手写工具循环。

后续 Agent 层选择 Pipecat Flows 渐进集成,继续坚持 A 方案,不重写已经验证的 传输层。因为 Flows 使用 Pipecat LLMService 与 context aggregator,而项目当前 使用自有 ConversationEngine,必须先做独立兼容性 spike,并保留 legacy feature flag。详细交接见 P1-Pipecat Flows Agent化与记忆改造交接。

八、复现

# 服务端(服务器上)
cd ~/ai-companion
.venv/bin/python services/bot_server.py --host 0.0.0.0 --port 7860

# 客户端(N5095 上)
cd ~/workspace/ai-companion
python client/test_client.py \
  --server http://cadadmin-server.taild0f91d.ts.net:7860 \
  --wav p0/stt_tts_bench/wavs/stt_1.wav --out /tmp/reply.wav

当前实测:连接成功,收到非静音语音回复;源音频结束到首个非静音约 5.8 秒。 浏览器通过 SSH 隧道访问 http://localhost:7860/client/ 也已完成真人对话; 2026-09-16 一轮服务端指标约 3.0 秒。

九、顺带发现的环境问题

局域网被重新编址了:两台机器从 192.168.0.x 变成 172.16.1.x (N5095 现为 172.16.1.99,服务器 172.16.1.101)。

已处理:客户端默认地址改用 tailscale 域名 (cadadmin-server.taild0f91d.ts.net),抗重新编址。

未处理:方案文档与 N5095 配置里仍有 192.168.0.20 的残留引用。