feat(report): 形象深度诊断全链路——POST /reports/{id}/full 派生 tier=full 报告,复用 basic 输入与 32 维结果;full 流水线(无视觉阶段)LLM 扩写四大块(十二季定位/规则色板/4场景穿搭/30天计划),三重校验+语义重试+规则兜底降级;删除终身一次硬校验(40920 下线,重复生成由配额兜底);validation 枚举一致性校验放宽(十二季词表);迁移 0003 SSOT 补 full_report 规则行

This commit is contained in:
xsl
2026-09-15 00:02:44 +08:00
parent a1d0171fc5
commit 5c4f1fc75a
16 changed files with 1491 additions and 69 deletions
+44 -29
View File
@@ -193,14 +193,15 @@ def check_absolute_claims(text: str) -> list[str]:
return [term for term in ABSOLUTE_CLAIM_TERMS if term in text]
def _find_dimension(
def _find_dimensions(
dimensions: Sequence[Mapping[str, Any]], key_tokens: Sequence[str]
) -> Mapping[str, Any] | None:
for dim in dimensions:
key = str(dim.get("key") or "")
if any(token in key for token in key_tokens):
return dim
return None
) -> list[Mapping[str, Any]]:
"""按 key 识别串收齐全部匹配维度(季型有 season_code 与 season_detailed 两个)。"""
return [
dim
for dim in dimensions
if any(token in str(dim.get("key") or "") for token in key_tokens)
]
def _check_enum_consistency(
@@ -211,34 +212,48 @@ def _check_enum_consistency(
维度 skipped/无值时,文案出现该类任何枚举词都算编造——LLM 没看到
这个维度却写出了结论,正是素材组装要防的幻觉从校验侧兜底。
每类只报第一条命中,避免同一问题刷屏冲淡修正指令。
两类放行:诊断值的「X型」写法(如「柔夏」→「柔夏型」,深度诊断的
十二季文案必经此路径);词表 token 是某个放行词的子串时不再单独追责
(「夏型」⊂「柔夏型」——十二季细分命名无法全量枚举,见 TD-18)。
"""
issues: list[str] = []
for category in _ENUM_CATEGORIES:
dim = _find_dimension(dimensions, category.key_tokens)
if dim is None:
matched = _find_dimensions(dimensions, category.key_tokens)
if not matched:
continue
value = dim.get("value")
if dim.get("status") == "ok" and value:
canonical = str(value)
allowed = {_normalize_for_match(canonical)}
for alias in category.aliases.get(canonical, ()): # 值本身可能就是别名键
allowed.add(_normalize_for_match(alias))
# 诊断值也可能是别名列里的写法(如存了「沙漏型」),反查一次
for canonical_key, alias_values in category.aliases.items():
if _normalize_for_match(canonical) == _normalize_for_match(canonical_key) or any(
_normalize_for_match(canonical) == _normalize_for_match(a) for a in alias_values
):
allowed.add(_normalize_for_match(canonical_key))
allowed.update(_normalize_for_match(a) for a in alias_values)
else:
allowed = set()
allowed: set[str] = set()
for dim in matched:
value = dim.get("value")
if dim.get("status") == "ok" and isinstance(value, str) and value:
canonical = value
candidates = {canonical, f"{canonical}型"}
candidates.update(category.aliases.get(canonical, ())) # 值本身可能就是别名键
# 诊断值也可能是别名列里的写法(如存了「沙漏型」),反查一次
for canonical_key, alias_values in category.aliases.items():
if _normalize_for_match(canonical) == _normalize_for_match(
canonical_key
) or any(
_normalize_for_match(canonical) == _normalize_for_match(a)
for a in alias_values
):
candidates.add(canonical_key)
candidates.update(alias_values)
allowed.update(_normalize_for_match(c) for c in candidates)
for token in category.vocabulary:
normalized_token = _normalize_for_match(token)
if normalized_token in copy_text_normalized and normalized_token not in allowed:
issues.append(
f"{category.label}用词「{token}」与诊断值「{value or '未诊断'}」不一致"
)
break
if normalized_token not in copy_text_normalized:
continue
if normalized_token in allowed:
continue
if any(normalized_token in entry for entry in allowed):
continue
observed = next(
(str(d.get("value")) for d in matched if d.get("status") == "ok"),
"未诊断",
)
issues.append(f"{category.label}用词「{token}」与诊断值「{observed}」不一致")
break
return issues