""" 对话 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