处理 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)),能看到返回格式和长度线索。
整理记录时把字段分成两类:从示例脚本或返回原文里直接看到的,标为已确认;靠命名习惯推断的,标为推测。推测项不要写进结论,只作为下一步验证的线索。
调整图像与文本的先后顺序各跑一次
在基线跑通后,只改 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>']}
两种写法哪个成立,取决于示例脚本里的实际形态:值是标量还是数组,直接决定用哪一种。不确定时先试追加元素,报错再换列表写法。
资源采样建议开一个旁路窗口跑:
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 打出来并让断言失败,因为解析失败本身往往意味着服务端返回了非预期结构,例如网关错误页或参数校验信息,这类信息比正常返回更值得优先看。