fix(vision-v2): 修正prompt解析-删掉开头匹配,DB有值直接追加JSON schema
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m0s
AI Code Review / AI Code Review (pull_request) Successful in 6m44s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 40s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m21s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m23s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 12m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Failing after 12m55s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Successful in 26m41s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 4s

#2211的判定逻辑有bug:用'开头是否是默认XML标记'判断是否用户自定义,
但用户后台配的prompt就是以这句话开头的XML格式,会被误判为内置默认不生效。

修正为简单直接规则(不做字符串匹配判断):
- DB有is_active=true的image_analysis记录(含种子默认XML和用户修改版):
  system = DB.system_prompt + JSON_SCHEMA_APPEND(追加完整JSON字段schema,
  末尾硬约束'不要输出XML标签',覆盖XML输出格式要求)
- DB无记录/连接异常/返回空:纯硬编码JSON schema prompt
- 直接调prompt_loader._load_from_db查DB(不用get_template避免被
  DEFAULT_TEMPLATES fallback混淆)
- 30s TTL缓存,fast追加22字段schema,pro追加13字段精简schema
This commit is contained in:
xiaoxia
2026-10-05 22:37:24 +08:00
parent 0bf4f359a7
commit 5dd126750f
+102 -50
View File
@@ -1,6 +1,12 @@
# -*- coding: utf-8 -*-
"""V2 prompt 解析:优先读后台 viral_video_prompt_templates 配置,30s TTL 热加载;
DB 不可用/读到默认XML模板时,fallback 到硬编码 JSON schema prompt。
"""V2 prompt 解析:优先读后台 viral_video_prompt_templates 表(prompt_type='image_analysis'
且 is_active=true),30s TTL 热加载;DB 无有效记录/异常时,fallback 到纯硬编码 JSON schema prompt。
规则(简单直接,不做字符串匹配判断):
- DB 有 is_active=true 的 image_analysis 记录(含种子默认XML和用户修改后的版本):
* system = DB.system_prompt + JSON_SCHEMA_APPEND(追加完整JSON字段schema,覆盖XML等其他输出格式要求)
* user = DB.user_prompt_template 渲染后使用;渲染后为空则用硬编码默认
- DB 无记录/连接异常/返回空:system/user 全部用纯硬编码 JSON schema prompt
"""
from __future__ import annotations
@@ -11,9 +17,9 @@ from typing import Any
logger = logging.getLogger(__name__)
# ---- 默认硬编码 prompt(DB 不可用或读到默认XML模板时使用) ----
# ---- 纯硬编码 JSON schema(DB 无有效配置时全量使用) ----
DEFAULT_FAST_SYSTEM = (
_FAST_JSON_SCHEMA = (
"你是图片结构化识别器。严格按下方 JSON schema 返回一个对象,不要任何解释、"
"不要markdown、不要代码块、不要前后缀文字。字段值不确定时填 null 或空数组。\n"
"{\n"
@@ -39,12 +45,13 @@ DEFAULT_FAST_SYSTEM = (
' "pattern": "图案,如纯色/条纹/波点/格子/印花/碎花/Logo等",\n'
' "colors": ["主色数组"],\n'
' "mood": "整体氛围/情绪,如清新/活力/高级/温暖/冷峻/甜美/复古等"\n'
"}"
"}\n\n"
"你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释、XML标签或markdown。"
)
DEFAULT_FAST_USER = "识别这张图片的人物穿搭与主体信息,只返回JSON对象。"
DEFAULT_PRO_SYSTEM = (
"你是图片分析专家。严格按下方 JSON schema 返回一个对象,不要解释、不要markdown、不要代码块。\n"
_PRO_JSON_SCHEMA = (
"你是图片分析专家。严格按下方 JSON schema 返回一个对象,不要解释、不要markdown、不要代码块、不要XML标签。\n"
"{\n"
' "has_person": true/false,\n'
' "gender": "男"/"女"/null,\n'
@@ -60,51 +67,101 @@ DEFAULT_PRO_SYSTEM = (
' "product_name": "产品名,非产品图填null",\n'
' "brand": "品牌,无则null",\n'
' "key_features": ["核心特征数组,3-6个短语"]\n'
"}"
"}\n\n"
"你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释、XML标签或markdown。"
)
DEFAULT_PRO_USER = "分析这张图片,返回符合schema的JSON。"
# 用户自定义 prompt 末尾追加的硬约束
_JSON_TAIL_FAST = "\n\n你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释或markdown。"
_JSON_TAIL_PRO = "\n\n你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释或markdown。"
# DB 配置存在时,追加在用户 system_prompt 末尾的JSON schema约束
_FAST_JSON_APPEND = (
"\n\n【输出格式要求】无论上文如何要求,最终你必须只返回一个合法的JSON对象,"
"严格包含以下字段(字段值不确定时填null或空数组):\n"
"{\n"
' "has_person": true/false,\n'
' "gender": "男"/"女"/null,\n'
' "age_range": "儿童"/"青少年"/"青年"/"中年"/"老年"/null,\n'
' "upper_wear": "上装款式字符串",\n'
' "upper_color": "上装主色",\n'
' "lower_wear": "下装款式(穿连衣裙时填null)",\n'
' "lower_color": "下装主色",\n'
' "dress_color": "连衣裙主色(穿连衣裙时填)",\n'
' "accessories": ["配饰数组"],\n'
' "hairstyle": "发型",\n'
' "expression": "表情",\n'
' "pose": "姿势",\n'
' "scene": "场景",\n'
' "style": "风格",\n'
' "has_product": true/false,\n'
' "category": "产品类目:服饰/鞋包/美妆/数码/食品/家居/配饰/母婴/非产品图",\n'
' "product_name": "产品名称,非产品图填null",\n'
' "brand": "品牌或文字标识,无则null",\n'
' "material": "材质",\n'
' "pattern": "图案",\n'
' "colors": ["主色数组"],\n'
' "mood": "整体氛围"\n'
"}\n"
"不要输出任何其他文字、解释、XML标签或markdown。"
)
# 默认模板特征头(用于识别是否是内置XML模板)
_DEFAULT_XML_MARKER = "你是电商商品视觉分析师"
_PRO_JSON_APPEND = (
"\n\n【输出格式要求】无论上文如何要求,最终你必须只返回一个合法的JSON对象,"
"严格包含以下字段(字段值不确定时填null或空数组):\n"
"{\n"
' "has_person": true/false,\n'
' "gender": "男"/"女"/null,\n'
' "age_range": "儿童"/"青少年"/"青年"/"中年"/"老年"/null,\n'
' "outfit": "整体穿着描述(含颜色款式)",\n'
' "hair": "发型发色",\n'
' "pose": "姿势",\n'
' "expression": "表情",\n'
' "scene": "场景",\n'
' "mood": "氛围",\n'
' "has_product": true/false,\n'
' "category": "类目:服饰/鞋包/美妆/数码/食品/家居/配饰/母婴/非产品图",\n'
' "product_name": "产品名,非产品图填null",\n'
' "brand": "品牌,无则null",\n'
' "key_features": ["核心特征3-6个短语"]\n'
"}\n"
"不要输出任何其他文字、解释、XML标签或markdown。"
)
_cache_lock = threading.Lock()
_cache: dict[str, tuple[float, Any]] = {}
_CACHE_TTL = 30.0
def _load_template() -> Any | None:
"""从DB读image_analysis模板,失败返回None。"""
def _load_db_template() -> Any | None:
"""直接查DB viral_video_prompt_templates 中 is_active=true 的 image_analysis 记录;
DB不可达/无记录/异常返回None。
复用 prompt_loader._load_from_db,它只查DB不做DEFAULT_TEMPLATES fallback,
返回None表示DB无记录或异常。"""
try:
from packages.application.viral_video.prompt_loader import get_template
return get_template("image_analysis")
from packages.application.viral_video.prompt_loader import _load_from_db
return _load_from_db("image_analysis")
except Exception as e:
logger.warning("[vision.v2] 读取后台prompt配置失败: %s", e)
logger.warning("[vision.v2] 查询DB prompt配置失败: %s", e)
return None
def _is_custom(template: Any) -> bool:
"""判断读到的模板是不是用户自定义的(不是内置默认XML长prompt)。"""
if not template:
return False
sp = getattr(template, "system_prompt", "") or ""
# 默认模板开头是"你是电商商品视觉分析师...",XML格式,不适合qwen+JSON
if _DEFAULT_XML_MARKER in sp[:30]:
return False
# 其他有内容的system_prompt视为用户自定义
return bool(sp.strip())
def _render_user(tpl: Any | None, default_user: str) -> str:
if not tpl:
return default_user
tpl_str = getattr(tpl, "user_prompt_template", "") or ""
if not tpl_str.strip():
return default_user
rendered = (tpl_str
.replace("{image_count}", "1")
.replace("{industry}", "通用")
.replace("{image_urls}", "")
.strip())
return rendered or default_user
def resolve_fast_prompt() -> tuple[str, str]:
"""返回 (system_prompt, user_prompt) 给 qwen3.8-flash fast 路径。"""
return _resolve("fast")
def resolve_pro_prompt() -> tuple[str, str]:
"""返回 (system_prompt, user_prompt) 给 qwen3.7-plus fallback 路径。"""
return _resolve("pro")
@@ -116,32 +173,27 @@ def _resolve(kind: str) -> tuple[str, str]:
if hit and now - hit[0] < _CACHE_TTL:
return hit[1]
default_sys = DEFAULT_FAST_SYSTEM if kind == "fast" else DEFAULT_PRO_SYSTEM
default_sys = _FAST_JSON_SCHEMA if kind == "fast" else _PRO_JSON_SCHEMA
default_user = DEFAULT_FAST_USER if kind == "fast" else DEFAULT_PRO_USER
tail = _JSON_TAIL_FAST if kind == "fast" else _JSON_TAIL_PRO
append = _FAST_JSON_APPEND if kind == "fast" else _PRO_JSON_APPEND
sys_prompt = default_sys
usr_prompt = default_user
try:
tpl = _load_template()
if _is_custom(tpl):
custom_sys = (getattr(tpl, "system_prompt", "") or "").strip()
custom_user_tpl = getattr(tpl, "user_prompt_template", "") or ""
if custom_sys:
sys_prompt = custom_sys + tail
if custom_user_tpl:
# V2 是单图调用,简单替换几个常用占位符;缺键原样保留
usr_prompt = (custom_user_tpl
.replace("{image_count}", "1")
.replace("{industry}", "通用")
.replace("{image_urls}", "")
.strip())
if not usr_prompt:
usr_prompt = default_user
logger.info("[vision.v2] 使用后台自定义prompt (kind=%s version=%s)",
kind, getattr(tpl, "version", "?"))
tpl = _load_db_template()
if tpl is not None:
db_sys = (getattr(tpl, "system_prompt", "") or "").strip()
if db_sys:
sys_prompt = db_sys + append
usr_prompt = _render_user(tpl, default_user)
logger.info("[vision.v2] 使用DB image_analysis prompt (kind=%s version=%s sys_len=%d)",
kind, getattr(tpl, "version", "?"), len(db_sys))
else:
logger.debug("[vision.v2] DB image_analysis system_prompt为空,使用默认JSON (kind=%s)", kind)
else:
logger.debug("[vision.v2] DB无image_analysis记录/不可达,使用默认JSON prompt (kind=%s)", kind)
except Exception as e:
logger.warning("[vision.v2] 解析后台prompt失败,使用默认: %s", e)
logger.warning("[vision.v2] 解析DB prompt异常,使用默认: %s", e)
with _cache_lock:
_cache[cache_key] = (now, (sys_prompt, usr_prompt))