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, ('生效策略未配置模型:请在模型治理→组织容错策略配置主/备模型,'
|
||||
'或在调用时指定模型名')
|
||||
# ⑤.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 = ''
|
||||
chosen = None
|
||||
@ -496,6 +515,8 @@ async def govern_resolve(org_id: str, user_id: str = '', model_name: str = '',
|
||||
# 本机构链全不可用 → 容错落 owner 链
|
||||
if chosen is None and policy_org != '0':
|
||||
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:
|
||||
ok, cand = await _pick_candidate(sor, m, pref0)
|
||||
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 ''
|
||||
|
||||
|
||||
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=''):
|
||||
"""机构可用模型注册名列表(本机构 + 系统级共享)。空 = 机构未配置模型。"""
|
||||
db, dbname = _get_sor()
|
||||
@ -185,4 +335,6 @@ def load_selection():
|
||||
env.llm_model_options = model_options
|
||||
env.llm_resolve_model_name = resolve_model_name
|
||||
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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user