Files
ylhan 20cfaa6ef7 基线:姓名档案修复、疑问句防误写、搜索与传输测试
这是进入 Pipecat Flows 改造前的回滚点。
提交时已核对本地与线上代码 md5 一致(engine.py 7e97bad8、
bot_server.py 1cf543ae),所以这个提交就是当前线上状态。

内容(此前一直在线上运行但未提交的改动):
- 姓名档案:用户自报"我叫林"同步写 confirmed 姓名,重连后恢复
- 疑问句防误写:"谁吗"之类的疑问句不再被覆盖成姓名
- 新增 test_profile_memory.py(姓名档案专项回归)
- 新增 test_profile_live.py(需要真实 LLM 的现场测试)
- 搜索、身份、记忆测试的相应补充

背景:下一步要按 P1-Pipecat Flows Agent化与记忆改造交接.md
渐进引入 Pipecat Flows,交接文档明确要求先保护基线、不得
reset/checkout/覆盖现有改动。此提交即该基线。
2026-09-16 12:48:18 +08:00

114 lines
4.5 KiB
Python

"""提示词构造。
关键约束(实测,P0-本地LLM验证报告.md §四):
本机 vLLM **只接受一条 system 消息**,多条返回
400 "System message must be at the beginning."
所以记忆不能作为"第二条 system"注入,必须**并进同一条里、每轮重建**。
这其实比追加式更干净,也正好满足方案 §8 的要求:
"切换身份清理上一人的私有检索上下文"、"注入的记忆每轮替换,不是追加"——
每轮从零拼出来的 system 天然不会有上一人的残留。
"""
from __future__ import annotations
from typing import Sequence
from .identity.base import IdentityDecision
from .memory.base import Memory
#: 语音助手的回复风格。方案 §5:"默认简短自然回复"。
#: 这几条不是"风格偏好",是**功能性的**——放出去的文本会直接进 TTS,
#: 带 Markdown 的话会被逐字念出来("井号井号 第一步")。
PERSONA_RULES = """\
你是一个桌面陪伴助手,通过语音和用户交流。用户听到的是合成语音,不是文字。
必须遵守:
1. 回复简短自然,适合朗读,默认一到两句话。不要长篇解释。
2. 绝对不要使用 Markdown、星号、井号、列表、表格、表情符号或括号注释。
3. 数字、日期、金额按中文口语写法,例如"一千二百元""下午三点半"。
4. 不确定的事直说不确定,不要编造。没做过的事不要说已经做了。
5. 需要操作设备时调用工具。不要假装已经执行,也不要说"正在为您打开"。
6. 用户打断时立刻停止,不要坚持把上一句说完。"""
def build_system_prompt(
identity: IdentityDecision,
memories: Sequence[Memory] = (),
*,
extra: str = "",
) -> str:
"""拼出本轮唯一的 system 消息。
每轮调用一次,不要缓存后复用——身份或记忆一变,缓存就会串。
"""
parts = [PERSONA_RULES]
if identity.confirmed:
if identity.display_name:
parts.append(
f"\n当前用户希望被称为“{identity.display_name}”。"
"这是用户可见称呼,可以在合适时自然使用。"
)
else:
parts.append("\n当前连接已绑定到一个用户档案,但尚未记录用户称呼。")
parts.append(
"内部 user_id 只用于数据隔离,绝不能把它当作用户名、姓名或称呼告诉用户。"
"如果用户问‘我是谁’而没有已知称呼,应坦白还不知道,而不是回答内部 ID。"
)
else:
# 方案 §6:未知身份只用访客上下文,不做个性化
parts.append("\n当前用户身份未确认,按访客对待。"
"不要提及任何私人信息,也不要假定你知道对方是谁。")
if memories:
lines = ["\n已知的用户信息(供参考,不要逐条复述):"]
for m in memories:
lines.append(f"- {m.text}")
parts.append("\n".join(lines))
if identity.suggestions and not identity.confirmed:
# 有人脸候选但不自动切换。让助手可以自然地问一句,
# 而不是自作主张切换档案(方案 §6:"看到熟人不等于确认他正在说话")。
names = "、".join(
s.candidate_user_id for s in identity.suggestions[:2])
parts.append(
f"\n注意:摄像头可能认出了 {names},但这不代表对方正在说话。"
f"不要据此切换身份或提及该用户的私人信息。")
if extra:
parts.append("\n" + extra.strip())
return "\n".join(parts)
def trim_history(
history: Sequence[dict],
*,
max_turns: int,
max_chars: int = 4000,
) -> list[dict]:
"""裁掉过旧的对话轮次。
方案 §8 要求"对话摘要限长、按需检索",不默认永久保存逐字稿。
这里做最简单的一档:保留最近 max_turns 轮,再按字符数兜底。
上下文越短,本地 LLM 的首 token 越短——这不是省资源,是直接买延迟。
"""
msgs = list(history)
# 一轮 = 一问一答,所以保留的消息数是轮数的两倍
keep = max_turns * 2
if len(msgs) > keep:
msgs = msgs[-keep:]
# 按字符数从旧到新裁,保证裁完仍然从 user 消息开始
while msgs and sum(len(m.get("content") or "") for m in msgs) > max_chars:
msgs.pop(0)
while msgs and msgs[0].get("role") != "user":
msgs.pop(0)
return msgs
__all__ = ["build_system_prompt", "trim_history", "PERSONA_RULES"]