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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user