交接文档规定"未经 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 要点。
257 lines
11 KiB
Markdown
257 lines
11 KiB
Markdown
# P1 · Pipecat Flows 兼容性 Spike 结论
|
||
|
||
> 日期:2026-09-16
|
||
> 状态:**spike 完成,7 项验证全部通过**
|
||
> 代码:`p1/flows_spike.py`(探索脚本)、`app/tests/test_flows_spike.py`(断言版)
|
||
> 前置:[P1-Pipecat Flows Agent化与记忆改造交接](P1-Pipecat%20Flows%20Agent化与记忆改造交接.md)
|
||
|
||
交接文档规定:**未经 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` 会静默失效
|
||
|
||
```python
|
||
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 完成标准达成后再开始。
|
||
|
||
---
|
||
|
||
## 九、复现
|
||
|
||
```bash
|
||
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
|
||
```
|