2512 字
13 分钟
33. 多模态 AI:从文字请求到图像理解

本章目标:亲手看懂并发送一次“文字 + 图片”的模型请求,理解多模态输入的 Python 形态、模型能力边界和项目里的调用位置。

本章只推进图像理解。音频、视频、OCR 专用模型、视觉 RAG 和多模态 Agent 放到后面,不让它们阻塞本章。

本章沿用上一章的学习方式:先看到真实代码,再追踪输入、模型和输出。上一章的文本链路是:

文本 -> tokenizer/processor -> 模型 -> 文本或向量

本章新增的是输入端:

文字 + 图片 -> 多模态消息 -> 视觉语言模型 -> 文字回答

本章在课程中的位置#

已经会的内容本章新增能力暂时不展开
ChatDeepSeekHumanMessage、模型配置、Hugging Face 模型对象把图片作为消息内容的一部分交给支持视觉的模型音频、视频、视觉向量库、模型训练、复杂多模态 Agent

本章产物:

官方依据与版本边界#

本项目当前使用 LangChain 消息对象和 ChatDeepSeek。消息的具体图片字段是否被接受,还取决于你配置的模型供应商和模型本身;MODEL_NAME 能生成文字,不代表它一定能看图。需要视觉能力时,优先单独设置:

VISION_MODEL_NAME=你的视觉语言模型名称

不要把“消息格式正确”和“模型具备视觉能力”混为一件事。

第一关:先建立心智模型#

一句话#

多模态模型不是把图片“变成一段普通字符串”,而是接收不同类型的输入块,并由对应的处理器和模型共同理解它们。

准确术语#

术语这里是什么意思
modality(模态)一种信息形式,例如文本、图片、音频或视频。
multimodal message(多模态消息)一条消息的 content 中同时放入不同类型的内容块。
vision-language model(视觉语言模型,VLM)能同时处理视觉输入和语言输入,并生成语言结果的模型。
content block(内容块)content 列表中的一个字典,例如文本块或图片块。
processor(处理器)多模态模型的预处理入口,通常负责图片处理、tokenizer 和输入张量的拼接。

最关键的对比#

文本模型常见形态:

HumanMessage(content="这是什么?")

多模态消息形态:

HumanMessage(
content=[
{"type": "text", "text": "这是什么?"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.jpg"},
},
]
)

区别不是“把图片地址拼到字符串里”,而是 contentstr 变成了“内容块列表”。每个块都有自己的 type,框架和模型供应商据此知道该如何处理它。

第二关:读懂最小真实代码#

打开 app/multimodal_vision_demo.py。先只看下面这一段:

message = HumanMessage(
content=[
{
"type": "text",
"text": "请描述图片中的主要对象。",
},
{
"type": "image_url",
"image_url": {"url": IMAGE_URL},
},
]
)
response = model.invoke([message])

逐行追踪:

  1. HumanMessage(...) 创建一条用户消息对象。
  2. content=[...] 表示这条消息由多个内容块组成。
  3. 第一个块是文本,告诉模型要做什么。
  4. 第二个块是图片地址,告诉模型要看什么。
  5. [message] 是消息列表;即使这里只有一条消息,聊天模型的输入仍然按列表传入。
  6. model.invoke(...) 把消息交给模型客户端;客户端负责把 LangChain 消息转换为供应商 API 能理解的请求。
  7. response.content 是模型返回的文字,不是图片本身,也不是图片的向量数组。

数据流可以写成:

IMAGE_URL: str
-> image_url 内容块
-> HumanMessage.content: list[dict]
-> model.invoke([message])
-> 视觉模型读取图片和文字
-> response.content: str

这里的 model 从哪里来#

model = build_vision_llm()

build_vision_llm() 是项目普通函数;它读取 .env,创建 ChatDeepSeek 实例。这个函数不负责把图片转成像素,也不负责判断事实,它只负责构造模型客户端。

return ChatDeepSeek(
model=model_name,
api_base=...,
api_key=SecretStr(api_key),
)

这里的 ChatDeepSeek 仍然是上一章见过的聊天模型类。新变化不是模型类的创建方式,而是传给 invoke() 的消息内容形态。

第三关:为什么普通文本模型可能失败#

要同时满足两个条件:

消息结构支持图片
+
模型本身支持图片输入
=
可能完成图像理解

只满足第一条仍然可能失败:

  • API 直接拒绝 image_url
  • 模型只返回“我无法查看图片”。
  • 供应商要求另一种图片字段或只接受公开 URL。
  • 图片地址无法访问、格式不支持或超过限制。

所以排错顺序是:

  1. 图片 URL 能否在浏览器或 curl 中访问。
  2. 当前模型名称是否明确标注支持视觉输入。
  3. 当前供应商的 OpenAI-compatible 接口是否实现图片字段。
  4. 再检查 LangChain 消息格式和模型参数。

这和第 32 章的 Embedding 一致性问题很像:不能只看“Python 代码长得像不像”,还要看模型能力和服务端契约是否匹配。

第四关:运行一次最小请求#

1. 配置视觉模型#

.env 中增加视觉模型配置,不要覆盖文本模型配置:

VISION_MODEL_NAME=你的视觉语言模型名称
VISION_IMAGE_URL=https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg

VISION_MODEL_NAME 的真实值要以你所用模型供应商的模型列表和模型卡为准。本章不要求背模型名称,也不把一个文本模型强行当视觉模型。

2. 运行#

Terminal window
poetry run python -m app.multimodal_vision_demo

3. 预期结果#

成功时得到一段描述图片的文本,例如:

图片中有一只蜜蜂,停在一朵花附近。

实际措辞会变化。验收重点是:

  • 请求确实包含文本块和图片块。
  • 模型返回了文本结果。
  • response.content 可以被打印或继续交给后续流程。

4. 安全提醒#

图片 URL 可能包含隐私、临时签名或访问令牌。不要把带有密钥的 URL 写进 Git,也不要把用户原图和完整模型请求直接写入日志。生产环境还要限制图片大小、格式、来源和保存时长。

第五关:项目中的使用位置#

本章示例是独立入口,暂时不把图片输入硬塞进现有 /ai/chat/rag/chat。原因是现有路由的请求模型和提示词都是文本契约,直接改动会扩大本章范围。

未来接入 FastAPI 时,边界会是:

上传文件或图片 URL
-> 校验大小、类型和权限
-> 转成 image content block
-> 调用视觉模型
-> 返回 response.content

这不是另一个神秘的 Agent。它仍然是:

HTTP 输入 -> Python 校验 -> 消息对象 -> 模型调用 -> HTTP 输出

先把消息和模型契约学清楚,再做上传接口。

第六关:三遍主动练习#

第一遍:读懂#

回答下面四个问题:

  1. 为什么 content 在文本消息中常是字符串,在多模态消息中变成列表?
  2. image_url 是图片内容本身,还是图片的访问地址?
  3. build_vision_llm()model.invoke() 分别负责什么?
  4. 为什么消息格式正确仍不能证明模型支持视觉输入?

第二遍:跟写#

只改提示词,不改调用结构:

message = HumanMessage(
content=[
{"type": "text", "text": "请判断图片中是否有文字;如果有,只抄出你看清楚的文字。"},
{"type": "image_url", "image_url": {"url": IMAGE_URL}},
]
)

观察模型回答有什么变化。不要把“看不清”强行改成确定答案。

第三遍:独立迁移#

写一个 describe_image(image_url: str, question: str) -> str 函数:

  • 输入图片 URL 和用户问题。
  • 组装一条 HumanMessage
  • 调用模型一次。
  • 返回 str(response.content)

验收:换一个公开图片 URL,函数仍能工作;如果 URL 不是 http://https://,先抛出 ValueError

常见坑#

1. 把图片地址拼进普通文本#

HumanMessage(content=f"请看这张图:{IMAGE_URL}")

这只是告诉文本模型一个字符串地址,不等于把图片作为视觉输入发送。

2. 把文本模型名称当视觉模型名称#

MODEL_NAME 能回答文字问题,不代表它能接受图片。视觉能力必须看模型卡和供应商能力说明。

3. 把 response.content 当成结构化视觉事实#

模型返回的是生成文本,可能看错、漏看或编造。涉及身份证、账单、医疗影像和合同等高风险内容时,必须保留人工复核和权限边界。

4. 把图片理解和 OCR 混为一谈#

视觉语言模型可以尝试读取图片中的文字,但它不是专用 OCR 的同义词。对精确数字、表格和证件字段,应单独评估 OCR 或文档解析方案。

5. 没有检查图片来源#

服务端直接请求用户提供的 URL 可能引入 SSRF、超大文件和内网地址风险。生产接口不能只把 URL 原样交给模型。

本章压缩回顾#

文本块 + 图片块
-> content: list[content block]
-> HumanMessage
-> 支持视觉输入的模型
-> response.content: 文本回答

最重要的工程结论:

多模态开发同时受“消息契约”和“模型能力”约束;代码形状正确,不代表当前模型就能看图。

本章通过标准#

  • 能写出文本块和图片块组成的 HumanMessage
  • 能解释 content 为什么从 str 变成 list[dict]
  • 能区分模型客户端、消息对象、图片 URL 和模型返回文本。
  • 能说明视觉模型与普通文本模型的能力边界。
  • 能完成 describe_image() 的最小迁移练习。

本章完成后进入音频:第 34 章先学语音转文字(STT),第 35 章学习文字转语音(TTS),第 36 章把它们接入已有的文本模型、RAG 或 Agent。

33. 多模态 AI:从文字请求到图像理解
https://enkiud.com/posts/course-33/
作者
Enkidu
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0