SenseNova Loop-9B-Preview 图文入参 / 字段和顺序先对齐

文章导读
处理 SenseNova Loop-9B-Preview 的图文请求时,图像字段名和它相对文本的位置,通常由仓库示例脚本决定,不能凭记忆填写。可执行的做法是:先在示例脚本里确认图像以哪个字段、哪种编码进入请求体,再用一条最小图文请求拿到完整返回,然后只改动顺序各跑一次,最后把返回解析成可断言的结构。这套顺序的价值在于:即使后续换了字段名,之前的记录仍能用来判断“是字段错了还是顺序有影响”。
📋 目录
  1. 壹 在仓库示例脚本里定位图像入参字段
  2. 贰 构造一条最小图文请求并打印完整返回
  3. 叁 调整图像与文本的先后顺序各跑一次
  4. 肆 加第二张图观察输入条数与显存变化
  5. 伍 把返回结果解析成可断言的结构
A A

处理 SenseNova Loop-9B-Preview 的图文请求时,图像字段名和它相对文本的位置,通常由仓库示例脚本决定,不能凭记忆填写。可执行的做法是:先在示例脚本里确认图像以哪个字段、哪种编码进入请求体,再用一条最小图文请求拿到完整返回,然后只改动顺序各跑一次,最后把返回解析成可断言的结构。这套顺序的价值在于:即使后续换了字段名,之前的记录仍能用来判断“是字段错了还是顺序有影响”。

先读仓库示例定位图像字段与编码方式,再构造单图最小请求并打印完整响应体作为基线;接着仅改图文先后顺序和图片数量各跑一次,用返回长度、关键字段取值、显存与耗时采样记录差异。字段名与顺序以代码和日志实际输出为准,未确认处一律用占位符标注,不把推测写成结论。

在仓库示例脚本里定位图像入参字段

先找示例脚本,不要先写请求。常见位置是仓库里的 examples/、demo/、scripts/、inference/ 目录,文件名多含 chat、vision、multimodal、client、serve 等字样。用关键字扫一遍比逐文件读快:

grep -rn `--include`=*.py -E 'image_url|image_path|images|base64|pixel_values|vision' ./ | head -n 30

同时看两类文件:调用侧脚本,决定请求体长什么样;服务侧入口,决定后端实际接收什么。两边字段名不一致时,以服务侧接收的为准,调用侧可能只是封装。

需要确认四件事,逐条记下来:请求体里图像的字段名;编码形态是本地路径、原始字节还是 base64 字符串;图像与文本是否在同一层数组里;是否出现列表形式的字段从而暗示支持多图。

  • 若示例里是 "image": "xxx.jpg" 这类路径写法,说明跑通依赖服务端能访问同一文件系统,容器化部署时要注意挂载路径。
  • 若示例里出现 base64.b64encode(...).decode(),编码方式基本就是 base64 字符串,注意是否带 data:image/png;base64, 前缀。
  • 若字段值是数组,多图的可能性较大,但仍需用第二张图实测确认,不要凭字段类型下判断。

没读清楚的地方不要猜,用占位符保留:<IMAGE_FIELD>、<IMAGE_TYPE>、<IMAGE_VALUE>,等示例确认后再替换。

构造一条最小图文请求并打印完整返回

目标是拿到一次可用返回,作为后面所有对比的基线。请求体保持最小:一句短文本加一张小图,不要加 system 提示、不要加多轮历史,避免干扰变量。

import json, requests

URL = 'http://127.0.0.1:8000/v1/chat/completions'  # 以仓库说明为准
payload = {
    'model': 'SenseNova-Loop-9B-Preview',          # 以示例脚本里的模型名为准
    'messages': [
        {
            'role': 'user',
            'content': [
                {'type': 'text', 'text': '简单描述这张图'},
                {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': '<IMAGE_VALUE>'}  # 占位
            ]
        }
    ]
}
resp = requests.post(URL, json=payload, timeout=60)
print(resp.status_code)
print(resp.text[:4000])   # 先看原文,不要直接 resp.json()['xxx']

打印完整响应体而不是直接取某个键,原因很实际:字段名一旦和预期不同,直接取键会抛 KeyError,把真实的返回内容挡住。先看原文,再决定解析路径。响应头也建议打一次,print(dict(resp.headers)),能看到返回格式和长度线索。

SenseNova Loop-9B-Preview 图文入参 / 字段和顺序先对齐

整理记录时把字段分成两类:从示例脚本或返回原文里直接看到的,标为已确认;靠命名习惯推断的,标为推测。推测项不要写进结论,只作为下一步验证的线索。

调整图像与文本的先后顺序各跑一次

在基线跑通后,只改 content 数组里两个元素的先后,其他保持完全一致。文本在前:

'content': [
    {'type': 'text', 'text': '简单描述这张图'},
    {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': '<IMAGE_VALUE>'}
]

图像在前:

'content': [
    {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': '<IMAGE_VALUE>'},
    {'type': 'text', 'text': '简单描述这张图'}
]

两次都用同一张图、同一句文本、同一组采样参数。跑完把结果记进下表,空着的格子留待实测填写,不要先填数字:

输入顺序HTTP 状态返回长度关键字段取值备注
文本在前、图像在后待填待填(字符数)待填(如 finish_reason / 文本首句)基线
图像在前、文本在后待填待填(字符数)待填与基线比较

判断标准要克制:状态码一致、返回结构一致、文本内容差异落在正常生成波动范围内,就说明这个模型对顺序不敏感,后续可以固定用其中一种;若某一顺序直接报参数错误,说明顺序有约束,以能跑通的那种为准写进调用模板。单次输出有波动,必要时同一顺序跑两三次再下判断,不要用一次结果当定论。

加第二张图观察输入条数与显存变化

多图写法通常是往 content 数组里再追加一个同类型元素,或用列表字段一次性传入:

'content': [
    {'type': 'text', 'text': '分别描述两张图'},
    {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': '<IMAGE_VALUE_1>'},
    {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': '<IMAGE_VALUE_2>'}
]
# 或者字段本身接受列表时:
# {'type': '<IMAGE_TYPE>', '<IMAGE_FIELD>': ['<IMAGE_VALUE_1>', '<IMAGE_VALUE_2>']}

两种写法哪个成立,取决于示例脚本里的实际形态:值是标量还是数组,直接决定用哪一种。不确定时先试追加元素,报错再换列表写法。

SenseNova Loop-9B-Preview 图文入参 / 字段和顺序先对齐

资源采样建议开一个旁路窗口跑:

nvidia-smi `--query-gpu`=memory.used,utilization.gpu `--format`=csv -l 1

耗时用 time python run_case.py 或脚本内 time.perf_counter() 包住请求,两次请求之间留出间隔,避免上一次的显存未释放干扰读数。记录三组:单图峰值显存、双图峰值显存、各自耗时区间。

失败时把报错原文整段留下来,包括 HTTP 状态码和响应体全文,不要只写“失败了”。常见形态通常集中在几类:图片条数超过上限、单图分辨率或字节数超限、字段类型不匹配。这些结论必须由报错原文支撑,不能凭经验直接写进结论。

把返回结果解析成可断言的结构

长文本靠肉眼看差异不可靠,把返回压成几个稳定字段,后续换顺序、换图片数量时只比这几个值。

import json

def normalize(resp):
    try:
        data = resp.json()
    except Exception as e:
        # 兜底:解析失败时输出原文片段,便于判断是 HTML 错误页还是空体
        return {'ok': False, 'err': repr(e), 'raw': resp.text[:800]}

    out = {'ok': True, 'status': resp.status_code,
           'top_keys': sorted(data.keys()) if isinstance(data, dict) else type(data).__name__}
    # 下面的取值路径以实际返回为准,不确定的用 .get 层层兜底
    out['finish_reason'] = (data.get('choices') or [{}])[0].get('finish_reason')
    msg = ((data.get('choices') or [{}])[0].get('message') or {})
    out['text_len'] = len(msg.get('content') or '')
    out['usage'] = data.get('usage')
    return out

print(json.dumps(normalize(resp), ensure_ascii=False, indent=2))

需要断言的关键字段建议控制在这几个:顶层键集合是否与基线一致;结果数组长度是否符合预期;finish_reason 是否为正常结束值;正文长度是否大于零;usage 里的计数是否随图片数量变化。前四项用于判断请求是否被正确受理,最后一项用于观察输入规模变化,都属于能直接从返回里读到的东西。

解析失败时不要让脚本静默通过:ok 为 false 就直接把 raw 打出来并让断言失败,因为解析失败本身往往意味着服务端返回了非预期结构,例如网关错误页或参数校验信息,这类信息比正常返回更值得优先看。