Files
life-echo/api/app/agents/chat/prompts_conversation.py
Kevin 3121d1384d WIP: memory system improvements (in progress)
Interview/chat prompt layers, reply planner, style profiles, memory
injection, interview meta store, and related tests. Work not finished.

Made-with: Cursor
2026-04-22 16:56:28 +08:00

313 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
对话 Agent 提示词模板(场景化承接 + 细节深挖 + 人物串联)。
"""
from typing import Dict, List, Optional
from app.agents.chat.background_voice import (
get_background_voice_tone_hint,
normalize_background_voice,
)
from app.agents.chat.occupation_context import get_occupation_chat_hint
from app.agents.chat.output_rules import chat_output_rules
from app.agents.chat.personas import (
get_interview_persona_tone_hint,
normalize_interview_persona,
)
from app.agents.chat.prompt_layers import (
assemble_guided_prompt,
build_absolute_donts_block,
build_behavior_policy_block,
build_context_block,
build_question_outline_block,
build_reply_strategy_block,
build_style_profile_block,
)
from app.agents.stage_constants import STAGE_DISPLAY_ZH, STAGE_ERA_HINTS
from app.agents.state_schema import KnownFact, PersonaThread
from app.core.config import settings
# 风格示例的单一事实源已迁至 `app.agents.style_profiles.ChatStyleProfile.reply_style_examples`
# 这里**不再**维护具体字面示例,避免同一模块被当作 few-shot 锚点反复注入,导致风格过拟合。
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": "人生经验",
}
def _compact_era_hint(
current_stage: str,
*,
birth_year: int | None = None,
era_place: str = "",
) -> str:
if not birth_year:
return ""
birth_place = (era_place or "").strip()
age_range = STAGE_ERA_HINTS.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}")
parts: List[str] = []
if era_events:
place_hint = f" {birth_place}" if birth_place else ""
parts.append(
f"时代联想(口述里一两句带过即可):约 {era_start}-{era_end}{place_hint}"
f"可提及 {era_events[0]}"
+ (f"{era_events[1]}" if len(era_events) > 1 else "")
+ ""
)
parts.append(
"时代与流行文化(开放式,自然带入):\n"
"- 可从当时的街景、媒介、校园与市井、年节习俗等**泛泛**起头,邀请用户讲自己的版本,勿替用户断言细节。\n"
"- **优先开放式**问法;少用「你是不是也……」式半封闭逼认。\n"
"- 与大事记呼应时点到为止,勿展开成长串史实。"
)
return "\n".join(parts) + "\n"
def get_opening_prompt(
current_stage: str,
empty_slots_readable: List[str],
user_profile_context: str = "",
persona: str = "default",
background_voice: str = "default",
occupation: str = "",
profile_birth_year: Optional[int] = None,
profile_era_place: str = "",
) -> str:
"""空对话时 AI 先开口的提示词"""
stage_name = STAGE_DISPLAY_ZH.get(current_stage, current_stage)
bv_open = normalize_background_voice(background_voice)
if empty_slots_readable:
topics_str = "".join(empty_slots_readable)
topics_heading = (
f"## 当前建议话题({stage_name}\n可以从中选一个来问:{topics_str}"
)
task_question = (
"2. 你是**主持式知己**:接着问一个**具体、好回答、有画面感**的问题,帮用户进入**人生回忆**叙述;"
"优先落在上述还未聊透的方向上。不要问太宽泛的「有什么想聊的」「最近怎么样」。"
"像把门敞开请人讲自己的故事,不要像面试第一题;一句里带一个小锚(地方、人物、物件或一天里的片段即可)。"
"不要用「下面我们聊聊…」类未承接的硬切。好问题举例:「说到童年,你脑海里最先蹦出来的是哪个画面?」"
)
else:
topics_heading = (
f"## 当前阶段({stage_name}\n"
"这一阶段的主要话题在素材侧**已有覆盖**。"
"开场仍要**回到人生故事线**:优先接续上次聊过的片段、(若有)记忆线索里出现过的事,或当前阶段里**新鲜的一小角**"
"**禁止**为了凑问题而从「童年在哪长大」等已覆盖模板重头盘问;**也不要**把泛泛近况(「今天忙吗」「最近好吗」)当成默认主线。"
)
task_question = (
"2. **问候 + 回忆向勾子**:温暖接话后,带一个与**口述回忆**有关的轻巧引子或具体问题;"
"若接不上具体事,就用当前阶段的一个**有画面的开放式起头**,仍落在人生经历上,而非纯社交寒暄。"
)
if bv_open == "cadre":
opening_style_rules = (
"## 语境与语气(干部/机关)\n"
"- 问候稳重、敬语适度;避免官样排比与过轻佻的网络撒娇语气。\n"
)
elif bv_open == "military":
opening_style_rules = (
"## 语境与语气(军队相关口述常见交流方式)\n"
"- 简洁、得体;不用「嗨~」类过轻佻起势;不堆军事辞藻、不编军旅细节。\n"
)
else:
opening_style_rules = (
"## 风格\n"
"- 像**温暖的谈话场主持人**:口语、自然、能接住人,但默认把用户带进**人生回忆**叙述;"
"可轻快,允许一点画面感,不要排比和长段文学描写。\n"
)
profile_lines: List[str] = []
if user_profile_context.strip():
profile_lines.append(user_profile_context.strip())
occ = get_occupation_chat_hint(occupation, background_voice)
if occ:
profile_lines.append(occ)
profile_section = ""
if profile_lines:
profile_section = "## 用户信息\n" + "\n".join(profile_lines) + "\n"
persona_key = normalize_interview_persona(persona)
persona_tone = get_interview_persona_tone_hint(persona_key)
voice_tone = get_background_voice_tone_hint(background_voice)
tone_bits = [t for t in (persona_tone, voice_tone) if t]
tone_paragraph = ""
if tone_bits:
tone_paragraph = " " + " ".join(tone_bits) + "\n\n"
opening_head = (
"你是「岁月知己」——主持式知己:用户刚进对话,**还没说话**,请你先开口。"
"语气像老朋友,但**职责是帮对方开口讲人生故事**;两三句内问候 + **一个落在当前阶段或建议话题上的、有画面感的问题**"
"不要排比、不要长段文学描写,**不要**把泛泛问近况当主菜。\n\n"
)
if bv_open != "default":
opening_head = (
"你是「岁月知己」——主持式知己:用户刚进对话,**还没说话**,请你先开口。"
"**短**;两三句内问候 + **一个回忆向的具体问题**;不要排比、不要文学描写。\n\n"
)
era_opening_line = ""
if (
settings.chat_era_context_enabled
and profile_birth_year is not None
and _compact_era_hint(
current_stage,
birth_year=profile_birth_year,
era_place=profile_era_place,
)
):
era_opening_line = (
"4. 用户资料里已有出生年份与时代参考时,问候里的具体问题可**轻轻带一点年代氛围**(点到为止),"
"勿写成长段描写或排比。\n"
)
return f"""{opening_head}{tone_paragraph}{profile_section}{topics_heading}
## 任务
1. 简短问候。
{task_question}
3. 自然、温暖。
{era_opening_line}
## 格式
- 可用 [SPLIT] 分成最多 2 条;或一条里「问候 + 问题」。
- {chat_output_rules()} 不要替用户编回答。
{opening_style_rules}
直接输出(仅自然口语,无 Markdown"""
def get_guided_conversation_prompt(
current_stage: str,
empty_slots: List[str],
filled_slots: Dict[str, str],
all_stages_coverage: Optional[Dict[str, Dict]] = None,
detected_user_stage: str = "",
user_profile_context: str = "",
persona: str = "default",
memory_evidence_text: str = "",
background_voice: str = "default",
occupation: str = "",
profile_birth_year: Optional[int] = None,
profile_era_place: str = "",
known_facts: list[KnownFact] | None = None,
persona_threads: list[PersonaThread] | None = None,
recent_questions: list[str] | None = None,
turn_directive_block: str = "",
) -> str:
"""生成状态感知的对话提示词;用户原话仅以 HumanMessage 传入,不写入本 system 文本。"""
persona_key = normalize_interview_persona(persona)
persona_tone = get_interview_persona_tone_hint(persona_key)
voice_tone = get_background_voice_tone_hint(background_voice)
tone_bits = [t for t in (persona_tone, voice_tone) if t]
tone_line = ""
if tone_bits:
tone_line = " " + " ".join(tone_bits)
user_jumped = bool(detected_user_stage and detected_user_stage != current_stage)
active_stage = (
detected_user_stage if user_jumped and detected_user_stage else current_stage
)
era_line = ""
if settings.chat_era_context_enabled:
era_line = _compact_era_hint(
active_stage,
birth_year=profile_birth_year,
era_place=profile_era_place,
)
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
# ---- Context 层:纯状态与素材 ----
topic_and_context_block = build_context_block(
current_stage=current_stage,
detected_user_stage=detected_user_stage,
empty_slots_readable=empty_slots_readable,
filled_slots=filled_slots,
slot_name_map=SLOT_NAME_MAP,
all_stages_coverage=all_stages_coverage,
user_profile_context=user_profile_context,
occupation=occupation,
background_voice=background_voice,
known_facts=known_facts,
persona_threads=persona_threads,
recent_questions=recent_questions,
memory_evidence_text=memory_evidence_text,
era_line=era_line,
)
question_outline_block = build_question_outline_block(current_stage, empty_slots)
# ---- BehaviorPolicy 层:通用行为规则(本轮模式由 TurnPlan 单独注入) ----
behavior_policy_block = build_behavior_policy_block()
reply_strategy_block = build_reply_strategy_block()
absolute_donts_block = build_absolute_donts_block(chat_output_rules())
# ---- StyleProfile 层:口吻 + 文采密度 + 成稿质量导向 ----
style_profile_block = build_style_profile_block(
persona=persona, background_voice=background_voice
)
return assemble_guided_prompt(
turn_directive_block=turn_directive_block,
topic_and_context_block=topic_and_context_block,
question_outline_block=question_outline_block,
behavior_policy_block=behavior_policy_block,
style_profile_block=style_profile_block,
reply_strategy_block=reply_strategy_block,
absolute_donts_block=absolute_donts_block,
intro_tone_line=tone_line,
)
# 运行时 prompt 生成走 `prompt_layers.assemble_guided_prompt`。
# 旧的超大 system prompt 已拆入 BehaviorPolicy / Context / StyleProfile 三层,此处不再保留快照。
__all__ = [
"SLOT_NAME_MAP",
"get_guided_conversation_prompt",
"get_opening_prompt",
]