feat(agent): 完备性判断改LLM裁决(2026-09-08用户定夺:判断归LLM,契约归数据,硬编码降级为兜底)
- judge_completeness:①模型有配套技能(skills/models/{model}/SKILL.md)→LLM按
技能输入契约vs用户已给内容判断缺什么;②无技能→LLM按能力语义(输入→输出
模态)判断;③LLM失败→_check_completeness_code能力表兜底(闸门不因裁决故障失效)
- load_model_skill读配套技能;_resolve_capability补返回description供裁决
- 裁决只在invoke_model(贵动作)发生,不是每条消息;宁放行不误拦,可选参数不拦
This commit is contained in:
parent
ba65d586b1
commit
83d11772d5
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user