Speed up streamed airport bulletin reads
The city card only needs the provider to interpret the latest METAR or official observation. Deterministic fields such as the high-temperature center, model cluster note and fallback risks are already available server-side, so the stream request now asks DeepSeek for only the observation read and concise reasoning. Constraint: City cards still need a complete payload for both Chinese and English UI modes Rejected: Keep generating the full decision schema in the stream | too much model output for every card Rejected: Retry failed streams by default | it can double latency and the fallback can use partial streamed text Confidence: high Scope-risk: moderate Tested: pytest tests/test_web_observability.py::test_city_ai_stream_request_only_asks_provider_for_observation_read tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_partial_json_trims_dangling_taf_clause tests/test_web_observability.py::test_city_ai_schema_completion_trims_dangling_taf_clause -q Tested: npm run build
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
|
||||
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
|
||||
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
|
||||
- 城市决策卡市场层改用完整 `all_buckets` 并严格识别 exact / range / or higher / or lower 温度桶方向,避免最高温中枢错配到不合理尾部桶
|
||||
- 温度桶标签统一规范化 `C/F/°C/°F`,修复 `31°°C` 这类重复单位展示
|
||||
- 决策卡展示文案将“概率差”收口为“模型-市场差”,明确口径为 `模型概率 - 市场隐含概率`
|
||||
|
||||
@@ -135,6 +135,36 @@ def test_city_ai_fallback_reasoning_identifies_fast_evidence_mode():
|
||||
assert "AI 增强可作为后续补充" not in payload["reasoning_zh"]
|
||||
|
||||
|
||||
def test_city_ai_stream_request_only_asks_provider_for_observation_read():
|
||||
request_payload = scan_terminal_service._build_city_ai_stream_request(
|
||||
{
|
||||
"city": "Tokyo",
|
||||
"city_display_name": "Tokyo",
|
||||
"temp_symbol": "°C",
|
||||
"deb": {"prediction": 17.8},
|
||||
"model_cluster": {"sources": [{"value": 17.0}, {"value": 17.8}]},
|
||||
"observation_anchor": {
|
||||
"is_airport_metar": True,
|
||||
"read_label_zh": "机场报文解读",
|
||||
},
|
||||
"airport_current": {
|
||||
"station_code": "RJTT",
|
||||
"temp": 16.0,
|
||||
"report_time": "21:30Z",
|
||||
"raw_metar": "RJTT 262130Z AUTO 00000KT 9999 FEW030 16/10 Q1015",
|
||||
},
|
||||
},
|
||||
locale="zh-CN",
|
||||
)
|
||||
|
||||
user_payload = request_payload["messages"][1]["content"]
|
||||
assert request_payload["stream"] is True
|
||||
assert request_payload["max_tokens"] <= 650
|
||||
assert request_payload["max_tokens"] < scan_terminal_service.SCAN_AI_MAX_TOKENS
|
||||
assert '"required_json_keys": ["metar_read_zh", "metar_read_en", "reasoning_zh", "reasoning_en"]' in user_payload
|
||||
assert "Do not return final_judgment" in user_payload
|
||||
|
||||
|
||||
def test_city_ai_partial_json_trims_dangling_taf_clause():
|
||||
payload = scan_terminal_service._build_city_ai_fallback(
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ SCAN_CITY_AI_TIMEOUT_SEC = _env_int(
|
||||
max_value=120,
|
||||
)
|
||||
SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR = str(
|
||||
os.getenv("POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR") or "true"
|
||||
os.getenv("POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR") or "false"
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
SCAN_AI_CACHE_TTL_SEC = max(
|
||||
30,
|
||||
@@ -94,11 +94,17 @@ SCAN_AI_MAX_TOKENS = _env_int(
|
||||
)
|
||||
SCAN_CITY_AI_MAX_TOKENS = _env_int(
|
||||
"POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS",
|
||||
900,
|
||||
min_value=800,
|
||||
650,
|
||||
min_value=300,
|
||||
max_value=64000,
|
||||
)
|
||||
SCAN_CITY_AI_PROMPT_VERSION = "city-observation-read-v5"
|
||||
SCAN_CITY_AI_STREAM_MAX_TOKENS = _env_int(
|
||||
"POLYWEATHER_SCAN_CITY_AI_STREAM_MAX_TOKENS",
|
||||
min(SCAN_CITY_AI_MAX_TOKENS, 650),
|
||||
min_value=300,
|
||||
max_value=64000,
|
||||
)
|
||||
SCAN_CITY_AI_PROMPT_VERSION = "city-observation-read-v6"
|
||||
|
||||
CITY_AI_REQUIRED_FIELDS = [
|
||||
"metar_read_zh",
|
||||
@@ -118,6 +124,13 @@ CITY_AI_REQUIRED_FIELDS = [
|
||||
"model_cluster_note_en",
|
||||
]
|
||||
|
||||
CITY_AI_STREAM_PROVIDER_FIELDS = [
|
||||
"metar_read_zh",
|
||||
"metar_read_en",
|
||||
"reasoning_zh",
|
||||
"reasoning_en",
|
||||
]
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> Optional[float]:
|
||||
try:
|
||||
@@ -778,6 +791,15 @@ def _city_ai_response_example(unit: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _city_ai_stream_response_example(unit: str) -> Dict[str, Any]:
|
||||
return {
|
||||
"metar_read_zh": f"最新 METAR 显示 37.0{unit or '°C'},报文时间 04:30Z;风和云量暂未显示强降温信号,后续报文用于确认升温路径。",
|
||||
"metar_read_en": f"The latest METAR shows 37.0{unit or '°C'} at 04:30Z; wind and cloud signals do not yet show a strong cooling break, so later reports should confirm the warming path.",
|
||||
"reasoning_zh": "当前观测仍贴近上午升温路径,若后续风向转为海风或云雨增强,再下修最高温中枢。",
|
||||
"reasoning_en": "The latest observation still fits the morning warming path; revise the daily-high center lower if later wind turns onshore or cloud/rain strengthens.",
|
||||
}
|
||||
|
||||
|
||||
def _complete_city_ai_payload(
|
||||
ai_raw: Dict[str, Any],
|
||||
ai_input: Dict[str, Any],
|
||||
@@ -1907,13 +1929,12 @@ def _build_city_ai_stream_request(
|
||||
read_label = str(observation_anchor.get("read_label_zh") or ("机场报文解读" if is_airport_metar else "官方观测解读"))
|
||||
source_instruction = str(observation_anchor.get("instruction_zh") or "")
|
||||
system_prompt = (
|
||||
f"你是 PolyWeather 的城市最高温与{role_label}。"
|
||||
f"你是 PolyWeather 的{role_label}。"
|
||||
"只返回一个紧凑 JSON object,不要 Markdown。"
|
||||
f"必须先写 metar_read_zh 和 metar_read_en 字段,便于前端流式显示{read_label};"
|
||||
"然后写 final_judgment_zh/final_judgment_en、predicted_max、range_low、range_high、unit、confidence、"
|
||||
"reasoning_zh/reasoning_en、risks_zh/risks_en、model_cluster_note_zh/model_cluster_note_en。"
|
||||
f"只解读最新观测/报文对今日最高温路径的影响,必须只写 metar_read_zh、metar_read_en、reasoning_zh、reasoning_en 四个字段,便于前端快速显示{read_label};"
|
||||
"最高温数值、模型一致性和风险清单由后端规则补齐,不要生成这些字段。"
|
||||
f"{source_instruction}"
|
||||
"观测解读必须具体说明观测/报文时间、温度、风向风速、云量/天气/能见度/露点中与温度路径相关的因素;"
|
||||
"观测解读必须具体说明观测/报文时间、温度、风向风速、云量/天气/能见度/露点中与温度路径相关的因素;每个字段最多 1 句。"
|
||||
"涉及风时要说明当前风向对站点最高温路径倾向增温、降温还是中性,并给出理由。"
|
||||
"如果 observation_anchor.is_airport_metar 为 false,不得使用 METAR、TAF、机场报文等称谓。"
|
||||
"所有 *_zh 字段写简体中文,所有 *_en 字段写英文,不得留空。"
|
||||
@@ -1922,7 +1943,7 @@ def _build_city_ai_stream_request(
|
||||
return {
|
||||
"model": SCAN_CITY_AI_MODEL,
|
||||
"temperature": 0.2,
|
||||
"max_tokens": SCAN_CITY_AI_MAX_TOKENS,
|
||||
"max_tokens": SCAN_CITY_AI_STREAM_MAX_TOKENS,
|
||||
"response_format": {"type": "json_object"},
|
||||
"stream": True,
|
||||
"messages": [
|
||||
@@ -1933,14 +1954,12 @@ def _build_city_ai_stream_request(
|
||||
{
|
||||
"locale": normalized_locale,
|
||||
"task": (
|
||||
"Return JSON keys in this exact order: metar_read_zh, metar_read_en, "
|
||||
"final_judgment_zh, final_judgment_en, predicted_max, range_low, range_high, "
|
||||
"unit, confidence, reasoning_zh, reasoning_en, risks_zh, risks_en, "
|
||||
"model_cluster_note_zh, model_cluster_note_en. Keep it compact. "
|
||||
"Return JSON keys in this exact order: metar_read_zh, metar_read_en, reasoning_zh, reasoning_en. "
|
||||
"Do not return final_judgment, predicted_max, ranges, risks or model_cluster_note. Keep it compact. "
|
||||
"Return exactly one JSON object and no markdown."
|
||||
),
|
||||
"required_json_keys": CITY_AI_REQUIRED_FIELDS,
|
||||
"json_example": _city_ai_response_example(
|
||||
"required_json_keys": CITY_AI_STREAM_PROVIDER_FIELDS,
|
||||
"json_example": _city_ai_stream_response_example(
|
||||
str(ai_input.get("temp_symbol") or "°C")
|
||||
),
|
||||
"city_snapshot": ai_input,
|
||||
@@ -2159,10 +2178,11 @@ def stream_scan_city_ai_forecast_payload(
|
||||
last_meta: Dict[str, Any] = {}
|
||||
try:
|
||||
logger.info(
|
||||
"scan city AI stream request city={} locale={} input_bytes={} timeout_sec={}",
|
||||
"scan city AI stream request city={} locale={} input_bytes={} max_tokens={} timeout_sec={}",
|
||||
ai_input.get("city"),
|
||||
normalized_locale,
|
||||
len(json.dumps(request_json, ensure_ascii=False, default=str).encode("utf-8")),
|
||||
request_json.get("max_tokens"),
|
||||
SCAN_CITY_AI_TIMEOUT_SEC,
|
||||
)
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
|
||||
Reference in New Issue
Block a user