fix(platform): 能力语义判断改LLM裁决(2026-09-08用户定夺:词匹配不当裁决者)

qwen-image-plus实测根因:词匹配守卫把描述"图像生成"的输出词当输入证据,
反复把t2i纠正成i2i,人工override也被改回,死循环。
- 新增_llm_capability_judge:utility模型按能力字典语义裁决(输入→输出模态),
  失败不裁决不纠正(宁可保守不让词匹配越权)
- extract/apply两处:词匹配只当廉价触发器报可疑,裁决交LLM
- 人工override权威:__human_cap_overrides__登记的模型apply守卫一律跳过
- LLM裁决改能力后重新_spec_save锚定(否则apply取回旧值裁决白做)
This commit is contained in:
yumoqing 2026-09-08 11:58:55 +08:00
parent 48735b98ba
commit 7a041cbb18

View File

@ -162,6 +162,12 @@ def _spec_overlay(base, overrides):
if vmid and newcap and newcap != m.get('capability'): if vmid and newcap and newcap != m.get('capability'):
fixed.append('%s: %s→%s' % (vmid, m.get('capability'), newcap)) fixed.append('%s: %s→%s' % (vmid, m.get('capability'), newcap))
m['capability'] = newcap m['capability'] = newcap
# 人工纠正登记(2026-09-08):apply 阶段的能力守卫必须跳过
# 这些模型——人工指令是权威,守卫再纠正=把用户的纠正改回去
# (qwen-image-plus 实测死循环的机制根源)。
hc = dict(out.get('__human_cap_overrides__') or {})
hc[vmid] = newcap
out['__human_cap_overrides__'] = hc
out['models'] = models out['models'] = models
if fixed: if fixed:
out['doc_notes'] = ((out.get('doc_notes') or '') + out['doc_notes'] = ((out.get('doc_notes') or '') +
@ -449,17 +455,35 @@ async def _h_extract_llm_api_spec(sor, params, ctx):
"需改动只传覆盖项(overrides 白名单:vendor_name/base_url/" "需改动只传覆盖项(overrides 白名单:vendor_name/base_url/"
"protocol/doc_url/doc_notes/model_capability)。" "protocol/doc_url/doc_notes/model_capability)。"
"禁止把本规格复制进 spec 参数——复制即编造。") "禁止把本规格复制进 spec 参数——复制即编造。")
# 能力一致性告警(2026-09-06 wan3.0 教训):提取完当场校验描述模态声明 vs # 能力一致性告警(2026-09-06 wan3.0 教训;2026-09-08 升级:词匹配只当触发器,
# capability,矛盾信息直接回给助手(用户纠正时传 overrides.model_capability 重 apply) # 裁决交 LLM——qwen-image-plus 实测词匹配把输出词当输入证据、t2i 被反复改成 i2i)
cap_alerts = [] cap_alerts = []
_cap_changed = False
for _m in (spec.get("models") or []): for _m in (spec.get("models") or []):
_fix, _warn = _capability_guard(_m.get("capability", ""), _m.get("description", "")) _fix, _warn = _capability_guard(_m.get("capability", ""), _m.get("description", ""))
if _warn: if _warn or _fix:
cap_alerts.append("%s: %s" % (_m.get("vendor_model_id", "?"), _warn)) # 词匹配报可疑 → LLM 语义裁决(裁决失败则只告警不纠正)
_jfix, _jnote = await _llm_capability_judge(
_m.get("capability", ""), _m.get("description", ""),
_m.get("vendor_model_id", ""))
if _jfix:
_m["capability"] = _jfix
_cap_changed = True
cap_alerts.append("%s: %s(已按 LLM 裁决更新提取规格)"
% (_m.get("vendor_model_id", "?"), _jnote))
elif _jnote:
cap_alerts.append("%s: %s" % (_m.get("vendor_model_id", "?"), _jnote))
elif _warn:
cap_alerts.append("%s: %s(词匹配可疑,LLM 裁决维持原判)"
% (_m.get("vendor_model_id", "?"), _warn))
# 裁决改了能力 → 必须重新锚定:_spec_save 在裁决前已存旧值,不重存则
# apply(use_last_extract=true) 取回的还是旧能力,LLM 裁决白做(实测根因)。
if _cap_changed:
_spec_save(ctx, spec)
if cap_alerts: if cap_alerts:
out["__capability_alert__"] = ("能力分类与文档描述矛盾(按示例误判的典型信号)," out["__capability_alert__"] = ("能力分类存疑项(LLM 语义裁决结果):"
"apply 时会自动纠正,也可用 overrides.model_capability " + ";".join(cap_alerts)
"人工指定:" + ";".join(cap_alerts)) + "。如需人工指定用 overrides.model_capability。")
return json.dumps(out, ensure_ascii=False) return json.dumps(out, ensure_ascii=False)
@ -481,6 +505,58 @@ _DESC_MEDIA_WORDS = {
_DESC_REF_WORDS = ('参考生', '全能参考', '参考图', '参考视频', '参考音频', '四模态', '多模态参考') _DESC_REF_WORDS = ('参考生', '全能参考', '参考图', '参考视频', '参考音频', '四模态', '多模态参考')
async def _llm_capability_judge(capability, description, vendor_model_id=''):
"""LLM 语义裁决能力分类(2026-09-08 用户定夺:语义判断必须 LLM 参与,
词匹配只能当廉价触发器,不能当裁决者)。
背景:缺陷2(qwen-image-plus)实测——关键词守卫把描述里「图像生成」的
「图像」(输出词)当输入证据,反复把 t2i 纠正成 i2i,人工 override 也被
改回去,死循环。词匹配分不清输入/输出语义,LLM 能。
分工:_capability_guard(词匹配)只报「可疑」;本函数用 utility 廉价模型
按能力字典语义裁决。LLM 失败时返回 (None, 原因)——不裁决就不纠正,
保持提取 LLM 的原判定(宁可保守也不让词匹配越权)。
"""
if not (description or '').strip():
return None, ''
prompt = (
'你是模型能力分类裁决器。模型 %s 的文档描述:\n%s\n\n'
'当前判定的能力分类:%s\n\n'
'能力分类字典(按输入→输出模态判定,描述里的输出媒体词不算输入证据):\n'
't2t=文本→文本;i2t=图像→文本;m2t=多媒体→文本;t2i=文本→图像;'
'i2i=图像→图像;t2v=文本→视频;i2v=图像→视频;r2v=参考媒体(图/视频/音频组合)→视频;'
'tts=文本→语音;asr=语音→文本;embedding=文本→向量;rerank=重排序\n\n'
'请判断当前分类是否与描述的输入/输出模态一致。只返回 JSON:'
'{"consistent": true/false, "capability": "正确分类(不一致时给出)", '
'"reason": "一句话依据"}' % (vendor_model_id or '(未命名)', description[:600],
capability or '(空)'))
try:
from pipeline_service.llm_bridge import llm_call_msgs
raw = await llm_call_msgs(
[{"role": "user", "content": prompt}],
temperature=0, org_id="0", purpose="utility", timeout=60)
except Exception as e:
return None, 'LLM 裁决调用失败(保持原判定不纠正): %s' % repr(e)[:120]
txt = (raw or '').strip()
m = re.search(r'\{[\s\S]*\}', txt)
if not m:
return None, 'LLM 裁决输出非 JSON(保持原判定不纠正): %s' % txt[:120]
try:
obj = json.loads(m.group(0))
except Exception:
return None, 'LLM 裁决 JSON 解析失败(保持原判定不纠正)'
if not isinstance(obj, dict):
return None, ''
if obj.get('consistent') is True:
return None, ''
newcap = str(obj.get('capability') or '').strip().lower()
reason = str(obj.get('reason') or '')[:150]
if newcap and newcap != (capability or ''):
return newcap, 'LLM 语义裁决 %s→%s:%s' % (capability, newcap, reason)
return None, ('LLM 裁决认为能力分类存疑但未给出替代(%s),请人工核对'
% reason) if reason else None
def _capability_guard(capability, description): def _capability_guard(capability, description):
"""描述模态声明 vs capability 一致性守卫(2026-09-06 wan3.0-video-prime 实测: """描述模态声明 vs capability 一致性守卫(2026-09-06 wan3.0-video-prime 实测:
描述自带"支持四模态全能参考"、capability 却是 t2v——提取 LLM 照调用示例判能力, 描述自带"支持四模态全能参考"、capability 却是 t2v——提取 LLM 照调用示例判能力,
@ -590,18 +666,35 @@ async def _h_apply_llm_config(sor, params, ctx):
if not models: if not models:
return "spec.models 为空——文档里没有可配置的模型?" return "spec.models 为空——文档里没有可配置的模型?"
# 能力一致性守卫前置(2026-09-06 wan3.0 二次教训):必须在 caps 计算/模板 # 能力一致性守卫前置(2026-09-06 wan3.0 二次教训;2026-09-08 用户定夺升级):
# 生成之前跑——上一版放在模型循环里,纠正 t2v→r2v 时模板已按 t2v 生成, # 必须在 caps 计算/模板生成之前跑——纠正能力后模板按新能力生成,否则
# profile_ids 无 r2v 键,模型更新的 IF 空值保护把旧 t2v 模板原样保留 # profile_ids 无该能力键、模型更新的 IF 空值保护把旧模板原样保留。
# (实测:template_notes 有纠正记录、库里 cap 仍是 t2v)。 # ⚠️ 语义判断必须 LLM 参与:词匹配只当廉价触发器(报可疑),裁决交
# _llm_capability_judge;**人工 override 的模型一律跳过守卫**——人工指令是
# 权威,守卫把用户纠正改回去正是 qwen-image-plus 死循环的机制根源。
cap_notes = [] cap_notes = []
human_caps = spec.get('__human_cap_overrides__') or {}
for m in models: for m in models:
vmid = (m.get('vendor_model_id') or m.get('name') or '').strip()
if vmid and vmid in human_caps:
cap_notes.append("%s: 能力按人工指定 %s(守卫已跳过,不再自动纠正)"
% (vmid, human_caps[vmid]))
continue
_fix, _warn = _capability_guard( _fix, _warn = _capability_guard(
m.get("capability") or "", m.get("description") or "") m.get("capability") or "", m.get("description") or "")
if _fix: if not (_fix or _warn):
m["capability"] = _fix continue
if _warn: _jfix, _jnote = await _llm_capability_judge(
cap_notes.append("%s: %s" % (m.get("vendor_model_id") or "?", _warn)) m.get("capability") or "", m.get("description") or "", vmid)
if _jfix:
m["capability"] = _jfix
cap_notes.append("%s: %s" % (vmid or "?", _jnote))
elif _jnote:
cap_notes.append("%s: %s" % (vmid or "?", _jnote))
else:
cap_notes.append("%s: 词匹配报可疑但 LLM 裁决维持 %s(%s)"
% (vmid or "?", m.get("capability") or "?",
(_warn or _fix or '')[:80]))
# 能力分类校验(2026-09-05 用户定夺的规则):模型能力必须是字典已登记的 # 能力分类校验(2026-09-05 用户定夺的规则):模型能力必须是字典已登记的
# 分类;不存在则拒绝落库——先加能力分类(appcodes_kv llm_capability, # 分类;不存在则拒绝落库——先加能力分类(appcodes_kv llm_capability,