diff --git a/apps/worker/worker_app/tasks/vision/fast_path.py b/apps/worker/worker_app/tasks/vision/fast_path.py index 715eb2194..323ee44ea 100644 --- a/apps/worker/worker_app/tasks/vision/fast_path.py +++ b/apps/worker/worker_app/tasks/vision/fast_path.py @@ -21,9 +21,9 @@ logger = logging.getLogger(__name__) # 可通过环境变量调参(有默认值,无需配置即可跑) _IMG_WORKERS = int(os.environ.get("VISION_V2_IMG_WORKERS", "8")) -_FAST_TIMEOUT = float(os.environ.get("VISION_V2_FAST_TIMEOUT", "6")) -_FAST_JSON_TIMEOUT = float(os.environ.get("VISION_V2_FAST_JSON_TIMEOUT", "5")) -_OCR_TIMEOUT = float(os.environ.get("VISION_V2_OCR_TIMEOUT", "5")) +_FAST_TIMEOUT = float(os.environ.get("VISION_V2_FAST_TIMEOUT", "8")) +_FAST_JSON_TIMEOUT = float(os.environ.get("VISION_V2_FAST_JSON_TIMEOUT", "8")) +_OCR_TIMEOUT = float(os.environ.get("VISION_V2_OCR_TIMEOUT", "6")) _PRO_TIMEOUT = float(os.environ.get("VISION_V2_PRO_TIMEOUT", "45")) _FALLBACK_RESULT = { 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 3b93b6580..c68cf739a 100644 --- a/apps/worker/worker_app/tasks/vision/vlm_fast_json.py +++ b/apps/worker/worker_app/tasks/vision/vlm_fast_json.py @@ -81,35 +81,79 @@ def call_fast_json( ) -> dict[str, Any] | None: """调用 lite VLM 返回结构化 dict;失败/非 JSON 返回 None。 - 注意:不做重试(外层竞速/降级逻辑负责),max_retries=0 由外层统一设置。 + 直接用 httpx 发最小 payload(关闭 thinking),不走 ai_client 包装: + - 关闭 thinking/推理链(reasoning_tokens 是延迟主因,单次要10-12s) + - 单次调用不重试(失败由外层走 pro 兜底) + - 温度=0.1 稳定输出 JSON """ t0 = time.time() + import httpx try: - from packages.shared.ai_client import get_doubao_client - - client = get_doubao_client() - if not client.is_available: - logger.warning("[vision.v2] doubao client 不可用,跳过 fast_json") + from packages.shared import get_shared_settings + settings = get_shared_settings() + api_key = settings.doubao_api_key + base_url = (settings.doubao_base_url or "https://ark.cn-beijing.volces.com/api/v3").rstrip("/") + if not api_key: + logger.warning("[vision.v2] doubao api_key 未配置,跳过 fast_json") return None use_model = model or DEFAULT_LITE_MODEL - # 强制不重试:lite 是快速路径,失败直接走外层 pro 兜底 - _orig_retries = client.max_retries - client.max_retries = 0 + url = f"{base_url}/chat/completions" + payload: dict[str, Any] = { + "model": use_model, + "messages": [ + {"role": "system", "content": _FAST_SYSTEM}, + {"role": "user", "content": [ + {"type": "image_url", "image_url": {"url": img_url}}, + {"type": "text", "text": _FAST_USER}, + ]}, + ], + "temperature": 0.1, + "max_tokens": max_tokens, + "stream": False, + } + # 关键:关闭 thinking(避免产生 reasoning_tokens 拖慢响应) + # 方舟/豆包 2.x 模型支持 thinking.type=disabled try: - raw = client.vision_completion( - messages=[ - {"role": "system", "content": _FAST_SYSTEM}, - {"role": "user", "content": _FAST_USER}, - ], - images=[img_url], - temperature=0.1, - max_tokens=max_tokens, - timeout=timeout, - model=use_model, - ) - finally: - client.max_retries = _orig_retries + payload["thinking"] = {"type": "disabled"} + except Exception: + pass + # 部分模型用 reasoning_effort 控制思考深度 + payload["reasoning_effort"] = "low" + + resp = httpx.post( + url, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json=payload, + 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(): + payload.pop("thinking", None) + payload.pop("reasoning_effort", None) + t1 = time.time() + resp = httpx.post(url, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json=payload, timeout=timeout, + ) + elapsed = time.time() - t0 + if resp.status_code != 200: + logger.warning("[vision.v2] fast_json 降级后 HTTP %d elapsed=%.1fs", resp.status_code, elapsed) + return None + else: + return None + data = resp.json() + raw = (data.get("choices") or [{}])[0].get("message", {}).get("content") + if raw is None: + logger.warning("[vision.v2] fast_json 返回 None elapsed=%.1fs", elapsed) + return None + usage = data.get("usage") or {} + logger.info("[vision.v2] fast_json 直连完成 model=%s elapsed=%.1fs in=%d out=%d reasoning=%d", + use_model, elapsed, usage.get("prompt_tokens",0), usage.get("completion_tokens",0), + usage.get("reasoning_tokens",0)) elapsed = time.time() - t0 if raw is None: logger.warning("[vision.v2] fast_json 返回 None elapsed=%.1fs model=%s", elapsed, use_model)