Revise fallback highs after observed breaks
Fast evidence mode should not say DEB and models support the center when the latest METAR has already exceeded that center or the model upper edge. The fallback now treats the live observation as a lower bound for the daily high and explains the upward revision pressure. Constraint: Fallback output must remain useful before the full AI bulletin read returns Rejected: Keep the original DEB center until AI completes | it can be lower than an already-observed temperature Confidence: high Scope-risk: narrow Tested: pytest tests/test_web_observability.py::test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_stream_request_only_asks_provider_for_observation_read -q Tested: npm run build
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
|
||||
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
|
||||
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
|
||||
- 城市决策卡兜底判断新增实测突破识别:当最新 METAR/观测已高于 DEB 中枢或模型上沿时,改为提示最高温中枢需要上修
|
||||
- 城市决策卡市场层改用完整 `all_buckets` 并严格识别 exact / range / or higher / or lower 温度桶方向,避免最高温中枢错配到不合理尾部桶
|
||||
- 温度桶标签统一规范化 `C/F/°C/°F`,修复 `31°°C` 这类重复单位展示
|
||||
- 决策卡展示文案将“概率差”收口为“模型-市场差”,明确口径为 `模型概率 - 市场隐含概率`
|
||||
|
||||
@@ -135,6 +135,44 @@ def test_city_ai_fallback_reasoning_identifies_fast_evidence_mode():
|
||||
assert "AI 增强可作为后续补充" not in payload["reasoning_zh"]
|
||||
|
||||
|
||||
def test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models():
|
||||
payload = scan_terminal_service._build_city_ai_fallback(
|
||||
{
|
||||
"city_display_name": "Manila",
|
||||
"temp_symbol": "°C",
|
||||
"deb": {"prediction": 34.0},
|
||||
"model_cluster": {
|
||||
"sources": [
|
||||
{"value": 32.5},
|
||||
{"value": 33.8},
|
||||
{"value": 34.0},
|
||||
{"value": 34.7},
|
||||
]
|
||||
},
|
||||
"observation_anchor": {
|
||||
"is_airport_metar": True,
|
||||
"station_code": "RPLL",
|
||||
},
|
||||
"airport_current": {
|
||||
"station_code": "RPLL",
|
||||
"temp": 35.0,
|
||||
"report_time": "03:00Z / 当地 11:00",
|
||||
"raw_metar": "RPLL 270300Z 34004KT CAVOK 35/24 Q1009",
|
||||
},
|
||||
},
|
||||
locale="zh-CN",
|
||||
reason="stream preview",
|
||||
)
|
||||
|
||||
assert payload["predicted_max"] == 35.0
|
||||
assert payload["range_high"] == 35.0
|
||||
assert "高于原先 34.0°C 中枢" in payload["final_judgment_zh"]
|
||||
assert "上修到至少 35.0°C" in payload["final_judgment_zh"]
|
||||
assert "共同支撑本轮最高温中枢" not in payload["reasoning_zh"]
|
||||
assert "超过模型上沿 34.7°C" in payload["reasoning_zh"]
|
||||
assert "继续上修最高温中枢" in payload["risks_zh"][0]
|
||||
|
||||
|
||||
def test_city_ai_stream_request_only_asks_provider_for_observation_read():
|
||||
request_payload = scan_terminal_service._build_city_ai_stream_request(
|
||||
{
|
||||
|
||||
@@ -685,6 +685,27 @@ def _build_city_ai_fallback(
|
||||
predicted = current_temp
|
||||
range_low = min(values) if values else predicted
|
||||
range_high = max(values) if values else predicted
|
||||
model_range_high = range_high
|
||||
current_above_predicted = (
|
||||
current_temp is not None
|
||||
and predicted is not None
|
||||
and current_temp > predicted + 0.2
|
||||
)
|
||||
current_above_model_range = (
|
||||
current_temp is not None
|
||||
and model_range_high is not None
|
||||
and current_temp > model_range_high + 0.2
|
||||
)
|
||||
observed_high_break = bool(current_above_predicted or current_above_model_range)
|
||||
original_predicted = predicted
|
||||
if observed_high_break:
|
||||
predicted = max(
|
||||
value
|
||||
for value in (predicted, current_temp)
|
||||
if value is not None
|
||||
)
|
||||
if range_high is not None and current_temp is not None:
|
||||
range_high = max(range_high, current_temp)
|
||||
city = str(ai_input.get("city_display_name") or ai_input.get("city") or "this city")
|
||||
station = str((airport_current.get("station_code") if is_airport_metar else None) or observation_anchor.get("station_code") or current_obs.get("station_code") or "")
|
||||
raw_metar = str(airport_current.get("raw_metar") or "").strip() if is_airport_metar else ""
|
||||
@@ -718,12 +739,18 @@ def _build_city_ai_fallback(
|
||||
metar_zh = f"当前没有可用的{source_name_zh}正文,暂以 DEB、多模型路径与最新实测为主。"
|
||||
metar_en = f"No raw {source_name_en} text is available, so DEB, latest observations and the model cluster carry the read."
|
||||
predicted_text = _format_ai_temperature(predicted, unit) or "--"
|
||||
current_text = _format_ai_temperature(current_temp, unit) or "--"
|
||||
original_predicted_text = _format_ai_temperature(original_predicted, unit) or "--"
|
||||
model_range_high_text = _format_ai_temperature(model_range_high, unit) or "--"
|
||||
if partial_ai.get("final_judgment_zh") or partial_ai.get("final_judgment_en"):
|
||||
final_zh = str(partial_ai.get("final_judgment_zh") or partial_ai.get("final_judgment_en") or "").strip()
|
||||
final_en = str(partial_ai.get("final_judgment_en") or partial_ai.get("final_judgment_zh") or "").strip()
|
||||
elif partial_ai:
|
||||
final_zh = f"{city} 预计最高温暂以 {predicted_text} 附近为中枢;AI 已先完成{bulletin_zh}解读,最高温结论结合 DEB、多模型与最新实测校准。"
|
||||
final_en = f"{city} daily high is centered near {predicted_text}; AI has already read the {bulletin_en}, with the high calibrated against DEB, the model cluster and latest observations."
|
||||
elif observed_high_break:
|
||||
final_zh = f"{city} 最新实测已达 {current_text},高于原先 {original_predicted_text} 中枢;最高温中枢需先上修到至少 {predicted_text} 附近。"
|
||||
final_en = f"{city} latest observation has reached {current_text}, above the prior {original_predicted_text} center; the daily-high center should be revised up to at least near {predicted_text}."
|
||||
elif timed_out:
|
||||
final_zh = f"{city} 预计最高温暂以 {predicted_text} 附近为中枢;当前已先用 DEB、多模型和{source_name_zh}快速证据模式判断。"
|
||||
final_en = f"{city} daily high is centered near {predicted_text}; the current read uses the fast DEB/model/{source_name_en} evidence mode."
|
||||
@@ -733,15 +760,27 @@ def _build_city_ai_fallback(
|
||||
reasoning_zh = str(partial_ai.get("reasoning_zh") or "").strip() or (
|
||||
f"AI {bulletin_zh}解读已用于校准日内节奏;DEB 与多模型集合继续约束最高温中枢,后续{source_name_zh}用于确认是否需要上调或下修。"
|
||||
if partial_ai
|
||||
else f"当前为快速证据模式;最新{source_name_zh}已高于原先 {original_predicted_text} 中枢{(',并超过模型上沿 ' + model_range_high_text) if current_above_model_range else ''},本轮最高温判断应优先承认实测突破并等待完整 AI {bulletin_zh}解读合并。"
|
||||
if observed_high_break
|
||||
else f"当前为快速证据模式;DEB、多模型集合和最新{source_name_zh}共同支撑本轮最高温中枢,完整 AI {bulletin_zh}解读返回后再合并。"
|
||||
)
|
||||
reasoning_en = str(partial_ai.get("reasoning_en") or "").strip() or (
|
||||
f"The AI {bulletin_en} read is already used to calibrate the intraday pace; DEB and the model cluster still constrain the high-temperature center, while later {source_name_en} updates confirm whether to revise it."
|
||||
if partial_ai
|
||||
else f"This is the fast evidence mode; latest {source_name_en} is above the prior {original_predicted_text} center{(' and above the model upper edge ' + model_range_high_text) if current_above_model_range else ''}, so the high-temperature read should first acknowledge the observed break and merge the full AI {bulletin_en} read when available."
|
||||
if observed_high_break
|
||||
else f"This is the fast evidence mode; DEB, the model cluster and latest {source_name_en} jointly support the current daily-high center, and the full AI {bulletin_en} read will be merged when available."
|
||||
)
|
||||
risks_zh = [f"后续{source_name_zh}若明显偏离模型路径,需及时修正最高温中枢。"]
|
||||
risks_en = [f"If later {source_name_en} updates diverge from the model path, revise the daily-high center promptly."]
|
||||
risks_zh = (
|
||||
[f"最新{source_name_zh}已突破原模型路径,若后续报文继续持平或升温,需要继续上修最高温中枢。"]
|
||||
if observed_high_break
|
||||
else [f"后续{source_name_zh}若明显偏离模型路径,需及时修正最高温中枢。"]
|
||||
)
|
||||
risks_en = (
|
||||
[f"Latest {source_name_en} has already broken above the prior model path; if later reports hold steady or warm further, keep revising the daily-high center upward."]
|
||||
if observed_high_break
|
||||
else [f"If later {source_name_en} updates diverge from the model path, revise the daily-high center promptly."]
|
||||
)
|
||||
return {
|
||||
"predicted_max": partial_ai.get("predicted_max", predicted),
|
||||
"range_low": partial_ai.get("range_low", range_low),
|
||||
|
||||
Reference in New Issue
Block a user