图片被当成纯文本,通常不是模型没能力看图,而是脚本把 prompt 拼成了一个字符串,图片路径只是字符串里的一段字符,没有作为独立的图片项进入请求。判断方向很直接:先看送进模型的对象是 str 还是 list,再看 list 里是否有一个与文本并列的图片元素。这两步能在改提示词之前排除大部分“看不到图”的情况。
接入多模态模型时,先确认输入是消息列表而不是拼接字符串,再按框架约定把图片作为独立内容项与文本并列放进同一轮 user 消息。改完后打印一次消息结构,用一张自己生成的、内容已知的小图做端到端问答,回答能复述图中具体信息才算真正读到图。字段名各框架不同,以所用版本的文档和实际打印结果为准,不要凭记忆硬写。
先确认脚本里的输入是消息列表还是纯字符串
最常见的写法是 prompt = "看图回答问题:" + image_path,然后整个字符串丢给 chat 接口。对纯文本模型这样没问题,对多模态模型就只是把路径当成了几个 token,模型自然按文字回答。下面两种写法并列摆出来,对比看差异的位置。
# 方式 A:纯字符串 —— 图片信息只是一段文字,模型看不到像素
prompt = '这张图里有什么? /data/pic/cat.png'
out = client.chat(prompt)
# 方式 B:消息列表 —— 图片作为独立内容项
messages = [
{
'role': 'user',
'content': [
{'type': 'text', 'text': '这张图里有什么?'},
{'type': 'image', 'image': '/data/pic/cat.png'}, # 图片字段放在这里
],
}
]
out = client.chat(messages)需要放图片字段的位置就是 content 列表里那个和 text 并列的元素;只把它写进 text 的字符串里,等于没传。也有框架支持顶层单独传一个 images 参数配合字符串 prompt,这同样可行,但前提是该版本确实支持这个参数,否则参数会被忽略而脚本不报错。先确认当前调用的函数签名里有没有图片入口,再决定用哪种写法。
按所用框架的多模态格式组装图片与文本
消息结构的通用骨架一般是角色加内容项列表,文本和图片各占一项。字段名差异很大:有的叫 image,有的叫 image_url,有的用 type: input_image,图片值可以是本地路径、URL、base64 或已经读好的张量。以所用框架文档为准,先用最小结构跑通再补其他参数。
# 通用骨架,字段名按实际框架替换
messages = [
{
'role': 'system', # 可选,部分框架用 system 字段或单独参数
'content': [{'type': 'text', 'text': '你是一个看图回答问题的助手。'}],
},
{
'role': 'user',
'content': [
{'type': 'text', 'text': '用户提问放在这里'},
{'type': 'image', 'image': '图片路径或 URL 或 base64'},
],
},
]替换项集中在三处:role 的取值、内容项的 type 名称、图片字段名。有的框架要求图片项排在文本项之前,有的要求图片走单独参数,这些只能靠文档加打印确认。路径、URL、base64 三者不要混着传,同一次调用里保持同一种形式,能减少解码失败导致的静默降级。
在送入模型前打印输入结构
组装完之后不要直接调用,先打印一次,确认图片确实被读进来了。
import os, pathlib
def dump_messages(messages):
for i, m in enumerate(messages):
print(f'[{i}] role={m.get("role")}')
content = m.get('content')
if isinstance(content, str):
print(' !! content 是字符串,图片不会被读取')
continue
for j, item in enumerate(content):
t = item.get('type')
val = item.get('image') or item.get('image_url') or item.get('text')
print(f' item{j} type={t} py_type={type(val).__name__}')
if isinstance(val, str) and os.path.sep in val:
p = pathlib.Path(val)
print(' 存在:', p.exists(), '大小:', p.stat().st_size if p.exists() else 0)
elif isinstance(val, (bytes, bytearray)):
print(' 字节长度:', len(val))
dump_messages(messages)需要核对的字段:content 是不是列表、每个 item 的 type 是否正确、图片项的值是不是非空的路径或字节、路径指向的文件是否真实存在且大小不为零。如果打印出来 content 是字符串,或者图片项的 py_type 是 str 而内容只是文件名,基本就能确认图片没进模型。
用一张已知内容的小图做端到端验证
验证图建议自己生成,不要用网图。做法:白底小图,画两个简单图形,比如一个红色圆形和一个蓝色三角形,再在图里写上一个自己编的短字符串,例如 ZQ-48。提问写成“图里写了什么字?一共有几个图形,分别是什么颜色?”
判定标准可以分三档:能准确说出 ZQ-48 和图形颜色,说明图片确实进入输入并被解码;只回答“这是一张图片”“看起来像图表”,说明图片没进去或解码失败;文字对了但图形颜色说错,先怀疑图片被过度缩放或裁剪,换更大的图再试一次。同时可以跑一次不带图片的同样提问做对照,两次回答风格一致,通常说明图片根本没起作用。
把可用的组装方式封装成函数
结构确认可用后,把它收进函数,避免每次调用都手写消息列表,也顺带把路径校验做掉。
def build_vlm_messages(image_paths, question, system=None):
"""image_paths: str 或 list[str];question: str;system: 可选系统提示。
返回可直接送入模型的 messages 列表。"""
if isinstance(image_paths, str):
image_paths = [image_paths]
if not image_paths:
raise ValueError('至少需要一张图片')
messages = []
if system:
messages.append({'role': 'system',
'content': [{'type': 'text', 'text': system}]})
content = [{'type': 'text', 'text': question}]
for p in image_paths:
if not os.path.exists(p):
raise FileNotFoundError(f'图片不存在: {p}') # 宁可报错,不要静默当文本
content.append({'type': 'image', 'image': p})
messages.append({'role': 'user', 'content': content})
return messages参数说明:image_paths 传单张时给字符串,多张时给列表,函数内部统一按列表处理并依次追加图片项,顺序和提问里说的“第一张、第二张”保持一致。多张图还要留意所用框架对图片数量和尺寸的限制,超出后可能被丢弃或被压缩。question 只放纯文本提问,不要在里面再拼路径;system 可省略。函数里对不存在的路径直接抛错,是为了避免路径字符串被当成普通文本送进去,那种情况下模型往往还能正常回话,问题会拖到很后面才被发现。