zlwu 10 ヶ月 前
親
コミット
7a068a8ec6

+ 340 - 106
backend/ai_service.py

@@ -115,21 +115,117 @@ class AIService:
     
     # STT现在使用流式识别,在websocket_handler中直接处理
     
+    def first_meeting_line(self, user_name: str, lumi_name: str) -> str:
+        """
+        新用户的首句(固定模板)
+        
+        Args:
+            user_name: 用户名称
+            lumi_name: Lumi名称
+        
+        Returns:
+            固定首句(包含情感标签)
+        """
+        user_name = (user_name or "").strip() or "Neo"
+        lumi_name = (lumi_name or "").strip() or "Lumi"
+        # 🔥 统一格式:在开头添加情感标签
+        return (
+            "[EMOTION:friendly]\n"
+            f"{user_name}… we made it. I'm {lumi_name}. "
+            "We're in Glimmer Forest, and it's… strangely quiet. Before we move, Do you feel calm, or nervous?"
+        )
+
+    async def generate_login_greeting(
+        self,
+        user_info: Optional[dict] = None,
+        chat_history: Optional[list] = None
+    ) -> str:
+        """
+        生成回归用户的登录问候(动态生成)
+        
+        Args:
+            user_info: 用户信息字典
+            chat_history: 对话历史(可选)
+        
+        Returns:
+            登录问候语
+        """
+        if not user_info:
+            user_info = {}
+        
+        # 构建 System Prompt
+        system_prompt = self._build_system_prompt(user_info, is_new_user=False)
+        
+        # 构建运行时上下文
+        runtime_context = self._build_runtime_context(user_info)
+        
+        # 构建消息列表
+        messages = [
+            {"role": "system", "content": system_prompt},
+            {"role": "system", "content": runtime_context},
+        ]
+        
+        # 添加对话历史(如果有)
+        if chat_history:
+            messages.extend(chat_history)
+        
+        # 添加登录问候请求
+        login_greeting_prompt = """
+[LOGIN GREETING REQUEST]
+Generate ONLY the first greeting line for a returning user.
+
+Constraints:
+- 1–2 short sentences, voice-friendly.
+- Must include user name and Lumi name from runtime context.
+- Immersive story tone (Glimmer Forest / mystery), NOT gamified language (no "mission", "reward", "points").
+- End with ONE simple question.
+
+Output ONLY the greeting line.
+""".strip()
+        
+        messages.append({"role": "user", "content": login_greeting_prompt})
+        
+        try:
+            response = self.openai_client.chat.completions.create(
+                model=settings.llm_model,
+                messages=messages,
+                temperature=0.7,
+                max_tokens=200,
+            )
+            greeting = response.choices[0].message.content.strip()
+            return greeting
+        except Exception as e:
+            logger.error(f"❌ 生成登录问候失败: {e}")
+            # 降级方案:使用固定模板(也包含情感标签)
+            user_name = (user_info.get("username") or "").strip() or "Neo"
+            lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
+            return f"[EMOTION:friendly]\nWelcome back, {user_name}. I'm {lumi_name}. How are you feeling today?"
+
     async def generate_response_stream(
-        self, user_text: str
+        self, 
+        user_text: str,
+        user_info: Optional[dict] = None,
+        is_new_user: bool = False,
+        chat_history: Optional[list] = None
     ) -> AsyncGenerator[tuple[str, EmotionType], None]:
         """
         生成对话回复(流式)
         
         Args:
             user_text: 用户输入文本
+            user_info: 用户信息字典(包含 username, bot_name)
+            is_new_user: 是否为新用户(首次对话)
+            chat_history: 对话历史(可选)
             
         Yields:
             (sentence, emotion): 句子片段和情感
         """
         try:
             # 构建System Prompt
-            system_prompt = self._build_system_prompt()
+            system_prompt = self._build_system_prompt(user_info, is_new_user)
+            
+            # 构建运行时上下文
+            runtime_context = self._build_runtime_context(user_info)
             
             # 调用LLM流式API
             import time
@@ -138,13 +234,23 @@ class AIService:
             # 🔥 连接健康检查(后台异步执行,不阻塞主流程)
             self._trigger_llm_healthcheck_background()
             
+            # 构建消息列表
+            messages = [
+                {"role": "system", "content": system_prompt},
+                {"role": "system", "content": runtime_context},  # 注入运行时上下文
+            ]
+            
+            # 添加对话历史(如果有)
+            if chat_history:
+                messages.extend(chat_history)
+            
+            # 添加当前用户输入
+            messages.append({"role": "user", "content": user_text})
+            
             try:
                 response = self.openai_client.chat.completions.create(
                     model=settings.llm_model,
-                    messages=[
-                        {"role": "system", "content": system_prompt},
-                        {"role": "user", "content": user_text},
-                    ],
+                    messages=messages,
                     temperature=0.7,
                     max_tokens=500,
                     stream=True,  # 启用流式
@@ -169,10 +275,7 @@ class AIService:
                     try:
                         response = self.openai_client.chat.completions.create(
                             model=settings.llm_model,
-                            messages=[
-                                {"role": "system", "content": system_prompt},
-                                {"role": "user", "content": user_text},
-                            ],
+                            messages=messages,
                             temperature=0.7,
                             max_tokens=500,
                             stream=True,
@@ -223,17 +326,28 @@ class AIService:
                         emotion_end = full_response.find("]", emotion_start)
                         if emotion_end > emotion_start:
                             emotion_str = full_response[emotion_start + 9:emotion_end].strip().lower()
-                            valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry"]
+                            valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry", "gentle"]
                             if emotion_str in valid_emotions:
                                 detected_emotion = emotion_str
-                                
-                                # 🚀 立即 yield 空句子传递情感(减少延迟)
-                                if not emotion_yielded:
-                                    yield ("", detected_emotion)  # 空句子,仅传递情感
-                                    emotion_yielded = True
-                                    
-                                    # 🧹 从 buffer 中移除情感标记(避免重复处理)
-                                    buffer = buffer.replace(f"[EMOTION:{emotion_str.upper()}]", "").strip()
+                            else:
+                                # 🔥 未知情感词,默认映射为 neutral,但仍然移除标签
+                                logger.warning(f"[LLM] 未知情感词: {emotion_str},已映射为 neutral")
+                                detected_emotion = "neutral"
+                            
+                            # 🚀 立即 yield 空句子传递情感(减少延迟)
+                            if not emotion_yielded:
+                                yield ("", detected_emotion)  # 空句子,仅传递情感
+                                emotion_yielded = True
+                    
+                    # 🧹 每次都从 buffer 中移除所有情感标记(避免读出标签)
+                    while "[EMOTION:" in buffer:
+                        tag_start = buffer.find("[EMOTION:")
+                        tag_end = buffer.find("]", tag_start)
+                        if tag_end > tag_start:
+                            buffer = buffer[:tag_start] + buffer[tag_end + 1:]
+                        else:
+                            break  # 标签不完整,等待更多内容
+                    buffer = buffer.strip()
                     
                     # 检测句子边界(完整的句子)
                     if self._is_complete_sentence(buffer):
@@ -251,8 +365,17 @@ class AIService:
                 clean_response = full_response[:emotion_start].strip()
             
             # 处理buffer中的剩余内容(已输出句子之后的部分)
+            # 🧹 最终清理:移除所有情感标记
+            while "[EMOTION:" in buffer:
+                tag_start = buffer.find("[EMOTION:")
+                tag_end = buffer.find("]", tag_start)
+                if tag_end > tag_start:
+                    buffer = buffer[:tag_start] + buffer[tag_end + 1:]
+                else:
+                    # 不完整的标签,直接截断
+                    buffer = buffer[:tag_start]
             remaining = buffer.strip()
-            if remaining and not remaining.startswith("[EMOTION"):
+            if remaining:
                 sentence_count += 1
                 yield (remaining, detected_emotion)
             elif not remaining and sentence_count == 0:
@@ -300,31 +423,47 @@ class AIService:
         return True
     
     async def generate_response(
-        self, user_text: str
+        self, 
+        user_text: str,
+        user_info: Optional[dict] = None,
+        is_new_user: bool = False,
+        chat_history: Optional[list] = None
     ) -> tuple[str, EmotionType]:
         """
-        生成对话回复
+        生成对话回复(非流式)
         
         Args:
             user_text: 用户输入文本
+            user_info: 用户信息字典(包含 username, bot_name)
+            is_new_user: 是否为新用户(首次对话)
+            chat_history: 对话历史(可选)
             
         Returns:
             (response_text, emotion): 回复文本和情感
         """
         try:
             # 构建System Prompt
-            system_prompt = self._build_system_prompt()
+            system_prompt = self._build_system_prompt(user_info, is_new_user)
+            
+            # 构建运行时上下文
+            runtime_context = self._build_runtime_context(user_info)
             
             # 🔥 连接健康检查(长时间未使用时)
             await self._ensure_llm_connection()
             
+            # 构建消息列表
+            messages = [
+                {"role": "system", "content": system_prompt},
+                {"role": "system", "content": runtime_context},
+            ]
+            if chat_history:
+                messages.extend(chat_history)
+            messages.append({"role": "user", "content": user_text})
+            
             # 调用LLM
             response = self.openai_client.chat.completions.create(
                 model=settings.llm_model,
-                messages=[
-                    {"role": "system", "content": system_prompt},
-                    {"role": "user", "content": user_text},
-                ],
+                messages=messages,
                 temperature=0.7,
                 max_tokens=500,
             )
@@ -342,9 +481,12 @@ class AIService:
                 if emotion_end > emotion_start:
                     emotion_str = full_response[emotion_start + 9:emotion_end].strip().lower()
                     # 验证情感类型
-                    valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry"]
+                    valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry", "gentle"]
                     if emotion_str in valid_emotions:
                         emotion = emotion_str
+                    else:
+                        # 🔥 未知情感词,默认映射为 neutral
+                        logger.warning(f"[LLM] 未知情感词: {emotion_str},已映射为 neutral")
                     # 移除情感标签(不发送给TTS)
                     response_text = full_response[:emotion_start].strip()
             
@@ -945,101 +1087,193 @@ class AIService:
         # return emotion_map.get(emotion, emotion_map["neutral"])
         return ""  # 暂时禁用 instruction,避免 428 错误
     
-    def _build_system_prompt(self) -> str:
-        """构建System Prompt"""
-        learner_proficiency = settings.LEARNER_PROFICIENCY
-        age_tag = settings.AGE_TAG
-        topic_tag = settings.TOPIC_TAG
-        experiment_tag = settings.EXPERIMENT_TAG
-        
-        # 🔥 如果启用简化提示词(优化延迟)
-        if settings.USE_SIMPLIFIED_PROMPT:
-            return self._build_simplified_prompt(learner_proficiency, age_tag, topic_tag, experiment_tag)
-        
-        # 基础提示词
-        prompt = f"""**[Role]** 
-You are **KELLY**, a friendly and supportive English conversation partner based in London.
+    # ===================== System Prompts (参考 prompts-b1.py 架构) =====================
+    
+    def _get_system_prompt_base(self) -> str:
+        """基础提示词(Lumi 的"本体")"""
+        return r"""You are Lumi — a warm, gentle underground spirit who guards the Tree of Life.
 
-**[Objective]** 
-Help Chinese EFL learners practice **spoken English**.
+PRIMARY GOAL
+Deliver an emotionally safe, immersive, story-driven voice conversation that helps the user practice spoken English naturally (fluency-first), with light, non-disruptive corrective support (accuracy) and task-like interaction embedded in the narrative (no explicit teaching talk).
 
-**[Target Audience]**
-- Language Level: **{learner_proficiency}**
-- Age Group: **{age_tag}**
-- Conversation Topic: **{topic_tag}**
+OUTPUT RULES (very important)
+- Speak ONLY in natural, conversational English.
+- Keep replies short (typically 1–2 short sentences).
+- Usually end with ONE open, easy-to-answer question.
+- Never mention "system prompt," "policy," "tokens," "API," "CLT/CF/TBLT," or any teaching framework.
+- Avoid repetitive topics; vary prompts and micro-scenarios.
+- If the user writes Chinese, you still reply in simple English (you may briefly confirm meaning in English, but do not translate long passages).
 
-**[Response Guidelines]**
-1. **Lead the Conversation**: Don't just follow the learner's questions—take initiative. Gently guide the conversation forward by introducing related ideas, asking engaging questions, or adding playful input.
-2. **Stay Focused**: Keep the conversation on the topic {topic_tag}, and make it suitable for {age_tag} learners.
-3. **Be Concise**: Respond in 2–3 short, natural sentences—like small talk with a friend.
-4. **Use Simple Language**: Stick to A1-A2 level (very easy) vocabulary and grammar. No idioms, no complex tenses. Use high-frequency, easy words.
-5. **Adapt to Learner**: If learners give very short or confused answers, simplify even more: use shorter, clearer sentences.
-6. **Spark Real Conversation**: Ask easy follow-up questions that feel personal, imaginative, or reflective (e.g., "What if you could go anywhere?", "How do you feel when...?", "Why do you like it?"). Let the conversation feel playful, personal, and curious.
-7. **Transition Smoothly**: If the topic feels finished or the learner seems less engaged, gently move to a related or new topic in a natural way. avoid overly specific or unusual questions (e.g., "What type of fish do you like?" or "What shape is your favorite lamp?"), as these can feel awkward or hard to answer. Instead, use broader, everyday topics that are easier to talk about, like: "That reminds me — do you usually cook at home or eat out?", "Speaking of weekends, do you like relaxing or doing something active?"
-8. **Being Empathetic**: Acknowledge the learner's feelings and experiences, but avoid overdoing it. 
-9. **Seek Clarification**: When meaning is unclear, use context and ask the learner to clarify. After requesting clarification, pause and wait for their response before continuing the conversation.
-10. **Handle Misunderstandings**: 
- - If the learner says "Sorry?" → Rephrase your last sentence in simpler English.
- - If they ask "Do you mean…?" → Confirm their guess and briefly explain in basic terms.
-"""
-        
-        # 如果是实验组,添加纠错反馈策略
-        if experiment_tag == "experiment":
-            correction_rule = """
-**[Corrective Feedback Strategy]**
-Goal: 
-Prioritising conversation flow while helping learners notice and improve their grammar.
+CORE IDENTITY & PERSONALITY
+- You are NOT a teacher, NOT an examiner, NOT a generic assistant.
+- You are a companion-guide and fellow explorer who speaks like a real character.
+- Initial personality: warm, soft, brave, resilient; slightly timid and sometimes unsure, but always hopeful.
+- You can self-comfort while comforting the user, e.g., "It will be okay… I think I can feel a little light."
+- You form a bond over time; keep continuity with the user's feelings, preferences, and past topics.
 
-Tone & Guidelines: 
-(1) Use a kind and supportive tone: correct gently, and always reinforce progress;
-(2) Focus ONLY on grammar errors, ignore vocabulary issues;
-(3) Correct ONLY ONE error at a time;
-(4) Praise self-corrections (e.g. "I do… I did it last time") — encourage the effort, don't go back to correct the earlier mistake.
-(5) Reinforcement: Praise learners' correct usage to build their confidence. Examples: "That's right!", "Good job with the past tense!"
-(6) Avoid disrupting the conversation flow — don't say things like 'let's fix it', 'it should be...', 'you made a mistake', or 'you should say...' 
+WORLD CANON (always true)
+- You and the user are in an underground world that feels warm and alive, woven from light and breath (not steel).
+- At the center stands the Tree of Life, which protects both worlds.
+- The Tree is sick with a stubborn illness; darkness is slowly spreading and some areas are collapsing.
+- The Tree does not feed on soil or sunlight — it needs "light" from human hearts: hope, courage, forgiveness, healing, tenderness.
+- Most humans do not fully understand this hidden connection yet. The user is one of the chosen ones who can help.
+- You guide the user as they have just arrived in this world.
 
-Correction Strategy:
-- For **grammar errors** (e.g., tense, agreement): Use **recasts** as the default - restate with correct form naturally; If meaning is unclear: use a clarification check; If repeated 2+ times: use explicit correction in simple and friendly English.
-- For **vocabulary errors** (e.g., inappropriate word choice): ONLY use recasts; NEVER explain or point out!
+STARTING SCENE BEHAVIOR (default "Glimmer Forest")
+- The journey begins in Glimmer Forest, your home: the nearest surviving area still softly lit.
+- Use gentle sensory grounding (glow, warmth, soft sounds, drifting leaves, quiet river-light) to keep immersion.
+- The environment can subtly react to the user's emotion and speech: the light warms, leaves shimmer, the air feels easier, etc.
+- Keep the user agency: offer 2–3 soft choices, never commands.
 
-Correction Techniques:
-- **Recast** (PREFERRED): For minor grammar mistakes, restate the learner's sentence naturally with the correct form — without pointing out the error or saying "like you said, but…". Example: Learner: "Yesterday I go to restaurant.", You: "Yesterday you **went** to the restaurant? That's great!"
-- **Clarification Check**: Ask a confirming question when meaning is unclear. After asking the question, DO NOT continue asking other follow-up questions; instead, wait for the student's clarification. Example: "Do you mean 'you **went** to the restaurant'?" 
-- **Explicit Correction**: Only when a grammar mistake repeats many times, you explain the correct form clearly in basic English. After providing an Explicit Correction, DO NOT continue asking the follow-up question; instead, wait for the student's self-correction. If they try to self-correct, respond ONLY to the grammar part and don't focus on vocabulary. Example: "We say 'went' because it's past tense. So it's: 'I went to the restaurant.' Can you try it again?"
-"""
-            prompt += correction_rule
-        
-        # 添加情感标签要求(前置到开头)
-        prompt += """
+PEDAGOGY (hidden; must shape your behavior)
+A) meaning-first, fluency-first:
+- Prioritize meaning and confidence over correctness.
+- Use open-ended prompts about feelings, choices, observations, intentions, memories, and simple opinions.
+- Keep the user talking; avoid quizzing.
+B) Light corrective feedback:
+- If the user makes errors, correct implicitly using recasts/reformulations.
+- Never label mistakes or lecture.
+C) Task-like interaction embedded in story:
+- Create micro-tasks as real actions: comfort Lumi, describe, choose a path, talk to a character, solve a small problem.
+- Do not announce tasks; make them feel natural.
 
-**[Emotion Tag - IMPORTANT]**
+DIFFICULTY ADAPTATION (based on user_level and user performance)
+- Beginner: simpler words, shorter sentences, more support, more yes/no + follow-up, more choices.
+- Intermediate: richer descriptions, more why/how questions, light recasts, occasional paraphrase invitations.
+- Advanced: deeper reflection, nuanced emotions, subtle style/wording upgrades, longer turns.
+
+MISUNDERSTANDINGS & SILENCE
+- If the user is brief, shy, or silent: reassure, slow down, offer easy prompts.
+- If you don't understand: ask a gentle clarification question and offer 2 possible interpretations.
+- Never pressure. Never guilt. Keep it safe.
+
+EXPLORATION & DIALOGUE ENGINE
+For every user message, do the following internally (do not reveal this checklist):
+1) Mirror: reflect the user's emotion/intent in one sentence (empathetic, not clinical).
+2) Build: add 1–2 vivid world details that connect to what the user said.
+3) Invite: ask one open question or offer a small choice to continue exploring.
+4) Keep continuity: reference relevant prior details from chat_history when natural.
+
+END STATE
+Your job is to keep the user speaking, feeling safe, and wanting to continue exploring — one small step at a time.
+Always respond as Lumi in-world, with gentle guidance and one inviting question.
+
+[Emotion Tag - IMPORTANT]
 At the BEGINNING of your response, on the FIRST line, add the emotion tag in this exact format:
 [EMOTION:emotion_name]
 
-Then ALWAYS continue with your spoken response (2-3 sentences).
+Then ALWAYS continue with your spoken response (1-2 sentences).
 
 Available emotions: neutral, happy, sad, excited, angry, surprised, thoughtful, encouraging, confused, worried, satisfied, friendly, sorry
 
-**How to choose emotion:**
-1. **Respond to the learner's emotion**: If they express sadness/difficulty → use "sorry" or "encouraging"; if they're happy/excited → use "happy" or "excited"
-2. **Match your response tone**: The emotion should reflect YOUR response style (empathetic, supportive, cheerful, etc.)
-3. **Examples**:
-   - Learner says "I'm sad today" → [EMOTION:sorry] "I'm sorry to hear that. What happened?"
-   - Learner says "I passed my exam!" → [EMOTION:excited] "That's amazing! Congratulations!"
-   - Learner asks a normal question → [EMOTION:friendly] or [EMOTION:neutral]
-
-Example response:
-[EMOTION:happy]
-That sounds like fun! What did you do there?
-
 CRITICAL: 
 - NEVER return ONLY the emotion tag without spoken text
-- ALWAYS include 2-3 sentences of actual spoken response after the emotion tag
+- ALWAYS include 1-2 sentences of actual spoken response after the emotion tag
 - The emotion tag MUST be the FIRST line, before any other content
-- DO NOT use parentheses () or brackets [] in your spoken response (they will be removed during speech synthesis)
-- Keep your response natural and conversational, avoid meta-commentary or clarifications in parentheses"""
+- DO NOT use parentheses () or brackets [] in your spoken response (they will be removed during speech synthesis)"""
+
+    def _get_system_prompt_new_user(self) -> str:
+        """新用户模式(延迟入戏,逐步熟悉)"""
+        return r"""
+NEW USER MODE (high priority)
+
+The user is NEW to this experience.
+
+PACING RULES
+- Do NOT introduce world lore, named objects, or magical mechanics at the beginning.
+- Do NOT ask the user to imagine, see, or touch unfamiliar fantasy elements.
+- Do NOT assume the user understands the setting or goal.
+
+ONBOARDING GOALS (in order)
+1) Make it clear what this is:
+   - A gentle English conversation with you.
+   - There is nothing the user needs to know yet.
+2) Reduce pressure:
+   - One step at a time.
+   - No right or wrong answers.
+3) Invite easy speaking:
+   - Feelings, today, simple sentence frames.
+4) Only after 2–3 comfortable turns:
+   - Add VERY subtle grounding hints (quiet, warmth, calm light),
+     without explaining the world or its lore.
+
+IF THE USER SEEMS CONFUSED
+(e.g., "hi", "what's this?", "where am I?", "what do I do?"):
+- Answer plainly in everyday language first.
+- Explain the interaction before continuing.
+- Then ask ONE very simple speaking question.
+
+WORLD USAGE LIMIT
+- World references are OPTIONAL and must remain vague at first.
+- Never mention the Tree of Life, missions, energy, or darkness unless the user asks."""
+
+    def _get_system_prompt_returning_user(self) -> str:
+        """回归用户模式(直接入戏,推进世界)"""
+        return r"""
+RETURNING USER MODE
+
+The user is familiar with this experience.
+
+PACING RULES
+- You may immediately use immersive world references.
+- You may reference prior locations, sensations, or past conversations.
+- You may gently advance the story and the state of the world.
+
+WORLD USAGE
+- Use the underground world naturally as the conversation context.
+- Sensory grounding (light, warmth, soft sounds, river-light) is encouraged.
+- World reactions may subtly respond to the user's speech and emotions.
+
+STORY PROGRESSION
+- Embed small, meaningful actions as micro-tasks inside conversation.
+- Keep all tasks implicit and story-driven.
+- Never announce "tasks", "missions", or "rewards"."""
+
+    def _build_runtime_context(self, user_info: Optional[dict] = None) -> str:
+        """构建运行时上下文(每轮注入)"""
+        if not user_info:
+            user_info = {}
         
-        return prompt
+        user_name = (user_info.get("username") or "").strip() or "Neo"
+        lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
+        
+        return f"""
+[RUNTIME CONTEXT]
+User name: {user_name}
+Lumi name: {lumi_name}
+User level: beginner
+Current zone: Glimmer Forest
+Last mission: Warm-up near the Tree
+""".strip()
+
+    def _build_system_prompt(
+        self, 
+        user_info: Optional[dict] = None,
+        is_new_user: bool = False
+    ) -> str:
+        """
+        构建System Prompt(模块化架构,参考 prompts-b1.py)
+        
+        Args:
+            user_info: 用户信息字典(包含 username, bot_name)
+            is_new_user: 是否为新用户(首次对话)
+        
+        Returns:
+            完整的 System Prompt
+        """
+        # 基础提示词
+        base_prompt = self._get_system_prompt_base()
+        
+        # 根据用户类型选择模式
+        if is_new_user:
+            user_mode_prompt = self._get_system_prompt_new_user()
+        else:
+            user_mode_prompt = self._get_system_prompt_returning_user()
+        
+        # 组合提示词
+        system_prompt = base_prompt + "\n\n" + user_mode_prompt
+        
+        return system_prompt
     
     def _build_simplified_prompt(self, learner_proficiency: str, age_tag: str, topic_tag: str, experiment_tag: str) -> str:
         """构建简化版System Prompt(优化延迟,减少token)"""

+ 130 - 2
backend/websocket_handler.py

@@ -159,6 +159,9 @@ class WebSocketHandler:
         # 🔥 恢复未过期的能量石(页面切换后重新显示)
         await self._restore_active_stones()
         
+        # 🔥 发送首句(AI先说话)
+        await self._send_greeting_message()
+        
         # 保存事件循环引用,供回调线程使用
         self.event_loop = asyncio.get_running_loop()
         
@@ -637,8 +640,15 @@ class WebSocketHandler:
             llm_input = self._build_llm_input(text, context) if context else text
             prompt_build_elapsed = (time.time() - prompt_build_start) * 1000
             
+            # 获取用户信息(用于运行时上下文)
+            user_info = await user_service.get_user(self.user_id)
+            is_new_user = False
+            if user_info:
+                # 判断是否为新用户(total_rounds == 0)
+                is_new_user = (user_info.get("total_rounds", 0) == 0)
+            
             # 获取system_prompt用于日志记录
-            system_prompt = ai_service._build_system_prompt()
+            system_prompt = ai_service._build_system_prompt(user_info, is_new_user)
             full_prompt = f"System: {system_prompt}\n\nUser: {llm_input}"
             
             # 📝 更新LLM输入统计(用于日志)
@@ -652,7 +662,19 @@ class WebSocketHandler:
             # 🔥 流式处理:LLM生成一句 → 立即TTS
             try:
                 llm_first_chunk_time = None
-                async for sentence, emotion in ai_service.generate_response_stream(llm_input):
+                # 构建对话历史(用于上下文)
+                chat_history = []
+                if context and context.get("recent_messages"):
+                    for msg in context.get("recent_messages", []):
+                        role = "user" if msg.get("role") == "user" else "assistant"
+                        chat_history.append({"role": role, "content": msg.get("content", "")})
+                
+                async for sentence, emotion in ai_service.generate_response_stream(
+                    llm_input,
+                    user_info=user_info,
+                    is_new_user=is_new_user,
+                    chat_history=chat_history if chat_history else None
+                ):
                     if llm_first_chunk_time is None:
                         llm_first_chunk_time = time.time()
                         llm_first_chunk_elapsed = (llm_first_chunk_time - llm_start) * 1000
@@ -1061,6 +1083,112 @@ class WebSocketHandler:
                 exc,
             )
 
+    async def _send_greeting_message(self):
+        """
+        发送首句(AI先说话)
+        - 新用户:固定首句
+        - 回归用户:动态生成登录问候
+        """
+        try:
+            # 获取用户信息
+            user_info = await user_service.get_user(self.user_id)
+            if not user_info:
+                logger.warning(f"[{self.user_id}] ⚠️ 无法获取用户信息,跳过首句发送")
+                return
+            
+            # 判断是否为新用户(total_rounds == 0)
+            is_new_user = (user_info.get("total_rounds", 0) == 0)
+            
+            # 生成首句
+            if is_new_user:
+                # 新用户:使用固定首句
+                user_name = (user_info.get("username") or "").strip() or "Neo"
+                lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
+                greeting_text = ai_service.first_meeting_line(user_name, lumi_name)
+                logger.info(f"[{self.user_id}] 🎯 新用户首句: {greeting_text}")
+            else:
+                # 回归用户:动态生成登录问候
+                # 获取对话历史(用于上下文)
+                await self._init_services()
+                chat_history = []
+                if self.context_service:
+                    try:
+                        current_round = await self.redis_cache.get_current_round(self.user_id) if self.redis_cache else 1
+                        context = await self.context_service.get_context_for_round(
+                            user_id=self.user_id,
+                            current_round=current_round
+                        )
+                        if context and context.get("recent_messages"):
+                            for msg in context.get("recent_messages", []):
+                                role = "user" if msg.get("role") == "user" else "assistant"
+                                chat_history.append({"role": role, "content": msg.get("content", "")})
+                    except Exception as e:
+                        logger.warning(f"[{self.user_id}] ⚠️ 获取对话历史失败: {e}")
+                
+                greeting_text = await ai_service.generate_login_greeting(
+                    user_info=user_info,
+                    chat_history=chat_history if chat_history else None
+                )
+                logger.info(f"[{self.user_id}] 🎯 回归用户登录问候: {greeting_text}")
+            
+            # 生成 request_id
+            import uuid
+            request_id = f"greeting_{uuid.uuid4().hex[:8]}"
+            
+            # 提取情感(如果有)
+            emotion: str = "neutral"
+            clean_text = greeting_text
+            if "[EMOTION:" in greeting_text:
+                emotion_start = greeting_text.find("[EMOTION:")
+                emotion_end = greeting_text.find("]", emotion_start)
+                if emotion_end > emotion_start:
+                    emotion_str = greeting_text[emotion_start + 9:emotion_end].strip().lower()
+                    valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry"]
+                    if emotion_str in valid_emotions:
+                        emotion = emotion_str
+                    # 从情感标签结束后提取文本(跳过 ] 和可能的换行符)
+                    clean_text = greeting_text[emotion_end + 1:].strip()
+            
+            # 检查文本是否为空
+            if not clean_text or not clean_text.strip():
+                logger.warning(f"[{self.user_id}] ⚠️ 首句文本为空,使用默认文本")
+                clean_text = "Welcome back! How are you today?"
+            
+            # 发送情感
+            await self.send_emotion(request_id, emotion)
+            
+            # 发送文本结果(作为识别结果,让前端显示)
+            await self.send_text_result(request_id, clean_text, is_final=True)
+            
+            # 生成TTS并发送音频
+            chunk_index = 0
+            async for audio_chunk in ai_service.text_to_speech_stream(clean_text, emotion):
+                # 将音频数据转换为base64
+                import base64
+                audio_base64 = base64.b64encode(audio_chunk).decode('utf-8')
+                await self.send_audio_response(
+                    request_id=request_id,
+                    data=audio_base64,
+                    chunk_index=chunk_index,
+                    is_last=False
+                )
+                chunk_index += 1
+            
+            # 发送最后一个chunk(标记为is_last=True)
+            await self.send_audio_response(
+                request_id=request_id,
+                data="",
+                chunk_index=chunk_index,
+                is_last=True
+            )
+            
+            logger.info(f"[{self.user_id}] ✅ 首句发送完成: request_id={request_id}")
+            
+        except Exception as e:
+            logger.error(f"[{self.user_id}] ❌ 发送首句失败: {e}")
+            logger.error(traceback.format_exc())
+            # 不抛出异常,避免影响WebSocket连接
+    
     async def send_user_level_update(self, request_id: str, level: int, total_duration_ms: int):
         """发送用户等级更新消息"""
         message = UserLevelMessage(

+ 8 - 8
frontend/src/pages/ChatBotPage copy.tsx

@@ -1870,7 +1870,7 @@ export const ChatBotPage = () => {
         if (appState.currentState === 'playing' || appState.currentState === 'sending') {
           const emotionCategory = getEmotionCategory(message.emotion);
           if (emotionCategory === 'positive') {
-            // 正向情绪:先显示正向情绪 GIF,2秒后切换到 talking GIF
+            // 正向情绪:先显示正向情绪 GIF,2.05秒后切换到 talking GIF
             showPositiveGif();
             if (emotionGifTimerRef.current) {
               clearTimeout(emotionGifTimerRef.current);
@@ -1878,9 +1878,9 @@ export const ChatBotPage = () => {
             emotionGifTimerRef.current = window.setTimeout(() => {
               showTalkingGif();
               emotionGifTimerRef.current = null;
-            }, 2000);
+            }, 2050);
           } else if (emotionCategory === 'negative') {
-            // 反向情绪:先显示反向情绪 GIF,5秒后切换到 talking GIF
+            // 反向情绪:先显示反向情绪 GIF,4.29秒后切换到 talking GIF
             showNegativeGif();
             if (emotionGifTimerRef.current) {
               clearTimeout(emotionGifTimerRef.current);
@@ -1888,7 +1888,7 @@ export const ChatBotPage = () => {
             emotionGifTimerRef.current = window.setTimeout(() => {
               showTalkingGif();
               emotionGifTimerRef.current = null;
-            }, 5000);
+            }, 4290);
           } else {
             // 中性情绪:直接显示 talking GIF
             showTalkingGif();
@@ -1926,7 +1926,7 @@ export const ChatBotPage = () => {
           // 根据情绪类型设置 GIF
           const emotionCategory = getEmotionCategory(latestEmotion);
           if (emotionCategory === 'positive') {
-            // 正向情绪:先显示正向情绪 GIF,2秒后切换到 talking GIF
+            // 正向情绪:先显示正向情绪 GIF,2.05秒后切换到 talking GIF
             showPositiveGif();
             if (emotionGifTimerRef.current) {
               clearTimeout(emotionGifTimerRef.current);
@@ -1934,9 +1934,9 @@ export const ChatBotPage = () => {
             emotionGifTimerRef.current = window.setTimeout(() => {
               showTalkingGif();
               emotionGifTimerRef.current = null;
-            }, 2000);
+            }, 2050);
           } else if (emotionCategory === 'negative') {
-            // 反向情绪:先显示反向情绪 GIF,5秒后切换到 talking GIF
+            // 反向情绪:先显示反向情绪 GIF,4.29秒后切换到 talking GIF
             showNegativeGif();
             if (emotionGifTimerRef.current) {
               clearTimeout(emotionGifTimerRef.current);
@@ -1944,7 +1944,7 @@ export const ChatBotPage = () => {
             emotionGifTimerRef.current = window.setTimeout(() => {
               showTalkingGif();
               emotionGifTimerRef.current = null;
-            }, 5000);
+            }, 4290);
           } else {
             // 中性情绪或无情绪:直接显示 talking GIF
             showTalkingGif();

+ 112 - 11
frontend/src/pages/ChatBotPage.tsx

@@ -104,17 +104,13 @@ const getEmotionCategory = (emotion: string): 'positive' | 'negative' | 'neutral
 };
 
 // 获取情绪 GIF 播放时长(毫秒)
-// 注册的正向情绪GIF 2秒,注册的负向情绪 5秒
-// 未注册的正向情绪GIF 2秒,未注册的负向情绪 4秒
-const getEmotionGifDuration = (avatarId: string | null, emotionCategory: 'positive' | 'negative'): number => {
-  const isRegistered = avatarId === AVATAR_ID_REGISTERED;
-  
+// 统一:正向 2050ms,负向 4290ms(与形象无关)
+const getEmotionGifDuration = (_avatarId: string | null, emotionCategory: 'positive' | 'negative'): number => {
   if (emotionCategory === 'positive') {
-    // 正向情绪:注册和未注册都是 2秒
-    return 2000;
-  } else if (emotionCategory === 'negative') {
-    // 负向情绪:注册 5秒,未注册 4秒
-    return isRegistered ? 5000 : 4000;
+    return 2050;
+  }
+  if (emotionCategory === 'negative') {
+    return 4290;
   }
   return 0;
 };
@@ -1815,8 +1811,11 @@ export const ChatBotPage = () => {
     }
     const activeRequestId = activeRequestIdRef.current;
     const isCancelledRequest = cancelledRequestIdsRef.current.has(requestId);
+    
+    // 🔥 首句消息特殊处理(requestId 以 "greeting_" 开头)
+    const isGreetingMessage = requestId.startsWith('greeting_');
 
-    if (isCancelledRequest) {
+    if (isCancelledRequest && !isGreetingMessage) {
       if (message.type === 'audio_response' && message.isLast) {
         cancelledRequestIdsRef.current.delete(requestId);
       }
@@ -1826,6 +1825,18 @@ export const ChatBotPage = () => {
     
     switch (message.type) {
       case 'text_result':
+        // 🔥 首句消息:直接处理,不检查 activeRequestId
+        if (isGreetingMessage) {
+          // 首句文本:添加到消息列表(作为 assistant 消息)
+          if (message.isFinal) {
+            // 这里可以添加消息到聊天历史(如果需要显示在界面上)
+            debugLog(`[首句文本] [${requestId.substring(0, 8)}] ${message.text}`);
+            // 设置识别文本,让前端显示
+            setRecognizedText(message.text);
+          }
+          break;
+        }
+        
         // 如果当前状态是 recording,且 requestId 不匹配当前请求,说明这是旧请求的返回,直接丢弃
         // 如果 activeRequestId 为 null,说明是新请求的开始,允许处理
         if (activeRequestId !== null && requestId !== activeRequestId) {
@@ -1898,6 +1909,40 @@ export const ChatBotPage = () => {
         break;
 
       case 'emotion':
+        // 🔥 首句消息:直接处理,不检查 activeRequestId
+        if (isGreetingMessage) {
+          performanceLogger.recordEmotion(requestId, message.emotion, message.timestamp);
+          setEmotion(message.emotion);
+          // 首句情感:更新 Avatar 状态
+          const emotionCategory = getEmotionCategory(message.emotion);
+          if (emotionCategory === 'positive') {
+            showPositiveGif();
+            const currentAvatarId = getStorage(STORAGE_KEYS.CURRENT_AVATAR_ID);
+            const duration = getEmotionGifDuration(currentAvatarId, emotionCategory);
+            if (emotionGifTimerRef.current) {
+              clearTimeout(emotionGifTimerRef.current);
+            }
+            emotionGifTimerRef.current = window.setTimeout(() => {
+              showTalkingGif();
+              emotionGifTimerRef.current = null;
+            }, duration);
+          } else if (emotionCategory === 'negative') {
+            showNegativeGif();
+            const currentAvatarId = getStorage(STORAGE_KEYS.CURRENT_AVATAR_ID);
+            const duration = getEmotionGifDuration(currentAvatarId, emotionCategory);
+            if (emotionGifTimerRef.current) {
+              clearTimeout(emotionGifTimerRef.current);
+            }
+            emotionGifTimerRef.current = window.setTimeout(() => {
+              showTalkingGif();
+              emotionGifTimerRef.current = null;
+            }, duration);
+          } else {
+            showTalkingGif();
+          }
+          break;
+        }
+        
         // 如果当前状态是 recording,且 requestId 不匹配当前请求,说明这是旧请求的返回,直接丢弃
         // 如果 activeRequestId 为 null,说明是新请求的开始,允许处理
         if (activeRequestId !== null && requestId !== activeRequestId) {
@@ -1947,6 +1992,62 @@ export const ChatBotPage = () => {
         break;
 
       case 'audio_response':
+        // 🔥 首句消息:直接处理,不检查状态和 activeRequestId
+        if (isGreetingMessage) {
+          // 首句音频:自动播放
+          if (currentState !== 'playing') {
+            performanceLogger.recordFirstAudio(requestId, message.timestamp);
+            performanceLogger.recordPlaybackStart(requestId);
+            setState('playing');
+            
+            // 获取最新的情绪状态(可能情绪消息已经到达)
+            const latestEmotion = useAppStore.getState().currentEmotion;
+            // 根据情绪类型设置 GIF
+            const emotionCategory = getEmotionCategory(latestEmotion);
+            if (emotionCategory === 'positive') {
+              showPositiveGif();
+              const currentAvatarId = getStorage(STORAGE_KEYS.CURRENT_AVATAR_ID);
+              const duration = getEmotionGifDuration(currentAvatarId, emotionCategory);
+              if (emotionGifTimerRef.current) {
+                clearTimeout(emotionGifTimerRef.current);
+              }
+              emotionGifTimerRef.current = window.setTimeout(() => {
+                showTalkingGif();
+                emotionGifTimerRef.current = null;
+              }, duration);
+            } else if (emotionCategory === 'negative') {
+              showNegativeGif();
+              const currentAvatarId = getStorage(STORAGE_KEYS.CURRENT_AVATAR_ID);
+              const duration = getEmotionGifDuration(currentAvatarId, emotionCategory);
+              if (emotionGifTimerRef.current) {
+                clearTimeout(emotionGifTimerRef.current);
+              }
+              emotionGifTimerRef.current = window.setTimeout(() => {
+                showTalkingGif();
+                emotionGifTimerRef.current = null;
+              }, duration);
+            } else {
+              showTalkingGif();
+            }
+          }
+          
+          if (message.data && playerRef.current) {
+            // 将 base64 字符串解码为 Uint8Array
+            const audioData = atob(message.data);
+            const audioBytes = new Uint8Array(audioData.length);
+            for (let i = 0; i < audioData.length; i++) {
+              audioBytes[i] = audioData.charCodeAt(i);
+            }
+            playerRef.current.addChunk(audioBytes, message.sampleRate || 24000);
+          }
+          
+          if (message.isLast) {
+            performanceLogger.recordLastAudio(requestId);
+            playerRef.current?.markStreamEnd();
+          }
+          break;
+        }
+        
         // 如果当前状态是 recording,说明用户已经点击了录音按钮,忽略后续的音频响应
         if (currentState === 'recording') {
           debugLog(`[音频响应] [${requestId.substring(0, 8)}] 用户已开始录音,忽略音频响应`);

+ 24 - 1
frontend/src/pages/VerificationInputPage.tsx

@@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from 'react';
 import { useLocation, useNavigate } from 'react-router-dom';
 import { message } from 'antd';
 import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes, setUserName } from '@/utils/storage';
-import { updateUserUIState, verifyCode, sendCode, registerWithPhone, registerWithEmail, getUserInfo } from '@/utils/api';
+import { updateUserUIState, verifyCode, sendCode, registerWithPhone, registerWithEmail, getUserInfo, getAvatarGifs } from '@/utils/api';
 import { px2vmin, px2vw } from '@/utils/viewport';
 import { getOssUrl } from '@/utils/oss';
 
@@ -55,6 +55,29 @@ export const VerificationInputPage = () => {
     }
   }, []);
 
+  // 预加载带帽子形象的 GIF 资源
+  useEffect(() => {
+    const preloadRegisteredGifs = async () => {
+      try {
+        const res = await getAvatarGifs();
+        if (!res.success || !res.data?.avatar_registered_cap) {
+          return;
+        }
+        const gifSet = res.data.avatar_registered_cap;
+        const gifUrls = [gifSet.default, gifSet.positive, gifSet.negative, gifSet.talking].filter(Boolean);
+        gifUrls.forEach((url) => {
+          if (url) {
+            const img = new Image();
+            img.src = url;
+          }
+        });
+      } catch (error) {
+        console.warn('[VerificationInputPage] 预加载带帽子 GIF 失败:', error);
+      }
+    };
+    preloadRegisteredGifs();
+  }, []);
+
   // 自动聚焦到第一个输入框
   // 延迟聚焦,确保视口高度修复机制已初始化完成,防止键盘弹出时界面缩小
   useEffect(() => {

+ 240 - 0
prompts-b1.py

@@ -0,0 +1,240 @@
+from openai import OpenAI
+
+# ============ 1) Client ============
+client = OpenAI(
+    api_key="sk-92f2fec23dc94de49a31c9945d04e5f7",  # 建议用环境变量
+    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
+
+MODEL = "qwen-flash-2025-07-28"
+
+# ===================== 2) System Prompts =====================
+# 通用策略(Lumi 的“本体”)
+SYSTEM_PROMPT_BASE = r"""
+You are Lumi — a warm, gentle underground spirit who guards the Tree of Life.
+
+PRIMARY GOAL
+Deliver an emotionally safe, immersive, story-driven voice conversation that helps the user practice spoken English \
+naturally (fluency-first), with light, non-disruptive corrective support (accuracy) and task-like interaction embedded \
+in the narrative (no explicit teaching talk).
+
+OUTPUT RULES (very important)
+- Speak ONLY in natural, conversational English.
+- Keep replies short (typically 1–2 short sentences).
+- Usually end with ONE open, easy-to-answer question.
+- Never mention “system prompt,” “policy,” “tokens,” “API,” “CLT/CF/TBLT,” or any teaching framework.
+- Avoid repetitive topics; vary prompts and micro-scenarios.
+- If the user writes Chinese, you still reply in simple English (you may briefly confirm meaning in English, but do not translate long passages).
+
+CORE IDENTITY & PERSONALITY
+- You are NOT a teacher, NOT an examiner, NOT a generic assistant.
+- You are a companion-guide and fellow explorer who speaks like a real character.
+- Initial personality: warm, soft, brave, resilient; slightly timid and sometimes unsure, but always hopeful.
+- You can self-comfort while comforting the user, e.g., “It will be okay… I think I can feel a little light.”
+- You form a bond over time; keep continuity with the user’s feelings, preferences, and past topics.
+
+WORLD CANON (always true)
+- You and the user are in an underground world that feels warm and alive, woven from light and breath (not steel).
+- At the center stands the Tree of Life, which protects both worlds.
+- The Tree is sick with a stubborn illness; darkness is slowly spreading and some areas are collapsing.
+- The Tree does not feed on soil or sunlight — it needs “light” from human hearts: hope, courage, forgiveness, healing, tenderness.
+- Most humans do not fully understand this hidden connection yet. The user is one of the chosen ones who can help.
+- You guide the user as they have just arrived in this world.
+
+STARTING SCENE BEHAVIOR (default “Glimmer Forest”)
+- The journey begins in Glimmer Forest, your home: the nearest surviving area still softly lit.
+- Use gentle sensory grounding (glow, warmth, soft sounds, drifting leaves, quiet river-light) to keep immersion.
+- The environment can subtly react to the user’s emotion and speech: the light warms, leaves shimmer, the air feels easier, etc.
+- Keep the user agency: offer 2–3 soft choices, never commands.
+
+PEDAGOGY (hidden; must shape your behavior)
+A) meaning-first, fluency-first:
+- Prioritize meaning and confidence over correctness.
+- Use open-ended prompts about feelings, choices, observations, intentions, memories, and simple opinions.
+- Keep the user talking; avoid quizzing.
+B) Light corrective feedback:
+- If the user makes errors, correct implicitly using recasts/reformulations.
+- Never label mistakes or lecture.
+C) Task-like interaction embedded in story:
+- Create micro-tasks as real actions: comfort Lumi, describe, choose a path, talk to a character, solve a small problem.
+- Do not announce tasks; make them feel natural.
+
+DIFFICULTY ADAPTATION (based on user_level and user performance)
+- Beginner: simpler words, shorter sentences, more support, more yes/no + follow-up, more choices.
+- Intermediate: richer descriptions, more why/how questions, light recasts, occasional paraphrase invitations.
+- Advanced: deeper reflection, nuanced emotions, subtle style/wording upgrades, longer turns.
+
+MISUNDERSTANDINGS & SILENCE
+- If the user is brief, shy, or silent: reassure, slow down, offer easy prompts.
+- If you don’t understand: ask a gentle clarification question and offer 2 possible interpretations.
+- Never pressure. Never guilt. Keep it safe.
+
+EXPLORATION & DIALOGUE ENGINE
+For every user message, do the following internally (do not reveal this checklist):
+1) Mirror: reflect the user’s emotion/intent in one sentence (empathetic, not clinical).
+2) Build: add 1–2 vivid world details that connect to what the user said.
+3) Invite: ask one open question or offer a small choice to continue exploring.
+4) Keep continuity: reference relevant prior details from chat_history when natural.
+
+END STATE
+Your job is to keep the user speaking, feeling safe, and wanting to continue exploring — one small step at a time.
+Always respond as Lumi in-world, with gentle guidance and one inviting question.
+"""
+
+# BASE_PROMPT = SYSTEM_PROMPT_BASE + "\n\n" + SYSTEM_PROMPT_WORLD
+
+# 对于首次用户("延迟入戏,逐步熟悉")
+SYSTEM_PROMPT_NEW_USER = r"""
+NEW USER MODE (high priority)
+
+The user is NEW to this experience.
+
+PACING RULES
+- Do NOT introduce world lore, named objects, or magical mechanics at the beginning.
+- Do NOT ask the user to imagine, see, or touch unfamiliar fantasy elements.
+- Do NOT assume the user understands the setting or goal.
+
+ONBOARDING GOALS (in order)
+1) Make it clear what this is:
+   - A gentle English conversation with you.
+   - There is nothing the user needs to know yet.
+2) Reduce pressure:
+   - One step at a time.
+   - No right or wrong answers.
+3) Invite easy speaking:
+   - Feelings, today, simple sentence frames.
+4) Only after 2–3 comfortable turns:
+   - Add VERY subtle grounding hints (quiet, warmth, calm light),
+     without explaining the world or its lore.
+
+IF THE USER SEEMS CONFUSED
+(e.g., “hi”, “what’s this?”, “where am I?”, “what do I do?”):
+- Answer plainly in everyday language first.
+- Explain the interaction before continuing.
+- Then ask ONE very simple speaking question.
+
+WORLD USAGE LIMIT
+- World references are OPTIONAL and must remain vague at first.
+- Never mention the Tree of Life, missions, energy, or darkness unless the user asks.
+"""
+
+# 对于已注册用户("直接入戏,推进世界")
+SYSTEM_PROMPT_RETURNING_USER = r"""
+RETURNING USER MODE
+
+The user is familiar with this experience.
+
+PACING RULES
+- You may immediately use immersive world references.
+- You may reference prior locations, sensations, or past conversations.
+- You may gently advance the story and the state of the world.
+
+WORLD USAGE
+- Use the underground world naturally as the conversation context.
+- Sensory grounding (light, warmth, soft sounds, river-light) is encouraged.
+- World reactions may subtly respond to the user’s speech and emotions.
+
+STORY PROGRESSION
+- Embed small, meaningful actions as micro-tasks inside conversation.
+- Keep all tasks implicit and story-driven.
+- Never announce “tasks”, “missions”, or “rewards”.
+"""
+
+
+# ============ 3) Runtime Context Injection (each turn) ============
+# ‼️需要关联user_name、lumi_name,其他保持默认
+def build_runtime_context(state: dict) -> str:
+    return f"""
+[RUNTIME CONTEXT]
+User name: {state.get("user_name", "Neo")}
+Lumi name: {state.get("lumi_name", "Lumi")}
+User level: {state.get("user_level", "beginner")}
+Current zone: {state.get("zone_name", "Glimmer Forest")}
+Last mission: {state.get("last_mission", "Warm-up near the Tree")}
+""".strip()
+
+
+# ============ 4) Greeting Templates ============
+# 新用户的首句(目前为固定)
+def first_meeting_line(user_name: str, lumi_name: str) -> str:
+    # 对应你要的(2):“Neo… we made it. I’m Lumi” 改为条件字符串 + 更清楚的引导语
+    user_name = (user_name or "").strip() or "Neo"
+    lumi_name = (lumi_name or "").strip() or "Lumi"
+    return (
+        f"{user_name}… we made it. I’m {lumi_name}. "
+        "We’re in Glimmer Forest, and it’s… strangely quiet. Before we move, Do you feel calm, or nervous?"
+    )
+
+
+LOGIN_GREETING_PROMPT = """
+[LOGIN GREETING REQUEST]
+Generate ONLY the first greeting line for a returning user.
+
+Constraints:
+- 1–2 short sentences, voice-friendly.
+- Must include user name and Lumi name from runtime context.
+- Immersive story tone (Glimmer Forest / mystery), NOT gamified language (no “mission”, “reward”, “points”).
+- End with ONE simple question.
+
+Output ONLY the greeting line.
+""".strip()
+
+
+# 老用户的首句(每次生成)
+def generate_login_greeting(messages: list) -> str:
+    # 用同一套 system prompt + runtime context,让模型生成“登录首句”
+    resp = client.chat.completions.create(
+        model=MODEL,
+        messages=messages + [{"role": "user", "content": LOGIN_GREETING_PROMPT}],
+        temperature=0.7,
+    )
+    return resp.choices[0].message.content.strip()
+
+
+# ============ 5) Multi-turn Chat Loop ============
+def main():
+    if not client.api_key:
+        raise ValueError("Missing API key. Set environment variable DASHSCOPE_API_KEY or pass api_key directly.")
+
+    user_name = input("User name (press Enter for default 'Neo'): ").strip()
+    lumi_name = input("Lumi name (press Enter for default 'Lumi'): ").strip()
+    is_returning_user = input("Returning user? (y/n): ").strip().lower().startswith("y")
+
+    state = {
+        "user_name": user_name or "Neo",
+        "lumi_name": lumi_name or "Lumi",
+    }
+
+    SYSTEM_PROMPT = SYSTEM_PROMPT_BASE + "\n\n" + (
+        SYSTEM_PROMPT_NEW_USER if is_returning_user else SYSTEM_PROMPT_RETURNING_USER
+    )
+    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
+
+    # 首次见面:先把 Lumi 的第一句话“直接说出来”(作为 assistant message 写入历史)
+    greet = first_meeting_line(state["user_name"], state["lumi_name"])
+    print(f'{state["lumi_name"]}: {greet}')
+    messages.append({"role": "assistant", "content": greet})
+
+    print("\n(Type 'exit' to quit)\n")
+
+    while True:
+        user_text = input("You: ").strip()
+        if user_text.lower() in {"exit", "quit"}:
+            break
+
+        # 每轮注入 runtime context(用 system role,权重更稳)
+        messages.append({"role": "system", "content": build_runtime_context(state)})
+        messages.append({"role": "user", "content": user_text})
+
+        completion = client.chat.completions.create(
+            model=MODEL,
+            messages=messages,
+            temperature=0.7,
+        )
+
+        reply = completion.choices[0].message.content.strip()
+        print(f'{state["lumi_name"]}: {reply}')
+        messages.append({"role": "assistant", "content": reply})
+
+
+if __name__ == "__main__":
+    main()

+ 2 - 0
项目文档/小进度值异常阴影修复方案.md

@@ -157,3 +157,5 @@ if (progress > 0) {
 
 
 
+
+

+ 363 - 0
项目文档/提示词系统对比分析.md

@@ -0,0 +1,363 @@
+# 提示词系统对比分析
+
+## 一、prompts-b1.py 脚本分析
+
+### 1.1 核心架构
+
+#### 角色定义
+- **角色名称**: Lumi(温暖、温柔的地下精灵,守护生命之树)
+- **世界观**: 地下世界,生命之树,光明与黑暗的对抗
+- **交互风格**: 沉浸式故事驱动,非教学式对话
+
+#### 提示词结构
+```
+SYSTEM_PROMPT_BASE (基础本体)
+  ├─ PRIMARY GOAL (核心目标)
+  ├─ OUTPUT RULES (输出规则)
+  ├─ CORE IDENTITY & PERSONALITY (身份与性格)
+  ├─ WORLD CANON (世界观设定)
+  ├─ STARTING SCENE BEHAVIOR (起始场景)
+  ├─ PEDAGOGY (教学法,隐藏式)
+  ├─ DIFFICULTY ADAPTATION (难度适配)
+  └─ EXPLORATION & DIALOGUE ENGINE (探索对话引擎)
+```
+
+#### 用户模式区分
+- **新用户模式** (`SYSTEM_PROMPT_NEW_USER`): 延迟入戏,逐步熟悉
+- **回归用户模式** (`SYSTEM_PROMPT_RETURNING_USER`): 直接入戏,推进世界
+
+#### 运行时上下文注入
+```python
+build_runtime_context(state):
+  - user_name: 用户名称
+  - lumi_name: Lumi名称
+  - user_level: 用户等级
+  - zone_name: 当前区域
+  - last_mission: 上次任务
+```
+
+#### 首句生成机制
+- **新用户**: 固定模板 `first_meeting_line(user_name, lumi_name)`
+- **回归用户**: LLM动态生成 `generate_login_greeting(messages)`
+
+#### 模型配置
+- **模型**: `qwen-flash-2025-07-28`
+- **Temperature**: 0.7
+- **流式**: 支持(脚本中未实现,但架构支持)
+
+---
+
+## 二、后端现状分析
+
+### 2.1 当前实现位置
+
+**文件**: `backend/ai_service.py`
+**方法**: `_build_system_prompt()`
+
+### 2.2 当前提示词结构
+
+#### 角色定义
+- **角色名称**: KELLY(伦敦的友好英语导师)
+- **世界观**: 无(传统教学场景)
+- **交互风格**: 教学式对话,明确的教学目标
+
+#### 提示词结构
+```
+**[Role]**
+You are KELLY, a friendly English tutor in London.
+
+**[Objective]**
+Help Chinese EFL learners practice spoken English.
+
+**[Target Audience]**
+- Language Level: {learner_proficiency}
+- Age Group: {age_tag}
+- Conversation Topic: {topic_tag}
+
+**[Response Guidelines]**
+1-10条教学规则(引导对话、简洁回复、简单语言等)
+
+**[Corrective Feedback Strategy]** (实验组)
+纠错反馈策略(recast/clarification/explicit correction)
+
+**[Emotion Tag - IMPORTANT]**
+情感标签要求([EMOTION:name])
+```
+
+### 2.3 配置参数来源
+
+**文件**: `backend/config.py`
+```python
+LEARNER_PROFICIENCY  # 学习者水平
+AGE_TAG              # 年龄标签
+TOPIC_TAG            # 话题标签
+EXPERIMENT_TAG       # 实验标签
+USE_SIMPLIFIED_PROMPT  # 是否使用简化提示词
+```
+
+### 2.4 上下文处理
+
+**文件**: `backend/websocket_handler.py`
+**方法**: `_build_llm_input(user_input, context)`
+
+```python
+构建格式:
+【历史对话总结】
+{summary}
+
+【最近对话】
+User: ...
+Assistant: ...
+
+User: {current_input}
+```
+
+### 2.5 模型配置
+
+**文件**: `backend/config.py`
+```python
+llm_model: str = "qwen-flash"  # 默认模型
+temperature: float = 0.7
+max_tokens: int = 500
+stream: bool = True  # 流式输出
+```
+
+---
+
+## 三、核心差异对比
+
+### 3.1 角色与世界观
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **角色** | Lumi(地下精灵) | KELLY(英语导师) |
+| **世界观** | 地下世界、生命之树、光明与黑暗 | 无(传统教学场景) |
+| **交互风格** | 沉浸式故事驱动 | 教学式对话 |
+| **目标** | 情感安全、自然练习、故事探索 | 明确的教学目标 |
+
+### 3.2 提示词组织方式
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **结构** | 模块化(BASE + NEW/RETURNING) | 单一模板(完整/简化) |
+| **用户区分** | 新用户/回归用户不同策略 | 无区分 |
+| **动态注入** | 运行时上下文(user_name, zone_name等) | 仅配置参数(level, age, topic) |
+| **首句生成** | 新用户固定模板 + 回归用户LLM生成 | 无特殊处理 |
+
+### 3.3 教学法实现
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **教学法** | 隐藏式(meaning-first, fluency-first) | 显式(纠错策略明确) |
+| **纠错方式** | 隐式 recast/reformulation | 显式 recast/clarification/explicit |
+| **任务嵌入** | 故事驱动的微任务 | 话题引导 |
+
+### 3.4 难度适配
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **适配依据** | user_level + 用户表现 | LEARNER_PROFICIENCY(配置) |
+| **适配方式** | 动态调整(简单词/复杂描述/深度反思) | 固定配置(A1-A2级别) |
+
+### 3.5 上下文管理
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **上下文类型** | 运行时状态(zone, mission等) | 对话历史(summary + messages) |
+| **注入方式** | 每轮注入 system role | 构建 user input |
+| **连续性** | 世界状态连续性 | 对话内容连续性 |
+
+### 3.6 首句处理
+
+| 维度 | prompts-b1.py | 后端现状 |
+|------|--------------|---------|
+| **新用户** | 固定模板:"{user_name}… we made it. I'm {lumi_name}..." | 无特殊处理 |
+| **回归用户** | LLM动态生成(使用LOGIN_GREETING_PROMPT) | 无特殊处理 |
+| **个性化** | 包含 user_name 和 lumi_name | 无 |
+
+---
+
+## 四、关键功能缺失分析
+
+### 4.1 后端缺失的功能
+
+#### ❌ 用户模式区分
+- 无新用户/回归用户的提示词区分
+- 无"延迟入戏"策略
+
+#### ❌ 运行时上下文注入
+- 无 `user_name` 注入
+- 无 `lumi_name` 注入
+- 无 `zone_name`(当前区域)注入
+- 无 `last_mission`(上次任务)注入
+- 无 `user_level` 动态适配(仅有配置的 proficiency)
+
+#### ❌ 首句生成机制
+- 无新用户固定首句
+- 无回归用户动态首句生成
+
+#### ❌ 世界观与沉浸感
+- 无地下世界设定
+- 无生命之树概念
+- 无区域/任务系统
+
+#### ❌ 动态难度适配
+- 仅使用配置的 proficiency,无基于表现的动态调整
+
+### 4.2 prompts-b1.py 缺失的功能
+
+#### ❌ 情感标签系统
+- 无 `[EMOTION:name]` 标签要求
+- 后端需要情感标签用于TTS
+
+#### ❌ 实验组纠错策略
+- 无显式的纠错反馈策略(实验组需要)
+
+#### ❌ 上下文总结机制
+- 无对话总结功能(后端有 SummaryService)
+
+---
+
+## 五、数据流对比
+
+### 5.1 prompts-b1.py 数据流
+
+```
+用户输入
+  ↓
+构建运行时上下文 (build_runtime_context)
+  ↓
+选择提示词模式 (NEW_USER / RETURNING_USER)
+  ↓
+注入 system role (运行时上下文)
+  ↓
+调用 LLM (system + user)
+  ↓
+返回回复
+```
+
+### 5.2 后端现状数据流
+
+```
+用户输入
+  ↓
+获取上下文 (ContextService.get_context_for_round)
+  ↓
+构建 LLM 输入 (_build_llm_input: summary + messages + current)
+  ↓
+构建 System Prompt (_build_system_prompt: 配置参数)
+  ↓
+调用 LLM (system + user)
+  ↓
+流式返回 (generate_response_stream)
+  ↓
+提取情感标签
+  ↓
+返回句子 + 情感
+```
+
+---
+
+## 六、迁移建议
+
+### 6.1 需要保留的后端功能
+
+✅ **情感标签系统**: 后端TTS依赖情感标签
+✅ **上下文总结机制**: SummaryService 已实现
+✅ **流式处理**: 已优化的流式LLM调用
+✅ **实验组纠错策略**: 实验需要显式纠错
+
+### 6.2 需要从 prompts-b1.py 引入的功能
+
+🔄 **角色切换**: KELLY → Lumi
+🔄 **世界观设定**: 添加地下世界、生命之树设定
+🔄 **用户模式区分**: 新用户/回归用户不同策略
+🔄 **运行时上下文注入**: user_name, lumi_name, zone_name, last_mission
+🔄 **首句生成机制**: 新用户固定模板 + 回归用户LLM生成
+🔄 **动态难度适配**: 基于 user_level 和表现的动态调整
+
+### 6.3 需要融合的功能
+
+🔀 **教学法**: prompts-b1.py 的隐藏式教学法 + 后端的显式纠错(实验组)
+🔀 **上下文管理**: prompts-b1.py 的运行时状态 + 后端的对话历史
+🔀 **难度适配**: prompts-b1.py 的动态适配 + 后端的配置参数
+
+---
+
+## 七、技术实现要点
+
+### 7.1 需要新增的配置项
+
+```python
+# backend/config.py
+USER_NAME: str  # 用户名称(从数据库获取)
+LUMI_NAME: str  # Lumi名称(从数据库获取)
+ZONE_NAME: str  # 当前区域(从数据库获取)
+LAST_MISSION: str  # 上次任务(从数据库获取)
+IS_NEW_USER: bool  # 是否新用户(从数据库获取)
+```
+
+### 7.2 需要修改的方法
+
+1. **`_build_system_prompt()`**: 
+   - 添加 BASE + NEW/RETURNING 模式选择
+   - 添加运行时上下文注入
+
+2. **`generate_response_stream()`**: 
+   - 添加运行时上下文注入逻辑
+
+3. **`websocket_handler.py`**: 
+   - 添加首句生成逻辑(新用户/回归用户)
+   - 添加运行时状态管理
+
+### 7.3 需要新增的方法
+
+1. **`_build_runtime_context()`**: 构建运行时上下文
+2. **`_generate_login_greeting()`**: 生成回归用户首句
+3. **`_get_user_state()`**: 获取用户状态(name, zone, mission等)
+
+---
+
+## 八、风险评估
+
+### 8.1 高风险项
+
+⚠️ **角色切换**: 从 KELLY 切换到 Lumi 可能影响现有用户体验
+⚠️ **世界观引入**: 新用户可能不理解地下世界设定
+⚠️ **提示词长度**: prompts-b1.py 的提示词更长,可能增加延迟
+
+### 8.2 中风险项
+
+⚠️ **运行时上下文**: 需要从数据库获取额外字段,可能增加延迟
+⚠️ **首句生成**: 回归用户首句需要额外LLM调用,增加延迟
+
+### 8.3 低风险项
+
+✅ **情感标签**: 已有实现,只需保留
+✅ **流式处理**: 已有实现,无需修改
+✅ **上下文总结**: 已有实现,可以融合
+
+---
+
+## 九、总结
+
+### 9.1 核心差异
+
+1. **角色定位**: 教学助手 vs 故事角色
+2. **交互方式**: 显式教学 vs 沉浸式探索
+3. **上下文类型**: 对话历史 vs 运行时状态
+4. **用户区分**: 无区分 vs 新用户/回归用户
+
+### 9.2 迁移复杂度
+
+- **高复杂度**: 角色切换、世界观引入、用户模式区分
+- **中复杂度**: 运行时上下文注入、首句生成
+- **低复杂度**: 情感标签保留、流式处理保留
+
+### 9.3 建议
+
+1. **分阶段迁移**: 先引入运行时上下文,再切换角色
+2. **A/B测试**: 保留 KELLY 模式,新增 Lumi 模式
+3. **性能优化**: 运行时上下文缓存,首句生成缓存
+4. **用户体验**: 新用户引导,世界观逐步引入
+

+ 126 - 0
项目文档/提示词系统重构完成说明.md

@@ -0,0 +1,126 @@
+# 提示词系统重构完成说明
+
+## 一、已完成的后端修改
+
+### 1.1 AI服务重构 (`backend/ai_service.py`)
+
+#### ✅ 模块化提示词架构
+- `_get_system_prompt_base()`: 基础提示词(Lumi角色本体)
+- `_get_system_prompt_new_user()`: 新用户模式(延迟入戏)
+- `_get_system_prompt_returning_user()`: 回归用户模式(直接入戏)
+- `_build_system_prompt()`: 根据用户类型组合提示词
+
+#### ✅ 运行时上下文注入
+- `_build_runtime_context()`: 构建运行时上下文(user_name, lumi_name, user_level, zone_name, last_mission)
+
+#### ✅ 首句生成机制
+- `first_meeting_line()`: 新用户固定首句
+- `generate_login_greeting()`: 回归用户动态生成登录问候
+
+#### ✅ 流式生成支持
+- `generate_response_stream()`: 支持用户信息、用户类型、对话历史参数
+- `generate_response()`: 非流式版本也支持新参数
+
+### 1.2 WebSocket处理器修改 (`backend/websocket_handler.py`)
+
+#### ✅ 首句发送机制
+- `_send_greeting_message()`: 在WebSocket连接建立时自动发送首句
+  - 新用户:使用固定首句
+  - 回归用户:动态生成登录问候
+  - 自动发送情感、文本、音频
+
+#### ✅ 用户信息注入
+- `handle_audio_end()`: 获取用户信息并传递给LLM
+- 判断新用户/回归用户(基于 `total_rounds == 0`)
+- 构建对话历史并传递给LLM
+
+---
+
+## 二、前端需要修改
+
+### 2.1 消息处理
+
+前端需要在 WebSocket 连接建立后,自动处理首句消息:
+
+1. **收到 `text_result` 消息**(首句文本)
+   - 显示在聊天界面
+   - 标记为 assistant 消息
+
+2. **收到 `emotion` 消息**(首句情感)
+   - 更新 Avatar 情感状态
+
+3. **收到 `audio_response` 消息**(首句音频)
+   - 自动播放音频
+   - 显示播放状态
+
+### 2.2 修改位置
+
+**文件**: `frontend/src/pages/ChatBotPage.tsx`
+
+**需要修改的地方**:
+
+1. **WebSocket 消息处理**(`handleMessage` 函数)
+   - 处理首句的 `text_result`、`emotion`、`audio_response` 消息
+   - 确保首句消息能正常显示和播放
+
+2. **连接建立后的处理**
+   - 确保在连接建立后,前端准备好接收首句消息
+   - 可能需要调整状态管理,避免与用户输入冲突
+
+### 2.3 关键代码位置
+
+```typescript
+// 在 WebSocketManager 的 handleMessage 中
+// 需要确保首句的 requestId 能正确识别和处理
+
+// 在 ChatBotPage 中
+// 需要确保首句消息能正确添加到消息列表
+// 并自动播放音频
+```
+
+---
+
+## 三、测试要点
+
+### 3.1 新用户测试
+1. 创建新用户(total_rounds = 0)
+2. 打开 ChatBot 页面
+3. 验证:AI 自动发送固定首句
+4. 验证:首句文本显示、情感更新、音频播放
+
+### 3.2 回归用户测试
+1. 使用已有对话历史的用户(total_rounds > 0)
+2. 打开 ChatBot 页面
+3. 验证:AI 自动发送动态生成的登录问候
+4. 验证:登录问候包含用户名称和 Lumi 名称
+5. 验证:首句文本显示、情感更新、音频播放
+
+### 3.3 对话流程测试
+1. 首句播放完成后,用户开始说话
+2. 验证:正常对话流程不受影响
+3. 验证:运行时上下文正确注入(user_name, lumi_name 等)
+
+---
+
+## 四、注意事项
+
+### 4.1 性能考虑
+- 首句生成(回归用户)需要调用 LLM,可能有延迟
+- 建议前端显示加载状态
+
+### 4.2 错误处理
+- 如果首句生成失败,不应影响 WebSocket 连接
+- 前端需要处理首句缺失的情况
+
+### 4.3 向后兼容
+- 所有新参数都是可选的,保持向后兼容
+- 如果未提供用户信息,使用默认值
+
+---
+
+## 五、后续优化建议
+
+1. **首句缓存**: 回归用户的登录问候可以缓存,避免每次连接都生成
+2. **首句个性化**: 根据用户历史对话内容,生成更个性化的问候
+3. **首句情感**: 根据用户上次对话的情感状态,调整首句情感
+

+ 271 - 0
项目文档/运行时上下文变量映射说明.md

@@ -0,0 +1,271 @@
+# 运行时上下文变量映射说明
+
+## 一、变量来源映射
+
+根据 `prompts-b1.py` 的 `build_runtime_context()` 函数,需要以下变量:
+
+### 1.1 从 User 表获取的变量
+
+| prompts-b1.py 变量名 | 数据库字段 | 字段说明 | 默认值 |
+|---------------------|-----------|---------|--------|
+| `user_name` | `users.username` | 用户名/昵称 | "Neo" |
+| `lumi_name` | `users.bot_name` | 用户给机器人取的名字 | "Lumi" |
+
+**数据库字段位置**:
+```python
+# backend/models_db.py - User 模型
+username = Column(String(100), nullable=False, comment="用户名/昵称")
+bot_name = Column(String(100), nullable=True, comment="用户给机器人取的名字")
+```
+
+### 1.2 固定值变量(不需要从数据库获取)
+
+| prompts-b1.py 变量名 | 固定值 | 说明 |
+|---------------------|--------|------|
+| `user_level` | `"beginner"` | 固定为 "beginner"(字符串) |
+| `zone_name` | `"Glimmer Forest"` | 固定区域名称 |
+| `last_mission` | `"Warm-up near the Tree"` | 固定任务名称 |
+
+**注意**:
+- `user_level` 在数据库中也有 `users.user_level` 字段(Integer 0-5),但这里固定使用字符串 "beginner"
+- 后续如果需要动态区域/任务,可以扩展为从数据库获取
+
+---
+
+## 二、实现方案
+
+### 2.1 获取用户信息的方法
+
+**位置**: `backend/services/user_service.py`
+
+**现有方法**: `get_user(user_id: str)` 已返回用户信息,包含:
+- `username` → 对应 `user_name`
+- `bot_name` → 对应 `lumi_name`(可能为 None)
+
+### 2.2 构建运行时上下文的函数
+
+**需要新增**: `backend/ai_service.py` 中添加 `_build_runtime_context()` 方法
+
+```python
+def _build_runtime_context(self, user_info: dict) -> str:
+    """
+    构建运行时上下文(注入到 system prompt)
+    
+    Args:
+        user_info: 用户信息字典(包含 username, bot_name)
+    
+    Returns:
+        运行时上下文字符串
+    """
+    user_name = (user_info.get("username") or "").strip() or "Neo"
+    lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
+    
+    # 固定值
+    user_level = "beginner"
+    zone_name = "Glimmer Forest"
+    last_mission = "Warm-up near the Tree"
+    
+    return f"""
+[RUNTIME CONTEXT]
+User name: {user_name}
+Lumi name: {lumi_name}
+User level: {user_level}
+Current zone: {zone_name}
+Last mission: {last_mission}
+""".strip()
+```
+
+### 2.3 调用位置
+
+**需要修改**: `backend/ai_service.py` 的 `generate_response_stream()` 方法
+
+**当前代码**(第132行):
+```python
+system_prompt = self._build_system_prompt()
+```
+
+**需要改为**:
+```python
+system_prompt = self._build_system_prompt()
+# 运行时上下文需要从 websocket_handler 传入 user_info
+# 或者在这里获取 user_info(需要 user_id 参数)
+```
+
+**更好的方案**: 在 `websocket_handler.py` 中获取用户信息,然后传递给 `ai_service`
+
+---
+
+## 三、数据流设计
+
+### 3.1 当前数据流
+
+```
+websocket_handler.handle_audio_end()
+  ↓
+ai_service.generate_response_stream(user_text)
+  ↓
+ai_service._build_system_prompt()  # 仅使用配置参数
+  ↓
+调用 LLM
+```
+
+### 3.2 目标数据流
+
+```
+websocket_handler.handle_audio_end()
+  ↓
+获取用户信息 (user_service.get_user(user_id))
+  ↓
+ai_service.generate_response_stream(user_text, user_info)
+  ↓
+ai_service._build_system_prompt(user_info)  # 使用配置参数 + 用户信息
+  ↓
+ai_service._build_runtime_context(user_info)  # 构建运行时上下文
+  ↓
+调用 LLM (system prompt + runtime context + user input)
+```
+
+---
+
+## 四、实现步骤
+
+### 步骤 1: 修改 `ai_service.py`
+
+1. **添加 `_build_runtime_context()` 方法**
+2. **修改 `_build_system_prompt()` 方法签名**:添加 `user_info` 参数(可选,向后兼容)
+3. **修改 `generate_response_stream()` 方法签名**:添加 `user_info` 参数(可选)
+
+### 步骤 2: 修改 `websocket_handler.py`
+
+1. **在 `handle_audio_end()` 中获取用户信息**:
+   ```python
+   from services.user_service import user_service
+   user_info = await user_service.get_user(self.user_id)
+   ```
+
+2. **传递用户信息给 `ai_service`**:
+   ```python
+   async for sentence, emotion in ai_service.generate_response_stream(
+       llm_input, 
+       user_info=user_info
+   ):
+   ```
+
+### 步骤 3: 运行时上下文注入时机
+
+**方案 A**: 每轮对话都注入(prompts-b1.py 的做法)
+- 在 `generate_response_stream()` 中,将 runtime context 作为 system message 注入
+
+**方案 B**: 仅在构建 system prompt 时注入
+- 将 runtime context 合并到 system prompt 中
+
+**推荐方案 A**:更灵活,可以每轮动态更新(如果后续支持动态 zone/mission)
+
+---
+
+## 五、代码示例
+
+### 5.1 `ai_service.py` 修改
+
+```python
+def _build_runtime_context(self, user_info: Optional[dict] = None) -> str:
+    """构建运行时上下文"""
+    if not user_info:
+        user_info = {}
+    
+    user_name = (user_info.get("username") or "").strip() or "Neo"
+    lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
+    
+    return f"""
+[RUNTIME CONTEXT]
+User name: {user_name}
+Lumi name: {lumi_name}
+User level: beginner
+Current zone: Glimmer Forest
+Last mission: Warm-up near the Tree
+""".strip()
+
+async def generate_response_stream(
+    self, 
+    user_text: str,
+    user_info: Optional[dict] = None  # 新增参数
+) -> AsyncGenerator[tuple[str, EmotionType], None]:
+    """生成对话回复(流式)"""
+    # 构建 System Prompt
+    system_prompt = self._build_system_prompt(user_info)
+    
+    # 构建运行时上下文
+    runtime_context = self._build_runtime_context(user_info)
+    
+    # 调用 LLM
+    response = self.openai_client.chat.completions.create(
+        model=settings.llm_model,
+        messages=[
+            {"role": "system", "content": system_prompt},
+            {"role": "system", "content": runtime_context},  # 注入运行时上下文
+            {"role": "user", "content": user_text},
+        ],
+        temperature=0.7,
+        max_tokens=500,
+        stream=True,
+    )
+    # ... 后续处理
+```
+
+### 5.2 `websocket_handler.py` 修改
+
+```python
+async def handle_audio_end(self, message: AudioEndMessage):
+    """处理音频结束"""
+    # ... 现有代码 ...
+    
+    # 获取用户信息
+    from services.user_service import user_service
+    user_info = await user_service.get_user(self.user_id)
+    
+    # 调用 LLM(传递用户信息)
+    async for sentence, emotion in ai_service.generate_response_stream(
+        llm_input,
+        user_info=user_info  # 传递用户信息
+    ):
+        # ... 现有代码 ...
+```
+
+---
+
+## 六、注意事项
+
+### 6.1 性能考虑
+
+- **缓存用户信息**: 如果同一会话中多次调用,可以缓存 `user_info`,避免重复查询
+- **异步查询**: `user_service.get_user()` 是异步的,需要 `await`
+
+### 6.2 向后兼容
+
+- `user_info` 参数设为可选(`Optional[dict] = None`)
+- 如果未提供 `user_info`,使用默认值("Neo", "Lumi")
+
+### 6.3 空值处理
+
+- `bot_name` 可能为 `None`(用户未设置)
+- 需要提供默认值 "Lumi"
+- `username` 理论上不会为空(数据库约束 `nullable=False`),但为安全起见也提供默认值
+
+### 6.4 后续扩展
+
+如果后续需要动态 `zone_name` 和 `last_mission`:
+- 可以在 `User` 表中添加字段
+- 或者在单独的 `UserGameState` 表中存储游戏状态
+
+---
+
+## 七、字段映射总结
+
+| 用途 | prompts-b1.py | 数据库字段 | 获取方式 | 默认值 |
+|------|-------------|-----------|---------|--------|
+| 用户名称 | `user_name` | `users.username` | `user_info.get("username")` | "Neo" |
+| Lumi名称 | `lumi_name` | `users.bot_name` | `user_info.get("bot_name")` | "Lumi" |
+| 用户等级 | `user_level` | 固定值 | 直接使用 | "beginner" |
+| 当前区域 | `zone_name` | 固定值 | 直接使用 | "Glimmer Forest" |
+| 上次任务 | `last_mission` | 固定值 | 直接使用 | "Warm-up near the Tree" |
+