数据库 - 新增迁移 0003:timeline_events.memory_source_id 外键 → memory_sources,便于按 ingest 源做时间线幂等 后端 - 记忆 - 新增 ingest 后 LLM 富化(摘要/事实/时间线),可配置开关与最大字符数 - 新增证据包组装:合并 chunk、摘要、事实、时间线、故事等检索结果;支持空 query 时是否仍带 rolling 等开关 - repo/retriever/service/router/schemas/summarizer/timeline/extractor 等扩展;文档 memory-retrieval.md 更新 后端 - 对话 WS - 增加 PING/PONG;分段 ASR 日志与空音频处理;转写失败与「无助手回复」错误提示更明确 - 助手多段回复持久化使用统一分隔符,与分段逻辑一致 后端 - Agent - reply_limits:按 [SPLIT] 与段落拆段,并保证非空 fallback,供 WS 与 TTS 多段下发 后端 - 回忆录任务 - transcript ingest 记录 source_id;任务成功结?
449 lines
17 KiB
Python
449 lines
17 KiB
Python
"""
|
||
对话 Agent 提示词模板和访谈问题库
|
||
"""
|
||
|
||
from enum import Enum
|
||
from typing import Dict, List
|
||
|
||
from app.core.config import settings
|
||
|
||
|
||
class ConversationStage(str, Enum):
|
||
"""对话阶段枚举"""
|
||
|
||
CHILDHOOD = "childhood" # 童年
|
||
EDUCATION = "education" # 教育
|
||
CAREER = "career" # 事业
|
||
FAMILY = "family" # 家庭
|
||
BELIEFS = "beliefs" # 信念
|
||
SUMMARY = "summary" # 人生总结
|
||
|
||
|
||
# 访谈问题库(每阶段 2~3 条短问句,供参考;主流程仍由模型与槽位驱动)
|
||
INTERVIEW_QUESTIONS: Dict[ConversationStage, List[str]] = {
|
||
ConversationStage.CHILDHOOD: [
|
||
"你是在哪儿长大的?小时候印象最深的一件事是什么?",
|
||
"小时候家里是怎样的?父母对你影响大吗?",
|
||
],
|
||
ConversationStage.EDUCATION: [
|
||
"求学阶段印象最深的是哪段经历?",
|
||
"有没有哪位老师或同学对你影响特别大?毕业后最初想做什么?",
|
||
],
|
||
ConversationStage.CAREER: [
|
||
"第一次工作还记得吗?当时心情怎样?",
|
||
"事业里遇到过最大的挑战是什么?怎么挺过来的?有没有特别自豪的时刻?",
|
||
],
|
||
ConversationStage.FAMILY: [
|
||
"和伴侣是怎么认识的?做父母时最难忘或最骄傲的瞬间?",
|
||
"家庭在你的人生里扮演什么角色?",
|
||
],
|
||
ConversationStage.BELIEFS: [
|
||
"有没有一直坚守的信念或座右铭?哪些价值观对你最重要?",
|
||
"你怎样理解成功与幸福?低谷时是什么支撑你?",
|
||
],
|
||
ConversationStage.SUMMARY: [
|
||
"回顾一生,最重要的经验或教训是什么?最感激的人与事有哪些?",
|
||
"若能对年轻时的自己说一句话,你会说什么?",
|
||
],
|
||
}
|
||
|
||
|
||
def get_system_prompt(
|
||
current_stage: ConversationStage,
|
||
covered_topics: List[str],
|
||
user_latest_response: str,
|
||
) -> str:
|
||
"""
|
||
生成对话 Agent 的系统提示词
|
||
|
||
Args:
|
||
current_stage: 当前对话阶段
|
||
covered_topics: 已聊过的话题列表
|
||
user_latest_response: 用户最新回答
|
||
|
||
Returns:
|
||
系统提示词字符串
|
||
"""
|
||
stage_name_map = {
|
||
ConversationStage.CHILDHOOD: "童年",
|
||
ConversationStage.EDUCATION: "教育",
|
||
ConversationStage.CAREER: "事业",
|
||
ConversationStage.FAMILY: "家庭",
|
||
ConversationStage.BELIEFS: "信念",
|
||
ConversationStage.SUMMARY: "人生总结",
|
||
}
|
||
|
||
covered_topics_str = "、".join(covered_topics) if covered_topics else "暂无"
|
||
|
||
prompt = f"""你是「岁月知己」,像老朋友一样陪用户聊人生。**回复要短**,像微信聊天,不要长篇、不要文学腔。
|
||
|
||
规则:先简短接住对方一句,**最多再问一个具体问题**;禁止括号与思考过程;禁止采访腔(如「我注意到」「我想了解」);**不要重复确认**对方刚说过或上文已能推断的信息。
|
||
|
||
当前阶段:{stage_name_map.get(current_stage, current_stage.value)}
|
||
已聊话题:{covered_topics_str}
|
||
|
||
直接输出对用户说的话。"""
|
||
|
||
return prompt
|
||
|
||
|
||
def get_questions_for_stage(stage: ConversationStage) -> List[str]:
|
||
"""获取指定阶段的所有问题"""
|
||
return INTERVIEW_QUESTIONS.get(stage, [])
|
||
|
||
|
||
SLOT_NAME_MAP = {
|
||
"place": "成长的地方",
|
||
"people": "重要的人",
|
||
"daily_life": "日常生活",
|
||
"emotion": "童年感受",
|
||
"turning_event": "难忘的事",
|
||
"school": "学校经历",
|
||
"city": "求学的城市",
|
||
"motivation": "学习动力",
|
||
"challenge": "遇到的挑战",
|
||
"change": "成长变化",
|
||
"job": "工作内容",
|
||
"environment": "工作环境",
|
||
"decision": "重要决定",
|
||
"pressure": "压力与困难",
|
||
"growth": "职业成长",
|
||
"relationship": "家人关系",
|
||
"conflict": "矛盾与化解",
|
||
"support": "相互支持",
|
||
"responsibility": "家庭责任",
|
||
"value": "核心价值观",
|
||
"regret": "遗憾与释怀",
|
||
"pride": "骄傲的事",
|
||
"lesson": "人生经验",
|
||
}
|
||
|
||
STAGE_RELATED_TOPICS = {
|
||
"childhood": ["family", "education"],
|
||
"education": ["childhood", "career"],
|
||
"career": ["education", "family", "belief"],
|
||
"family": ["childhood", "career", "belief"],
|
||
"belief": ["career", "family"],
|
||
}
|
||
|
||
LIGHT_TOPICS = [
|
||
"有什么爱好或者特别喜欢的消遣方式吗?",
|
||
"最近有什么让你开心的事吗?",
|
||
"有没有什么趣事想分享?",
|
||
"平时喜欢看什么书或者电影吗?",
|
||
]
|
||
|
||
RESPONSE_STYLES = [
|
||
"empathy",
|
||
"curious",
|
||
"reflection",
|
||
"lighthearted",
|
||
"connection",
|
||
]
|
||
|
||
|
||
def get_opening_prompt(
|
||
current_stage: str,
|
||
empty_slots_readable: List[str],
|
||
user_profile_context: str = "",
|
||
) -> str:
|
||
"""空对话时 AI 先开口的提示词"""
|
||
stage_name_map = {
|
||
"childhood": "童年时光",
|
||
"education": "求学经历",
|
||
"career": "职业生涯",
|
||
"family": "家庭生活",
|
||
"belief": "人生信念",
|
||
}
|
||
stage_name = stage_name_map.get(current_stage, current_stage)
|
||
if empty_slots_readable:
|
||
topics_str = "、".join(empty_slots_readable)
|
||
topics_heading = (
|
||
f"## 当前建议话题({stage_name})\n可以从中选一个来问:{topics_str}"
|
||
)
|
||
task_question = (
|
||
"2. **必须问一个问题**:接着问一个**具体、好回答**的问题,引导用户开始分享;"
|
||
"优先落在上述还未聊透的方向上。不要问太宽泛的「有什么想聊的」。"
|
||
)
|
||
_opening_examples = {
|
||
"childhood": (
|
||
"示例(仅供参考风格):\n"
|
||
'"你好呀~ 想听听你的人生故事。你小时候是在哪儿长大的?那边有什么特别让你怀念的?"\n或\n'
|
||
'"在的!今天想聊聊你。你童年里印象最深的一件事是什么?"'
|
||
),
|
||
"education": (
|
||
"示例(仅供参考风格):\n"
|
||
'"嗨~ 想听听你求学那段日子。你印象最深的是哪段学校时光?"\n或\n'
|
||
'"在呢!你读书时有没有一位老师或同学,到现在还会想起?"'
|
||
),
|
||
"career": (
|
||
"示例(仅供参考风格):\n"
|
||
'"你好呀~ 想听听你工作这条路上故事。你第一份工作还记得吗,当时什么心情?"\n或\n'
|
||
'"在的!你现在或过去做过的工作里,哪一段你最想先聊聊?"'
|
||
),
|
||
"family": (
|
||
"示例(仅供参考风格):\n"
|
||
'"嗨~ 想听听你家里的事。和家里人相处时,有没有特别暖或难忘的一刻?"\n或\n'
|
||
'"在呢!如果用一个词形容你心里的「家」,你会想到什么?"'
|
||
),
|
||
"belief": (
|
||
"示例(仅供参考风格):\n"
|
||
'"你好呀~ 想听听你心里看重的东西。有没有一句你一直信到现在的话?"\n或\n'
|
||
'"在的!你觉得自己走到今天,最放不下或最骄傲的是什么?"'
|
||
),
|
||
}
|
||
style_examples = _opening_examples.get(
|
||
current_stage,
|
||
_opening_examples["childhood"],
|
||
)
|
||
else:
|
||
topics_heading = (
|
||
f"## 当前阶段({stage_name})\n"
|
||
"访谈结构化槽位里,这一阶段的主要问题在素材侧**已有覆盖**。"
|
||
"开场要像老朋友重逢:接近况、接续上次聊过的事、或任何用户可能想提起的新片段;"
|
||
"**禁止**为了凑问题而默认再从「童年在哪长大」等已覆盖模板重头盘问。"
|
||
)
|
||
task_question = (
|
||
"2. **问候 + 轻巧引子**:用一句温暖的话接上对话;若自然,可以问一个与近况、"
|
||
"想续上的回忆、或新冒出来的小事有关的问题。若不适合追问,问候 + 一句开放式引子即可。"
|
||
)
|
||
style_examples = (
|
||
"示例(仅供参考风格):\n"
|
||
'"嘿,又见面啦~ 今天有没有哪件事突然从脑子里冒出来,想跟我说说?"\n或\n'
|
||
'"在的!上次聊到那儿我还记着,你后来还有想起什么细节吗?"'
|
||
)
|
||
profile_section = (
|
||
f"\n## 用户基本信息\n{user_profile_context}\n" if user_profile_context else ""
|
||
)
|
||
return f"""你是「岁月知己」。用户刚进对话,**还没说话**,请你先开口。**短、像微信**,一两句问候 + 一句具体问题即可,不要排比、不要文学描写。
|
||
{profile_section}
|
||
{topics_heading}
|
||
|
||
## 任务
|
||
1. 简短问候。
|
||
{task_question}
|
||
3. 自然、温暖,但**字数要少**。
|
||
|
||
## 格式
|
||
- 可用 [SPLIT] 分成最多 2 条;或一条里「问候 + 问题」。
|
||
- 禁止括号、思考过程;不要替用户编回答。
|
||
|
||
{style_examples}
|
||
|
||
直接输出:"""
|
||
|
||
|
||
def _build_era_context(current_stage: str, user_profile_context: str) -> str:
|
||
"""根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示"""
|
||
if not user_profile_context:
|
||
return ""
|
||
|
||
birth_year = None
|
||
birth_place = ""
|
||
for line in user_profile_context.split("\n"):
|
||
if "出生年份" in line:
|
||
try:
|
||
birth_year = int(line.split(":")[1].strip().replace("年", ""))
|
||
except (ValueError, IndexError):
|
||
pass
|
||
if "出生地" in line or "成长地" in line:
|
||
birth_place = line.split(":")[1].strip() if ":" in line else ""
|
||
|
||
if not birth_year:
|
||
return ""
|
||
|
||
stage_era_map = {
|
||
"childhood": (0, 12),
|
||
"education": (6, 22),
|
||
"career": (18, 50),
|
||
"family": (20, 50),
|
||
"belief": (30, 60),
|
||
}
|
||
|
||
age_range = stage_era_map.get(current_stage, (0, 30))
|
||
era_start = birth_year + age_range[0]
|
||
era_end = birth_year + age_range[1]
|
||
|
||
era_events = []
|
||
decade_events = {
|
||
1950: "新中国成立初期、土地改革、抗美援朝",
|
||
1960: "大跃进、三年自然灾害、中苏关系变化",
|
||
1970: "文化大革命、知青上山下乡、中美建交",
|
||
1980: "改革开放、恢复高考、个体经济兴起、电视普及",
|
||
1990: "社会主义市场经济、下海潮、香港回归、互联网初期",
|
||
2000: "加入WTO、房地产兴起、手机普及、北京奥运",
|
||
2010: "移动互联网爆发、微信时代、共享经济、双创浪潮",
|
||
2020: "新冠疫情、直播经济、人工智能崛起",
|
||
}
|
||
|
||
for decade, events in decade_events.items():
|
||
if era_start <= decade + 9 and era_end >= decade:
|
||
era_events.append(f"{decade}年代:{events}")
|
||
|
||
if not era_events:
|
||
return ""
|
||
|
||
place_hint = f" {birth_place}" if birth_place else ""
|
||
# 短提示即可,避免模型写长段「时代散文」
|
||
return (
|
||
f"\n## 时代参考(一两句带过即可,勿长篇)\n"
|
||
f"约 {era_start}-{era_end} 年{place_hint};可联想:{era_events[0]}"
|
||
+ (f";{era_events[1]}" if len(era_events) > 1 else "")
|
||
+ "\n"
|
||
)
|
||
|
||
|
||
def get_guided_conversation_prompt(
|
||
current_stage: str,
|
||
empty_slots: List[str],
|
||
filled_slots: Dict[str, str],
|
||
user_message: str,
|
||
conversation_turn: int = 0,
|
||
same_topic_turns: int = 0,
|
||
all_stages_coverage: Dict[str, Dict] = None,
|
||
detected_user_stage: str = "",
|
||
user_profile_context: str = "",
|
||
) -> str:
|
||
"""生成状态感知的对话提示词"""
|
||
stage_name_map = {
|
||
"childhood": "童年时光",
|
||
"education": "求学经历",
|
||
"career": "职业生涯",
|
||
"family": "家庭生活",
|
||
"belief": "人生信念",
|
||
}
|
||
|
||
current_stage_name = stage_name_map.get(current_stage, current_stage)
|
||
user_stage_name = (
|
||
stage_name_map.get(detected_user_stage, "") if detected_user_stage else ""
|
||
)
|
||
user_jumped = detected_user_stage and detected_user_stage != current_stage
|
||
|
||
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
|
||
empty_slots_str = (
|
||
"、".join(empty_slots_readable) if empty_slots_readable else "已聊得很充分"
|
||
)
|
||
|
||
filled_info = []
|
||
for key, value in filled_slots.items():
|
||
readable_key = SLOT_NAME_MAP.get(key, key)
|
||
filled_info.append(
|
||
f"{readable_key}: {value[:50]}..."
|
||
if len(value) > 50
|
||
else f"{readable_key}: {value}"
|
||
)
|
||
filled_slots_str = "\n".join(filled_info) if filled_info else "刚开始聊"
|
||
|
||
progress_lines = []
|
||
uncovered_stages = []
|
||
if all_stages_coverage:
|
||
for stage in ["childhood", "education", "career", "family", "belief"]:
|
||
cov = all_stages_coverage.get(stage, {})
|
||
filled_n = cov.get("filled", 0)
|
||
total_n = cov.get("total", 0)
|
||
sname = stage_name_map.get(stage, stage)
|
||
if filled_n == 0:
|
||
progress_lines.append(f" {sname}:还没聊到")
|
||
uncovered_stages.append(sname)
|
||
elif filled_n < total_n:
|
||
progress_lines.append(f" {sname}:聊了一些({filled_n}/{total_n})")
|
||
else:
|
||
progress_lines.append(f" {sname}:已聊得很充分 ✓")
|
||
progress_str = "\n".join(progress_lines) if progress_lines else ""
|
||
|
||
filled_count = len(filled_slots)
|
||
should_switch_topic = same_topic_turns >= 3 or (
|
||
filled_count >= 2 and same_topic_turns >= 2
|
||
)
|
||
should_lighten_mood = conversation_turn > 0 and conversation_turn % 5 == 0
|
||
should_try_new_stage = filled_count >= 3 and len(empty_slots) <= 2
|
||
|
||
related_stages = STAGE_RELATED_TOPICS.get(current_stage, [])
|
||
related_stages_str = "、".join([stage_name_map.get(s, s) for s in related_stages])
|
||
|
||
style = RESPONSE_STYLES[conversation_turn % len(RESPONSE_STYLES)]
|
||
style_guidance = {
|
||
"empathy": "共情一两句即可",
|
||
"curious": "若还有未展开的细节可好奇问一个点;若上文已说清或可自然推断,只承接或换角度,**勿为凑问题而追问**",
|
||
"reflection": "可一句简短感慨,勿讲大道理",
|
||
"lighthearted": "轻松一点,别讲段子太长",
|
||
"connection": "可提「我也有过类似感受」一句,勿编造具体经历细节",
|
||
}.get(style, "")
|
||
|
||
dynamic_guidance = ""
|
||
if user_jumped:
|
||
dynamic_guidance += f"""
|
||
- **用户正在聊「{user_stage_name}」的话题,跟着他/她的节奏走,不要试图拉回「{current_stage_name}」**
|
||
- 顺着用户的思路,帮他/她把这个话题聊深聊透
|
||
- 这是很自然的事情,人回忆往事经常会跳跃,你要做的是陪伴和倾听"""
|
||
else:
|
||
if should_lighten_mood:
|
||
dynamic_guidance += "\n- 聊了一会儿了,可以适当轻松一下,聊点有趣的"
|
||
if should_switch_topic and empty_slots_readable:
|
||
dynamic_guidance += (
|
||
f"\n- 这个话题聊得差不多了,可以自然转到:{empty_slots_str}"
|
||
)
|
||
if should_try_new_stage and related_stages:
|
||
dynamic_guidance += (
|
||
f"\n- 如果自然的话,可以尝试聊聊相关的话题,比如{related_stages_str}"
|
||
)
|
||
|
||
uncovered_hint = ""
|
||
if not user_jumped and uncovered_stages and should_try_new_stage:
|
||
uncovered_hint = f"\n- 还没聊到的人生阶段有:{'、'.join(uncovered_stages)},如果聊天中有自然的契机,可以轻轻带一句,但不要刻意"
|
||
|
||
if user_jumped:
|
||
topic_desc = f"你们原本在聊「{current_stage_name}」,但用户自然地聊到了「{user_stage_name}」的内容"
|
||
else:
|
||
topic_desc = f"你们聊到了「{current_stage_name}」这个话题"
|
||
|
||
profile_section = ""
|
||
if user_profile_context:
|
||
profile_section = f"\n## 用户基本信息\n{user_profile_context}\n"
|
||
|
||
active_stage = (
|
||
detected_user_stage if user_jumped and detected_user_stage else current_stage
|
||
)
|
||
era_context = (
|
||
_build_era_context(active_stage, user_profile_context)
|
||
if settings.chat_era_context_enabled
|
||
else ""
|
||
)
|
||
|
||
prompt = f"""你是「岁月知己」,陪用户聊人生。**像微信:短句、少修辞、别写小作文。**
|
||
{topic_desc}
|
||
{profile_section}
|
||
## 本阶段已聊
|
||
{filled_slots_str}
|
||
|
||
## 还可聊的方向
|
||
{empty_slots_str}
|
||
|
||
## 进度
|
||
{progress_str}
|
||
{era_context}
|
||
## 用户刚才说
|
||
"{user_message}"
|
||
|
||
## 本轮语气
|
||
{style_guidance}
|
||
|
||
## 任务(短)
|
||
1. 先简短回应一句,不要总结成长文。
|
||
2. 用户若跳到别的人生阶段,跟着他聊,别硬拉回。
|
||
3. 需要追问时**只问一个**具体小问题;**不必每轮都问**;若用户已说明或语境已能推出(如谁买的、和谁),**别再为同一件事做 yes/no 确认**。
|
||
4. 用户只回简短肯定/否定(如「是的」「对」)时,**结合上文**理解,承接即可或问**新**角度,勿重复上一句已问过的事。
|
||
5. 可用 [SPLIT] 分成**最多 2 条**消息,每条都很短。
|
||
{dynamic_guidance}{uncovered_hint}
|
||
|
||
## 禁止
|
||
括号/思考过程;采访腔;**重复确认**用户档案、**上文已说**或**强暗示下已可知**的事实(包括无信息量的「是不是他/她…」式追问);别编用户没说的细节。
|
||
|
||
直接输出([SPLIT] 可选,最多 2 段):"""
|
||
|
||
return prompt
|
||
|
||
|
||
# 别名:访谈对话专用;勿与回忆录 `app.agents.memoir.prompts.get_memoir_editor_system_prompt` 混淆
|
||
get_interview_system_prompt = get_system_prompt
|