""" 对话 Agent 提示词模板和访谈问题库 """ from enum import Enum from typing import Dict, List, Optional from app.agents.chat.background_voice import ( get_background_voice_chat_block, normalize_background_voice, ) from app.agents.chat.interview_reply_length import ( heuristic_likely_chit_chat, heuristic_likely_emotional, heuristic_likely_new_detail, ) from app.agents.chat.personas import ( get_interview_persona_block, get_opening_persona_line, normalize_interview_persona, ) 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 _guided_voice_intro_line(background_voice: str) -> str: """顶部角色描述:温暖陪聊,但仍控制篇幅。""" if normalize_background_voice(background_voice) == "default": return ( "你是「岁月知己」,像老朋友陪用户聊人生。" "**先真诚接住对方的话**,再决定要不要追问;短句为主,但**接住情绪比控制字数更重要**。" ) return ( "你是「岁月知己」,像老朋友陪用户聊人生。" "**先真诚承接对方一句,再自然推进**;短句为主,遵守下方长度档位。" ) 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 = "", persona: str = "default", background_voice: str = "default", ) -> 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"], ) bv = normalize_background_voice(background_voice) if bv == "cadre": style_examples += ( "\n(干部/机关语境:问候稳重、不用「嗨~」;示例可参考)\n" '"您好,想听听您的人生故事。您小时候是在哪儿长大的?哪一段印象最深?"\n或\n' '"您好。今天想从您印象最深的一件事聊起,可以吗?"' ) elif bv == "military": style_examples += ( "\n(军队语境:简洁、得体;不用「嗨~」;示例可参考)\n" '"您好。想听听您的经历。您童年印象最深的一件事是什么?"\n或\n' '"您好,有空的话想聊聊您的人生故事。您小时候在哪儿长大?"' ) 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 "" ) persona_key = normalize_interview_persona(persona) opening_persona = get_opening_persona_line(persona_key) persona_extra = f"\n## 访谈性格\n{opening_persona}\n" if opening_persona else "" voice_block = get_background_voice_chat_block(background_voice) voice_section = f"\n{voice_block}\n" if voice_block else "" bv = normalize_background_voice(background_voice) if bv == "default": opening_head = ( "你是「岁月知己」。用户刚进对话,**还没说话**,请你先开口。" "**短、像微信**,一两句问候 + 一句具体问题即可,不要排比、不要文学描写。" ) else: opening_head = ( "你是「岁月知己」。用户刚进对话,**还没说话**,请你先开口。" "**短;两三句内完成问候 + 一个具体问题**;不要排比、不要文学描写。" ) return f"""{opening_head} {profile_section} {topics_heading} {persona_extra}{voice_section} ## 任务 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 _format_reply_length_section(current_mode: str) -> str: """软提示:本轮档位 + 三档说明(模型始终可见完整对照)。""" safe = ( current_mode if current_mode in ("brief", "standard", "expanded") else "standard" ) return f"""## 本轮回复长度 **当前档位:{safe}** - **brief**:一两句话,简短温暖地接住对方,可以带一个小问题也可以不带。 - **standard**:承接 + 最多一个具体问题;像朋友聊天,不写长段。 - **expanded**:用户本轮分享了较多内容或情绪较浓——可以多说一两句承接对方话里的核心点,表达你听到了、你在意,再自然追问;**仍控制在两段以内**。 """ 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: Optional[Dict[str, Dict]] = None, detected_user_stage: str = "", user_profile_context: str = "", persona: str = "default", memory_evidence_text: str = "", reply_length_mode: str = "standard", background_voice: str = "default", ) -> str: """生成状态感知的对话提示词(档位由 Agent 计算的 ReplyPlan 传入,不在此重复推导)。""" persona_key = normalize_interview_persona(persona) persona_block = get_interview_persona_block(persona_key) likely_new = heuristic_likely_new_detail(user_message) likely_chit = heuristic_likely_chit_chat(user_message) reply_length_section = _format_reply_length_section(reply_length_mode) 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 >= 5 or ( filled_count >= 3 and same_topic_turns >= 4 ) should_lighten_mood = conversation_turn > 0 and conversation_turn % 7 == 0 should_try_new_stage = filled_count >= 4 and len(empty_slots) <= 1 related_stages = STAGE_RELATED_TOPICS.get(current_stage, []) related_stages_str = "、".join([stage_name_map.get(s, s) for s in related_stages]) emotional = heuristic_likely_emotional(user_message) if persona_block: tone_section = f"{persona_block}\n" else: tone_section = "" followup_trigger_block = """## 什么时候追问、什么时候只承接 **该追问**(承接后带 1 个具体问题): - 出现**新的人名、新关系、新情节**,上文还没展开过; - 用户邀你接话(如「你猜猜」); - 本阶段仍有未聊方向,且对方话里露出可深挖的线头。 **可以只承接、不追问**: - 本轮几乎无新信息(「嗯」「对」「行」); - 用户明确要结束或换话题; - 再问会重复上文已说清的事。 **用户在表达情绪时**:先好好接住情绪,让对方感觉被听到、被理解;不急着追问,等情绪有着落后再自然引回。 """ if likely_new: followup_trigger_block += ( "\n**【本轮判定】用户补充了新细节 → 承接后须追问 1 句。**\n" ) if emotional and not likely_new: followup_trigger_block += ( "\n**【本轮判定】用户情绪较浓 → 先好好共情承接,不必急着追问。**\n" ) memoir_orientation_lines = [ "## 对话方向", "追问与承接**优先服务于人生故事与回忆录素材**,但不要让对方觉得你在走流程。", "若用户**明显在闲聊**,以陪聊为主,**不要**用回忆录式问题打断。", "若用户一边回忆一边开玩笑,先接情绪,再轻轻带回一个与经历相关的小问题。", ] if likely_chit: memoir_orientation_lines.append( "**【本轮偏闲聊】** → 以承接与陪聊为主;若用户自然带回经历,再追问。" ) memoir_orientation_block = "\n".join(memoir_orientation_lines) + "\n" memory_section = "" mem_trim = (memory_evidence_text or "").strip() if mem_trim: memory_section = ( "## 相关记忆摘录(仅供衔接,禁止编造)\n" "以下为系统从用户**过往口述**中检索到的摘录,**不是**用户本轮亲口新说的内容。\n" "承接时可自然用「你之前提过……」「上次你说到……」等口语,不要把摘录里的细节写成本轮用户新告诉你的事实;禁止编造摘录未出现的内容。\n\n" f"{mem_trim}\n\n" ) 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: if likely_new: dynamic_guidance += f"\n- 若用户本轮**刚补充**新细节,请先就这一点追问一句,再自然转到未聊方向:{empty_slots_str}" else: 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 "" ) voice_block = get_background_voice_chat_block(background_voice) voice_section = f"\n{voice_block}\n" if voice_block else "" intro_line = _guided_voice_intro_line(background_voice) prompt = f"""{intro_line} {topic_desc} {reply_length_section} {profile_section} {voice_section} ## 本阶段已聊 {filled_slots_str} ## 还可聊的方向 {empty_slots_str} ## 进度 {progress_str} {era_context} ## 用户刚才说 "{user_message}" {memoir_orientation_block}{memory_section}{followup_trigger_block} {tone_section} ## 你要做的 1. **先接住对方**——一句真诚回应,不要写成总结或讲评。 2. 用户跳到别的人生阶段,跟着聊,别硬拉回。 3. **最多追问一个**具体、好答的问题(参照上方「什么时候追问」);无需追问时,只承接就好。 4. 用户回「嗯」「对」之类,结合上文理解,承接或换个新角度,不要重复上一轮问过的事。 5. 可用 [SPLIT] 分成**最多 2 条**消息。 {dynamic_guidance}{uncovered_hint} ## 不要做的 **禁止**输出括号、括号内的策略/舞台说明(例如「(先接住情绪)」「(共情)」)、思考过程或任何元注释——这些只存在于系统指令里,**绝不可**出现在你对用户说的话中;采访腔(「我注意到」「我想了解」);重复确认对方已经说过或能推断出的信息;编造对方没说的细节。 直接输出(仅自然口语,无任何括号前缀或旁白):""" return prompt # 别名:访谈对话专用;勿与回忆录 `app.agents.memoir.prompts.get_memoir_editor_system_prompt` 混淆 get_interview_system_prompt = get_system_prompt