diff --git a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md index 4b15bf2..66a6592 100644 --- a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md +++ b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md @@ -396,13 +396,15 @@ P5通过Home Assistant统一调用。按实际设备比较官方Xiaomi Home和Xi ## 10. 运行维护与当前下一步 -### 已完成(截至 2026-09-15) +### 已完成(截至 2026-09-16) - Ubuntu 24.04 已确认,USB 接口已盘点;依赖版本固定在代码仓库的 `requirements.txt` - 助手本体在服务器上实现并跑通,端到端测试 16/16(**用文件音频,非实时链路**) - STT/TTS/LLM 三方基准实测;DeepSeek 四条路径验证 - 记忆与身份的数据层实现并测试(88 项断言) - 代码剥离为独立仓库 `~/workspace/ai-companion/` +- 网页文字与语音对话、助手文字显示、DeepSeek Function Calling + 私有 SearXNG 已部署 +- 姓名档案跨连接恢复已修复;内部 user_id 与用户称呼隔离 ### 下一步顺序(按风险从高到低) @@ -417,6 +419,7 @@ Pipecat Playground 做了真人浏览器对话,麦克风 → STT → LLM → T | 0 | **音频边界定稿** | ✅ 2026-09-15 完成(见 §5) | 决定了客户端接口形态 | | 1 | **服务端 WebRTC 入口 + 客户端骨架** | ✅ **实时链路已闭环**(见 `P0-Pipecat传输层实验报告.md`) | 已验证:信令、ICE、data channel、帧路由、STT→LLM→TTS、文件与浏览器真人非静音回复 | | 1b | **查清帧路由根因 → 定 A/B 方案** | ✅ **根因已定位,采用 A 方案** | NLTK 运行时下载导致 setup timeout;不是模型不兼容。网页 RTVI 保留 | +| 1c | **Agent 与通用记忆工具化** | 🟡 **已选 Pipecat Flows,待兼容性 spike** | 保留现有实时链路与 MemoryStore;先文字、后语音,legacy 可回滚。见 `P1-Pipecat Flows Agent化与记忆改造交接.md` | | 2 | **N5095 客户端骨架** | 🔶 雏形已有(`client/test_client.py`) | 非 trickle ICE、data channel 心跳、文件音频实时发送、回复录音都已实现。**不做 AEC/播客参考/打断**——那些要真实声卡 | | 3 | **接入真实麦克风与音箱** | 阻塞:硬件未购;网页仅验证普通麦克风对话 | 唯一能完整验证阵列 AEC、打断、本地唤醒的一步,也是 P2 的全部内容 | | 4 | **STT 中文识别正确率** | 阻塞:需录音 | 见下"最大功能风险" | @@ -444,6 +447,24 @@ ICE 候选、Playground 需要 `/start`、`run()` 要 WorkerParams、后台异 **理由**:语音链路是全局瓶颈。它若走不通,后面的表情、转头做得再好也没用; 而它若走通,其余部分都是可分解的独立工作。 +### Agent、记忆与打断决策(2026-09-16) + +当前系统是固定实时对话流水线加一个最多两轮的 `web_search` 工具循环,不是通用 +Agent。框架对比后决定渐进引入 **Pipecat Flows**,不整体迁移 OpenClaw、Agno、 +LangGraph、LiveKit Agents 或 smolagents。原因是现有 Pipecat/WebRTC 链路已经闭环, +Flows 能提供状态、函数注册和对话节点,而身份、隐私、记忆与低延迟策略仍保留在 +项目内。 + +必须先验证接口差异:Flows 期待 Pipecat `LLMService + context aggregator`,当前 +DeepSeek fallback、历史、STT/TTS 和工具循环都在自有 `ConversationEngine` 中。 +因此先做独立 spike,再以 `legacy|flows` feature flag 渐进迁移,禁止直接替换线上 +路径。完整实施范围、测试和回滚要求见 [[P1-Pipecat Flows Agent化与记忆改造交接]]。 + +当前通用长期记忆尚未闭环:姓名是同步 confirmed 档案;“记住/我喜欢/我习惯”等 +规则仍统一写 candidate,默认检索排除,也没有确认晋升入口。当前打断也只是能量 VAD +切分话段加任务取消能力;助手说话时检测到用户声音只记日志,尚未同时取消生成并清空 +客户端播放缓冲。这两项是 Flows 改造后的核心验收,不得写成“已完成”。 + ### 最大功能风险:STT 中文识别率 Zipformer 的速度没问题(服务器 RTF 0.03),但**中文真人识别率至今没有可信数据**—— diff --git a/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md index bcfa915..adea1d5 100644 --- a/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md +++ b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md @@ -122,6 +122,21 @@ metrics 与 interruption,不重写已有的记忆、身份、LLM/TTS 适配。 - **不做 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化与记忆改造交接]]。 + ## 八、复现 ```bash diff --git a/有趣的项目/AI桌面陪伴助手/P1-Flows兼容性Spike结论.md b/有趣的项目/AI桌面陪伴助手/P1-Flows兼容性Spike结论.md new file mode 100644 index 0000000..bbacbc7 --- /dev/null +++ b/有趣的项目/AI桌面陪伴助手/P1-Flows兼容性Spike结论.md @@ -0,0 +1,256 @@ +# 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 +``` diff --git a/有趣的项目/AI桌面陪伴助手/P1-Pipecat Flows Agent化与记忆改造交接.md b/有趣的项目/AI桌面陪伴助手/P1-Pipecat Flows Agent化与记忆改造交接.md new file mode 100644 index 0000000..75ee01c --- /dev/null +++ b/有趣的项目/AI桌面陪伴助手/P1-Pipecat Flows Agent化与记忆改造交接.md @@ -0,0 +1,501 @@ +# P1 · Pipecat Flows Agent 化与记忆改造交接 + +> 日期:2026-09-16 +> 状态:方案已选定,等待实现 +> 实现仓库:`~/workspace/ai-companion` +> 部署目录:`yonglin@cadadmin-server.taild0f91d.ts.net:~/ai-companion` + +## 一、给 Claude Code 的任务摘要 + +在**不推翻现有实时语音链路**的前提下,渐进引入 Pipecat Flows,把当前手写的 +单工具循环升级为受策略约束的 Agent 工具层,并补齐通用长期记忆的确认、读取、 +修改和忘记流程。 + +必须遵守以下边界: + +1. 保留现有 `MemoryStore`、SQLite 数据、身份隔离、DeepSeek fallback、SearXNG、 + RTVI 网页显示和流式 TTS。 +2. 先做 Pipecat Flows 与当前 `ConversationEngine` 的兼容性 spike;验证通过前, + 不得替换线上默认路径。 +3. 使用 `AI_AGENT_RUNTIME=legacy|flows` 保留可立即回滚的 legacy 路径。 +4. 模型不得决定或传入 `owner_user_id`;用户身份必须由服务端会话上下文注入。 +5. 用户明确要求记忆与模型推测必须分开。模型推测不得直接成为 confirmed。 +6. 不把 Pipecat Flow state 当成长期记忆;长期数据仍通过 `MemoryStore` 持久化。 +7. 不顺手实现唤醒词、硬件 AEC、人脸自动切换、向量数据库或 systemd。 + +## 二、开始前必须知道的仓库状态 + +### 代码和部署 + +- Git 已提交基线包含: + - `c3d2c7f`:修复 Pipecat 实时链路并补齐网页测试 + - `719fc13`:修复网页对话并接入 SearXNG 搜索 +- 2026-09-16 的姓名档案修复、疑问句防误写和测试目前仍是**未提交改动**。 +- 线上服务已运行这些未提交改动,不等于服务器代码与 Git HEAD 相同。 +- 开始工作前必须运行 `git status --short` 和 `git diff`;不得 reset、checkout + 或覆盖现有改动。 +- 线上健康接口:`http://127.0.0.1:7860/health`。 +- 网页调试入口通过 SSH 隧道访问:`http://localhost:7860/client/`。 + +### 当前已验证能力 + +- SmallWebRTC 信令、ICE、data channel、音频输入输出已闭环。 +- 浏览器真人麦克风 → STT → DeepSeek → Qwen3-TTS → 浏览器播放正常。 +- 网页文字输入、用户文本、助手文本和语音回复正常。 +- DeepSeek 能自主调用 `web_search`,SearXNG 工具结果能回灌后继续回答。 +- 用户自报“我叫林”会同步写入 confirmed 姓名档案,disconnect/reconnect 后恢复。 +- 姓名疑问句不会再把“谁吗”之类内容覆盖为姓名。 +- 当前专项回归: + - 姓名档案 22/22 + - 记忆存储 30/30 + - 身份 19/19 + - 搜索 12/12 + - 传输 4/4 + +### 线上姓名数据库备份 + +- 首次档案迁移前: + `/home/yonglin/.local/share/ai_assistant/memory.db.before-name-profile-20260916` +- 疑问句误写修复前: + `/home/yonglin/.local/share/ai_assistant/memory.db.before-name-question-20260916-1140` + +不要删除这些备份。后续若改 schema,必须另建带时间戳的备份。 + +## 三、当前系统到底是什么 + +当前不是完整通用 Agent,而是: + +```text +SmallWebRTC / RTVI + ↓ +EngineProcessor + ↓ +SimpleVAD → StreamingSTT + ↓ +ConversationEngine + 身份 → 自动记忆检索 → DeepSeek/搜索工具循环 → 分句 TTS + ↓ +WebRTC 音频与 RTVI 文本 +``` + +`ConversationEngine._stream_and_speak()` 手写了最多两轮的 `web_search` Function +Calling 循环。模型能自主决定是否搜索,但还不能自主调用记忆工具。 + +Pipecat 当前主要负责传输、Frame、PipelineWorker 和 RTVI;STT、LLM、TTS 语义链路 +仍由自有引擎调度。 + +## 四、当前记忆决策与真实缺口 + +### 写入 + +| 输入 | 当前行为 | 实际是否可复用 | +|---|---|---| +| “我叫林” | 同步更新 `users.display_name`,写 confirmed 姓名 | 是 | +| “记住我不吃香菜” | 回复后规则抽取,写 candidate | 否,默认检索排除 | +| “我喜欢咖啡” | 回复后规则抽取,写 candidate | 否,默认检索排除 | +| 普通聊天 | 只保留当前内存历史和 user turn | 重连后不可用 | + +当前没有 candidate 的确认、晋升或管理入口。因此除姓名外,“写进数据库”和“助手会在 +以后使用”不是一回事。 + +### 读取 + +- 每条已确认身份的用户消息都会尝试检索,不是模型按需决定。 +- 默认只读取 confirmed、private/shared、未过期记忆。 +- 最多 5 条,超时 300ms 后无记忆继续回答。 +- 当前是中文字符/二元组字面相似度;记忆总量很小时,无命中会退回最近记忆。 +- 用户姓名作为档案固定注入,不依赖语义检索。 + +### 必须达到的目标语义 + +1. **稳定档案**:姓名、称呼、语言和明确的稳定偏好;连接开始时加载。 +2. **显式长期记忆**:用户明确说“记住/以后/别忘了”,经策略校验后 confirmed。 +3. **候选观察**:普通聊天中推断出的可能信息只能 candidate,不直接影响回答。 +4. **事件/摘要**:会话事件与逐字稿不冒充永久事实,可过期、可删除。 +5. **矛盾更新**:同 owner + kind + key 的 confirmed 新值覆盖或 supersede 旧值。 +6. **透明控制**:支持查看、确认、修改、忘记;模糊删除必须追问。 + +## 五、为什么选择 Pipecat Flows + +### 候选比较 + +| 框架 | 优点 | 与本项目的主要问题 | 结论 | +|---|---|---|---| +| Pipecat Flows | 已在 Pipecat 生态;状态、节点、函数注册和校验;支持 DeepSeek 等 OpenAI-compatible 服务 | 期待 Pipecat `LLMService + context aggregator`,与当前自有引擎有接口差异 | **选择,渐进集成** | +| Agno | 内置 user/session/memory,最接近轻量版 OpenClaw | 会与现有会话、历史和记忆层重叠 | 暂不采用 | +| PydanticAI | 类型安全、依赖注入、流式工具事件、较轻 | 长期记忆策略仍要自建;与现有工具循环收益有限 | 备选 | +| LangGraph | 持久状态、checkpoint、长任务和人工审批成熟 | 对当前实时陪伴链路过重 | 暂不采用 | +| LiveKit Agents | 完整实时语音 Agent、工具、handoff | 相当于替换已跑通的 Pipecat/WebRTC;长期记忆仍需外接 | 不迁移 | +| smolagents | 代码少、多步工具 Agent | 偏任务型,多步延迟高,API仍偏实验 | 不用于主链路 | +| OpenClaw | 通用 Agent runtime、分层记忆和后台整理完整 | 运行时过重、默认个人 Agent 假设、实时语音和身份边界仍需定制 | 不整体迁移 | + +官方资料: + +- [Pipecat Flows Overview](https://docs.pipecat.ai/api-reference/pipecat-flows/overview) +- [Pipecat Function Calling](https://docs.pipecat.ai/pipecat/learn/function-calling) +- [Agno Memory](https://docs.agno.com/examples/memory/overview) +- [PydanticAI Agents](https://ai.pydantic.dev/agents/) +- [LangGraph Memory](https://docs.langchain.com/oss/python/langgraph/add-memory) +- [LiveKit Chat Context](https://docs.livekit.io/agents/logic/chat-context/) +- [OpenClaw Memory](https://docs.openclaw.ai/concepts/memory) + +### 选型原则 + +选择 Pipecat Flows,不是为了追求“框架化”,而是为了: + +- 复用现有实时媒体栈; +- 把工具 schema、执行、结果回灌和状态切换从手写循环中抽离; +- 给记忆确认/删除等多轮流程提供显式状态; +- 保留产品特有的身份、隐私和低延迟策略。 + +## 六、最重要的集成风险 + +Pipecat Flows 的正常构造需要: + +- `PipelineTask` +- Pipecat `LLMService` +- context aggregator +- 由 Pipeline Frame 驱动的函数调用和消息更新 + +当前项目则是: + +- `PipelineWorker` + 自定义 `EngineProcessor` +- 自有 `FallbackProvider`,不是 Pipecat `LLMService` +- 自有消息历史与 prompt 构造 +- 自有 STT/TTS 调用,不是标准 Pipecat service processor +- RTVI 事件由 `EngineProcessor` 手工补发 + +因此不能直接“安装 Flows,然后包住 ConversationEngine”。如果忽略这一点,很容易出现: + +- 两套消息历史同时增长; +- 工具调用重复执行; +- token 既被自有引擎又被 Pipecat aggregator 消费; +- RTVI 文本重复或消失; +- TTS 重复播报; +- 取消只停止其中一套任务; +- 身份切换后旧 context 泄漏。 + +## 七、目标架构 + +```text + ┌─────────────────────────────┐ +WebRTC / RTVI ─────▶│ Pipecat Pipeline │ + │ transport + turn + metrics │ + └──────────────┬──────────────┘ + │ + ┌──────────────▼──────────────┐ + │ Flows Runtime(可切换) │ + │ node / state / tools │ + └──────────────┬──────────────┘ + │ + ┌────────────────────────┼────────────────────────┐ + ▼ ▼ ▼ + LLMService 适配器 Memory Policy Web Search + DeepSeek/Fallback + MemoryStore SearXNG + │ │ + ▼ ▼ + 流式文本与 TTS SQLite(继续使用) +``` + +### Flow state 只保存会话控制状态 + +建议字段: + +- `session_id` +- 服务端绑定的 `user_id`(内部使用,不展示给模型) +- `display_name` +- `identity_version` +- `pending_memory_action` +- `pending_confirmation` +- 当前 node + +不要把全部记忆内容复制进 Flow state,也不要把 state 当数据库。 + +### 初始节点 + +第一版只需要两个节点,不要设计庞大状态图: + +1. `conversation` + - 普通陪伴对话; + - 可用只读搜索和受限记忆工具; + - 默认节点。 +2. `confirm_memory_change` + - 只在修改、删除目标不明确或敏感写入时进入; + - 用户确认/取消后返回 `conversation`。 + +## 八、工具设计 + +### 全局工具 + +#### `web_search(query)` + +- 复用 `app/search.py` 与 SearXNG。 +- 保留最多两次连续搜索限制。 +- 搜索失败返回工具错误结果,不让整轮异常退出。 +- 来源 URL 保留给网页,TTS 继续不朗读来源段。 + +#### `memory_search(query)` + +- `owner_user_id` 从会话注入,禁止模型传入。 +- 只返回 confirmed、未过期、授权 scope。 +- 不确认身份时返回不可用,不搜索任何私人记忆。 +- 结果条数和超时沿用配置上限。 + +#### `memory_remember(kind, key, value)` + +- 仅确认身份可用。 +- 只有当前用户明确表达“记住/以后/别忘了/我的偏好是”等意图时,才能 confirmed。 +- 模型推断或总结只能写 candidate。 +- `owner_user_id`、source session/turn、scope 由服务端填充。 +- 默认 private;模型不得扩大成 shared。 +- 同 key confirmed 写入保持幂等并更新旧值。 +- 姓名确定性快路径暂时保留,不立即交给模型。 + +#### `memory_forget(selector)` + +- 仅确认身份可用。 +- 精确匹配一条时可返回待删除摘要;删除前应有明确用户意图。 +- 多条或模糊匹配进入 `confirm_memory_change`,不得猜测删除目标。 +- 删除必须同时匹配 owner;不得只凭 memory_id。 +- 返回“已删除/没有找到/需要确认”,不能把“请求已接收”说成“已经删除”。 + +#### `memory_update(key, value)` + +- 用于用户明确纠正已有事实或偏好。 +- 更新必须保留来源和更新时间。 +- 若未来增加 superseded 状态,先做向后兼容 migration,不删旧 schema。 + +#### `profile_update(display_name)` + +- 第一阶段可继续使用现有确定性姓名提取,不必立即暴露给模型。 +- 如果以后工具化,也必须经过同一疑问句和角色描述过滤。 + +### 记忆执行策略必须独立于 prompt + +不要仅依赖“请不要误记”的 system prompt。新增类似: + +```text +app/agent/memory_policy.py +``` + +策略层输入至少包含: + +- 当前原始 user text +- 工具请求 +- 已确认身份 +- 当前 session/turn +- 已有同 key 记忆 + +策略层输出: + +- allow_confirmed +- allow_candidate +- needs_confirmation +- reject + +模型可以提出动作,策略层拥有最终决定权。 + +## 九、分阶段实施 + +### Phase 0:保护基线 + +1. 运行 `git status --short`、保存 `git diff`。 +2. 运行当前相关测试。 +3. 记录网页文字、网页语音、搜索、姓名重连的基线延迟与行为。 +4. 不修改线上数据库 schema。 + +完成标准:现有未提交修复没有丢失,基线测试结果有记录。 + +### Phase 1:独立兼容性 spike + +新增独立实验,不先改生产入口,例如: + +```text +p1/flows_spike.py +app/tests/test_flows_spike.py +``` + +需要验证: + +1. 找到与 `pipecat-ai==1.10.0` 兼容的 `pipecat-ai-flows` 版本并精确 pin。 +2. DeepSeek OpenAI-compatible 接口能在 Flows 中流式输出中文内容。 +3. 流式 function-call arguments 能被正确聚合一次,不重复拼接。 +4. 工具结果回灌后能生成最终回答。 +5. 任务取消能终止 LLM 请求和后续工具执行。 +6. context aggregator 不产生第二条 system message。 +7. 能获得 token 级文本事件,满足网页显示和分句 TTS。 + +若任何一项不成立,停止生产集成,先记录结论;不得为了使用框架而绕过现有正确行为。 + +### Phase 2:抽出工具和策略层 + +建议新增: + +```text +app/agent/ +├── __init__.py +├── tools.py +├── memory_policy.py +├── flows_runtime.py +└── llm_service.py +``` + +- 先把现有 `web_search` 执行从 `ConversationEngine` 抽成可复用函数。 +- 实现记忆工具,但先用 fake store 单测。 +- legacy 路径继续工作;不要让 Flows 成为唯一调用者。 + +完成标准:工具层可被 legacy 和 flows 两条路径复用,策略测试不依赖真实 LLM。 + +### Phase 3:先接网页文字路径 + +- 新增 `AI_AGENT_RUNTIME`,默认仍为 `legacy`。 +- 只让 RTVI `send-text` 在显式 `flows` 配置下走新路径。 +- 保持网页用户文本、token 增量、完整 assistant 文本和可选 TTS。 +- 语音路径暂时保持 legacy。 + +完成标准:同一服务可切换两条 runtime;legacy 回归无变化;flows 文字路径通过工具、 +姓名、记忆和搜索验收。 + +### Phase 4:补齐通用记忆闭环 + +至少覆盖: + +1. “记住我不吃香菜” → confirmed。 +2. disconnect/reconnect → “我不吃什么”能召回香菜。 +3. “我今天吃了香菜”不得自动变成稳定偏好。 +4. “其实我现在吃香菜了”能明确更新旧偏好,而不是保留冲突 active 值。 +5. “忘掉我的饮食偏好”能删除;目标模糊时先确认。 +6. 访客无法读取或写入私人记忆。 +7. 用户 A 的记忆绝不出现在用户 B 的 prompt、工具结果或日志中。 + +### Phase 5:迁移语音路径 + +只有 Phase 3、4 稳定后才迁移语音: + +- 保持 STT transcript 和原始 user text 一致; +- 保持首句尽快进入 TTS; +- 工具调用期间不得把工具 JSON 念出来; +- 来源段不进入 TTS; +- 用户打断时取消 Flows、LLM、工具和 TTS 的同一轮 execution; +- `ConversationEngine` 中已经稳定的身份与记忆接口可以拆分复用,不要求一次删除旧类。 + +### Phase 6:打断控制链 + +当前 `SimpleVAD` 只做 RMS 话段切分。下一步先实现控制闭环,不在本阶段承诺真实硬件 +声学效果: + +```text +user-started-speaking + ├─ 发出 InterruptionFrame / 客户端停播信号 + ├─ 取消当前 Flows/LLM/TTS execution + ├─ 清空未发送和客户端已缓冲音频 + └─ 保留新用户话语的 pre-roll 并继续收集 +``` + +要求: + +- 不等待 700ms 尾静音才停止助手; +- 使用开始阈值、连续帧或正式 VAD,避免单个噪声帧误触发; +- `_bot_speaking` 不能只代表服务端推帧结束,要考虑客户端仍在播放; +- 文字请求期间的麦克风回采不得错误抢占; +- 先做合成 Frame 回归;真实 AEC、扬声器回声和远场误触发留到硬件 P2。 + +### Phase 7:部署与清理 + +- flows 默认值只有在线上验收后才能从 legacy 改为 flows。 +- 更新 README、部署说明和本交接文档。 +- 在修改数据库前创建新备份。 +- 确认 `/health`、网页文字、网页语音、搜索和重连记忆。 +- 确认日志没有 API key、私人全文记忆或内部 ID 泄漏。 + +## 十、测试与验收矩阵 + +### 必须新增的单元测试 + +- 工具参数 schema 与非法参数。 +- 模型不能传 owner_user_id/scope 越权。 +- 未确认身份拒绝 private memory。 +- 明确“记住”写 confirmed。 +- 推断信息只写 candidate。 +- 同 key 更新幂等。 +- 模糊忘记返回 needs_confirmation。 +- 精确忘记只删除当前 owner。 +- Flows node 切换后不会重复执行工具。 +- 取消后不再写记忆或发送音频。 + +### 必须保留的现有测试 + +```bash +.venv/bin/python app/tests/test_profile_memory.py +.venv/bin/python app/tests/test_memory.py +.venv/bin/python app/tests/test_identity.py +.venv/bin/python app/tests/test_search.py +.venv/bin/python app/tests/test_transport.py +.venv/bin/python app/tests/test_engine_e2e.py +``` + +### 真人网页验收 + +1. 文字:“记住我不吃香菜。” +2. disconnect/reconnect。 +3. 文字:“我不吃什么?” +4. 语音重复 1–3。 +5. 问实时问题,确认会搜索且显示来源。 +6. 助手播长回答时插话,确认音频停止、插话未丢失。 +7. 问“我是谁”,只能回答用户称呼,不能回答内部 ID。 + +### 性能验收 + +- 无工具普通对话不能因为 Flows 固定增加一次额外 LLM 调用。 +- 记忆检索仍有超时和失败降级。 +- 分别记录 VAD、STT、记忆检索、LLM 首 token、工具、TTS 首段和首音。 +- 对比 legacy/flows 的 P50/P95;无工具首音出现明显回退时不得切默认。 + +## 十一、明确非目标 + +本次不要做: + +- 唤醒词和 20 秒连续对话窗口; +- N5095 产品客户端、声卡 AEC 或阵列波束形成; +- 人脸自动确认说话者; +- Mem0、Chroma、Graphiti、LanceDB 等新存储; +- OpenClaw、Agno、LangGraph 或 LiveKit 迁移; +- 多 Agent、子 Agent 或复杂任务规划; +- 米家/Home Assistant 工具; +- systemd、自启动和公网暴露; +- 清理或重写所有 legacy 代码。 + +## 十二、回滚要求 + +1. `AI_AGENT_RUNTIME=legacy` 必须能绕过全部 Flows 代码。 +2. 新数据库字段必须向后兼容,禁止 drop/rename 现有表或列。 +3. 工具执行要有幂等键或 source turn,避免重试重复写入。 +4. 新路径异常时,本轮可以无记忆继续,但不能自动切换后重复执行有副作用工具。 +5. 部署前保留数据库与上一版本代码;回滚不能依赖重新生成用户数据。 + +## 十三、建议 Claude Code 的第一轮动作 + +```bash +cd ~/workspace/ai-companion +git status --short +git diff --check + +# 先读这些文件,不要先改 +sed -n '1,240p' services/bot_server.py +sed -n '240,560p' services/bot_server.py +sed -n '180,500p' app/engine.py +sed -n '1,220p' app/memory/base.py +sed -n '220,380p' app/memory/sqlite_store.py +``` + +然后只完成 Phase 1 spike,提交一份结论: + +- 兼容的 flows 版本; +- 选择复用 Pipecat OpenAI-compatible `LLMService`,还是实现 + `FallbackProvider → LLMService` 适配器; +- token、tool-call、取消和 context 的实测结果; +- 建议的最小生产集成 diff; +- 是否会破坏现有分句 TTS、RTVI 和 fallback。 + +未经 spike 结论,不进入 Phase 3。 +