跳到正文
Elaine Blog
返回

模型怎样看图和听声音:多模态与实时对话

更新于:
LLM 基础

用户先问:“商品有质量问题,运费谁承担?”随后发来一张杯子照片,又用语音补充:“不是杯口,是底部裂了。”客服助手面对的已经不只是一段文字:它要知道图片里有什么,理解语音更正,结合政策回答,还要在用户插话时停下来。

文本模型的输入是 Token 序列,那么图片和声音怎么进入计算?界面上都叫“发送消息”,底层却多了感知、编码和时间同步。理解这些步骤,才能判断错误来自图片没看清、语音听错、模型推断过头,还是播放了已经过时的回答。

本文先用一条典型图文模型路径讲清表示,再扩展到语音和实时交互。不同多模态模型的结构差异很大,下面的切块、投影和连接方式是一种常见设计,不是所有闭源模型的内部说明。

图片在模型里不是一个网址

用户上传照片后,应用可能拿到文件路径或下载地址。它们只是找到图片的方式;模型要分析的是解码后的像素及其派生表示。把本机 /photos/cup.jpg 当正文发给远端文本接口,不会使远端自动读到文件。

一张 RGB 图片可以看成 [高度, 宽度, 3] 的数组,最后三项是颜色通道。预处理通常会调整尺寸、裁剪、转换颜色并按模型要求归一化。每一步都会改变实际可见信息:如果杯底裂纹在裁剪范围外,后面的语言推理再强也无法从这份输入看到它。

例如原图是 4000×3000,裂纹只占十几像素。缩到很小后,裂纹可能与背景混在一起。直接提高分辨率也有代价:需要处理的视觉位置、裁剪块或计算量可能增加。正确问题是“当前任务的关键细节是否还在”,而不只是“文件成功上传了吗”。

因此,应用应保留原始资产与处理版本的关系:哪张原图、裁了哪里、用什么尺寸输入。这样模型说“没有看到裂纹”时,才能区分原图确实没有、输入裁掉了,还是模型没识别出来。

从像素块到视觉向量,再连接语言模型

用一个教学视觉编码器说明:把 224×224 图片分成 16×16 的小块,每边 14 块,共 196 块。每块 RGB 像素展开成 16×16×3 = 768 个数,再经过投影,得到固定维度的向量。不同块还有位置表示,用来保留它们在图片中的相对布局。

这里的视觉“Token”通常指一个可供后续网络处理的视觉位置表示,不必是文本词表里的某个整数。它不是先把左上角翻译成一个汉字,再交给语言模型。模型可以在连续向量空间中学习颜色、边缘、形状和更复杂的关系。

视觉编码器还会让图片位置之间交换信息。例如细线究竟是杯子上的裂纹,还是背景纹理,需要结合周围区域。一个区域的表示会在编码过程中带上其他区域的信息,机制上与文本序列从孤立向量走向上下文表示有相似之处。

接下来还存在维度和表示习惯的差异。假设视觉输出每个位置 768 维,语言模型隐藏维度为 2048,就需要连接模块把前者转换为语言模型可以使用的表示。最简单可以是线性投影,也可能是多层网络、查询式压缩模块或交叉注意力等结构。

在一类拼接设计中,连接后的视觉向量占据输入序列中的指定位置,文本向量接在旁边或后面,语言模型生成回答时可以读取它们。示意链如下:

杯子照片 → 尺寸与颜色处理 → 视觉编码器 → 连接模块 → 视觉表示
用户问题 → 聊天模板与 Tokenizer → Token Embedding → 文本表示

                          按模型结构组合 → 语言模型 → 回答

另一些模型通过交叉注意力读取视觉表示,结构并非简单拼接。理解重点是图片必须进入一个模型训练过的表示与连接路径,不能把任意图像向量直接塞进文本模型并期待它理解。LLaVA 的视觉指令微调论文展示了一种视觉编码器连接语言模型的路线。

模型怎样学会把图像与文字对应起来

只有维度相同还不够。随机投影也能把 768 个数变成 2048 个数,但后面的模型不知道它们表示杯子还是天空。训练需要让图像信息与语言任务产生可学习的关系。

例如图像配有描述“白色杯子的底部有一道裂纹”,模型根据图片和前缀预测描述,可以获得把视觉特征用于语言输出的信号。再通过图文指令数据学习“描述图片”“定位损坏”“根据图片回答问题”等行为。不同训练阶段可能冻结或更新不同模块,不能统一说所有多模态模型都从头共同训练。

训练数据还会影响它依赖哪种信息。如果很多样本中“破损”问题总配有破损照片,模型可能过度依赖问题措辞,而没有充分读取图片。实际应用中可以比较同一问题配不同照片时的变化,但不能只凭一个看似合理的回答就证明它看对了。

回到售后例子,视觉输出最好区分观察和推断。“杯底可见一道深色线状区域”描述可见现象;“一定是出厂缺陷”还需要更多证据。照片能支持部分事实,通常不能单独证明损坏时间、原因和运费责任。

如果虚构政策要求“三十天内且经核实属于质量问题”,图片只参与核实环节的一部分。订单签收时间来自订单系统,责任条件来自政策文本。多模态能力没有取消这些信息来源的区别。

OCR 与视觉理解保留的信息不同

假设任务是读取发票金额,OCR 可以先输出文字与位置,再由程序校验金额格式、税额关系或订单记录。这样容易定位是识别错字,还是后续业务校验失败。

但若任务是判断杯底是否出现裂纹,OCR 可能只读到品牌文字,几乎丢掉问题相关信息。把图片统一转成文字会形成信息瓶颈:没有被转写出来的形状、颜色和空间关系,后续文本模型就无法利用。

原生图文输入则允许模型直接使用视觉表示,但它的输出也可能遗漏细节或把反光当裂纹。选择两条路线时,要看任务需要保留什么证据。混合方式也常见:OCR 保留精确编号,原图用于视觉判断,两者都附带来源。

例如产品标签是 P204,OCR 读成 P2O4,语言模型可能根据常见模式“纠正”为另一个编号。可靠做法是保留原始识别文本,并去产品表核验候选,而不是把语言模型润色后的编号直接当事实。

做一次真实图文输入,观察送进了哪些张量

下面使用小型图文模型,读取你指定的本地图片,提出英文观察问题。英文提示用于匹配这个演示模型的使用场景,不代表它适合所有中文业务。它会下载模型并执行本地推理,回答取决于实际图片与模型输出。

下载:image_question.py下载。依赖与运行命令在配套指南下载。示例采用模型卡提供的 Processor 与 Vision2Seq 接入方式,并使用 CPU 浮点 32 位以减少设备差异;它展示接口,不承诺识别精度。

import argparse
import torch
from PIL import Image, ImageOps
from transformers import AutoProcessor, AutoModelForVision2Seq

parser = argparse.ArgumentParser(description="读取本地图片并进行图文问答")
parser.add_argument("image_path", help="待观察的本地图片路径,例如 cup.jpg")
args = parser.parse_args()

# EXIF 中可能记录拍摄方向;先矫正方向,再转为模型使用的 RGB。
# 使用文件实际像素,不把路径字符串当作图片内容发送给模型。
with Image.open(args.image_path) as source:
    picture = ImageOps.exif_transpose(source).convert("RGB")

model_id = "HuggingFaceTB/SmolVLM-256M-Instruct"
processor = AutoProcessor.from_pretrained(model_id)
model = AutoModelForVision2Seq.from_pretrained(
    model_id, torch_dtype=torch.float32, _attn_implementation="eager"
).eval()
messages = [{"role": "user", "content": [
    {"type": "image"},
    {"type": "text", "text": "Describe visible damage, if any. Do not infer its cause."},
]}]
# 图像占位与 images 中的图片一一对应,预处理由配套 Processor 完成。
prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
inputs = processor(text=prompt, images=[picture], return_tensors="pt")
for name, value in inputs.items():
    # 观察文本 ID、像素等张量形状,不假设视觉表示等于文本 Token 数。
    if hasattr(value, "shape"):
        print(name, tuple(value.shape))

with torch.inference_mode():
    output = model.generate(**inputs, max_new_tokens=96, do_sample=False)
# 对此类因果生成模型,返回序列带有输入前缀;只解码新生成部分。
new_ids = output[:, inputs["input_ids"].shape[1]:]
print(processor.batch_decode(new_ids, skip_special_tokens=True)[0])

观察输入名称和形状,可以看见正文并非唯一输入。图像预处理可能还做切块或尺寸调整,所以不要把前面的“196 个块”套成这个模型的实际输出数量。真实处理方式以 SmolVLM 模型卡和配置为准。

还可以对比原图与裁剪图,但要把它作为不同输入记录。若裁掉杯底之后回答仍声称底部破裂,可能是措辞诱导或模型错误;若保留杯底也没识别出来,可能是分辨率、模型能力或图像歧义。一次输出不足以直接确定原因。

声音先变成什么,模型才能处理

数字音频是一串随时间采样的振幅。假设单声道采样率为 16000 Hz,一秒包含 16000 个采样点。采样率表示时间分辨率,不等于模型每秒生成多少 Token。

一种常见语音处理路径把波形分成短时间窗,再转换成描述频率能量的特征,例如梅尔频谱。语音编码器从这些连续特征形成表示,ASR 解码器输出文字。也有模型采用不同的音频表示或离散音频 Token;不能把所有系统都画成同一种频谱结构。

最直观的语音助手是 语音 → ASR 文字 → 文本 LLM → 回答文字 → TTS 音频。它容易接入现有文本应用:ASR 把“不是杯口,是底部裂了”变成文本,LLM 根据对话修正关注部位,TTS 再把回答读出来。

每段都可能引入误差。ASR 若把“底部”听成“顶部”,后面的语言模型可能流畅解释错误部位;TTS 即使声音自然,也不能修正内容。级联延迟还包括各阶段等待,若必须等整段转写完才开始生成,首段声音会更晚到达。

原生语音模型则可以直接利用音频表示,并可能直接输出音频或与文本联合输出。它有机会保留语气、节奏等转写文字没有表达的信息,但“原生”不意味着没有编码、分块或时间同步,也不保证所有任务都优于级联方案。

例如“好啊”可能表达同意,也可能带反讽。纯转写只保留两个字,声学信息更丰富;但应用不能仅凭模型判断语气就执行退款。需要用户明确确认的业务动作,仍应保留清晰、可核验的确认流程。

实时对话为什么难在“什么时候说”和“什么时候停”

录完一段音频再处理,输入边界由录音结束决定。实时对话中,用户可能停顿一下又继续:“运费……我是说退回去的运费。”系统若把短暂停顿当作结束,会提前回答;等待太久,又显得迟钝。

VAD 检测是否存在语音活动,端点判断进一步决定一轮话是否结束。背景声音、拖长音和短暂停顿都会影响它们。系统可以结合时间阈值和语义线索,但需要承认“检测到静音”与“用户说完了”不是同一个事实。

输出侧同样有三条进度:模型已经生成多少、客户端缓冲了多少、扬声器实际播放了多少。假设生成了五秒音频,播放到两秒时用户插话:“等等,我已经签收四十天了。”系统必须停止继续播放旧音频,尽可能取消旧生成,并让新一轮看到更正信息。

若只停止生成,缓冲中的后三秒可能继续播放;若只停播放器,服务端仍可能浪费计算,旧事件还会不断到达。最容易遗漏的是历史:用户只听到前两秒,不能把五秒完整话都记作“已经向用户说过”。

下面用预先标注了文字的音频片段模拟这个过程,不生成声音,也不调用模型。下载:voice_interrupt.py下载

# 教学片段的文本与结束时刻是人工对齐的;真实系统要使用实际播放时间或对齐事件。
segments = [
    {"end_ms": 1000, "text": "我需要先核对签收时间。"},
    {"end_ms": 2500, "text": "如果符合质量退货条件,"},
    {"end_ms": 4000, "text": "再判断运费承担方。"},
]
played_ms = 1800
active_response = "response-7"

# 插话使当前响应失效;先更新有效 ID,后来的旧响应事件也不能重新进入播放队列。
cancelled_response = active_response
active_response = None
playback_queue = []  # 模拟清空尚未播放的缓冲;真实应用要调用播放器停止接口。

# 只记录完整播放过的片段。第二段已经播放一部分,但不能宣称整段已被听到。
heard = "".join(part["text"] for part in segments if part["end_ms"] <= played_ms)
history_event = {
    "response_id": cancelled_response,
    "status": "interrupted",
    "played_ms": played_ms,
    "confirmed_complete_text": heard,
    "partial_segment": True,
}
print("历史记录:", history_event)

# 模拟一条取消后迟到的旧事件:响应 ID 不再有效,因此丢弃。
late_event = {"response_id": "response-7", "audio": "旧音频片段"}
if late_event["response_id"] == active_response:
    playback_queue.append(late_event["audio"])
print("剩余播放队列:", playback_queue)

这里故意没有把部分播放的第二段推断成完整文本。真实系统如果没有精确对齐,宁可保存音频截止位置与“不完整”状态,也不要捏造用户听到的最后一个词。下一轮可以明确说“刚才被打断,我们重新核对签收时间”,而不是依赖未送达内容继续推理。

响应 ID 用于区分哪次生成仍然有效,工具调用 ID 则用于区分具体动作。如果旧响应触发过后台请求,停止播放不会自动撤销已经执行的业务操作,需要单独查明状态。这是实时交互与业务状态之间的边界。

视频增加的是时间关系,不只是更多图片

把视频均匀抽成几帧,可以利用图文模型描述静态场景,但可能漏掉短暂事件。用户展示“杯子先完整,拆开包装后发现裂纹”的过程时,帧顺序与发生时刻就有意义。

如果只取第一帧和最后一帧,模型未必知道中间发生了什么;若把帧顺序打乱,也可能误解动作先后。输入应尽量保留时间戳、采样规则和必要音轨,而不能只把若干张图片当作无序相册。

提高采样频率增加可见细节,也增加视觉处理与上下文成本。选取方法要围绕问题:读取箱体编号可能只需一张清晰帧,判断跌落过程需要足够密集的连续片段。模型没看到的时间段,不能被它的流畅描述补成事实。

多模态扩大了可用信息,也扩大了处理链。对每个结论追问“它来自像素、转写、订单记录还是政策”,就能把观察与推断分开。最后一篇将这些能力放回应用,讨论能力、显存、速度、成本与可靠性怎样取舍


分享这篇文章:

上一篇
一次对话怎样运行:上下文、KV Cache 与流式生成
下一篇
怎样把模型用进应用:能力、显存、速度、成本与可靠性