Files
note_sync/有趣的项目/AI桌面陪伴助手/P1-Flows兼容性Spike结论.md
T
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

11 KiB
Raw Blame History

P1 · Pipecat Flows 兼容性 Spike 结论

日期:2026-09-16 状态:spike 完成,7 项验证全部通过 代码:p1/flows_spike.py(探索脚本)、app/tests/test_flows_spike.py(断言版) 前置:P1-Pipecat Flows Agent化与记忆改造交接

交接文档规定:未经 spike 结论,不进入 Phase 3。本文即该结论。


一、结论摘要

问题 结论
兼容的 flows 版本 不装独立包。Pipecat 1.10.0 内置 Flows(pipecat.flows)
LLMService 选择 复用 Pipecat 的 DeepSeekLLMService;代价是失去本地 vLLM 降级
token 级文本 ✅ 实测 14–22 块/回复,满足网页显示与分句 TTS
工具调用 ✅ 参数聚合完整,无重复拼接;工具结果回灌后能出最终回答
取消 ✅ worker.cancel() 能立即终止 LLM 流
context ⚠️ 会产生两条 system 消息(见 §五)
是否破坏现有功能 见 §六——有两处需要处理

二、版本:不需要独立包,而且装了有害

这是最重要的一条发现。

pipecat-ai 1.10.0 已经内置了 Pipecat Flows,模块路径 pipecat.flows。 装独立的 pipecat-ai-flows 包时,Pipecat 会直接报错提示:

ERROR | pipecat:_warn_if_standalone_flows_installed:45 -
The separate `pipecat-ai-flows` package is installed alongside a version of
Pipecat that already includes Pipecat Flows as `pipecat.flows`.
You do not need both — uninstall `pipecat-ai-flows` and import Flows from `pipecat.flows`.

而且独立包的版本区间有陷阱:

独立包版本 要求的 pipecat-ai 后果
1.4.0 >=1.4.0,<1.5.0 会把 pipecat 从 1.10.0 降到 1.4.0
1.3.0 >=1.4.0,<2 可安装,pytest dry-run 只新增自己

(1.4.0 是"更新"的版本,却钉死在更老的 pipecat 区间——如果按"装最新版"的直觉操作, 会静默把整个框架降级。)

决定:

  • requirements.txt 不添加 pipecat-ai-flows
  • 代码 from pipecat.flows import FlowManager, NodeConfig, FlowsFunctionSchema
  • 内置版与独立包 API 一致(FlowManager / NodeConfig / FlowsFunctionSchema 签名相同), 所以这个切换不影响任何写法

内置版的导出比独立包更多(多出 Flow、FlowConfig、FlowProblem、FlowReferenceError), 像是更演进的 API,但 spike 只用到了两者共有的部分。


三、LLMService 选择

结论:复用 Pipecat 的 DeepSeekLLMService,不写 FallbackProvider → LLMService 适配器。

理由:

  1. 它自带 DeepSeekLLMAdapter,源码注释明写 "Supplies the reasoning_content DeepSeek requires on assistant messages in thinking mode"——这正是我们在 P0 阶段踩过的坑(见 P0-DeepSeek验证报告.md:开思考时多轮/工具回灌一律 400)。 框架已经处理了。
  2. thinking 默认就是 disabled,与 P0 的结论一致。
  3. 支持自定义 base_url,将来要接本地 vLLM 也能用。
  4. 无法适配 FallbackProvider:Flows 要求的是 LLMService 实例 (LLMService | LLMSwitcher),而 pipecat_flows 里那个 LLMAdapter 只管摘要生成,不是 provider 适配器。要保留 fallback 必须自己写 LLMService 子类,成本中等。

必须覆盖模型名:默认是 deepseek-v4-flash,该模型不存在,用默认值会 400。 必须显式设 DeepSeekLLMService.Settings(model="deepseek-flash")。

失去本地 vLLM 降级的代价:目前本机 27B 的 vLLM 已停用,fallback 实际上没有可用的 后备目标。所以 Phase 3 先只用 DeepSeek 是合理的。将来若要恢复双后端, Pipecat 有 LLMSwitcher 可用,不必自写。


四、实测结果(7 项,9 个断言全通过)

=== 2. DeepSeek 在 Flows 中流式输出中文 ===
      回复: '我是你的桌面陪伴小助手,平时就待在这儿陪你聊聊天、说说话。'
      分块数: 18
  [PASS] 有中文回复
  [PASS] 回复不含 Markdown
  [PASS] 7. token 级文本事件(>1 块) — 18 块

=== 3+4. 流式工具参数聚合 / 工具结果回灌 ===
      工具调用参数: [{'city': '北京'}]
      最终回复: '北京现在是下午三点半。'
  [PASS] 3. 工具被调用 — 1 次
  [PASS] 3. 参数聚合完整(无重复拼接) — {"city": "北京"}
  [PASS] 4. 工具结果回灌后有最终回答

=== 5. 取消 ===
      取消前 13 块 → 取消后 1.5s 内 13 块(又收到 0 块)
  [PASS] 5. 取消后停止输出

=== 6. system message ===
      context 消息角色序列: ['system', 'user', 'assistant']
  [PASS] 6. system 消息不超过 1 条
  [PASS] 6. 有 user 与 assistant 轮次

补充观察:

  • role_message 生效:设"你是桌面陪伴助手"后,模型自称"桌面陪伴小助手", 不再是通用的"DeepSeek AI 助手"
  • 工具调用只有 1 次(修好 runner 之前会失败重试 2 次,见 §五)

五、暴露的问题(4 个坑,都已定位)

坑 1:FlowManager.initialize 是协程,漏 await 会静默失效

inspect.signature(FlowManager.initialize)   # (self, initial_node=None) -> None
inspect.iscoroutinefunction(...)            # True  ← 签名上看不出来

漏 await 时不报错,只是整个 flow 从不初始化:role_message、task_messages、 工具注册全部没生效,而 LLM 照样会回复(因为 run_llm=True 直接触发推理)。

现象极具误导性:会让人误判成"Flows 不支持这些特性"。实测中我一度得出 "role_message 不生效"的结论,就是这个原因。

坑 2:必须用 PipelineRunner 跑 worker,不能直接 worker.run()

直接 worker.run(...) 时 runner 没挂上,工具执行会失败:

Error executing function call [get_time]:
Worker 'PipelineWorker#0': runner is not set; call attach() first.

这个异常被 Pipecat 转成给 LLM 的通用错误串 "The function X failed and returned no result.",handler 从不执行, 日志里默认看不到真实原因。要开 DEBUG 才看得到。

正确写法:await PipelineRunner().run(worker)

坑 3:respond_immediately 默认 True,initialize 会立刻触发一轮回复

FlowManager.initialize() 会 queue 一个 LLMRunFrame,在用户说话之前 就产生一轮 LLM 回复。

做测试/归因时如果不清空观察窗口,会把"节点初始化的自动回复"当成 "对用户问题的回复"——我第一版 spike 就误判了第 2 项。

生产上这通常是想要的行为(节点进入即主动开口),但必须知道它存在。

坑 4:会产生两条 system 消息

实际发给 DeepSeek 的请求里:

[system] '你是桌面陪伴助手……'          ← role_message(走 system_instruction)
[system] '当前任务:普通陪伴对话。'      ← task_messages
[user] '……'

role_message 通过 LLMUpdateSettingsFrame(system_instruction=...) 下发, 而 task_messages 里的 system 消息留在 context 里,两者都进请求。

交接文档 §九 Phase 1 第 6 项要求"context aggregator 不产生第二条 system message"—— 严格说,context 里确实只有一条(task_messages 那条), role_message 走的是 LLM settings 而非 context。但发出去的请求有两条 system。

风险:

  • 多数模型能处理多条 system,但优先级不保证(谁覆盖谁取决于实现)
  • 我们之前在本机 vLLM 上就吃过"多条 system 直接 400"的亏 (见 P0-本地LLM验证报告.md)——如果将来把 LLM 切回本地 vLLM,这条会立刻炸

建议:Phase 3 集成时只用一种。要么把节点任务说明也写进 role_message, 要么不用 role_message、全部走 task_messages。不要两个都用。

坑 5(环境):NLTK punkt_tab 本地也要装

Pipecat 启动会预热 NLTK 分句器,缺 punkt_tab 会尝试联网下载并被安全策略拦 (SSRF attempt to restricted IP),setup 超时后管线卡住。

服务器上已装(~/.local/share/ai-companion/nltk_data),本地开发机也要装, 否则 spike 跑不起来。p1/flows_spike.py 已加 fail-fast 检查与拷贝命令提示。


六、对现有功能的影响

现有功能 是否受影响 说明
分句 TTS 不受影响 Flows 输出 token 级 TextFrame(实测 18 块),自有分句逻辑照用
RTVI 网页显示 不受影响 token 增量与完整文本都能拿到
记忆 / 身份 / SQLite 不受影响 Flows 的 state 只存会话控制状态,长期数据仍走 MemoryStore
SearXNG 搜索 需迁移 现在的 web_search 手写循环要改成 Flows 工具
本地 vLLM fallback 会失去 FallbackProvider 不能被 Flows 直接使用(见 §三)
两条 system 消息 需要注意 见坑 4;切回本地 vLLM 时会 400

七、建议的最小生产集成 diff(Phase 3 用)

不改动现有 EngineProcessor 的音频路径,只在文字路径上分流:

  1. app/config.py 加 AI_AGENT_RUNTIME=legacy|flows,默认 legacy
  2. app/agent/ 新增 tools.py(工具定义)、memory_policy.py(记忆策略)、 flows_runtime.py(FlowManager 装配)
  3. services/bot_server.py 里,RTVI send-text 按 AI_AGENT_RUNTIME 分流: legacy → 现有 EngineProcessor;flows → 新建的 Flows 路径
  4. 语音路径暂不动,留到 Phase 5
  5. requirements.txt 不加 pipecat-ai-flows

关键注意点:

  • 用 PipelineRunner().run(worker),不要直接 worker.run()
  • await flow.initialize(node)
  • role_message 与 task_messages 的 system 消息二选一
  • DeepSeekLLMService.Settings(model="deepseek-flash") 必须显式覆盖

八、下一步

spike 已给出结论,可以进入 Phase 2(抽出工具与策略层):

  • 把现有 web_search 执行从 ConversationEngine 抽成可复用函数
  • 实现记忆工具,先用 fake store 单测
  • legacy 路径继续工作,Flows 不是唯一调用者

Phase 3(接网页文字路径)等 Phase 2 完成标准达成后再开始。


九、复现

cd ~/workspace/ai-companion

# 探索脚本(带请求体抓取与逐项诊断)
~/venvs/ai_assistant/bin/python p1/flows_spike.py
~/venvs/ai_assistant/bin/python p1/flows_spike.py --only 3

# 断言版(接 CI 用)
~/venvs/ai_assistant/bin/python app/tests/test_flows_spike.py

# 需要 DEBUG 日志排查时
SPIKE_DEBUG=1 ~/venvs/ai_assistant/bin/python p1/flows_spike.py --only 3