From 83d11772d5c3f659519cd6273f692a72fbd7856c Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 8 Sep 2026 12:31:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E5=AE=8C=E5=A4=87=E6=80=A7?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E6=94=B9LLM=E8=A3=81=E5=86=B3(2026-09-08?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=AE=9A=E5=A4=BA:=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E5=BD=92LLM,=E5=A5=91=E7=BA=A6=E5=BD=92=E6=95=B0=E6=8D=AE,?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E9=99=8D=E7=BA=A7=E4=B8=BA=E5=85=9C?= =?UTF-8?q?=E5=BA=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - judge_completeness:①模型有配套技能(skills/models/{model}/SKILL.md)→LLM按 技能输入契约vs用户已给内容判断缺什么;②无技能→LLM按能力语义(输入→输出 模态)判断;③LLM失败→_check_completeness_code能力表兜底(闸门不因裁决故障失效) - load_model_skill读配套技能;_resolve_capability补返回description供裁决 - 裁决只在invoke_model(贵动作)发生,不是每条消息;宁放行不误拦,可选参数不拦 --- pipeline_service/platform_model_tools.py | 169 +++++++++++++++++++++-- 1 file changed, 154 insertions(+), 15 deletions(-) diff --git a/pipeline_service/platform_model_tools.py b/pipeline_service/platform_model_tools.py index 156d1a1..438b7b9 100644 --- a/pipeline_service/platform_model_tools.py +++ b/pipeline_service/platform_model_tools.py @@ -91,12 +91,12 @@ def _biz_media(biz, key): return bool(str(v or "").strip()) -def check_completeness(capability, task, biz): - """完备性门禁:该能力契约要求的输入是否齐备。 +def _check_completeness_code(capability, task, biz): + """能力契约代码表校验(**兜底层**,2026-09-08 用户定夺后降级)。 - 返回 '' = 完备可执行;否则返回需要向用户追问的话术(QUESTION 内容)。 - 契约未登记的能力(如 embedding/新能力)不拦——宁可放行也别误伤, - 但生成类主流能力全部登记在册。 + 判断主体是 LLM(judge_completeness);本函数只在 LLM 裁决失败 + (调用异常/输出不可解析)时兜底——花钱闸门不能因裁决故障失效。 + 返回 '' = 完备;否则返回追问话术。 """ cap = (capability or "").strip().lower() required = _CAP_REQUIRED.get(cap) @@ -120,6 +120,139 @@ def check_completeness(capability, task, biz): return "" +# ───────────────────────────────────────────────────────────────── +# 模型配套技能(2026-09-08 用户定夺): +# 「每个模型的加入需要有配套的 skill,模型需要什么样的输入、用户已经 +# 提供的内容缺什么,应该由 LLM 判断而不是硬编码;没有 skill 的,按 +# 能力的输入要求对照用户已给输入决策是否需要补充。」 +# +# 判断分层(判断归 LLM,契约归数据,代码表只兜底): +# ① 模型有配套技能(skills/models/{model}/SKILL.md,上线时自动生成) +# → LLM 按技能里的输入契约 vs 用户已给内容判断缺什么 +# ② 无技能 → LLM 按能力语义(能力字典 输入→输出模态)vs 用户输入判断 +# ③ LLM 裁决失败(异常/输出不可解析)→ _check_completeness_code 代码表 +# 兜底——花钱闸门不能因裁决故障失效 +# ───────────────────────────────────────────────────────────────── + +_CAP_MODALS = { + "t2t": "文本→文本", "i2t": "图像→文本", "m2t": "多媒体→文本", + "t2i": "文本→图像", "i2i": "图像→图像", "t2v": "文本→视频", + "i2v": "图像→视频", "r2v": "参考媒体(图/视频/音频)→视频", + "tts": "文本→语音", "asr": "语音→文本", + "embedding": "文本→向量", "rerank": "候选集→排序", +} + + +def load_model_skill(model_name): + """读模型配套技能正文(skills/models/{model}/SKILL.md)。 + + 查找顺序:全局模板(get_skills_base(),应用目录)→ 机构运行时目录 + (/d/pipeline/workspaces/{org}/skills 由各调用方 base_dir 决定,模型 + 技能是平台级资产放全局模板即可)。无技能返回 ''。 + 目录约定:skills/models/ 不在 SkillLoader 扫描范围(global/pipelines/ + orgs/users),不会污染 agent 技能目录层。 + """ + import os + try: + from pipeline_core.skill_pack import get_skills_base + base = get_skills_base() + except Exception: + base = os.path.join(os.getcwd(), "skills") + safe = "".join(ch for ch in (model_name or "") + if ch.isalnum() or ch in "-_.") + if not safe: + return "" + path = os.path.join(base, "models", safe, "SKILL.md") + try: + if os.path.isfile(path): + with open(path, encoding="utf-8") as f: + return f.read()[:4000] + except Exception as e: + logger.warning("load_model_skill(%s) 失败: %s", model_name, e) + return "" + + +async def judge_completeness(model_name, capability, task, biz, + description="", org_id="0", user_id=""): + """完备性判断主体(LLM 裁决,2026-09-08 用户定夺)。 + + ① 模型有配套技能 → 按技能输入契约判断;② 无技能 → 按能力语义判断; + ③ LLM 失败 → 代码表兜底。 + 返回 '' = 完备可执行;否则返回追问话术(QUESTION 内容)。 + """ + cap = (capability or "").strip().lower() + skill = load_model_skill(model_name) + + if skill: + contract = ("该模型有配套技能(权威输入契约),按其「输入要求」判断:\n" + + skill[:3000]) + else: + modal = _CAP_MODALS.get(cap, "") + contract = ("该模型无配套技能,按能力语义判断。能力 %s(%s):%s" + % (cap or "?", modal or "模态未知", + "必备输入=箭头左侧模态的素材/文本;右侧是产物。" + if modal else "无法确定契约,宽松放行。")) + + got = ["任务描述: " + (task or "(空)")] + for k in ("image_files", "audio_files", "video_files"): + if _biz_media(biz or {}, k): + v = biz.get(k) + n = len(v) if isinstance(v, (list, tuple)) else 1 + got.append("已提供 %s: %d 项" % (k, n)) + extra = {k: str(v)[:60] for k, v in (biz or {}).items() + if k not in ("image_files", "audio_files", "video_files")} + if extra: + got.append("其他业务参数: " + json.dumps(extra, ensure_ascii=False)[:300]) + + prompt = ( + "你是平台模型调用的完备性裁决器。即将调用模型「%s」(能力 %s%s)完成生成任务," + "调用会产生真实费用。请判断当前输入是否足以完成该任务。\n\n" + "%s\n\n模型描述:%s\n\n用户已提供的输入:\n%s\n\n" + "判断规则:\n" + "1. 只把「没有就无法完成本能力任务」的输入判为缺失(如文生图没有画面描述、" + "图生视频没有输入图)。\n" + "2. 有默认值/可选的偏好参数(尺寸/风格/时长/音色等)缺失**不算**不完备,不要拦。\n" + "3. 用户已提供的内容里隐含了该输入的(如任务描述里已含图片 URL),算已提供。\n" + "4. 判断不了就放行(complete=true),宁可放行也别误拦。\n\n" + "只返回 JSON:{\"complete\": true/false, \"ask\": \"缺失时向用户追问的话术" + "(说清缺什么、要什么格式、一句话),完备时空串\"}" + % (model_name, cap or "?", + ("," + _CAP_DESC.get(cap, "")) if cap in _CAP_DESC else "", + contract, (description or "(无)")[:200], "\n".join(got))) + + try: + from pipeline_service.llm_bridge import llm_call_msgs + raw = await llm_call_msgs( + [{"role": "user", "content": prompt}], + temperature=0, org_id=org_id or "0", user_id=user_id or "", + purpose="utility", timeout=60) + except Exception as e: + logger.warning("judge_completeness LLM 失败(走代码表兜底): %s", repr(e)[:150]) + return _check_completeness_code(cap, task, biz) + + import re as _re + txt = (raw or "").strip() + m = _re.search(r"\{[\s\S]*\}", txt) + if not m: + logger.warning("judge_completeness 输出非 JSON(走代码表兜底): %s", txt[:120]) + return _check_completeness_code(cap, task, biz) + try: + obj = json.loads(m.group(0)) + except Exception: + return _check_completeness_code(cap, task, biz) + if not isinstance(obj, dict): + return _check_completeness_code(cap, task, biz) + + if obj.get("complete") is True: + return "" + ask = str(obj.get("ask") or "").strip() + if not ask: + # LLM 判不完备但没给追问话术 → 代码表兜底生成话术(追问必须可行动) + return _check_completeness_code(cap, task, biz) + return ("调用「%s」模型前完备性检查未通过,已停在调用前、未产生任何费用。%s" + % (model_name or (_CAP_DESC.get(cap, cap)), ask)) + + async def tool_list_platform_models(params, org_id): """列出平台可用模型(本机构+平台owner机构,含能力类型与描述)。""" cap = str((params or {}).get("capability") or "").strip().lower() @@ -148,19 +281,20 @@ async def tool_list_platform_models(params, org_id): async def _resolve_capability(model_name, org_id): - """按模型注册名反查能力类型(门禁需要知道该查哪份输入契约)。 - - 走 models_catalog(与推理链同一机构可见性),查不到返回 ''。 + """按模型注册名反查 (能力类型, 描述)(门禁需知道查哪份输入契约; + 描述供 LLM 裁决参考)。走 models_catalog(与推理链同一机构可见性), + 查不到返回 ('', '')。 """ try: from pipeline_llm.selection import models_catalog models = await models_catalog(org_id or "0", capabilities=()) for m in models: if model_name in (m.get("name"), m.get("vendor_model_id")): - return (m.get("capability") or "").strip().lower() + return ((m.get("capability") or "").strip().lower(), + m.get("description") or "") except Exception as e: logger.warning("_resolve_capability(%s) 失败: %s", model_name, e) - return "" + return "", "" async def tool_invoke_model(params, org_id, user_id=""): @@ -214,12 +348,17 @@ async def tool_invoke_model(params, org_id, user_id=""): except Exception as e: return "ERROR: 自动选型失败: " + str(e)[:300] - # ── 完备性硬门禁(2026-09-08 用户定夺:一C+二A)── + # ── 完备性门禁(2026-09-08 用户定夺:判断归 LLM,代码表只兜底)── # 必须在 llm_infer 之前:缺输入直接 QUESTION 追问用户,上游一分钱不花。 - # capability 未显式给出时按选中模型反查(门禁需知道查哪份输入契约)。 - if not cap: - cap = await _resolve_capability(model, org_id) - gap = check_completeness(cap, task, biz) + # 判断分层:① 模型配套技能(skills/models/{model}/SKILL.md)→ LLM 按契约判; + # ② 无技能 → LLM 按能力语义判;③ LLM 失败 → 能力契约代码表兜底。 + # capability/description 未显式给出时按选中模型反查。 + desc = "" + cap2, desc = await _resolve_capability(model, org_id) + cap = cap or cap2 + gap = await judge_completeness(model, cap, task, biz, + description=desc, org_id=org_id or "0", + user_id=user_id or "") if gap: logger.info("invoke_model 完备性门禁拦截(%s): %s", cap or "?", gap[:80]) return "QUESTION: " + gap