Phase 3:Flows 接管网页文字路径(默认仍 legacy)

按 P1-Pipecat Flows Agent化与记忆改造交接.md 的 Phase 3 完成。
**语音路径保持 legacy**,文字路径可由 AI_AGENT_RUNTIME=flows 切换。
服务器实测通过工具、姓名、记忆三项验收,随后按文档要求改回 legacy。

新增 app/agent/flows_runtime.py
- FlowsBridge:LLM 文本帧 → RTVI 消息 + 分句 TTS,产出与 legacy 的
  _run_turn 一致(bot-llm-started → 逐 token bot-llm-text → stopped
  → bot-transcription / bot-output)
- UserTextRecorder:截下用户原话供策略层判定语义等级
- build_flows_parts / make_flow_manager:拆成两段,解决
  "FlowManager 要 worker、worker 要等管线" 的先后依赖
- 节点只用 task_messages,**不用 role_message**(避免两条 system 消息)
- respond_immediately=False(不自动开口)
- handler 用闭包捕获 ctx_factory,不用模块级全局(并发会话会串身份)

新增 app/agent/llm_service.py
- 固化 Phase 1 spike 的选型:复用 DeepSeekLLMService,显式覆盖模型名
  (Pipecat 默认的 deepseek-v4-flash 不存在)

配置 app/config.py
- 新增 AgentConfig + AI_AGENT_RUNTIME,**默认 legacy**

改造 services/bot_server.py
- EngineProcessor 加 owns_text:flows 模式下语音仍归它,文字帧往下传
- runtime_deps() 暴露 memory/identity/session/tts,两条路径共用同一份资源
  (避免各建一份 SQLite 连接或各加载一次 TTS)
- **_run_task 改用 PipelineRunner**:直接 task.run() 时 runner 没挂上,
  Flows 的工具执行会失败("runner is not set; call attach() first"),
  异常被转成给 LLM 的通用错误串,默认日志看不到。这正是 Phase 1 spike
  记录的坑 2,集成时又踩了一次。

新增 app/tests/test_flows_runtime.py(22 项,不依赖网络)

新增 deploy.sh
- **不用 --delete**、一次只同步一个目录、显式排除清单。
  起因见下。

客户端 client/test_client.py 加 --text 模式(RTVI send-text)
- 信封字段实测校正两次:**是 id 不是 msg_id**,**data.content 不是 text**;
  写错会被服务端以 "Invalid RTVI transport message" 拒掉且客户端无感知
- 等待条件改为"等到非静音音频",不能数音频块——WebRTC 空闲时也在推静音

服务器实测(2026-09-16)
- legacy 文字:11.2s 音频 / 25 个有声段
- flows 文字:9.3s 音频 / 18 个有声段,首音约 2.8s
- flows 记忆闭环:只说"记住我不吃香菜" → 工具写入 confirmed 记忆
  → 数据库确认 #4 [confirmed] preference 饮食偏好=不吃香菜
- 称呼"林"从档案恢复

已知问题(留给 Phase 4/7)
- Pipecat 报 "Both system_instruction and an initial system message in
  context are set" 警告。来源未查明(我只用了 task_messages)。
  切回本地 vLLM 时可能 400,因为该后端拒绝多条 system 消息。
- Flows 路径暂不暴露 web_search(交接文档要求分阶段,搜索仍走 legacy)
This commit is contained in:
ylhan
2026-09-16 14:13:32 +08:00
parent 4fe2755fd6
commit 4f621dc95b
6 changed files with 798 additions and 9 deletions
Executable
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# 部署到服务器。
#
# **为什么要有这个脚本**:2026-09-16 我用一条 rsync 命令把服务器上的东西
# 删了一大片,教训有三条,都固化在这里:
#
# 1. **绝不用 `--delete`。** 服务器上还有本地没有的东西(.venv、models 软链、
# logs、数据库软链等)。`--delete` 会把它们全删掉。
# 实测后果:.venv/bin 与 100+ 个 *.dist-info 被删、services/ 被清空、
# README/部署说明被删、models 软链被删。
# 2. **一次只同步一个目录,不要多源。** 多源 rsync 不带 --relative 时,
# 会把 `app/` 的内容**平铺**到目标根目录,而不是放进 `app/` 下。
# 3. **排除清单要显式。** 即使不用 --delete,也别把 __pycache__ 之类带过去。
#
# 用法:
# ./deploy.sh # 同步代码
# ./deploy.sh --restart # 同步后重启两个服务
set -uo pipefail
HOST="${AI_DEPLOY_HOST:-yonglin@cadadmin-server.taild0f91d.ts.net}"
DEST="${AI_DEPLOY_DEST:-~/ai-companion}"
HERE="$(cd "$(dirname "$0")" && pwd)"
# 服务器上不该被本地覆盖的东西——保留在服务器上
EXCLUDES=(
--exclude '__pycache__'
--exclude '*.pyc'
--exclude '.baseline'
--exclude '.venv'
--exclude 'logs'
--exclude 'models'
--exclude 'out'
--exclude 'p0/stt_tts_bench/models'
)
sync_one() { # sync_one <本地目录> <远端子路径>
echo " → $2"
rsync -az "${EXCLUDES[@]}" -e "ssh -o BatchMode=yes" \
"$HERE/$1" "$HOST:$DEST/$2"
}
echo "部署到 $HOST:$DEST"
sync_one "app/" "app/"
sync_one "services/" "services/"
sync_one "client/" "client/"
sync_one "p1/" "p1/"
for f in README.md 部署说明.md requirements.txt; do
[ -f "$HERE/$f" ] && rsync -az -e "ssh -o BatchMode=yes" "$HERE/$f" "$HOST:$DEST/"
done
# 把 requirements 里没有、但运行需要的包装上(幂等)
ssh -o BatchMode=yes "$HOST" "cd $DEST && .venv/bin/pip -q install pipecat-ai-small-webrtc-prebuilt 2>&1 | tail -1"
if [ "${1:-}" = "--restart" ]; then
echo "重启服务"
ssh -o BatchMode=yes "$HOST" 'bash -s' <<REMOTE
cd ~/ai-companion
for p in \$(pgrep -f "services/bot_server|tts_server.py" 2>/dev/null); do kill "\$p" 2>/dev/null; done
sleep 3
cd ~/tts-qwen && setsid nohup .venv/bin/python tts_server.py --port 18040 > logs/tts_service.log 2>&1 < /dev/null &
sleep 1
cd ~/ai-companion && setsid nohup .venv/bin/python services/bot_server.py --host 0.0.0.0 --port 7860 > logs/bot_server.log 2>&1 < /dev/null &
for i in \$(seq 1 30); do
curl -sf -o /dev/null http://127.0.0.1:7860/health 2>/dev/null && { echo " bot ✅"; break; }
sleep 3
done
curl -sf http://127.0.0.1:18040/health 2>/dev/null | grep -q model_loaded && echo " tts ✅" || echo " tts ⚠️"
REMOTE
fi
echo "完成"