feat(llm): 会话agent可选模型收窄为对话能力t2t/i2t/m2t+能力字典加m2t(2026-09-06用户定夺)

- selection.CHAT_CAPS=('t2t','i2t','m2t') 作为会话形态能力白名单唯一事实源
- model_options/resolve_model_name 加 capabilities 参数('chat'哨兵),
  IN 列表展开占位符(sqlor传list会崩),capability 空串按 t2t 归一(COALESCE+NULLIF)
- chat_inference 同步门禁从「只认 t2t」放宽到 CHAT_CAPS——
  实测根因:测试库 6 个机构策略主模型全是 qwen3.8-max(i2t),
  16:28 已产生 FAILED「capability mismatch: i2t != t2t」,会话agent选它必挂
- m2t 入字典四处同步:种子 init/data.json + 端点注释 models.dspy
  + design-spec §6 能力表(+ 提取提示词在 pipeline-platform)
This commit is contained in:
yumoqing 2026-09-06 19:32:22 +08:00
parent 671d320d59
commit 8e32cb85f3
6 changed files with 72 additions and 9 deletions

View File

@ -79,6 +79,23 @@
均可Jinja `is string` 动态判断),变长输入用 `{% for %}` 按文档示例组装。
- **异步模型执行器**sync_mode=async 时走提交→按 query_profile_ids 轮询→
取结果;等待预算默认 600 秒,`_timeout` 可覆盖(上限 900
- **会话 agent 模型选择收窄为对话能力2026-09-06 用户定夺)**
`selection.CHAT_CAPS = ('t2t','i2t','m2t')` 是会话形态能力白名单的**唯一事实源**——
所有会话 agent 模型下拉agent_model_options / cockpit_model_options /
get_model_options 角色配置)传 `capabilities='chat'` 过滤;持久化入口
set_agent_model 个人默认 / cockpit_agent 角色模型 / pipeline_service
gateway._resolve_and_persist_model 项目模型)同样只接受对话能力,防绕过下拉。
`chat_inference` 同步门禁从「只认 t2t」放宽到 CHAT_CAPS实测根因测试库
机构策略主模型 qwen3.8-max 能力是 i2t旧门禁直接 FAILED
「capability mismatch: i2t != t2t」会话 agent 选它必挂)。
存量模型 `capability` 空串按 t2t 语义归一COALESCE+NULLIF
embedding/rerank/图视频生成等非对话模型各有专属入口list_models 按能力分栏)。
- **m2t多媒体生文入能力字典四处同步m0013**:种子 `init/data.json` +
端点注释 `api/v1/models.dspy` + `docs/design-spec.md §6` + 提取提示词
`pipeline_platform.platform_ability._EXTRACT_PROMPT`,判定表补
「多媒体输入→文本 ⇒ m2t」「仅图像→文本 ⇒ i2t」「仅文本→文本 ⇒ t2t」
且 r2v 判定加「输出为视频」限定防误吸文本输出模型);
`_MEDIA_INPUT_CAPS` 加 m2t模板生成按输入媒体处理
## API 文档端点2026-09-05

View File

@ -232,6 +232,7 @@ async def llm_call(prompt, model=None, org_id=None, user_id=None, task_ref='', *
| t2t | 文本对话/生成 | sync |
| t2i | 文生图 | async_task |
| i2t | 图生文(多模态理解) | sync |
| m2t | 多媒体生文(视频/音频等多媒体输入→文本) | sync |
| t2v | 文生视频 | async_task |
| embedding | 文本向量化 | sync |
| mm-embedding | 多模态向量化 | sync |

View File

@ -15,6 +15,7 @@
{"k": "t2t", "v": "文本对话/生成"},
{"k": "t2i", "v": "文生图"},
{"k": "i2t", "v": "图生文"},
{"k": "m2t", "v": "多媒体生文"},
{"k": "t2v", "v": "文生视频"},
{"k": "i2v", "v": "图生视频"},
{"k": "r2v", "v": "参考生视频"},

View File

@ -721,13 +721,16 @@ async def chat_inference(org_id, user_id, payload, model_name='', task_ref=''):
model_row = ctx.get('model_row') or {}
if (model_row.get('sync_mode') or 'sync') == 'async':
return await _async_inference(ctx, payload, req_timeout)
# 同步模型守能力边界:聊天入口只认 t2tembedding/rerank 等另有入口)
# 同步模型守能力边界聊天入口只认文本输出的对话能力t2t/i2t/m2t——
# embedding/rerank/图视频生成等另有专属入口list_models 按能力分栏选择)。
from .selection import CHAT_CAPS
cap = (model_row.get('capability') or 't2t').strip().lower()
if cap != 't2t':
if cap not in CHAT_CAPS:
from .gateway import govern_settle
await govern_settle(ctx, False, 0, 0, 'capability mismatch: %s != t2t' % cap)
raise GovernError('模型「%s」能力为 %s,聊天入口仅支持 t2t——请按能力分类选择模型' % (
model_row.get('name', ''), cap))
await govern_settle(ctx, False, 0, 0,
'capability mismatch: %s not in chat caps' % cap)
raise GovernError('模型「%s」能力为 %s,聊天入口仅支持 %s——请按能力分类选择模型' % (
model_row.get('name', ''), cap, '/'.join(CHAT_CAPS)))
try:
data = await _call_upstream_chat(ctx, payload)
except Exception as e:

View File

@ -19,6 +19,23 @@ from ahserver.serverenv import ServerEnv
logger = logging.getLogger("pipeline_llm.selection")
# 会话/chat 形态可用的能力类型2026-09-06 用户定夺):文本输出的对话能力。
# 会话 agent 模型下拉只列这些embedding/rerank/图视频生成等形态各有专属入口。
CHAT_CAPS = ('t2t', 'i2t', 'm2t')
def _norm_caps(capabilities):
"""capabilities 参数归一:'chat' 哨兵 → CHAT_CAPS逗号串 → tuple
tuple/list 原样返回 tuple = 不过滤dspy 薄壳只传 'chat' import"""
if isinstance(capabilities, str):
s = capabilities.strip().lower()
if not s:
return ()
if s == 'chat':
return CHAT_CAPS
return tuple(c.strip().lower() for c in s.split(',') if c.strip())
return tuple(str(c).strip().lower() for c in (capabilities or ()) if str(c or '').strip())
def _get_sor():
env = ServerEnv()
@ -33,16 +50,21 @@ def _get_sor():
async def model_options(org_id, uid='', session_id='', pipeline_id='',
value_field='id'):
value_field='id', capabilities=()):
"""模型选择下拉的唯一数据源(对齐 llmage 分类capability=能力类型)。
机构语义与推理链一致本机构 + 系统级共享org_id /'0'可见
selected 标记项目已设模型sd_projects.default_model name>
个人全局选择pipeline_agent_settings.default_llm_id id
value_field: 'id'=下拉值用模型 idAgentIO 场景'name'=用注册名角色模型配置场景
capabilities: 能力类型白名单tuple/list如会话 agent CHAT_CAPS=('t2t','i2t','m2t')
= 不过滤历史行为注意存量模型 capability 为空的行按 't2t' 语义对待
过滤时用 COALESCE+NULLIF 把空串归一成 't2t' 再比对DDL 有默认值
但历史迁移行可能是空串
"""
db, dbname = _get_sor()
rows = []
caps = _norm_caps(capabilities)
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT m.id, m.name, m.vendor_model_id, m.capability, v.name AS vendor_name "
"FROM llm_model m LEFT JOIN llm_vendor v ON v.id=m.vendor_id "
@ -51,6 +73,15 @@ async def model_options(org_id, uid='', session_id='', pipeline_id='',
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.name"
recs = await sor.sqlExe(sql, params)
@ -97,18 +128,22 @@ async def model_options(org_id, uid='', session_id='', pipeline_id='',
return rows
async def resolve_model_name(model_ref, org_id=''):
async def resolve_model_name(model_ref, org_id='', capabilities=()):
"""模型引用解析的唯一入口id / vendor_model_id / name → 模型注册名。
机构隔离非系统级机构只能解析本机构 + 系统级共享模型
capabilities: 能力白名单'chat' 哨兵 / tuple非空时能力不符也解析失败
会话 agent 个人默认模型/角色模型入口传 'chat'防把 embedding 等非对话
模型持久化成会话模型
解析不到返回 ''调用方决定回退/报错禁止自行另查表
"""
if not model_ref:
return ''
caps = _norm_caps(capabilities)
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT name, org_id FROM llm_model WHERE status='active' "
"SELECT name, org_id, capability FROM llm_model WHERE status='active' "
"AND (id=${m}$ OR vendor_model_id=${m}$ OR name=${m}$) LIMIT 1",
{"m": model_ref})
await sor.sqlExe("COMMIT", {})
@ -118,6 +153,12 @@ async def resolve_model_name(model_ref, org_id=''):
if org_id and org_id != '0' and m_org not in ('', '0', org_id):
logger.warning("resolve_model_name: 模型 %s 不属于机构 %s", model_ref, org_id)
return ''
if caps:
cap = (getattr(recs[0], 'capability', '') or 't2t').strip().lower()
if cap not in caps:
logger.warning("resolve_model_name: 模型 %s 能力 %s 不在白名单 %s",
model_ref, cap, caps)
return ''
return getattr(recs[0], 'name', '') or ''

View File

@ -1,7 +1,7 @@
# models.dspy — 按能力分类列出可用模型(对齐 llmage v1/models 的分类方式)
#
# URL: /pipeline_llm/api/v1/models?catelogid=t2t
# catelogid 可选t2t / t2i / t2v / i2v / r2v / i2i / embedding / rerank / tts / asr / i2t ...
# catelogid 可选t2t / t2i / t2v / i2v / r2v / i2i / embedding / rerank / tts / asr / i2t / m2t ...
# (能力类型标准 = 模型治理 appcodes llm_capability新增走评审禁止野生标签
# 不传 = 全部能力
#