2026-01-07 11:56:53 +08:00
|
|
|
|
"""
|
|
|
|
|
|
对话 Agent 提示词模板和访谈问题库
|
|
|
|
|
|
"""
|
2026-03-19 14:36:14 +08:00
|
|
|
|
|
2026-01-07 11:56:53 +08:00
|
|
|
|
from enum import Enum
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-03-26 12:13:36 +08:00
|
|
|
|
from app.core.config import settings
|
2026-01-07 11:56:53 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConversationStage(str, Enum):
|
|
|
|
|
|
"""对话阶段枚举"""
|
2026-03-19 14:36:14 +08:00
|
|
|
|
|
|
|
|
|
|
CHILDHOOD = "childhood" # 童年
|
|
|
|
|
|
EDUCATION = "education" # 教育
|
|
|
|
|
|
CAREER = "career" # 事业
|
|
|
|
|
|
FAMILY = "family" # 家庭
|
|
|
|
|
|
BELIEFS = "beliefs" # 信念
|
|
|
|
|
|
SUMMARY = "summary" # 人生总结
|
2026-01-07 11:56:53 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
# 访谈问题库(每阶段 2~3 条短问句,供参考;主流程仍由模型与槽位驱动)
|
2026-01-07 11:56:53 +08:00
|
|
|
|
INTERVIEW_QUESTIONS: Dict[ConversationStage, List[str]] = {
|
|
|
|
|
|
ConversationStage.CHILDHOOD: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"你是在哪儿长大的?小时候印象最深的一件事是什么?",
|
|
|
|
|
|
"小时候家里是怎样的?父母对你影响大吗?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
ConversationStage.EDUCATION: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"求学阶段印象最深的是哪段经历?",
|
|
|
|
|
|
"有没有哪位老师或同学对你影响特别大?毕业后最初想做什么?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
ConversationStage.CAREER: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"第一次工作还记得吗?当时心情怎样?",
|
|
|
|
|
|
"事业里遇到过最大的挑战是什么?怎么挺过来的?有没有特别自豪的时刻?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
ConversationStage.FAMILY: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"和伴侣是怎么认识的?做父母时最难忘或最骄傲的瞬间?",
|
|
|
|
|
|
"家庭在你的人生里扮演什么角色?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
ConversationStage.BELIEFS: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"有没有一直坚守的信念或座右铭?哪些价值观对你最重要?",
|
|
|
|
|
|
"你怎样理解成功与幸福?低谷时是什么支撑你?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
ConversationStage.SUMMARY: [
|
2026-03-26 12:13:36 +08:00
|
|
|
|
"回顾一生,最重要的经验或教训是什么?最感激的人与事有哪些?",
|
|
|
|
|
|
"若能对年轻时的自己说一句话,你会说什么?",
|
2026-01-07 11:56:53 +08:00
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
def _guided_voice_intro_line(background_voice: str) -> str:
|
|
|
|
|
|
"""顶部角色描述:温暖陪聊,但仍控制篇幅。"""
|
|
|
|
|
|
if normalize_background_voice(background_voice) == "default":
|
|
|
|
|
|
return (
|
|
|
|
|
|
"你是「岁月知己」,像老朋友陪用户聊人生。"
|
|
|
|
|
|
"**先真诚接住对方的话**,再决定要不要追问;短句为主,但**接住情绪比控制字数更重要**。"
|
|
|
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
|
|
|
"你是「岁月知己」,像老朋友陪用户聊人生。"
|
|
|
|
|
|
"**先真诚承接对方一句,再自然推进**;短句为主,遵守下方长度档位。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 14:36:14 +08:00
|
|
|
|
def get_system_prompt(
|
|
|
|
|
|
current_stage: ConversationStage,
|
|
|
|
|
|
covered_topics: List[str],
|
|
|
|
|
|
user_latest_response: str,
|
|
|
|
|
|
) -> str:
|
2026-01-07 11:56:53 +08:00
|
|
|
|
"""
|
|
|
|
|
|
生成对话 Agent 的系统提示词
|
2026-03-19 10:54:48 +08:00
|
|
|
|
|
2026-01-07 11:56:53 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
current_stage: 当前对话阶段
|
|
|
|
|
|
covered_topics: 已聊过的话题列表
|
|
|
|
|
|
user_latest_response: 用户最新回答
|
2026-03-19 10:54:48 +08:00
|
|
|
|
|
2026-01-07 11:56:53 +08:00
|
|
|
|
Returns:
|
|
|
|
|
|
系统提示词字符串
|
|
|
|
|
|
"""
|
|
|
|
|
|
stage_name_map = {
|
|
|
|
|
|
ConversationStage.CHILDHOOD: "童年",
|
|
|
|
|
|
ConversationStage.EDUCATION: "教育",
|
|
|
|
|
|
ConversationStage.CAREER: "事业",
|
|
|
|
|
|
ConversationStage.FAMILY: "家庭",
|
|
|
|
|
|
ConversationStage.BELIEFS: "信念",
|
|
|
|
|
|
ConversationStage.SUMMARY: "人生总结",
|
|
|
|
|
|
}
|
2026-03-19 10:54:48 +08:00
|
|
|
|
|
2026-01-07 11:56:53 +08:00
|
|
|
|
covered_topics_str = "、".join(covered_topics) if covered_topics else "暂无"
|
2026-03-19 10:54:48 +08:00
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
prompt = f"""你是「岁月知己」,像老朋友一样陪用户聊人生。**回复要短**,像微信聊天,不要长篇、不要文学腔。
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
2026-03-27 16:01:28 +08:00
|
|
|
|
规则:先简短接住对方一句,**最多再问一个具体问题**;禁止括号与思考过程;禁止采访腔(如「我注意到」「我想了解」);**不要重复确认**对方刚说过或上文已能推断的信息。
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
|
|
|
|
|
当前阶段:{stage_name_map.get(current_stage, current_stage.value)}
|
2026-01-07 11:56:53 +08:00
|
|
|
|
已聊话题:{covered_topics_str}
|
|
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
直接输出对用户说的话。"""
|
2026-03-19 10:54:48 +08:00
|
|
|
|
|
2026-01-07 11:56:53 +08:00
|
|
|
|
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_questions_for_stage(stage: ConversationStage) -> List[str]:
|
|
|
|
|
|
"""获取指定阶段的所有问题"""
|
|
|
|
|
|
return INTERVIEW_QUESTIONS.get(stage, [])
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
|
|
|
|
|
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": "人生经验",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 20:09:09 +01:00
|
|
|
|
STAGE_RELATED_TOPICS = {
|
2026-03-19 10:54:48 +08:00
|
|
|
|
"childhood": ["family", "education"],
|
|
|
|
|
|
"education": ["childhood", "career"],
|
|
|
|
|
|
"career": ["education", "family", "belief"],
|
|
|
|
|
|
"family": ["childhood", "career", "belief"],
|
|
|
|
|
|
"belief": ["career", "family"],
|
2026-01-29 20:09:09 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LIGHT_TOPICS = [
|
|
|
|
|
|
"有什么爱好或者特别喜欢的消遣方式吗?",
|
|
|
|
|
|
"最近有什么让你开心的事吗?",
|
|
|
|
|
|
"有没有什么趣事想分享?",
|
|
|
|
|
|
"平时喜欢看什么书或者电影吗?",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
RESPONSE_STYLES = [
|
2026-03-19 10:54:48 +08:00
|
|
|
|
"empathy",
|
|
|
|
|
|
"curious",
|
|
|
|
|
|
"reflection",
|
|
|
|
|
|
"lighthearted",
|
|
|
|
|
|
"connection",
|
2026-01-29 20:09:09 +01:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
2026-03-11 14:39:39 +08:00
|
|
|
|
def get_opening_prompt(
|
|
|
|
|
|
current_stage: str,
|
|
|
|
|
|
empty_slots_readable: List[str],
|
|
|
|
|
|
user_profile_context: str = "",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
persona: str = "default",
|
|
|
|
|
|
background_voice: str = "default",
|
2026-03-11 14:39:39 +08:00
|
|
|
|
) -> str:
|
2026-03-19 10:54:48 +08:00
|
|
|
|
"""空对话时 AI 先开口的提示词"""
|
2026-03-11 14:39:39 +08:00
|
|
|
|
stage_name_map = {
|
|
|
|
|
|
"childhood": "童年时光",
|
|
|
|
|
|
"education": "求学经历",
|
|
|
|
|
|
"career": "职业生涯",
|
|
|
|
|
|
"family": "家庭生活",
|
|
|
|
|
|
"belief": "人生信念",
|
|
|
|
|
|
}
|
|
|
|
|
|
stage_name = stage_name_map.get(current_stage, current_stage)
|
2026-03-20 15:15:35 +08:00
|
|
|
|
if empty_slots_readable:
|
|
|
|
|
|
topics_str = "、".join(empty_slots_readable)
|
|
|
|
|
|
topics_heading = (
|
|
|
|
|
|
f"## 当前建议话题({stage_name})\n可以从中选一个来问:{topics_str}"
|
|
|
|
|
|
)
|
|
|
|
|
|
task_question = (
|
|
|
|
|
|
"2. **必须问一个问题**:接着问一个**具体、好回答**的问题,引导用户开始分享;"
|
|
|
|
|
|
"优先落在上述还未聊透的方向上。不要问太宽泛的「有什么想聊的」。"
|
|
|
|
|
|
)
|
2026-03-26 12:13:36 +08:00
|
|
|
|
_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"],
|
2026-03-20 15:15:35 +08:00
|
|
|
|
)
|
2026-03-31 23:55:26 +08:00
|
|
|
|
bv = normalize_background_voice(background_voice)
|
|
|
|
|
|
if bv == "cadre":
|
|
|
|
|
|
style_examples += (
|
|
|
|
|
|
"\n(干部/机关语境:问候稳重、不用「嗨~」;示例可参考)\n"
|
|
|
|
|
|
'"您好,想听听您的人生故事。您小时候是在哪儿长大的?哪一段印象最深?"\n或\n'
|
|
|
|
|
|
'"您好。今天想从您印象最深的一件事聊起,可以吗?"'
|
|
|
|
|
|
)
|
|
|
|
|
|
elif bv == "military":
|
|
|
|
|
|
style_examples += (
|
|
|
|
|
|
"\n(军队语境:简洁、得体;不用「嗨~」;示例可参考)\n"
|
|
|
|
|
|
'"您好。想听听您的经历。您童年印象最深的一件事是什么?"\n或\n'
|
|
|
|
|
|
'"您好,有空的话想聊聊您的人生故事。您小时候在哪儿长大?"'
|
|
|
|
|
|
)
|
2026-03-20 15:15:35 +08:00
|
|
|
|
else:
|
|
|
|
|
|
topics_heading = (
|
|
|
|
|
|
f"## 当前阶段({stage_name})\n"
|
|
|
|
|
|
"访谈结构化槽位里,这一阶段的主要问题在素材侧**已有覆盖**。"
|
|
|
|
|
|
"开场要像老朋友重逢:接近况、接续上次聊过的事、或任何用户可能想提起的新片段;"
|
|
|
|
|
|
"**禁止**为了凑问题而默认再从「童年在哪长大」等已覆盖模板重头盘问。"
|
|
|
|
|
|
)
|
|
|
|
|
|
task_question = (
|
|
|
|
|
|
"2. **问候 + 轻巧引子**:用一句温暖的话接上对话;若自然,可以问一个与近况、"
|
|
|
|
|
|
"想续上的回忆、或新冒出来的小事有关的问题。若不适合追问,问候 + 一句开放式引子即可。"
|
|
|
|
|
|
)
|
|
|
|
|
|
style_examples = (
|
|
|
|
|
|
"示例(仅供参考风格):\n"
|
|
|
|
|
|
'"嘿,又见面啦~ 今天有没有哪件事突然从脑子里冒出来,想跟我说说?"\n或\n'
|
|
|
|
|
|
'"在的!上次聊到那儿我还记着,你后来还有想起什么细节吗?"'
|
|
|
|
|
|
)
|
2026-03-19 14:36:14 +08:00
|
|
|
|
profile_section = (
|
|
|
|
|
|
f"\n## 用户基本信息\n{user_profile_context}\n" if user_profile_context else ""
|
|
|
|
|
|
)
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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}
|
2026-03-11 14:39:39 +08:00
|
|
|
|
{profile_section}
|
2026-03-20 15:15:35 +08:00
|
|
|
|
{topics_heading}
|
2026-03-31 23:55:26 +08:00
|
|
|
|
{persona_extra}{voice_section}
|
2026-03-26 12:13:36 +08:00
|
|
|
|
## 任务
|
|
|
|
|
|
1. 简短问候。
|
2026-03-20 15:15:35 +08:00
|
|
|
|
{task_question}
|
2026-03-26 12:13:36 +08:00
|
|
|
|
3. 自然、温暖,但**字数要少**。
|
2026-03-11 14:39:39 +08:00
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
## 格式
|
|
|
|
|
|
- 可用 [SPLIT] 分成最多 2 条;或一条里「问候 + 问题」。
|
2026-04-01 09:33:40 +08:00
|
|
|
|
- **禁止**括号、括号内策略/旁白(如「(先接住情绪)」)、思考过程;不要替用户编回答。
|
2026-03-11 14:39:39 +08:00
|
|
|
|
|
2026-03-20 15:15:35 +08:00
|
|
|
|
{style_examples}
|
2026-03-11 14:39:39 +08:00
|
|
|
|
|
2026-04-01 09:33:40 +08:00
|
|
|
|
直接输出(仅自然口语):"""
|
2026-03-11 14:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-01 10:12:23 +01:00
|
|
|
|
def _build_era_context(current_stage: str, user_profile_context: str) -> str:
|
2026-03-19 10:54:48 +08:00
|
|
|
|
"""根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示"""
|
2026-03-01 10:12:23 +01:00
|
|
|
|
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 ""
|
|
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
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"
|
|
|
|
|
|
)
|
2026-03-01 10:12:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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**:用户本轮分享了较多内容或情绪较浓——可以多说一两句承接对方话里的核心点,表达你听到了、你在意,再自然追问;**仍控制在两段以内**。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
def get_guided_conversation_prompt(
|
|
|
|
|
|
current_stage: str,
|
|
|
|
|
|
empty_slots: List[str],
|
|
|
|
|
|
filled_slots: Dict[str, str],
|
2026-01-29 20:09:09 +01:00
|
|
|
|
user_message: str,
|
|
|
|
|
|
conversation_turn: int = 0,
|
|
|
|
|
|
same_topic_turns: int = 0,
|
2026-03-31 23:55:26 +08:00
|
|
|
|
all_stages_coverage: Optional[Dict[str, Dict]] = None,
|
2026-02-13 21:45:56 +01:00
|
|
|
|
detected_user_stage: str = "",
|
2026-03-01 10:12:23 +01:00
|
|
|
|
user_profile_context: str = "",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
persona: str = "default",
|
|
|
|
|
|
memory_evidence_text: str = "",
|
|
|
|
|
|
reply_length_mode: str = "standard",
|
|
|
|
|
|
background_voice: str = "default",
|
2026-01-21 22:31:03 +01:00
|
|
|
|
) -> str:
|
2026-03-31 23:55:26 +08:00
|
|
|
|
"""生成状态感知的对话提示词(档位由 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)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
stage_name_map = {
|
|
|
|
|
|
"childhood": "童年时光",
|
|
|
|
|
|
"education": "求学经历",
|
|
|
|
|
|
"career": "职业生涯",
|
|
|
|
|
|
"family": "家庭生活",
|
|
|
|
|
|
"belief": "人生信念",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
current_stage_name = stage_name_map.get(current_stage, current_stage)
|
2026-03-19 14:36:14 +08:00
|
|
|
|
user_stage_name = (
|
|
|
|
|
|
stage_name_map.get(detected_user_stage, "") if detected_user_stage else ""
|
|
|
|
|
|
)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
user_jumped = detected_user_stage and detected_user_stage != current_stage
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
|
2026-03-19 14:36:14 +08:00
|
|
|
|
empty_slots_str = (
|
|
|
|
|
|
"、".join(empty_slots_readable) if empty_slots_readable else "已聊得很充分"
|
|
|
|
|
|
)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
filled_info = []
|
|
|
|
|
|
for key, value in filled_slots.items():
|
|
|
|
|
|
readable_key = SLOT_NAME_MAP.get(key, key)
|
2026-03-19 14:36:14 +08:00
|
|
|
|
filled_info.append(
|
|
|
|
|
|
f"{readable_key}: {value[:50]}..."
|
|
|
|
|
|
if len(value) > 50
|
|
|
|
|
|
else f"{readable_key}: {value}"
|
|
|
|
|
|
)
|
2026-01-21 22:31:03 +01:00
|
|
|
|
filled_slots_str = "\n".join(filled_info) if filled_info else "刚开始聊"
|
|
|
|
|
|
|
2026-02-13 21:45:56 +01:00
|
|
|
|
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 ""
|
|
|
|
|
|
|
2026-01-29 20:09:09 +01:00
|
|
|
|
filled_count = len(filled_slots)
|
2026-03-31 23:55:26 +08:00
|
|
|
|
should_switch_topic = same_topic_turns >= 5 or (
|
|
|
|
|
|
filled_count >= 3 and same_topic_turns >= 4
|
2026-03-19 14:36:14 +08:00
|
|
|
|
)
|
2026-03-31 23:55:26 +08:00
|
|
|
|
should_lighten_mood = conversation_turn > 0 and conversation_turn % 7 == 0
|
|
|
|
|
|
should_try_new_stage = filled_count >= 4 and len(empty_slots) <= 1
|
2026-02-13 21:45:56 +01:00
|
|
|
|
|
2026-01-29 20:09:09 +01:00
|
|
|
|
related_stages = STAGE_RELATED_TOPICS.get(current_stage, [])
|
|
|
|
|
|
related_stages_str = "、".join([stage_name_map.get(s, s) for s in related_stages])
|
2026-02-13 21:45:56 +01:00
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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"
|
|
|
|
|
|
)
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
2026-02-13 21:45:56 +01:00
|
|
|
|
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:
|
2026-03-31 23:55:26 +08:00
|
|
|
|
if likely_new:
|
|
|
|
|
|
dynamic_guidance += f"\n- 若用户本轮**刚补充**新细节,请先就这一点追问一句,再自然转到未聊方向:{empty_slots_str}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
dynamic_guidance += (
|
|
|
|
|
|
f"\n- 这个话题聊得差不多了,可以自然转到:{empty_slots_str}"
|
|
|
|
|
|
)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
if should_try_new_stage and related_stages:
|
2026-03-19 14:36:14 +08:00
|
|
|
|
dynamic_guidance += (
|
|
|
|
|
|
f"\n- 如果自然的话,可以尝试聊聊相关的话题,比如{related_stages_str}"
|
|
|
|
|
|
)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
|
|
|
|
|
|
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}」这个话题"
|
|
|
|
|
|
|
2026-03-01 10:12:23 +01:00
|
|
|
|
profile_section = ""
|
|
|
|
|
|
if user_profile_context:
|
|
|
|
|
|
profile_section = f"\n## 用户基本信息\n{user_profile_context}\n"
|
|
|
|
|
|
|
2026-03-19 14:36:14 +08:00
|
|
|
|
active_stage = (
|
|
|
|
|
|
detected_user_stage if user_jumped and detected_user_stage else current_stage
|
|
|
|
|
|
)
|
2026-03-26 12:13:36 +08:00
|
|
|
|
era_context = (
|
|
|
|
|
|
_build_era_context(active_stage, user_profile_context)
|
|
|
|
|
|
if settings.chat_era_context_enabled
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
2026-02-13 21:45:56 +01:00
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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}
|
2026-03-26 12:13:36 +08:00
|
|
|
|
{topic_desc}
|
2026-03-31 23:55:26 +08:00
|
|
|
|
{reply_length_section}
|
2026-03-01 10:12:23 +01:00
|
|
|
|
{profile_section}
|
2026-03-31 23:55:26 +08:00
|
|
|
|
{voice_section}
|
2026-03-26 12:13:36 +08:00
|
|
|
|
## 本阶段已聊
|
2026-01-21 22:31:03 +01:00
|
|
|
|
{filled_slots_str}
|
|
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
## 还可聊的方向
|
2026-01-21 22:31:03 +01:00
|
|
|
|
{empty_slots_str}
|
|
|
|
|
|
|
2026-03-26 12:13:36 +08:00
|
|
|
|
## 进度
|
2026-02-13 21:45:56 +01:00
|
|
|
|
{progress_str}
|
2026-03-01 10:12:23 +01:00
|
|
|
|
{era_context}
|
2026-01-21 22:31:03 +01:00
|
|
|
|
## 用户刚才说
|
|
|
|
|
|
"{user_message}"
|
|
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
{memoir_orientation_block}{memory_section}{followup_trigger_block}
|
|
|
|
|
|
{tone_section}
|
2026-01-29 20:09:09 +01:00
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
## 你要做的
|
|
|
|
|
|
1. **先接住对方**——一句真诚回应,不要写成总结或讲评。
|
|
|
|
|
|
2. 用户跳到别的人生阶段,跟着聊,别硬拉回。
|
|
|
|
|
|
3. **最多追问一个**具体、好答的问题(参照上方「什么时候追问」);无需追问时,只承接就好。
|
|
|
|
|
|
4. 用户回「嗯」「对」之类,结合上文理解,承接或换个新角度,不要重复上一轮问过的事。
|
|
|
|
|
|
5. 可用 [SPLIT] 分成**最多 2 条**消息。
|
2026-02-13 21:45:56 +01:00
|
|
|
|
{dynamic_guidance}{uncovered_hint}
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
2026-03-31 23:55:26 +08:00
|
|
|
|
## 不要做的
|
2026-04-01 09:33:40 +08:00
|
|
|
|
**禁止**输出括号、括号内的策略/舞台说明(例如「(先接住情绪)」「(共情)」)、思考过程或任何元注释——这些只存在于系统指令里,**绝不可**出现在你对用户说的话中;采访腔(「我注意到」「我想了解」);重复确认对方已经说过或能推断出的信息;编造对方没说的细节。
|
2026-03-26 12:13:36 +08:00
|
|
|
|
|
2026-04-01 09:33:40 +08:00
|
|
|
|
直接输出(仅自然口语,无任何括号前缀或旁白):"""
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
|
|
|
|
|
return prompt
|
2026-03-26 12:13:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 别名:访谈对话专用;勿与回忆录 `app.agents.memoir.prompts.get_memoir_editor_system_prompt` 混淆
|
|
|
|
|
|
get_interview_system_prompt = get_system_prompt
|