feat & fix: 新增打个招呼选项 创建新会话;修复ai重复性提问的问题;修复输入键盘覆盖对话气泡的问题

This commit is contained in:
yangshilin
2026-03-11 14:39:39 +08:00
parent 4b4dea8a45
commit 4d2c31b5a6
9 changed files with 309 additions and 59 deletions

View File

@@ -12,7 +12,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from services.llm_service import llm_service
from services.redis_service import redis_service
from .prompts import ConversationStage, get_conversation_prompt, get_guided_conversation_prompt
from .prompts import ConversationStage, get_conversation_prompt, get_guided_conversation_prompt, get_opening_prompt
from .prompts.profile_prompts import (
get_profile_greeting_prompt,
get_profile_extraction_prompt,
@@ -20,6 +20,7 @@ from .prompts.profile_prompts import (
format_user_profile_context,
get_missing_profile_fields,
)
from .prompts.conversation_prompts import SLOT_NAME_MAP
from .state_schema import MemoirStateSchema
logger = logging.getLogger(__name__)
@@ -138,19 +139,83 @@ class ConversationAgent:
logger.error(f"生成资料收集开场白失败: {e}")
return ["你好!在我们开始聊人生故事之前,能先简单介绍一下你自己吗?比如你是哪一年出生的?"]
async def extract_profile_from_message(self, user_message: str, missing_fields: List[str]) -> Dict[str, Any]:
"""从用户消息中提取基础资料信息"""
async def generate_opening_message(
self,
conversation_id: str,
memoir_state: MemoirStateSchema,
user_profile_context: str = "",
) -> List[str]:
"""
空对话时 AI 先开口:用户通过「打个招呼」进入,尚未发任何消息。
生成问候 + 一个引导问题,写入 Redis 并返回消息列表。
"""
if not self.llm:
return ["你好呀~ 有空聊聊你的人生故事吗?你小时候是在哪儿长大的?"]
try:
empty_slots = memoir_state.empty_slots_for_current_stage()
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
if not empty_slots_readable:
empty_slots_readable = ["成长的地方", "难忘的事", "重要的人"]
prompt = get_opening_prompt(
current_stage=memoir_state.current_stage,
empty_slots_readable=empty_slots_readable,
user_profile_context=user_profile_context,
)
full_prompt = f"{prompt}\n\nAssistant:"
response = await self.llm.ainvoke(full_prompt)
response_text = response.content if hasattr(response, "content") else str(response)
await self._save_message(conversation_id, "ai", response_text)
messages = [msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()]
return messages[:3] if messages else [response_text]
except Exception as e:
logger.error(f"生成开场白失败: {e}", exc_info=True)
return ["你好呀~ 有空聊聊你的人生故事吗?你童年里印象最深的一件事是什么?"]
async def extract_profile_from_message(
self,
user_message: str,
missing_fields: List[str],
conversation_id: Optional[str] = None,
) -> Dict[str, Any]:
"""从用户消息中提取基础资料信息;若提供 conversation_id会结合最近几轮对话一起提取避免漏提。"""
if not self.llm or not missing_fields:
return {}
recent_dialogue = ""
if conversation_id:
history_messages = await self._get_history_messages(conversation_id)
# 取最近 4 条2 轮),不包含本轮;本轮由 user_message 单独传入
recent = history_messages[-4:] if len(history_messages) > 4 else history_messages
parts = []
for msg in recent:
if isinstance(msg, HumanMessage):
parts.append(f"用户: {msg.content}")
elif isinstance(msg, AIMessage):
parts.append(f"助手: {msg.content}")
recent_dialogue = "\n".join(parts) if parts else ""
try:
prompt = get_profile_extraction_prompt(user_message, missing_fields)
prompt = get_profile_extraction_prompt(
user_message, missing_fields, recent_dialogue=recent_dialogue or None
)
response = await self.llm.ainvoke(prompt)
content = response.content.strip()
parsed = json.loads(content)
result = {}
if "birth_year" in parsed and isinstance(parsed["birth_year"], int):
result["birth_year"] = parsed["birth_year"]
if "birth_year" in parsed and parsed["birth_year"] is not None:
raw = parsed["birth_year"]
if isinstance(raw, int) and 1900 <= raw <= 2100:
result["birth_year"] = raw
elif isinstance(raw, str) and raw.isdigit():
y = int(raw)
if y < 100: # "65" -> 1965
y = 1900 + y if y >= 50 else 2000 + y
if 1900 <= y <= 2100:
result["birth_year"] = y
if "birth_place" in parsed and parsed["birth_place"]:
result["birth_place"] = str(parsed["birth_place"])
if "grew_up_place" in parsed and parsed["grew_up_place"]:

View File

@@ -6,6 +6,7 @@ from .conversation_prompts import (
get_system_prompt as get_conversation_prompt,
get_questions_for_stage,
get_guided_conversation_prompt,
get_opening_prompt,
INTERVIEW_QUESTIONS,
)
from .memory_prompts import (
@@ -34,6 +35,7 @@ __all__ = [
"get_conversation_prompt",
"get_questions_for_stage",
"get_guided_conversation_prompt",
"get_opening_prompt",
"INTERVIEW_QUESTIONS",
"get_memory_prompt",
"get_chapter_classification_prompt",

View File

@@ -173,6 +173,47 @@ RESPONSE_STYLES = [
]
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] 分隔:第一条问候,第二条问题;或合并成一条「问候 + 问题」。
- 禁止输出括号、注释、思考过程。
示例(仅供参考风格):
"你好呀~ 有空的话想听听你的人生故事。你小时候是在哪儿长大的?那边有什么特别让你怀念的?"
"在的!今天想聊聊你。你童年里印象最深的一件事是什么?"
直接输出你要说的话(多条用 [SPLIT] 分隔):"""
def _build_era_context(current_stage: str, user_profile_context: str) -> str:
"""
根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示。
@@ -402,6 +443,7 @@ def get_guided_conversation_prompt(
- 禁止反复追问同一件事
- 禁止每次都以问题结尾
- **禁止在用户聊别的话题时强行拉回之前的话题**
- **禁止询问或再次确认「用户基本信息」中已列出的内容**(如出生年份、出生地、成长地、职业等,这些你已经知道,不要问第二遍)
## 好的回应示例
- "哈哈,你这说的让我想起..."(轻松)

View File

@@ -48,16 +48,25 @@ def get_profile_greeting_prompt(missing_fields: List[str], nickname: str = "") -
直接输出你要说的话:"""
def get_profile_extraction_prompt(user_message: str, missing_fields: List[str]) -> str:
"""从用户回答中提取基础资料信息"""
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}
return f"""请从用户的回答中提取基础资料信息。
dialogue_section = ""
if recent_dialogue and recent_dialogue.strip():
dialogue_section = f"""
最近几轮对话(可从用户任一轮回答中提取):
{recent_dialogue.strip()}
用户的回答:
"""
return f"""请从以下内容中提取用户已提到的基础资料信息。{dialogue_section}用户本轮回答:
"{user_message}"
需要提取的字段(只提取确实提到的):
需要提取的字段(只提取确实在对话中出现过的):
{missing_names}
请返回 JSON 格式,只包含确实提到的字段:
@@ -69,8 +78,8 @@ def get_profile_extraction_prompt(user_message: str, missing_fields: List[str])
}}
规则:
1. birth_year 必须是整数(四位数年份),如"65年出生"转为 1965
2. 如果用户"在老家长大"而之前提到了出生地grew_up_place 可以和 birth_place 相同
1. birth_year 整数(四位数),如"65年出生"转为 1965
2. 如果用户在任一轮说过出生地/成长地/职业等,都要提取
3. 只提取明确提到的信息,不要猜测
4. 如果没有提取到任何信息,返回空对象 {{}}
@@ -107,17 +116,19 @@ def get_profile_followup_prompt(
return f"""你是「岁月知己」,正在和用户聊天收集基本信息。
已知信息
## 已知信息(严禁再次询问以下任何一项)
{filled_str}
还需要了解{missing_str}
## 还需要了解
{missing_str}
用户刚才说:"{user_message}"
请先对用户说的内容做出自然回应,然后继续询问还未了解的信息(每次问 1-2 个)。
请先对用户说的内容做出自然回应,然后**只**询问「还需要了解」中的信息(每次问 1-2 个)。
语气要像朋友聊天一样自然亲切。
严格禁止:
- **严禁再次询问「已知信息」中已列出的内容**(例如已知出生年份就绝不要再问哪年出生)
- 禁止输出括号注释、思考过程
- 禁止说"我注意到""我需要了解"