From cdd343131ede9048a0efe5c9f14eab628590b67e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 5 Oct 2026 19:06:02 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(vision):=20#2205=20thinking=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BA=92=E6=96=A5=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=8F=AA=E4=BC=A0thinking=3Ddisabled=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E6=8E=89reasoning=5Feffort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:上版同时传 thinking={type:disabled} + reasoning_effort=low,方舟返回400 Invalid combination of reasoning_effort and thinking type。降级重试分支 把两个参数都pop掉,模型回到默认thinking开启→响应9-12s超时,fast全败。 修复: 1. 只传 thinking={"type":"disabled"},去掉互斥的 reasoning_effort 2. 400降级只pop thinking,保留精简payload重试(不pop多个) 3. 先单独验证 lite 单次HTTP 200且reasoning_tokens=0再跑E2E,省一轮部署 --- .../worker_app/tasks/vision/vlm_fast_json.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py index 036ec8285..b67409a02 100644 --- a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py +++ b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py @@ -117,14 +117,9 @@ def call_fast_json( "max_tokens": max_tokens, "stream": False, } - # 关键:关闭 thinking(避免产生 reasoning_tokens 拖慢响应) - # 方舟/豆包 2.x 模型支持 thinking.type=disabled - try: - payload["thinking"] = {"type": "disabled"} - except Exception: - pass - # 部分模型用 reasoning_effort 控制思考深度 - payload["reasoning_effort"] = "low" + # 关键:关闭 thinking(reasoning_tokens 是延迟主因,单次要10-12s) + # 方舟/豆包 Seed 2.x 支持 thinking={type:"disabled"},且不要和 reasoning_effort 同时传(两者互斥会400) + payload["thinking"] = {"type": "disabled"} resp = httpx.post( url, @@ -133,15 +128,12 @@ def call_fast_json( timeout=timeout, ) elapsed = time.time() - t0 - if resp.status_code != 200: - logger.warning( - "[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200] - ) - # 如果400说明不支持thinking参数,降级重试一次 - if resp.status_code == 400 and "thinking" in resp.text.lower(): + # 400 说明模型不支持 thinking 参数(极少数旧模型),重试一次不带 thinking + if resp.status_code == 400: + body_preview = resp.text[:300].lower() + logger.warning("[vision.v2] fast_json HTTP 400 elapsed=%.1fs body=%s", elapsed, resp.text[:200]) + if "thinking" in body_preview or "reasoning" in body_preview: payload.pop("thinking", None) - payload.pop("reasoning_effort", None) - time.time() resp = httpx.post( url, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, @@ -150,10 +142,13 @@ def call_fast_json( ) elapsed = time.time() - t0 if resp.status_code != 200: - logger.warning("[vision.v2] fast_json 降级后 HTTP %d elapsed=%.1fs", resp.status_code, elapsed) + logger.warning("[vision.v2] fast_json 降级重试 HTTP %d elapsed=%.1fs", resp.status_code, elapsed) return None else: return None + elif resp.status_code != 200: + logger.warning("[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200]) + return None data = resp.json() raw = (data.get("choices") or [{}])[0].get("message", {}).get("content") if raw is None: -- 2.54.0 From 12f74b8d52199047f7831ebe3c042fd96ff0a776 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 5 Oct 2026 11:11:29 +0000 Subject: [PATCH 2/4] style: auto-format with black + isort + ruff + prettier [skip ci-format-check] --- apps/worker/worker_app/tasks/vision/vlm_fast_json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py index b67409a02..eecb492c8 100644 --- a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py +++ b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py @@ -147,7 +147,9 @@ def call_fast_json( else: return None elif resp.status_code != 200: - logger.warning("[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200]) + logger.warning( + "[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200] + ) return None data = resp.json() raw = (data.get("choices") or [{}])[0].get("message", {}).get("content") -- 2.54.0 From 7843b92ef8e314d04c3381070ee1fd49789b42c4 Mon Sep 17 00:00:00 2001 From: build-ops Date: Mon, 5 Oct 2026 19:22:03 +0800 Subject: [PATCH 3/4] style(vision): rename ambiguous l/r vars to lb/rb (ruff E741) --- apps/worker/worker_app/tasks/vision/vlm_fallback.py | 6 +++--- apps/worker/worker_app/tasks/vision/vlm_fast_json.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/worker/worker_app/tasks/vision/vlm_fallback.py b/apps/worker/worker_app/tasks/vision/vlm_fallback.py index 558500c17..4b479bff9 100644 --- a/apps/worker/worker_app/tasks/vision/vlm_fallback.py +++ b/apps/worker/worker_app/tasks/vision/vlm_fallback.py @@ -187,10 +187,10 @@ def call_pro_vlm( return None text = _strip_code_fence(raw) - l, r = text.find("{"), text.rfind("}") - if l >= 0 and r > l: + lb, rb = text.find("{"), text.rfind("}") + if lb >= 0 and rb > lb: try: - obj = json.loads(text[l : r + 1]) + obj = json.loads(text[lb : rb + 1]) if isinstance(obj, dict): logger.info("[vision.vlm] 图片 #%d pro VLM JSON 完成 elapsed=%.1fs", idx, elapsed) return { diff --git a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py index eecb492c8..4c2b2478c 100644 --- a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py +++ b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py @@ -172,10 +172,10 @@ def call_fast_json( text = _strip_code_fence(raw) # 截到第一个 { 和最后一个 } 之间,容忍前后偶发文字 - l = text.find("{") - r = text.rfind("}") - if l >= 0 and r > l: - text = text[l : r + 1] + lb = text.find("{") + rb = text.rfind("}") + if lb >= 0 and rb > lb: + text = text[lb : rb + 1] try: obj = json.loads(text) except json.JSONDecodeError: -- 2.54.0 From d6f39c18a534d4d5e99f494bba5e161687dcb2f8 Mon Sep 17 00:00:00 2001 From: build-ops Date: Mon, 5 Oct 2026 19:47:59 +0800 Subject: [PATCH 4/4] test(vision): update wiring tests for V2 batch analyze_images_v2 path --- tests/unit/test_viral_video_wiring.py | 47 +++++++++++++++++---------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_viral_video_wiring.py b/tests/unit/test_viral_video_wiring.py index fe5ada00e..cbb0b4e16 100644 --- a/tests/unit/test_viral_video_wiring.py +++ b/tests/unit/test_viral_video_wiring.py @@ -97,21 +97,28 @@ def invalidate_loader_cache(): class TestImageAnalysisWiring: - def test_uses_loader_template_and_xml_parse(self, job): + def test_v2_batch_analysis_returns_products(self, job): + """V2 路径:_step_image_analysis 批量调用 analyze_images_v2,返回 products。""" from apps.worker.worker_app.tasks import viral_video as vv - with patch("packages.shared.ai_service.call_vision", return_value=IMAGE_XML) as mock_v: - result = vv._analyze_single_image(0, "https://img/1.jpg", "vlm-lite", 15) + fake_product = { + "name": "lipstick", + "brand": "品牌X", + "category": "唇部彩妆", + "key_features": ["显白", "持久"], + "text_on_package": ["品牌X", "211"], + "_source": "v2_fast_json", + } + with patch("worker_app.tasks.vision.analyze_images_v2", return_value=[fake_product]) as mock_aiv2: + result = vv._step_image_analysis(job) - mock_v.assert_called_once() - # 验证调用时传入了 system_prompt(说明走了 loader 渲染的模板) - call_kwargs = mock_v.call_args.kwargs - assert "system_prompt" in call_kwargs and call_kwargs["system_prompt"] - # 结果包含从 XML 解析出的产品信息 - assert result["name"] == "lipstick" - assert result["brand"] == "品牌X" - assert "显白" in result["key_features"] - assert result["text_on_package"] == ["品牌X", "211"] + mock_aiv2.assert_called_once() + products = result["products"] + assert len(products) == 1 + assert products[0]["name"] == "lipstick" + assert products[0]["brand"] == "品牌X" + assert "显白" in products[0]["key_features"] + assert products[0]["text_on_package"] == ["品牌X", "211"] # ── 2) 意图解析走模板 ─────────────────────────────────────────────── @@ -252,18 +259,22 @@ class TestEndToEndLoaderUsed: called_types.append(prompt_type) return real_get(prompt_type, **kwargs) + # V2 图片分析不再走 prompt_loader(固定 lite JSON prompt),用 mock 产品代替 + img_res = { + "name": "lipstick", + "brand": "品牌X", + "key_features": ["显白", "持久"], + "text_on_package": ["品牌X"], + } with ( patch.object(pl, "get_template", side_effect=spy_get), - patch("packages.shared.ai_service.call_vision", return_value=IMAGE_XML), patch("packages.shared.ai_service.call_llm", return_value=INTENT_XML), ): - # 1) image - img_res = vv._analyze_single_image(0, "https://img/1.jpg", "vlm", 15) - # 2) intent + # intent intent_res = vv._step_intent_parsing(job, {"products": [img_res]}) - # 前两步分别调用了 image_analysis 和 intent_parsing - assert "image_analysis" in called_types + # V2 图片分析走固定 prompt(不经 loader);intent 仍走 loader + assert "image_analysis" not in called_types assert "intent_parsing" in called_types # script 和 review 单独验证(需要不同的 LLM 返回) -- 2.54.0