feat(selection): 任务自适应自动选型auto_select_model+models_catalog(本机构+owner机构候选,utility语义匹配,owner硬校验双保险);治理链govern_resolve加模型归属校验——他机构模型剔除顺延替代,显式指定越权模型报可行动错误(2026-09-07用户需求)
This commit is contained in:
parent
c9806aed6b
commit
08cf9272cd
@ -483,6 +483,25 @@ async def govern_resolve(org_id: str, user_id: str = '', model_name: str = '',
|
|||||||
return False, '__LEGACY__' # 指定模型不在新表 → 旧表兜底
|
return False, '__LEGACY__' # 指定模型不在新表 → 旧表兜底
|
||||||
return False, ('生效策略未配置模型:请在模型治理→组织容错策略配置主/备模型,'
|
return False, ('生效策略未配置模型:请在模型治理→组织容错策略配置主/备模型,'
|
||||||
'或在调用时指定模型名')
|
'或在调用时指定模型名')
|
||||||
|
# ⑤.5 模型归属硬校验(2026-09-07 用户需求):只有平台 owner('0'/系统级共享)
|
||||||
|
# 或本机构的模型可调用;他机构模型从链中剔除,链内替代模型(策略备链)自然顺延。
|
||||||
|
# 显式指定的模型被剔除且链内无替代 → 报可行动错误(禁止静默回退/越权放行)。
|
||||||
|
from .selection import _owner_allowed
|
||||||
|
allowed = [m for m in models if _owner_allowed(m.get('org_id'), org_id)]
|
||||||
|
if len(allowed) != len(models):
|
||||||
|
for m in models:
|
||||||
|
if not _owner_allowed(m.get('org_id'), org_id):
|
||||||
|
logger.info("pipeline_llm: 模型 %s 归属机构 %s,调用方 %s 无权使用,"
|
||||||
|
"已从模型链剔除(顺延替代模型)",
|
||||||
|
m.get('name'), m.get('org_id'), org_id or '(未指定)')
|
||||||
|
if not allowed:
|
||||||
|
if model_name:
|
||||||
|
return False, ('模型「%s」归属其他机构(非平台 owner 共享、也非本机构模型),'
|
||||||
|
'不允许调用。请选择本机构或平台共享模型'
|
||||||
|
'(模型治理→模型注册可查看归属)' % model_name)
|
||||||
|
return False, ('生效策略模型链全部归属其他机构,调用方 %s 无可调用模型——'
|
||||||
|
'请检查机构策略配置' % (org_id or '(未指定)'))
|
||||||
|
models = allowed
|
||||||
# ⑥ 逐模型尝试(辅助 → 主 → 备容错)
|
# ⑥ 逐模型尝试(辅助 → 主 → 备容错)
|
||||||
last_err = ''
|
last_err = ''
|
||||||
chosen = None
|
chosen = None
|
||||||
@ -496,6 +515,8 @@ async def govern_resolve(org_id: str, user_id: str = '', model_name: str = '',
|
|||||||
# 本机构链全不可用 → 容错落 owner 链
|
# 本机构链全不可用 → 容错落 owner 链
|
||||||
if chosen is None and policy_org != '0':
|
if chosen is None and policy_org != '0':
|
||||||
models0, pref0 = await _model_chain(sor, '0', '', purpose)
|
models0, pref0 = await _model_chain(sor, '0', '', purpose)
|
||||||
|
# owner 容错链同样过归属校验(策略里可能配了他机构注册的模型)
|
||||||
|
models0 = [m for m in models0 if _owner_allowed(m.get('org_id'), org_id)]
|
||||||
for m in models0:
|
for m in models0:
|
||||||
ok, cand = await _pick_candidate(sor, m, pref0)
|
ok, cand = await _pick_candidate(sor, m, pref0)
|
||||||
if ok and isinstance(cand, tuple):
|
if ok and isinstance(cand, tuple):
|
||||||
|
|||||||
@ -162,6 +162,156 @@ async def resolve_model_name(model_ref, org_id='', capabilities=()):
|
|||||||
return getattr(recs[0], 'name', '') or ''
|
return getattr(recs[0], 'name', '') or ''
|
||||||
|
|
||||||
|
|
||||||
|
def _owner_allowed(model_org, org_id):
|
||||||
|
"""模型归属校验(2026-09-07 用户需求):平台 owner('0'/空 = 系统级共享)
|
||||||
|
或本机构的模型才可调用;其他机构的模型一律拒绝(由调用方顺延替代候选)。"""
|
||||||
|
m = (model_org or '').strip()
|
||||||
|
return m in ('', '0') or m == (org_id or '').strip()
|
||||||
|
|
||||||
|
|
||||||
|
async def models_catalog(org_id, capabilities=(), limit=50):
|
||||||
|
"""本机构可用模型明细(本机构 + 平台 owner('0')/系统级共享)。
|
||||||
|
|
||||||
|
供 agent 的 list_models 工具与 auto_select_model 候选池共用——与推理链
|
||||||
|
(govern_resolve)同一机构可见性语义,杜绝两处 SQL 漂移。
|
||||||
|
返回 [dict]: name/vendor_model_id/capability/description/vendor/org_id。
|
||||||
|
"""
|
||||||
|
db, dbname = _get_sor()
|
||||||
|
caps = _norm_caps(capabilities)
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
sql = ("SELECT m.id, m.name, m.vendor_model_id, m.capability, m.description, "
|
||||||
|
"m.org_id, v.name AS vendor_name "
|
||||||
|
"FROM llm_model m LEFT JOIN llm_vendor v ON v.id=m.vendor_id "
|
||||||
|
"WHERE m.status='active'")
|
||||||
|
params = {}
|
||||||
|
if org_id and org_id != '0':
|
||||||
|
sql += " AND (m.org_id=${org}$ OR m.org_id='' OR m.org_id='0')"
|
||||||
|
params['org'] = org_id
|
||||||
|
if caps:
|
||||||
|
# sqlor 的 IN 列表必须展开占位符(传 list 会崩):${c0}$,${c1}$,...
|
||||||
|
ph = []
|
||||||
|
for i, c in enumerate(caps):
|
||||||
|
k = 'c%d' % i
|
||||||
|
ph.append('${%s}$' % k)
|
||||||
|
params[k] = c
|
||||||
|
sql += (" AND COALESCE(NULLIF(m.capability,''),'t2t') IN (%s)"
|
||||||
|
% ','.join(ph))
|
||||||
|
sql += " ORDER BY m.capability, m.name LIMIT %d" % int(limit)
|
||||||
|
recs = await sor.sqlExe(sql, params)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
out = []
|
||||||
|
for r in (recs or []):
|
||||||
|
out.append({
|
||||||
|
'name': getattr(r, 'name', '') or '',
|
||||||
|
'vendor_model_id': getattr(r, 'vendor_model_id', '') or '',
|
||||||
|
'capability': (getattr(r, 'capability', '') or 't2t').strip().lower(),
|
||||||
|
'description': (getattr(r, 'description', '') or '').strip(),
|
||||||
|
'vendor': getattr(r, 'vendor_name', '') or '',
|
||||||
|
'org_id': getattr(r, 'org_id', '') or '',
|
||||||
|
})
|
||||||
|
# owner 双保险(SQL 已过滤,防未来改动破坏不变量)
|
||||||
|
return [c for c in out if _owner_allowed(c['org_id'], org_id)]
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_select_model(org_id, user_input, user_id='', capabilities='chat'):
|
||||||
|
"""根据用户输入自动匹配最合适的模型(任务自适应路由)。
|
||||||
|
|
||||||
|
候选池 = models_catalog(本机构 + 平台 owner('0')/系统级共享,与推理链
|
||||||
|
可见性一致),按能力白名单过滤(缺省 'chat' = CHAT_CAPS 对话能力;
|
||||||
|
传 '' = 全能力,供 invoke_model 生成类任务自动选型)。
|
||||||
|
匹配 = utility 廉价模型语义选择(purpose='utility' 现成治理链,temperature=0),
|
||||||
|
依据 = 模型名 + 能力类型 + description(模型注册时填写的擅长说明)。
|
||||||
|
|
||||||
|
Owner 硬校验:LLM 返回的模型名逐个对照候选池(池外名字 = 幻觉,跳过),
|
||||||
|
命中候选即天然满足 owner ∈ ('', '0', 本机构)——池已过滤 + 此处双保险。
|
||||||
|
全部无效 → 兜底机构策略缺省模型(inference._pick_default_model_name)。
|
||||||
|
|
||||||
|
返回 (model_name, reason);无候选/匹配失败返回 ('', 原因)——调用方沿用
|
||||||
|
原有回退链(策略主备),不因此报错。
|
||||||
|
"""
|
||||||
|
candidates = await models_catalog(org_id, capabilities=capabilities)
|
||||||
|
if not candidates:
|
||||||
|
return '', '无可用模型候选(本机构+平台owner机构均无该能力的启用模型)'
|
||||||
|
if len(candidates) == 1:
|
||||||
|
# 唯一候选:跳过 LLM 匹配(省一次 utility 调用)
|
||||||
|
return candidates[0]['name'], '唯一候选'
|
||||||
|
|
||||||
|
# ── utility 模型语义匹配 ──
|
||||||
|
catalog = '\n'.join(
|
||||||
|
'- %s [%s]%s%s' % (
|
||||||
|
c['name'], c['capability'],
|
||||||
|
(' ' + c['vendor']) if c['vendor'] else '',
|
||||||
|
(':' + c['description'][:150]) if c['description'] else '')
|
||||||
|
for c in candidates)
|
||||||
|
task = (user_input or '').strip()[:600]
|
||||||
|
prompt = (
|
||||||
|
'你是平台模型调度器。以下是本机构当前可用的模型目录'
|
||||||
|
'(格式:模型名 [能力类型] 供应商:描述):\n%s\n\n'
|
||||||
|
'能力类型含义:t2t=文本对话 i2t=图像理解 m2t=多媒体理解 t2i=文生图 '
|
||||||
|
'i2v=图生视频 t2v=文生视频 r2v=参考生视频 tts=语音合成 asr=语音识别 '
|
||||||
|
'embedding=向量化 rerank=重排序\n\n'
|
||||||
|
'用户任务:\n%s\n\n'
|
||||||
|
'请根据任务的内容领域、语言、复杂度,从目录中选出最适合完成该任务的模型'
|
||||||
|
'(只能选目录中的模型名,按适合度从高到低排序,最多 3 个)。'
|
||||||
|
'只返回 JSON 对象:{"models": ["模型名1", "模型名2"], "reason": "一句话理由"}。'
|
||||||
|
'没有适合的模型时 models 返回空数组。' % (catalog, task))
|
||||||
|
|
||||||
|
from .inference import chat_inference
|
||||||
|
try:
|
||||||
|
data = await chat_inference(
|
||||||
|
org_id or '0', user_id or '',
|
||||||
|
{'messages': [{'role': 'user', 'content': prompt}],
|
||||||
|
'temperature': 0, '_purpose': 'utility'})
|
||||||
|
content = ((data.get('choices') or [{}])[0].get('message') or {}).get('content') or ''
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("auto_select_model 匹配调用失败(走策略链兜底): %s", e)
|
||||||
|
return '', '匹配调用失败: %s' % str(e)[:120]
|
||||||
|
|
||||||
|
# 解析 + 逐候选 owner/存在性校验
|
||||||
|
import json as _json
|
||||||
|
import re as _re
|
||||||
|
picked = []
|
||||||
|
reason = ''
|
||||||
|
m = _re.search(r'\{[\s\S]*\}', content or '')
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
obj = _json.loads(m.group(0))
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
picked = [str(x).strip() for x in (obj.get('models') or []) if str(x or '').strip()]
|
||||||
|
reason = str(obj.get('reason') or '')[:120]
|
||||||
|
except Exception:
|
||||||
|
picked = []
|
||||||
|
if not picked:
|
||||||
|
# 兜底:整体 JSON 解析失败时按候选名逐个在文本中找(顺序 = 目录序)
|
||||||
|
for c in candidates:
|
||||||
|
if c['name'] and c['name'] in (content or ''):
|
||||||
|
picked.append(c['name'])
|
||||||
|
break
|
||||||
|
by_name = {}
|
||||||
|
for c in candidates:
|
||||||
|
by_name[c['name']] = c
|
||||||
|
if c['vendor_model_id']:
|
||||||
|
by_name.setdefault(c['vendor_model_id'], c)
|
||||||
|
for name in picked:
|
||||||
|
c = by_name.get(name)
|
||||||
|
if c and _owner_allowed(c['org_id'], org_id):
|
||||||
|
return c['name'], (reason or '语义匹配')
|
||||||
|
if name:
|
||||||
|
logger.warning("auto_select_model: 跳过无效/越权候选 %s", name)
|
||||||
|
|
||||||
|
# LLM 结果全无效 → 机构策略缺省模型兜底(治理语义一致)
|
||||||
|
try:
|
||||||
|
from .inference import _pick_default_model_name
|
||||||
|
_caps = _norm_caps(capabilities)
|
||||||
|
cap0 = _caps[0] if _caps else 't2t'
|
||||||
|
fallback = await _pick_default_model_name(org_id or '0', cap0)
|
||||||
|
if fallback:
|
||||||
|
return fallback, '匹配无效,回退机构策略缺省模型'
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("auto_select_model 策略兜底失败: %s", e)
|
||||||
|
return '', '未能匹配到合适模型(沿用原回退链)'
|
||||||
|
|
||||||
|
|
||||||
async def org_available_models(org_id, capability=''):
|
async def org_available_models(org_id, capability=''):
|
||||||
"""机构可用模型注册名列表(本机构 + 系统级共享)。空 = 机构未配置模型。"""
|
"""机构可用模型注册名列表(本机构 + 系统级共享)。空 = 机构未配置模型。"""
|
||||||
db, dbname = _get_sor()
|
db, dbname = _get_sor()
|
||||||
@ -185,4 +335,6 @@ def load_selection():
|
|||||||
env.llm_model_options = model_options
|
env.llm_model_options = model_options
|
||||||
env.llm_resolve_model_name = resolve_model_name
|
env.llm_resolve_model_name = resolve_model_name
|
||||||
env.llm_org_available_models = org_available_models
|
env.llm_org_available_models = org_available_models
|
||||||
|
env.llm_models_catalog = models_catalog
|
||||||
|
env.llm_auto_select_model = auto_select_model
|
||||||
logger.info("[pipeline_llm] selection loaded")
|
logger.info("[pipeline_llm] selection loaded")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user