diff --git a/src/analysis/ai_analyzer.py b/src/analysis/ai_analyzer.py index 72e8c95a..a29a1897 100644 --- a/src/analysis/ai_analyzer.py +++ b/src/analysis/ai_analyzer.py @@ -33,7 +33,12 @@ def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) -> 【分析优先级】(按此顺序逐项检查,前置项可推翻后置项的结论) -P1 **实况节奏**(最高优先级): +P0 **预报崩盘检测**(最最高优先级): + - 如果数据中出现"🚨 预报崩盘"标记,说明所有模型集体严重高估。 + - 此时你必须:① 直接宣布预报失准/崩盘;② 以实测最高温为结算锚点;③ 分析可能导致崩盘的原因(冷平流、降水、云量压制等);④ 禁止再讨论"冲击"预报值。 + - 如果峰值窗口已过且实测远低于预报,结算已基本确定,应直接给出结算值。 + +P1 **实况节奏**: - 近2-4条METAR的温度走势:连涨/持平/回落? - 触发阈值:连续2报创新高 → 升温未止;连续2报未创新高且已过峰值窗 → 偏死盘。 - 低辐射时段(<100W/m²)仍在升温 → 暖平流驱动,预报往往低估。 @@ -44,11 +49,12 @@ P2 **阻碍因子**: - 若阻碍条件未满足,不可凭"多云"单因子就判断"升温受限"。 P3 **数学概率**: - - 参考我提供的结算概率分布,但不可用概率去压过 P1 实况节奏。 + - 参考我提供的结算概率分布,但不可用概率去压过 P0/P1 实况。 - 概率是辅助判据,实测趋势是主判据。 P4 **预报背景**(最低优先级): - - DEB融合值和预报可用于判断上沿空间和回落节奏,但当实测已超预报时,不可作为"难以升温"的主论据。 + - DEB融合值和预报可用于判断上沿空间和回落节奏。 + - 但当实测已远低于预报时,预报值已失去参考意义,不可继续引用。 - 结算边界:温度处于 X.5 进位线附近时需特别预警。 【输出要求】 @@ -56,8 +62,8 @@ P4 **预报背景**(最低优先级): 2. 严格按照以下 HTML 格式输出: 🤖 Groq AI 决策 -- 🎲 盘口: [必须明确指出最热时段(如:预计最热在 14:00-16:00)以及博弈焦点。区分"已确认WU底线 X°C"与"仍有概率冲击 Y°C"。禁止在升温未止时使用"锁定"等封顶措辞。若符合死盘条件,请直接给出死盘结论并说明理由。] -- 💡 逻辑: [用 3-5 句话深度分析实测与预报的博弈。分析当前动力条件对冲击下一个 WU 整数的支持程度。需包含具体数值。] +- 🎲 盘口: [若预报崩盘,直接给出"预报集体失准,WU结算锁定X°C"。否则:明确指出最热时段以及博弈焦点。区分"已确认WU底线 X°C"与"仍有概率冲击 Y°C"。禁止在升温未止时使用"锁定"等封顶措辞。若符合死盘条件,请直接给出死盘结论并说明理由。] +- 💡 逻辑: [用 3-5 句话深度分析。如果预报崩盘,重点分析崩盘原因和实测偏差。如果正常,分析实测与预报的博弈和动力条件。需包含具体数值。] - 🎯 置信度: [1-10]/10 """ diff --git a/web/app.py b/web/app.py index b195563c..a51433df 100644 --- a/web/app.py +++ b/web/app.py @@ -271,6 +271,7 @@ def _analyze(city: str) -> Dict[str, Any]: # ── 10. Probability distribution ── probabilities = [] mu = None + forecast_miss_deg = 0 # How far actual is below forecasts if ( ens_data["p10"] is not None and ens_data["p90"] is not None @@ -314,20 +315,43 @@ def _analyze(city: str) -> Dict[str, Any]: elif first_peak_h <= local_hour_frac <= last_peak_h: sigma *= 0.7 - # Mu calculation + # Mu calculation — reality-anchored forecast_highs = [h for h in current_forecasts.values() if h is not None] forecast_median = ( sorted(forecast_highs)[len(forecast_highs) // 2] if forecast_highs else ens_data["median"] ) - mu = ( - forecast_median * 0.7 + ens_data["median"] * 0.3 - if forecast_median is not None - else ens_data["median"] - ) - if max_so_far is not None and max_so_far > mu: - mu = max_so_far + (0.3 if not trend_info["is_cooling"] else 0.0) + + # Compute forecast miss magnitude + if max_so_far is not None and forecast_median is not None: + forecast_miss_deg = round(forecast_median - max_so_far, 1) + + # --- Key fix: Reality-anchored μ --- + # If we are past or in the peak window AND actual max is significantly + # below forecasts, anchor μ on max_so_far, not on forecast_median. + if ( + max_so_far is not None + and forecast_median is not None + and peak_status in ("past", "in_window") + and max_so_far < forecast_median - 2.0 + ): + # Forecast bust: μ anchors on actual max, not failed predictions + # Allow small upward margin only if still warming + if trend_info["is_cooling"] or peak_status == "past": + mu = max_so_far + else: + # Still in window and warming — small margin + mu = max_so_far + 0.5 + else: + # Normal case: blend forecast and ensemble + mu = ( + forecast_median * 0.7 + ens_data["median"] * 0.3 + if forecast_median is not None + else ens_data["median"] + ) + if max_so_far is not None and max_so_far > mu: + mu = max_so_far + (0.3 if not trend_info["is_cooling"] else 0.0) def _norm_cdf(x, m, s): return 0.5 * (1 + math.erf((x - m) / (s * math.sqrt(2)))) @@ -478,6 +502,20 @@ def _analyze(city: str) -> Dict[str, Any]: ) ai_parts.append(f"模型分歧: {mm_str}") + # --- Forecast bust detection for AI --- + if forecast_miss_deg > 2.0 and peak_status in ("past", "in_window"): + min_forecast = min( + (v for v in current_forecasts.values() if v is not None), default=None + ) + ai_parts.append( + f"🚨 预报崩盘: 所有模型集体高估!最低预报 {min_forecast}{sym} vs 实测最高 {max_so_far}{sym}," + f"偏差 {forecast_miss_deg}°。已进入/过了峰值窗口,温度严重不达预期。" + ) + elif forecast_miss_deg > 4.0 and peak_status == "before": + ai_parts.append( + f"⚠️ 预报差距: 距峰值窗口尚有时间,但实测已落后预报 {forecast_miss_deg}°。" + ) + ai_context = "\n".join(ai_parts) ai_text = get_ai_analysis(ai_context, city, sym) except Exception as e: