Phase 2:抽出工具与策略层

按 P1-Pipecat Flows Agent化与记忆改造交接.md 的 Phase 2 完成。
legacy 路径行为不变,只是改为复用新工具层。

新增 app/agent/
- memory_policy.py:记忆写入/删除的裁决层。模型只提议,策略决定。
  不依赖 LLM/网络/真实存储,可纯单元测试。
  - 身份未确认 → 一律 reject,不碰私人记忆
  - 模型传 owner_user_id/scope 等越权字段 → reject(不静默忽略)
  - 只有用户明确说"记住/以后/别忘了/我的偏好是"才写 confirmed
  - "记住我今天吃了香菜"这类一次性事件 → 降级 candidate
  - 不采信模型自称的"用户明确要求"
  - 模糊/多条删除 → needs_confirmation,不猜目标
- tools.py:工具 schema 与执行器,legacy 与 flows 共用。
  - 身份由 ToolContext 注入,**不在工具参数里**
  - 身份未确认时不暴露任何记忆工具(比暴露后拒绝更干净)
  - 写入结果如实回报:candidate 明确告知"尚未生效",
    删除如实返回"已删除/没有找到/需要确认",不把"请求已收到"说成"已做到"
  - 工具失败转成给模型的文本,不拖垮本轮对话
- llm_service.py:Pipecat LLMService 工厂,固化 Phase 1 spike 的选型结论
  (复用 DeepSeekLLMService;必须显式覆盖模型名为 deepseek-flash,
  因为 Pipecat 默认的 deepseek-v4-flash 不存在)

改造 app/engine.py
- _execute_tool 改为委托 app/agent/tools.execute(),删除重复实现
- 本轮用户原话挂到实例上,供策略层判定语义等级
- 移除不再使用的 SearchError 导入

新增 app/tests/test_agent_tools.py:45 项断言,**不依赖 LLM/网络/SQLite**
(用 fake store)。覆盖交接文档 §十 的清单:越权参数、未确认身份、
明确记住 vs 推断、同 key 幂等、模糊删除、精确删除、跨用户隔离。

回归:test_agent_tools 45/45、test_memory 30/30、test_identity 19/19、
test_search 12/12、test_transport 4/4、test_profile_memory 22/22,无退化。
This commit is contained in:
ylhan
2026-09-16 13:25:46 +08:00
parent c37a968548
commit 4fe2755fd6
6 changed files with 1150 additions and 19 deletions
+59
View File
@@ -0,0 +1,59 @@
"""Pipecat LLMService 工厂。
Phase 1 spike 的选型结论固化在这里:**复用 Pipecat 自带的 `DeepSeekLLMService`**,
不自写 `FallbackProvider → LLMService` 适配器。
理由(详见笔记库 `P1-Flows兼容性Spike结论.md`):
1. 它自带 `DeepSeekLLMAdapter`,处理"思考模式下必须把 reasoning_content
原样回传"这个约束——**正是本项目 P0 阶段踩过的坑**,框架已经做了。
2. `thinking` 默认就是 disabled,与本项目既有结论一致。
3. 支持自定义 `base_url`,将来要接本地 vLLM 也能用。
4. Flows 硬要求 `LLMService` 实例,而 `pipecat.flows` 的 `LLMAdapter`
**只管摘要生成**,不是 provider 适配器——想保留 `FallbackProvider`
必须自己写 `LLMService` 子类,成本不划算。
**代价**:失去 `FallbackProvider` 的本地 vLLM 降级。当前本机 vLLM 已停用,
fallback 实际没有可用目标,所以先只用 DeepSeek。将来若需双后端,
Pipecat 有 `LLMSwitcher` 可用,不必自写。
"""
from __future__ import annotations
import logging
from ..config import LLMConfig
log = logging.getLogger(__name__)
def create_llm_service(cfg: LLMConfig):
"""按配置造 Pipecat LLMService。
目前只支持 DeepSeek(OpenAI 兼容)。其它后端返回 None,调用方应当
回退到 legacy 路径,而不是带着一个坏 service 继续。
"""
if cfg.name != "deepseek":
log.warning("Flows 路径暂不支持后端 %r,请继续使用 legacy", cfg.name)
return None
from pipecat.services.deepseek.llm import DeepSeekLLMService
# **必须显式覆盖模型名。**
# Pipecat 的默认是 `deepseek-v4-flash`,该模型不存在,用默认值会 400。
# 见 P0-DeepSeek验证报告.md:真实清单只有 deepseek-flash 与 deepseek-v4-pro。
settings = DeepSeekLLMService.Settings(
model=cfg.model,
# 显式关闭思考。Pipecat 的默认恰好也是 disabled,但写出来更安全:
# 将来默认值变化时不会悄悄改变行为,也不至于把思考内容送进 TTS。
thinking=DeepSeekLLMService.ThinkingConfig(type="disabled"),
max_tokens=cfg.max_tokens,
)
return DeepSeekLLMService(
api_key=cfg.api_key,
base_url=cfg.base_url,
settings=settings,
)
__all__ = ["create_llm_service"]