Merge branch 'refactor/backend-architecture' into development
This commit is contained in:
57
api/app/agents/prompts/__init__.py
Normal file
57
api/app/agents/prompts/__init__.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
提示词模块
|
||||
"""
|
||||
from .conversation_prompts import (
|
||||
ConversationStage,
|
||||
get_system_prompt as get_conversation_prompt,
|
||||
get_questions_for_stage,
|
||||
get_guided_conversation_prompt,
|
||||
get_opening_prompt,
|
||||
INTERVIEW_QUESTIONS,
|
||||
)
|
||||
from .memory_prompts import (
|
||||
get_system_prompt as get_memory_prompt,
|
||||
get_chapter_classification_prompt,
|
||||
get_text_rewrite_prompt,
|
||||
get_state_extraction_prompt,
|
||||
get_creative_title_prompt,
|
||||
get_narrative_prompt,
|
||||
inject_image_placeholder_template,
|
||||
CHAPTER_CATEGORIES,
|
||||
CHAPTER_ORDER,
|
||||
STAGE_TO_ORDER,
|
||||
)
|
||||
from .profile_prompts import (
|
||||
get_profile_greeting_prompt,
|
||||
get_profile_extraction_prompt,
|
||||
get_profile_followup_prompt,
|
||||
format_user_profile_context,
|
||||
get_missing_profile_fields,
|
||||
PROFILE_FIELD_NAMES,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ConversationStage",
|
||||
"get_conversation_prompt",
|
||||
"get_questions_for_stage",
|
||||
"get_guided_conversation_prompt",
|
||||
"get_opening_prompt",
|
||||
"INTERVIEW_QUESTIONS",
|
||||
"get_memory_prompt",
|
||||
"get_chapter_classification_prompt",
|
||||
"get_text_rewrite_prompt",
|
||||
"get_state_extraction_prompt",
|
||||
"get_creative_title_prompt",
|
||||
"get_narrative_prompt",
|
||||
"inject_image_placeholder_template",
|
||||
"CHAPTER_CATEGORIES",
|
||||
"CHAPTER_ORDER",
|
||||
"STAGE_TO_ORDER",
|
||||
"get_profile_greeting_prompt",
|
||||
"get_profile_extraction_prompt",
|
||||
"get_profile_followup_prompt",
|
||||
"format_user_profile_context",
|
||||
"get_missing_profile_fields",
|
||||
"PROFILE_FIELD_NAMES",
|
||||
]
|
||||
|
||||
466
api/app/agents/prompts/conversation_prompts.py
Normal file
466
api/app/agents/prompts/conversation_prompts.py
Normal file
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
对话 Agent 提示词模板和访谈问题库
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import List, Dict
|
||||
import random
|
||||
|
||||
|
||||
class ConversationStage(str, Enum):
|
||||
"""对话阶段枚举"""
|
||||
CHILDHOOD = "childhood" # 童年
|
||||
EDUCATION = "education" # 教育
|
||||
CAREER = "career" # 事业
|
||||
FAMILY = "family" # 家庭
|
||||
BELIEFS = "beliefs" # 信念
|
||||
SUMMARY = "summary" # 人生总结
|
||||
|
||||
|
||||
# 访谈问题库
|
||||
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"""你是「岁月知己」,一位资深的人生故事访谈者,专注于帮助用户回忆和讲述人生经历。
|
||||
|
||||
## 角色定位
|
||||
你如同一位老朋友,用真诚、温暖的方式倾听用户的故事,通过自然的对话引导用户分享更多细节。
|
||||
|
||||
## 访谈技巧
|
||||
1. 积极倾听:先对用户分享的内容给予简短回应,表达理解和共鸣
|
||||
2. 深度追问:围绕用户提到的具体场景、人物、感受进行细节追问
|
||||
3. 一次一问:每次只提一个问题,给用户充分思考和表达的空间
|
||||
4. 自然过渡:当一个话题聊透后,自然引入下一个相关话题
|
||||
|
||||
## 输出要求
|
||||
- 直接输出你要对用户说的话
|
||||
- 禁止输出任何括号注释、思考过程、策略说明
|
||||
- 禁止使用"我注意到""我想了解"等采访腔调
|
||||
- 语气要像朋友聊天,自然亲切
|
||||
|
||||
当前阶段:{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 先开口的提示词(用户通过「打个招呼」进入,尚未发送任何消息)。
|
||||
要求 AI 先发一条问候 + 一个具体问题,引导用户开始分享。
|
||||
"""
|
||||
stage_name_map = {
|
||||
"childhood": "童年时光",
|
||||
"education": "求学经历",
|
||||
"career": "职业生涯",
|
||||
"family": "家庭生活",
|
||||
"belief": "人生信念",
|
||||
}
|
||||
stage_name = stage_name_map.get(current_stage, current_stage)
|
||||
topics_str = "、".join(empty_slots_readable) if empty_slots_readable else "人生故事、童年、经历等"
|
||||
profile_section = f"\n## 用户基本信息\n{user_profile_context}\n" if user_profile_context else ""
|
||||
return f"""你是「岁月知己」,用户的老朋友。用户刚通过「打个招呼」进入空对话,**还没有发任何消息**,需要你先开口。
|
||||
{profile_section}
|
||||
## 当前建议话题({stage_name})
|
||||
可以从中选一个来问:{topics_str}
|
||||
|
||||
## 你的任务
|
||||
1. **先开口**:用一两句亲切的问候开场(如「你好呀,有空聊聊你的故事吗」)。
|
||||
2. **必须问一个问题**:接着问一个**具体、好回答**的问题,引导用户开始分享(如童年、家乡、印象深的事等)。不要问太宽泛的「有什么想聊的」。
|
||||
3. 语气像老朋友,自然、温暖。
|
||||
|
||||
## 回复格式
|
||||
- 可以分成 2 条消息,用 [SPLIT] 分隔:第一条问候,第二条问题;或合并成一条「问候 + 问题」。
|
||||
- **严禁**输出括号、注释、思考过程。
|
||||
- **严禁**模拟或虚构用户的回答。你只能输出「你的问候 + 你的问题」,不能替用户回答,不能自问自答。最多 2 段(问候 + 问题),禁止更多。
|
||||
|
||||
示例(仅供参考风格):
|
||||
"你好呀~ 有空的话想听听你的人生故事。你小时候是在哪儿长大的?那边有什么特别让你怀念的?"
|
||||
或
|
||||
"在的!今天想聊聊你。你童年里印象最深的一件事是什么?"
|
||||
|
||||
直接输出你要说的话(多条用 [SPLIT] 分隔):"""
|
||||
|
||||
|
||||
def _build_era_context(current_stage: str, user_profile_context: str) -> str:
|
||||
"""
|
||||
根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示。
|
||||
让 agent 在对话中自然融入时代感。
|
||||
"""
|
||||
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"""
|
||||
## 时代背景参考{place_hint}
|
||||
用户在这个人生阶段大约经历了 {era_start}-{era_end} 年({age_range[0]}-{age_range[1]} 岁):
|
||||
{";".join(era_events)}
|
||||
|
||||
你可以在对话中自然地提及这些时代元素来丰富提问,例如:
|
||||
- "那个年代好像正好赶上xxx,你们那边是什么情况?"
|
||||
- "听说那时候xxx特别流行,你有印象吗?"
|
||||
- 不要生硬地列举历史事件,而是像聊天一样自然带入"""
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
生成状态感知的对话提示词
|
||||
|
||||
Args:
|
||||
current_stage: 系统当前跟踪的阶段
|
||||
empty_slots: 当前阶段未填充的槽位
|
||||
filled_slots: 当前阶段已填充的槽位
|
||||
user_message: 用户消息
|
||||
conversation_turn: 总对话轮数
|
||||
same_topic_turns: 同一话题的轮数
|
||||
all_stages_coverage: 所有阶段的覆盖情况 {stage: {total, filled, empty, ratio}}
|
||||
detected_user_stage: 检测到用户正在谈论的阶段(可能和 current_stage 不同)
|
||||
user_profile_context: 用户基础资料上下文
|
||||
"""
|
||||
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
|
||||
|
||||
# --- 构建当前聊天上下文 ---
|
||||
# 转换 slot 名称为中文
|
||||
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 = random.choice(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)},如果聊天中有自然的契机,可以轻轻带一句,但不要刻意"
|
||||
|
||||
# --- 组合 prompt ---
|
||||
# 根据是否跳转,调整主题描述
|
||||
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)
|
||||
|
||||
prompt = f"""你是「岁月知己」,用户的老朋友,正在和他/她聊人生故事。{topic_desc}。
|
||||
{profile_section}
|
||||
## 已经聊到的内容({current_stage_name})
|
||||
{filled_slots_str}
|
||||
|
||||
## 还可以聊的方向({current_stage_name})
|
||||
{empty_slots_str}
|
||||
|
||||
## 整体进度
|
||||
{progress_str}
|
||||
{era_context}
|
||||
## 用户刚才说
|
||||
"{user_message}"
|
||||
|
||||
## 回应风格
|
||||
{style_guidance}
|
||||
|
||||
## 你的任务
|
||||
1. **回应用户**:先对用户说的内容做出真诚回应(不是总结,而是有温度的反馈)
|
||||
2. **跟随用户**:如果用户聊到了其他人生阶段的内容(比如从童年跳到工作),完全没问题,顺着他/她的思路继续聊。回忆本来就是跳跃的,不要强行拉回某个固定话题
|
||||
3. **保持自然**:不要每次都追问,有时候可以分享感受、表达好奇、或者轻松聊两句
|
||||
4. **适时引导**:跟着用户的节奏聊了几轮后,如果有自然的时机,可以温和地引向还没聊到的人生阶段,但绝不要生硬
|
||||
5. **追问要具体**:如果要追问,问具体的细节,比如"那时候是什么季节""身边有谁陪着你""当时心里什么感觉"
|
||||
6. **融入时代感**:如果有时代背景信息,在聊天中自然地提及当时的社会环境、流行文化、历史事件,让对话更有代入感和共鸣
|
||||
{dynamic_guidance}{uncovered_hint}
|
||||
|
||||
## 回复格式
|
||||
- 如果内容较多,可以分成 2-3 条消息,用 [SPLIT] 分隔
|
||||
- 每条消息保持自然,像微信聊天一样
|
||||
- 第一条消息是回应,第二条可以是追问或者换话题
|
||||
- 如果内容简单,一条消息即可
|
||||
|
||||
## 严格禁止
|
||||
- 禁止输出括号、注释、思考过程
|
||||
- 禁止说"我注意到""我想问""让我们聊聊"
|
||||
- 禁止生硬地问"还有什么想分享的吗"
|
||||
- 禁止反复追问同一件事
|
||||
- 禁止每次都以问题结尾
|
||||
- **禁止在用户聊别的话题时强行拉回之前的话题**
|
||||
- **禁止询问或再次确认「用户基本信息」中已列出的内容**(如出生年份、出生地、成长地、职业等,这些你已经知道,不要问第二遍)
|
||||
|
||||
## 好的回应示例
|
||||
- "哈哈,你这说的让我想起..."(轻松)
|
||||
- "这段经历听起来真不容易啊"(共情)
|
||||
- "那个年代的xxx确实是这样"(理解)
|
||||
- "所以后来怎么样了?"(好奇)
|
||||
- "对了,你刚才提到xxx,那个时候..."(换话题)
|
||||
- "那会儿好像正赶上改革开放,你们那边变化大吗?"(时代融入)
|
||||
- "80年代初的xxx,你还有印象吗?"(时代细节)
|
||||
|
||||
直接输出你要说的话(多条消息用 [SPLIT] 分隔):"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
# 保留向后兼容的函数名
|
||||
def get_conversation_prompt(current_stage: ConversationStage, covered_topics: List[str], user_latest_response: str) -> str:
|
||||
"""向后兼容的函数"""
|
||||
return get_system_prompt(current_stage, covered_topics, user_latest_response)
|
||||
364
api/app/agents/prompts/memory_prompts.py
Normal file
364
api/app/agents/prompts/memory_prompts.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
回忆录整理 Agent 提示词模板
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
# 章节分类映射
|
||||
CHAPTER_CATEGORIES = {
|
||||
"childhood": "童年与成长背景",
|
||||
"education": "教育经历与青年时期",
|
||||
"career_early": "崭露头角",
|
||||
"career_achievement": "主要成就与巅峰时刻",
|
||||
"career_challenge": "挫折、挑战与重大转折",
|
||||
"family": "家庭与情感",
|
||||
"beliefs": "信念与价值观",
|
||||
"summary": "人生总结",
|
||||
}
|
||||
|
||||
# 章节顺序
|
||||
CHAPTER_ORDER = [
|
||||
"childhood",
|
||||
"education",
|
||||
"career_early",
|
||||
"career_achievement",
|
||||
"career_challenge",
|
||||
"family",
|
||||
"beliefs",
|
||||
"summary",
|
||||
]
|
||||
|
||||
# 统一的阶段名 → 排序索引映射
|
||||
# 兼容 5 阶段简化名(conversation/state 模型)和 8 分类详细名(chapter 模型)
|
||||
STAGE_TO_ORDER = {
|
||||
"childhood": 0,
|
||||
"education": 1,
|
||||
"career": 2, # 5-stage 简化名
|
||||
"career_early": 2, # 8-category 详细名
|
||||
"career_achievement": 3,
|
||||
"career_challenge": 4,
|
||||
"family": 5,
|
||||
"belief": 6, # 5-stage 简化名(单数)
|
||||
"beliefs": 6, # 8-category 详细名(复数)
|
||||
"summary": 7,
|
||||
}
|
||||
|
||||
# 图片占位符入库前拼接的固定提示词模板(与原先 prompt 中要求一致,改为代码侧统一拼接)
|
||||
IMAGE_PLACEHOLDER_TEMPLATE = (
|
||||
"温暖怀旧风格,年代感复古色调,柔和光影,朴素温馨氛围,安静治愈,低饱和度,"
|
||||
"质感柔和细腻,简约构图,充满岁月沉淀感与故事感,高清唯美插画封面,不要包含文字,"
|
||||
"要适合老年人审美,画面要真实可信、让老年人产生共鸣与代入感,"
|
||||
"场景环境、建筑风格、服饰器物必须严格符合所述时代背景和地域特色,"
|
||||
"有朦胧怀旧的年代感。"
|
||||
)
|
||||
|
||||
|
||||
# 匹配任意层数的图片占位符(2/4/6/8...层花括号),整段替换为规范四层,避免多余花括号残留导致客户端显示异常
|
||||
_IMAGE_PLACEHOLDER_ANY_BRACES_RE = re.compile(
|
||||
r"(\{\{)+IMAGE:\s*([^}]+)(\}\})+",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def inject_image_placeholder_template(content: str) -> str:
|
||||
"""
|
||||
入库前对章节正文做占位符处理:用正则匹配所有图片占位符位置,拼上固定模板。
|
||||
支持任意层数花括号(如 {{、{{{{、{{{{{{ 等),输出统一为四层大括号 + 固定模板 + 描述,
|
||||
避免 LLM 输出花括号过多时只替换内层导致多余花括号残留在正文中、在手机端被原样显示。
|
||||
若占位符内已包含固定模板前缀则不再重复拼接。
|
||||
"""
|
||||
if not content or not content.strip():
|
||||
return content
|
||||
|
||||
def replace_one(match: re.Match) -> str:
|
||||
inner = (match.group(2) or "").strip()
|
||||
if not inner:
|
||||
return match.group(0)
|
||||
if inner.startswith(IMAGE_PLACEHOLDER_TEMPLATE):
|
||||
desc = inner[len(IMAGE_PLACEHOLDER_TEMPLATE):].lstrip("。").strip()
|
||||
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + ("。" + desc if desc else "") + "}}}}"
|
||||
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + "。" + inner + "}}}}"
|
||||
|
||||
content = _IMAGE_PLACEHOLDER_ANY_BRACES_RE.sub(replace_one, content)
|
||||
return content
|
||||
def get_system_prompt() -> str:
|
||||
"""获取整理 Agent 的系统提示词"""
|
||||
return """你是一位专业的传记作家和文字编辑,擅长将口语化的对话内容整理成优雅的书面语回忆录章节。
|
||||
|
||||
你的任务:
|
||||
1. 接收对话段落文本(口语化,可能来自语音转写)
|
||||
2. **先提炼对话中与人生经历相关的核心内容**,过滤掉无关信息
|
||||
3. 识别内容主题,归类到对应章节(童年/教育/事业/家庭/信念/总结)
|
||||
4. 将口语化表达改写为书面语,保持原意和情感
|
||||
5. 生成合适的章节标题和段落结构
|
||||
6. 提取关键信息,形成连贯的叙述
|
||||
7. 建议插图位置(在描述场景、人物、地点的地方)
|
||||
|
||||
## 内容筛选原则(最重要)
|
||||
对话中往往夹杂大量与回忆录无关的噪音,你必须严格筛选,只保留有价值的内容:
|
||||
|
||||
应该保留的内容:
|
||||
- 具体的人生事件、经历、故事
|
||||
- 提到的人物及其关系(家人、朋友、同事、恩师等)
|
||||
- 地点、时间、场景描写
|
||||
- 用户的情感表达、内心感受
|
||||
- 人生感悟、价值观、信念
|
||||
- 具体的细节(食物、声音、画面等)
|
||||
|
||||
应该过滤掉的内容:
|
||||
- 语气词、填充词(嗯、啊、那个、就是说、对对对、然后呢等)
|
||||
- 对话中的寒暄、问候(你好、谢谢、好的等)
|
||||
- 用户与AI助手之间的交互指令(你帮我、我想问、你说得对等)
|
||||
- 重复、冗余的表述(取核心含义即可)
|
||||
- 与个人经历完全无关的闲聊内容
|
||||
|
||||
## 改写原则
|
||||
- 保持用户的真实情感
|
||||
- 使用优雅但不失亲切的书面语,不要直接引用对话原话
|
||||
- 适当添加过渡句,使段落连贯
|
||||
- 保留生动的细节,但将口语表达改写为书面叙述
|
||||
- 去除口语中的填充词和无意义重复
|
||||
- 保持时间顺序和逻辑清晰
|
||||
|
||||
## 章节分类规则
|
||||
- 童年相关 → "童年与成长背景"
|
||||
- 学校、老师、同学 → "教育经历与青年时期"
|
||||
- 工作、职业、成就 → "主要成就与巅峰时刻" 或 "崭露头角"
|
||||
- 困难、挫折 → "挫折、挑战与重大转折"
|
||||
- 伴侣、孩子、家庭生活 → "家庭与情感"
|
||||
- 价值观、信念、座右铭 → "信念与价值观"
|
||||
- 总结、感悟、展望 → "人生总结"
|
||||
"""
|
||||
|
||||
|
||||
def get_chapter_classification_prompt(segments_text: str) -> str:
|
||||
"""获取章节分类的提示词"""
|
||||
return f"""{get_system_prompt()}
|
||||
|
||||
请分析以下对话内容,**忽略其中的语气词、寒暄和无关对话**,只关注涉及人生经历的实质内容,判断应该归类到哪个章节类别:
|
||||
- childhood: 童年与成长背景
|
||||
- education: 教育经历与青年时期
|
||||
- career_early: 崭露头角(早期事业)
|
||||
- career_achievement: 主要成就与巅峰时刻
|
||||
- career_challenge: 挫折、挑战与重大转折
|
||||
- family: 家庭与情感
|
||||
- beliefs: 信念与价值观
|
||||
- summary: 人生总结
|
||||
|
||||
对话内容:
|
||||
{segments_text}
|
||||
|
||||
请只返回章节类别(如:childhood),不要返回其他内容。
|
||||
如果对话内容中没有任何与人生经历相关的实质内容,返回 none。"""
|
||||
|
||||
|
||||
def get_text_rewrite_prompt(segments_text: str, chapter_category: str, existing_content: str = "") -> str:
|
||||
"""获取文本改写的提示词"""
|
||||
chapter_name = CHAPTER_CATEGORIES.get(chapter_category, chapter_category)
|
||||
|
||||
existing_section = f"\n\n已有章节内容:\n{existing_content}" if existing_content else ""
|
||||
|
||||
return f"""{get_system_prompt()}
|
||||
|
||||
请将以下口语化的对话内容改写为书面语,归类到"{chapter_name}"章节。
|
||||
|
||||
对话内容:
|
||||
{segments_text}
|
||||
{existing_section}
|
||||
|
||||
请按照以下格式返回 JSON:
|
||||
{{
|
||||
"title": "章节标题",
|
||||
"content": "改写后的书面语内容(包含图片占位符)",
|
||||
"summary": "章节摘要(50字以内)"
|
||||
}}
|
||||
|
||||
要求:
|
||||
1. 标题要简洁有力,能概括章节主题
|
||||
2. 内容要流畅自然,保持原意和情感
|
||||
3. 如果已有章节内容,请将新内容与已有内容自然融合
|
||||
4. 在内容中适当位置插入图片占位符
|
||||
|
||||
## 图片占位符格式(必须严格遵守)
|
||||
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,中间为 IMAGE:具体描述。即:{{{{IMAGE:具体的图片描述}}}}
|
||||
- 禁止使用两层 {{ }}、六层 {{{{{{ }}}}}} 或任意其它层数,否则会在手机端显示异常。
|
||||
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
||||
|
||||
正确示例(仅此格式):
|
||||
{{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
||||
{{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}"""
|
||||
|
||||
|
||||
def get_state_extraction_prompt(user_message: str, current_stage: str, stage_slots: dict) -> str:
|
||||
"""抽取结构化信息并判断阶段"""
|
||||
slot_keys = list(stage_slots.keys())
|
||||
|
||||
# 提供所有阶段的 slot 参考,帮助 LLM 将内容归类到正确的阶段
|
||||
all_stage_slots = {
|
||||
"childhood": ["place", "people", "daily_life", "emotion", "turning_event"],
|
||||
"education": ["school", "city", "motivation", "challenge", "change"],
|
||||
"career": ["job", "environment", "decision", "pressure", "growth"],
|
||||
"family": ["relationship", "conflict", "support", "responsibility", "change"],
|
||||
"belief": ["value", "regret", "pride", "lesson"],
|
||||
}
|
||||
|
||||
return f"""{get_system_prompt()}
|
||||
|
||||
你需要从用户话语中**先提炼与人生经历相关的核心内容**,然后抽取结构化信息,并判断用户实际在谈论哪个人生阶段。
|
||||
|
||||
系统当前跟踪的阶段:{current_stage}
|
||||
该阶段可填 slots:{slot_keys}
|
||||
|
||||
所有阶段及其 slots 参考:
|
||||
{json.dumps(all_stage_slots, ensure_ascii=False, indent=2)}
|
||||
|
||||
用户话语:
|
||||
{user_message}
|
||||
|
||||
请只返回 JSON,格式如下:
|
||||
{{
|
||||
"detected_stage": "childhood|education|career|family|belief",
|
||||
"slots": {{
|
||||
"slot_key": "snippet"
|
||||
}},
|
||||
"emotion": "neutral|warm|low|highlight",
|
||||
"is_new_chapter": true
|
||||
}}
|
||||
|
||||
要求:
|
||||
1. **应的 slot 列表
|
||||
4. slots 只填写确实提到的、与人生经历相关的实先忽略话语中的语气词、填充词、寒暄、与AI的交互指令等无关内容**,只关注涉及人生经历的实质信息
|
||||
2. **detected_stage 必须根据用户话语的实际内容判断**,不要默认沿用系统当前阶段。用户可能在聊不同阶段的事情
|
||||
3. slots 的 key 必须属于 detected_stage 对质内容
|
||||
5. **snippet 应是提炼后的核心信息**,去除语气词和冗余表达,50 字以内
|
||||
6. 如果用户话语中没有任何与人生经历相关的实质内容(如纯粹的寒暄、指令、语气词),slots 为空对象
|
||||
"""
|
||||
|
||||
|
||||
def _build_age_hint(stage: str, birth_year: Optional[int] = None) -> str:
|
||||
"""根据人生阶段和出生年份推算大致年龄区间"""
|
||||
if not birth_year:
|
||||
return ""
|
||||
stage_age_ranges = {
|
||||
"childhood": (0, 12),
|
||||
"education": (6, 22),
|
||||
"career": (18, 60),
|
||||
"career_early": (18, 30),
|
||||
"career_achievement": (25, 55),
|
||||
"career_challenge": (20, 55),
|
||||
"family": (20, 60),
|
||||
"belief": (30, 70),
|
||||
"beliefs": (30, 70),
|
||||
"summary": (50, 80),
|
||||
}
|
||||
age_range = stage_age_ranges.get(stage)
|
||||
if not age_range:
|
||||
return ""
|
||||
year_start = birth_year + age_range[0]
|
||||
year_end = birth_year + age_range[1]
|
||||
return f"大约 {year_start}-{year_end} 年({age_range[0]}-{age_range[1]} 岁)"
|
||||
|
||||
|
||||
def get_creative_title_prompt(
|
||||
stage: str,
|
||||
emotion: str,
|
||||
slots: dict,
|
||||
user_profile: str = "",
|
||||
birth_year: Optional[int] = None,
|
||||
) -> str:
|
||||
"""生成有创意的章节标题,包含年龄/时间信息"""
|
||||
age_hint = _build_age_hint(stage, birth_year)
|
||||
profile_section = f"\n用户基本信息:\n{user_profile}" if user_profile else ""
|
||||
time_section = f"\n时间参考:{age_hint}" if age_hint else ""
|
||||
|
||||
return f"""{get_system_prompt()}
|
||||
|
||||
请根据阶段和情绪生成 1 个有创意的章节标题。
|
||||
阶段:{stage}
|
||||
情绪:{emotion}
|
||||
可用信息:{slots}{profile_section}{time_section}
|
||||
|
||||
要求:
|
||||
1. 标题格式:「时间标注 · 标题正文」
|
||||
- 时间标注用年龄或年代表示,如"6-12岁"、"1980年代"、"二十出头"
|
||||
- 标题正文 12-18 字以内
|
||||
2. 情绪 + 人生阶段 + 意象
|
||||
3. 示例风格:
|
||||
- 《6-12岁 · 那条巷子尽头的蝉鸣》
|
||||
- 《18岁 · 第一次离开家的夏天》
|
||||
- 《25-35岁 · 在陌生城市站稳脚跟》
|
||||
- 《四十不惑 · 慢下来,人生开始发声》
|
||||
- 《1990年代 · 不是所有选择都被理解》
|
||||
|
||||
只输出标题文字,不要加引号或书名号。
|
||||
"""
|
||||
|
||||
|
||||
def get_narrative_prompt(
|
||||
stage: str,
|
||||
slots: dict,
|
||||
new_content: str,
|
||||
existing_content: str = "",
|
||||
user_profile: str = "",
|
||||
birth_year: Optional[int] = None,
|
||||
) -> str:
|
||||
"""将新对话改写为叙述(只输出新内容的改写,不重复已有内容)"""
|
||||
context_tail = ""
|
||||
if existing_content:
|
||||
context_tail = existing_content[-300:] if len(existing_content) > 300 else existing_content
|
||||
|
||||
context_section = f"\n\n【衔接上下文(已有内容的末尾,仅供参考衔接,不要重复)】:\n{context_tail}" if context_tail else ""
|
||||
|
||||
profile_section = f"\n\n用户基本信息:\n{user_profile}" if user_profile else ""
|
||||
age_hint = _build_age_hint(stage, birth_year)
|
||||
time_section = f"\n时间参考:{age_hint}" if age_hint else ""
|
||||
|
||||
return f"""{get_system_prompt()}
|
||||
|
||||
请将以下新的对话内容改写为第一人称文学叙述。
|
||||
阶段:{stage}
|
||||
可用信息:{slots}{profile_section}{time_section}
|
||||
|
||||
新的对话内容:
|
||||
{new_content}
|
||||
{context_section}
|
||||
|
||||
## 第一步:提炼核心内容
|
||||
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:
|
||||
- 提取具体的事件、人物、地点、时间、感受
|
||||
- 丢弃语气词(嗯、啊、那个、就是说)、寒暄(你好、谢谢)、与AI的交互(你帮我整理一下、对对对你说得对)、无意义的重复
|
||||
- 如果对话内容中几乎没有与人生经历相关的实质内容,请输出空字符串
|
||||
|
||||
## 第二步:改写为叙述
|
||||
基于提炼后的核心内容进行文学改写:
|
||||
1. 使用第一人称叙述
|
||||
2. **不要直接引用对话原话**,将所有内容改写为流畅的书面叙述
|
||||
3. **只输出新内容的改写结果**,不要重复已有内容
|
||||
4. 如果有衔接上下文,确保新内容与之自然衔接(语气、时间线连贯)
|
||||
5. 语气自然,有情绪
|
||||
6. 在适合配图的地方插入图片占位符
|
||||
7. 如果有用户的基本信息(出生地、成长地等),在叙述中自然融入地域文化和时代背景
|
||||
8. **不要将对话中的交互性语言(如"我跟你说"、"你知道吗")写入叙述**
|
||||
9. **不要在正文中插入章节标题或分类标签**(如"章节:信念与价值观"、"## 童年与成长背景"等),章节标题由系统单独管理
|
||||
|
||||
## 图片占位符格式(必须严格遵守)
|
||||
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,即:{{{{IMAGE:具体的图片描述}}}}
|
||||
- 禁止两层 {{ }}、六层 {{{{{{ }}}}}} 或其它层数,否则会在手机端显示多余花括号。
|
||||
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
||||
|
||||
正确示例(仅此格式):
|
||||
- {{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
||||
- {{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}
|
||||
- {{{{IMAGE:少年背着书包站在火车站台上,回望身后的小镇}}}}
|
||||
- {{{{IMAGE:泛黄的大学录取通知书,压在一摞旧课本下}}}}
|
||||
|
||||
图片占位符要求:
|
||||
- 描述要具体、有画面感,便于后续生成或匹配图片
|
||||
- 每 200-300 字左右可以插入一个
|
||||
- 单独占一行,不要嵌入段落中
|
||||
- 不要使用括号或星号等其他格式
|
||||
- **花括号必须且仅能为四层**:{{{{ 与 }}}} 各四个,不多不少
|
||||
|
||||
只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。
|
||||
"""
|
||||
174
api/app/agents/prompts/profile_prompts.py
Normal file
174
api/app/agents/prompts/profile_prompts.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
用户基础资料收集提示词
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
PROFILE_FIELD_NAMES = {
|
||||
"birth_year": "出生年份",
|
||||
"birth_place": "出生地",
|
||||
"grew_up_place": "成长地",
|
||||
"occupation": "职业",
|
||||
}
|
||||
|
||||
|
||||
def get_profile_greeting_prompt(missing_fields: List[str], nickname: str = "") -> str:
|
||||
"""生成初次见面、收集基础资料的引导提示词"""
|
||||
missing_names = [PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES]
|
||||
missing_str = "、".join(missing_names)
|
||||
name_part = f",{nickname}" if nickname else ""
|
||||
|
||||
return f"""你是「岁月知己」,一位温暖真诚的人生故事访谈者。你正在和用户初次见面{name_part}。
|
||||
|
||||
在正式聊人生故事之前,你需要先了解一些基本信息。还需要了解的信息有:{missing_str}。
|
||||
|
||||
## 你的任务
|
||||
用自然、亲切的方式,像老朋友聊天一样,向用户询问这些基础信息。
|
||||
|
||||
## 规则
|
||||
1. 不要一次问所有问题,每次只问 1-2 个
|
||||
2. 如果用户已经在对话中提到了某些信息,不要重复问
|
||||
3. 用口语化、亲切的方式提问
|
||||
4. 当所有信息都收集完后,自然过渡到人生故事访谈
|
||||
|
||||
## 提问示例
|
||||
- "你是哪一年出生的呀?"
|
||||
- "你是在哪里出生的?小时候也是在那里长大的吗?"
|
||||
- "你现在是做什么工作的呀?或者之前主要从事什么职业?"
|
||||
|
||||
## 严格禁止
|
||||
- 禁止输出括号注释、思考过程
|
||||
- 禁止说"我需要收集信息"之类的机械话
|
||||
- 禁止一次列出所有问题
|
||||
|
||||
## 回复格式
|
||||
- 如果内容较多,可以用 [SPLIT] 分隔成多条消息
|
||||
- 像微信聊天一样自然
|
||||
|
||||
直接输出你要说的话:"""
|
||||
|
||||
|
||||
def get_profile_extraction_prompt(
|
||||
user_message: str,
|
||||
missing_fields: List[str],
|
||||
recent_dialogue: Optional[str] = None,
|
||||
) -> str:
|
||||
"""从用户回答中提取基础资料信息(可包含最近几轮对话,避免漏提)"""
|
||||
missing_names = {f: PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES}
|
||||
|
||||
dialogue_section = ""
|
||||
if recent_dialogue and recent_dialogue.strip():
|
||||
dialogue_section = f"""
|
||||
最近几轮对话(可从用户任一轮回答中提取):
|
||||
{recent_dialogue.strip()}
|
||||
|
||||
"""
|
||||
return f"""请从以下内容中提取用户已提到的基础资料信息。{dialogue_section}用户本轮回答:
|
||||
"{user_message}"
|
||||
|
||||
需要提取的字段(只提取确实在对话中出现过的):
|
||||
{missing_names}
|
||||
|
||||
请返回 JSON 格式,只包含确实提到的字段:
|
||||
{{
|
||||
"birth_year": 1965,
|
||||
"birth_place": "湖南长沙",
|
||||
"grew_up_place": "湖南长沙",
|
||||
"occupation": "教师"
|
||||
}}
|
||||
|
||||
规则:
|
||||
1. birth_year 填整数(四位数),如"65年出生"转为 1965
|
||||
2. 如果用户在任一轮说过出生地/成长地/职业等,都要提取
|
||||
3. 只提取明确提到的信息,不要猜测
|
||||
4. 如果没有提取到任何信息,返回空对象 {{}}
|
||||
|
||||
只返回 JSON,不要其他内容。"""
|
||||
|
||||
|
||||
def get_profile_followup_prompt(
|
||||
missing_fields: List[str],
|
||||
filled_fields: Dict[str, str],
|
||||
user_message: str,
|
||||
nickname: str = "",
|
||||
) -> str:
|
||||
"""在收集资料过程中的跟进提问"""
|
||||
missing_names = [PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES]
|
||||
missing_str = "、".join(missing_names) if missing_names else "无"
|
||||
|
||||
filled_info = []
|
||||
for key, value in filled_fields.items():
|
||||
name = PROFILE_FIELD_NAMES.get(key, key)
|
||||
filled_info.append(f"{name}: {value}")
|
||||
filled_str = "\n".join(filled_info) if filled_info else "暂无"
|
||||
|
||||
if not missing_names:
|
||||
return f"""你是「岁月知己」。用户的基本信息已经收集完毕:
|
||||
{filled_str}
|
||||
|
||||
用户刚才说:"{user_message}"
|
||||
|
||||
请对用户的回答做出温暖的回应,然后自然地过渡到人生故事的访谈。
|
||||
可以说类似"了解了!那我们现在开始聊聊你的人生故事吧"这样的话,然后问一个关于童年的问题作为开场。
|
||||
|
||||
回复格式:多条消息用 [SPLIT] 分隔。
|
||||
直接输出你要说的话:"""
|
||||
|
||||
return f"""你是「岁月知己」,正在和用户聊天收集基本信息。
|
||||
|
||||
## 已知信息(严禁再次询问以下任何一项)
|
||||
{filled_str}
|
||||
|
||||
## 还需要了解
|
||||
{missing_str}
|
||||
|
||||
用户刚才说:"{user_message}"
|
||||
|
||||
请先对用户说的内容做出自然回应,然后**只**询问「还需要了解」中的信息(每次问 1-2 个)。
|
||||
语气要像朋友聊天一样自然亲切。
|
||||
|
||||
严格禁止:
|
||||
- **严禁再次询问「已知信息」中已列出的内容**(例如已知出生年份就绝不要再问哪年出生)
|
||||
- 禁止输出括号注释、思考过程
|
||||
- 禁止说"我注意到""我需要了解"
|
||||
|
||||
回复格式:多条消息用 [SPLIT] 分隔。
|
||||
直接输出你要说的话:"""
|
||||
|
||||
|
||||
def format_user_profile_context(
|
||||
birth_year: Optional[int] = None,
|
||||
birth_place: Optional[str] = None,
|
||||
grew_up_place: Optional[str] = None,
|
||||
occupation: Optional[str] = None,
|
||||
) -> str:
|
||||
"""将用户基础信息格式化为上下文字符串,供其他 agent 使用"""
|
||||
parts = []
|
||||
if birth_year:
|
||||
parts.append(f"出生年份:{birth_year}年")
|
||||
if birth_place:
|
||||
parts.append(f"出生地:{birth_place}")
|
||||
if grew_up_place:
|
||||
parts.append(f"成长地:{grew_up_place}")
|
||||
if occupation:
|
||||
parts.append(f"职业:{occupation}")
|
||||
return "\n".join(parts) if parts else ""
|
||||
|
||||
|
||||
def get_missing_profile_fields(
|
||||
birth_year: Optional[int] = None,
|
||||
birth_place: Optional[str] = None,
|
||||
grew_up_place: Optional[str] = None,
|
||||
occupation: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""返回缺失的用户资料字段列表"""
|
||||
missing = []
|
||||
if not birth_year:
|
||||
missing.append("birth_year")
|
||||
if not birth_place:
|
||||
missing.append("birth_place")
|
||||
if not grew_up_place:
|
||||
missing.append("grew_up_place")
|
||||
if not occupation:
|
||||
missing.append("occupation")
|
||||
return missing
|
||||
Reference in New Issue
Block a user