""" 按人生阶段 × 叙述槽位(slot)固化「问题大纲」,供访谈提示词引用。 - 同一 slot key 在不同 stage 语义不同(如 education.change vs family.change),必须分 stage 配置。 - 条目是采集方向/示意问法,模型须在对话中情景化改写,禁止照抄题库原句。 """ from __future__ import annotations from app.agents.stage_constants import STAGE_SLOT_KEYS, VALID_CHAT_STAGES # (stage, slot) -> 有序大纲:从宽泛落到具体,供缺口推进时优先参考 SLOT_QUESTION_OUTLINES: dict[tuple[str, str], tuple[str, ...]] = { # --- 童年时光 --- ("childhood", "place"): ( "小时候主要是在哪儿长大的——城市、镇上还是村里,能具体到一片街区或一座院子吗?", "住的地方大概什么样,有没有到现在还记得清的角落(厨房、院子、门槛)?", "外人一眼能认出的地标或某种气味、声音(河、庙、集市、某种树)是什么?", ), ("childhood", "people"): ( "那时候最黏、最怕或最佩服的是谁,和你们日常是怎么相处的?", "带你玩或管你最多的是家人还是祖辈、邻居孩子?有没有一位到现在还会突然想起的人?", "有没有某个人一句话或一个动作,小时候的你特别当回事?", ), ("childhood", "daily_life"): ( "不上学的一天通常怎么过,有没有固定的小规矩或小乐趣?", "放学之后最常去哪儿、和谁在一起,那种日子现在回想是什么颜色的?", "记忆里常重复出现的一个场景(吃饭、睡午觉、出门玩)愿意多说两句吗?", ), ("childhood", "emotion"): ( "回想小时候,身体哪个地方会先「有感觉」——收紧、发暖还是发酸?", "那时候整体更像踏实、兴奋、还是有点孤单或紧张?能举一个具体小事吗?", "有没有一件当时觉得天塌了或特别亮堂的事,现在看又不太一样?", ), ("childhood", "turning_event"): ( "有没有一件事让小时候的你一下子觉得「我跟以前不一样了」?", "难忘不一定是大事——一次搬家、一次生病、一次被骂或一次被护着也算,你愿意从画面讲起吗?", "那之后别人对待你的方式,或你对自己的看法,哪儿变了?", ), # --- 求学经历 --- ("education", "school"): ( "印象最深的一所学校是哪段,校园里你记得清的细节是什么(光线、气味、铃声、操场)?", "有没有一门课、一位老师或一间教室,想到还会心里一动?", "那时候和同学之间,最典型的相处方式是什么样的?", ), ("education", "city"): ( "求学待过哪些城或镇,第一次到那儿下车或进校园时是什么感觉?", "和老家比,那边最陌生的地方是什么——人、节奏还是规矩?", "后来有没有哪条街、哪个小店或公交站,和你的一段日子绑在一起?", ), ("education", "motivation"): ( "当时为什么读书或选这个方向——是自愿、家里期待,还是偶然被推向那条路?", "有没有谁的一句话、一本书或一件事,真正推了你一把?", "撑不下去的时候,是靠什么念头把自己劝回书桌前的?", ), ("education", "challenge"): ( "求学阶段最难的一段是什么——考试、钱、适应环境,还是家里的事?", "最难的那会儿,一天里通常怎么扛过去,有没有人帮你垫了一下?", "回头看,那段难有没有改变你对自己的看法?", ), ("education", "change"): ( "毕业或离开那段求学生活时,和刚入学比,你觉得自己哪块最明显变了?", "是更知道自己要什么,还是更知道不要什么?能各举一个细节吗?", "如果有机会对当时的自己说一句话,你会叮嘱什么?", ), # --- 职业生涯 --- ("career", "job"): ( "那段工作里你具体在做什么,一天里真实在忙的核心事是什么?", "有没有一个小场景能代表那份活(开会、跑现场、对客户、写东西)?", "你最熟练或最头疼的环节是哪一块?", ), ("career", "environment"): ( "上班或干活的场子什么样——吵、静、乱中有序,还是总在移动?", "同事或搭档大概怎么相处,有没有固定的小 ritual(抽烟、吃饭、对表)?", "那个环境里,什么让你待得下去,什么让你想逃?", ), ("career", "decision"): ( "换工作、入行或接某个项目时,有没有一次关键决定是你拍板的?", "当时心里在权衡什么——钱、体面、家庭、兴趣、还是别的东西?", "如果重来,你还会那么选吗,哪一步可能会松一点或紧一点?", ), ("career", "pressure"): ( "压力最大的阶段是什么,外界逼你还是自己逼自己更多?", "身体或情绪上最先亮红灯的信号是什么(睡不着、胃、脾气)?", "后来是环境变了,还是你换了应对方式,才缓过来的?", ), ("career", "growth"): ( "哪一段经历让你觉得能力、心气或看人的眼光真的上了一层?", "有没有一件当时觉得过不去的事,做完反而长出了新本事?", "现在回望,你愿意把哪段职业经历称为自己的「底色」?", ), # --- 家庭生活 --- ("family", "relationship"): ( "和伴侣、父母或孩子之间,最近最常见的一个相处画面是什么?", "亲昵和疏远在你家里大概怎么分配——是话说得多还是活儿帮得多?", "有没有一种默契是你们家的「规矩外规矩」?", ), ("family", "conflict"): ( "家里有没有一次争执或冷战让你印象最深,起因其实是什么?", "后来是时间抹平的还是有人先伸手,中间最难受的是哪几天?", "那段矛盾过后,关系里是多了道疤还是多了一层理解?", ), ("family", "support"): ( "难的时候谁最站你这边,对方具体做过一件什么事你记到现在?", "你又是怎么支持家里人的——出钱、出力、还是稳稳在场?", "有没有一次你觉得「没有ta那次就真的悬了」?", ), ("family", "responsibility"): ( "在家里你扛的主责是什么——钱、照料、决策、还是情绪托底?", "从什么时候开始你觉得自己是「要主动揽过来」的那个人?", "这份责任有没有让你骄傲,也有没有让你委屈的时候?", ), ("family", "change"): ( "成家或角色变了(当父母、当主心骨)之后,生活节奏或心态最明显的一处变化是什么?", "和以前单身或二人世界比,你最怀念和最怕失去的各是什么?", "如果现在给家里关系打分,你会先改哪一件小事?", ), # --- 人生信念 --- ("belief", "value"): ( "若要把诚实、自由、家庭、成就、平安排个序,你心里默认的前两位是什么?", "有没有一条线是你不太愿意让步的,哪怕会吃亏?", "这套看重的东西,是家里传下来的还是你自己摔打出来的?", ), ("belief", "regret"): ( "若重来会犹豫的一件选择是什么,当时缺的是信息还是勇气?", "遗憾现在还在拉扯你,还是已经能放在一旁?你怎么和它相处?", "这份遗憾有没有悄悄改了你后来做决定的方式?", ), ("belief", "pride"): ( "不一定要大成就——一件你心底觉得「这事我做得对、做得像我自己」的事?", "那时候外人怎么看不重要,你自己凭什么觉得踏实?", "骄傲里有没有夹着一点后怕或侥幸?", ), ("belief", "lesson"): ( "如果只能留一句话给后辈或给年轻时的自己,你会留什么?", "这个道理是从谁身上、哪段经历里磨出来的?", "有没有一次你差点不信这句话,最后还是被生活按着头认了?", ), } def _validate_outlines() -> None: for stage, slots in STAGE_SLOT_KEYS.items(): for slot in slots: key = (stage, slot) if key not in SLOT_QUESTION_OUTLINES: raise RuntimeError(f"slot_question_bank missing outline for {key}") _validate_outlines() def outlines_for_stage_slots( stage: str, empty_slots: list[str] ) -> list[tuple[str, tuple[str, ...]]]: """返回 [(slot key, 大纲句...), ...],仅含本阶段且仍在 empty_slots 中的槽。""" if stage not in VALID_CHAT_STAGES: return [] empty_set = set(empty_slots) order = STAGE_SLOT_KEYS.get(stage, ()) out: list[tuple[str, tuple[str, ...]]] = [] for slot in order: if slot not in empty_set: continue key = (stage, slot) lines = SLOT_QUESTION_OUTLINES.get(key, ()) if not lines: continue out.append((slot, lines)) return out def format_slot_question_outline_block(stage: str, empty_slots: list[str]) -> str: """供 system prompt 注入;无缺口时返回空串。""" from app.agents.chat.prompts_conversation import SLOT_NAME_MAP rows = outlines_for_stage_slots(stage, empty_slots) if not rows: return "" blocks = [ "## 本阶段问题大纲(仅列仍缺的叙述槽;条目是**采集目标与思路**,不是台词)\n" "- 每条大纲按**由宽到细**排序;推进缺口时通常优先靠上的条目,但**用户情绪明显起伏时,宁可停在大纲外多接两轮**,不要为凑进度硬问。\n" "- 发问前必须把大纲句**打散、改写**,嵌进对方刚说的名词/动作/比喻;禁止整段背诵题库原句。\n" "- 每轮若需要推进缺口,**全段只服务一个槽、最多一个问句**(或无问句只承接)。\n\n" ] for slot, lines in rows: label = SLOT_NAME_MAP.get(slot, slot) blocks.append(f"### {label}\n") for i, line in enumerate(lines, start=1): blocks.append(f"{i}. {line}\n") blocks.append("\n") return "".join(blocks).rstrip() + "\n\n"