乐于分享
好东西不私藏

多模态进阶:让 AI 看懂视频、读懂文档

多模态进阶:让 AI 看懂视频、读懂文档

监控拍下一段十分钟的走廊录像,你想让 AI 一句话告诉你「几点几分有人进来、拿走了什么」;财务甩给你一沓 PDF 发票,你想让 AI 自动填进表格。这两件事,019 教的「看单张图」够用吗?不够——视频是会动的,文档是成沓的、还带着复杂版面。但别慌:进阶多模态没有另起炉灶,它只是在「看图」外面套了一层「怎么把视频和文档,变成模型能看的一串图片」。想通这一层,剩下的全是你熟悉的老动作。这一期,我们就把这层窗户纸捅破,再各写一段能跑的代码。

01从「看一张图」到「看懂视频、读懂文档」

回顾一下 019:我们把一张图片编码成 base64、塞进 content,模型就能读图表、认截图、做 OCR。那是多模态的「起步档」——一张、静态、内容不太复杂的图。

可一旦走进真实业务,你会立刻撞上两堵墙:

  1. 会动的画面
    :监控录像、操作教程、短视频……信息不在某一帧里,而在帧与帧之间的变化——谁先进门、球往哪飞、进度条走到哪。单看一张图,等于让人只瞄一眼电影剧照就复述整部片子。
  2. 成沓的文档
    :合同、发票、论文、财报……它们看似是「文字」,其实是图文混排——两栏排版、跨页表格、盖章、图表、公式。你以为复制粘贴就行,一试就知道版面全乱了。

这一期干的,就是把「看图」这门手艺,往视频文档两个方向各推进一档。方法出奇地一致,我们先看那把「总钥匙」。

02一把总钥匙:都拍平成「图片序列」

别被「视频理解」「文档解析」这些词唬住。它们背后是同一把钥匙

  • 视频 = 一叠按时间排好的图片
    。电影本来就是一张张画面快速翻页骗过眼睛的把戏(每秒 24~30 张)。把它按时间切开,就是一本连环画
  • 文档 = 一页页图文混排的图片
    。与其纠结怎么把版面「读」成文字,不如干脆把每一页当成一张图片整个看进去——版面、表格、盖章,一个都不丢。

于是进阶多模态就塌缩成了一句话:想办法把视频和文档拆成「一串图片」,剩下的就是 019 的看图力。而这些图片进了模型,又会像 019 说的那样,被切成 patch、变成「视觉 token」,和文字 token 混进同一条序列,用 001 那套注意力机制统一处理。

🔑 记住这把钥匙

进阶多模态 = 「拆」+「看」。难的不是「看」(019 已经会了),而是怎么把视频/文档拆成一串合适的图片——视频拆得太密会烧钱、太稀会漏戏;文档拆得不对会丢版面、错表格。所以本期真正的功夫,全在「怎么拆」这三个字上。

03视频理解的核心:抽帧(frame sampling)

先说视频。既然「视频 = 一叠图」,最笨的办法就是把每一帧都喂给模型——可这条路走不通。一段 10 分钟的视频,按每秒 30 帧算就有 18000 帧。全塞进去会怎样?

  • 贵到离谱
    :每张图都要吃掉成百上千个 token,上万帧直接把账单干爆(016 算过这笔账)。
  • 装不下
    :远远超出上下文窗口(004),模型根本收不下这么长的序列。
  • 纯属浪费
    :相邻两帧几乎一模一样,99% 是重复信息,喂进去也是白喂。

所以视频理解的第一课,就是抽帧(frame sampling):按一定策略,从成千上万帧里挑出有代表性的少数几帧,把一段视频压成一本薄薄的连环画,再喂给模型。

🧑‍🏫 就像看连环画猜剧情

你不需要看完电影的每一格胶片,只要翻几张关键剧照,就能脑补出「男主追车 → 撞了 → 下车吵架」。模型也一样:给它时间上均匀铺开的几张关键帧,它就能靠帧与帧之间的变化,推理出这段视频里发生了什么。抽帧,就是替模型挑出这几张「值得看的剧照」。

04动手:从视频里抽帧,让模型讲出剧情

理论说透,上代码。下面这段 videobot.py,用 OpenCV 把视频每隔几秒抽一帧,再把这串帧当成图片(一如 019)塞给模型。调用大模型的地方,照旧是官方 anthropic SDK:

# videobot.py —— 让模型「看懂」一段视频:抽帧 → 喂图 → 讲剧情from anthropic import Anthropicimport cv2, base64          # cv2 = OpenCV,负责把视频拆成一帧帧图片client = Anthropic()        # 老朋友,自动读环境变量 ANTHROPIC_API_KEY(密钥别写进代码,见 025)defsample_frames(path, every_sec=2):    # 每隔 every_sec 秒抽一帧,别一股脑全塞     cap = cv2.VideoCapture(path)     fps = cap.get(cv2.CAP_PROP_FPS)         # 这段视频每秒多少帧     frames, idx = [], 0whileTrue:         ok, img = cap.read()        ifnot ok: breakif idx % int(fps * every_sec) == 0:          # 到点了才留这一帧             _, buf = cv2.imencode(".jpg", img)         # 帧 → JPEG 字节             frames.append(base64.b64encode(buf).decode())         idx += 1     cap.release()    return frames                        # 一串 base64 图片,就是视频的「连环画」defwatch(path, question):     frames = sample_frames(path, every_sec=2)    print(f"抽了 {len(frames)} 帧")    # 第一块先交代「这是按时间顺序抽的帧」,模型才懂先后(关键!)     content = [{"type""text""text"f"下面是一段视频按时间先后抽出的若干帧。{question}"}]    for b64 in frames:                   # 把每一帧当成一张图片塞进去(回顾 019)         content.append({            "type""image",            "source": {"type""base64""media_type""image/jpeg""data": b64},         })     resp = client.messages.create(         model="claude-opus-4-8", max_tokens=500,         messages=[{"role""user""content": content}],     )    return"".join(b.text for b in resp.content if b.type == "text")print(watch("clip.mp4""按时间顺序讲讲视频里发生了什么?"))

看清楚这里唯一的「新东西」了吗?只有 sample_frames()——把视频拆成帧。往下的「把图片塞进 content、问个问题、取 resp.content 里的 text」,和 019 看单张图一模一样,只不过这次一口气塞了好几张。

🔑 帧是「有序」的,一定要告诉模型

那句 "下面是一段视频按时间先后抽出的若干帧" 不是废话——它是让模型理解先后、因果、动作方向的命门。少了这句,模型只当收到一堆无关的散图,答不出「先……然后……」。要更精确,还可以在每帧前插一句「第 12 秒:」把时间戳也告诉它。时间信息不会自己长在图片里,得你亲手喂。

05抽帧的学问:抽多密,就是在花多少钱

上面偷懒用了「每 2 秒一帧」的均匀抽帧。它简单,但不总聪明:一段大部分时间静止、只在某几秒有动作的监控,均匀抽会抽出一堆废帧、又可能恰好错过那个关键瞬间。于是有了更讲究的抽法:

策略
怎么抽
优点
缺点 / 适合
均匀抽帧
每隔固定秒数抽一张
实现最简单,覆盖均匀
可能全是废帧、也可能漏掉关键瞬间。适合内容平稳的教程、讲座
关键帧 / 场景切分
只在画面「变化大」处抽(镜头切换、场景转换)
信息密度高,帧少而精
要先做变化检测。适合电影、剪辑过的短视频
事件驱动
检测到特定事件才抽(有人进入、字幕变化)
只留真正关心的片段,最省
要额外的检测器。适合监控、体育、直播

怎么选?一句话:抽帧的密度,直接换算成你的账单和延迟。抽得越密越不容易漏戏,但 token 和成本蹭蹭涨。来看一段 10 分钟视频在不同密度下的估算,感受一下这条曲线有多陡:

⚠️ 采样率就是那个「烧钱旋钮」

图片是 token 大户,视频更是「按帧数乘法计费」。上线前务必先算账:能降采样就降、能只在关键片段抽就别全程抽、固定不变的画面别重复喂。先用最稀的密度跑通,发现「漏戏」了再逐步加密——永远别默认「每秒一帧」,那是账单刺客

06换个方向:为什么解析文档也是「多模态」问题

看完视频,转向文档。你可能第一反应是:「PDF 不就是文字吗?复制粘贴不就完事了?」——真动手你就会栽跟头:

  • 扫描件根本没有文字
    :很多合同、发票是打印后扫描的,整页就是一张图片,你一个字都复制不出来。
  • 电子版也会「丢版面」
    :就算能复制,双栏排版会被拉成一长串串行文字、表格的行列会错位、图表和公式直接消失——信息不只在文字里,更在「它排在哪」

换句话说,读懂一份文档,靠的从来不只是认字,还有看懂版面:这个数字属于「价税合计」那一行、这段话在「免责条款」那一栏。而「看懂版面」本质是个视觉任务——这就是为什么文档解析是个不折不扣的多模态问题。既然如此,最省事的姿势往往就是:把每一页当成一张图,整个看进去

🧑‍🏫 一张发票,一半信息在「排版」里

想象一张发票:金额「¥1,280」为什么是总价而不是单价?因为它排在「合计」那一栏、字号更大、在最下面一行。这些线索全藏在空间位置里,一旦拉成纯文本就荡然无存。把整页当图喂给多模态模型,等于让它像人一样连看带读,版面线索一个不落。

07两条路解析文档:OCR 流水线 vs 直接喂多模态

把文档变成能用的信息,业界有两条成熟路线,各有取舍:

维度
路线 A:OCR + 版面分析流水线
路线 B:直接喂多模态模型
怎么做
OCR 抽文字 + 检测表格/标题 + 规则拼装
整页当图片(或 PDF)发给模型,边看边懂
版面 / 表格
版面一花就崩,表格易错位
抗乱版面,表格、图表一起理解
语义理解
弱,只认字不懂意思
强,能直接答「谁欠谁多少钱」
成本 / 速度
便宜、快、可离线
较贵、较慢(图吃 token)
风险
成熟稳定、结果可复现
可能看错小字、幻视(第 09 节)
适合
海量、版式固定、预算敏感
版式多变、要语义、要直接出结构化

怎么选?如今越来越多新项目直接从路线 B 起步——省掉一长串工程、还能顺手做语义理解和结构化抽取。追求极致成本或超大批量时,再用 A+B 混合:先用便宜的 OCR 抽出文字兜底,再把「原图 + OCR 文字」一起喂给模型,让它对照着理解版面,又快又准。

🔑 别把「解析」和「理解」混为一谈

路线 A 擅长「解析」(把字抠出来),却不懂「理解」(这字是什么意思)。多模态模型的杀手锏,是把这两步合成一步:看一眼整页,直接告诉你「开票方是谁、合计多少、有没有超期」。下一节就把这一步写成代码。

08动手:把 PDF 解析成结构化 JSON

光「看懂」还不够,业务要的是能入库的结构化数据。这时就该请出 020 的老招——用 Tool Use 强制模型「填表」:把想要的字段写进 input_schema、用 tool_choice 逼它调用,直接拿到解析好的 JSON。而这次的输入,是一整张 PDF——anthropic SDK 支持把 PDF 当成 document 块直接发,模型自己看版面:

# invoice_parser.py —— 把一张 PDF 发票解析成结构化 JSON(多模态「看」+ 020 的「填表」)from anthropic import Anthropicimport base64  client = Anthropic() pdf_b64 = base64.b64encode(open("invoice.pdf""rb").read()).decode()  INVOICE_TOOL = {                       # 想要的「表格」长什么样,写死在 schema 里(见 020)"name""save_invoice",    "description""保存从发票中抽取的结构化字段",    "input_schema": {        "type""object",        "properties": {            "vendor": {"type""string""description""开票方名称"},            "date":   {"type""string""description""开票日期 YYYY-MM-DD"},            "total":  {"type""number""description""价税合计(元)"},            "items":  {"type""array""items": {"type""string"}, "description""商品明细"},         },        "required": ["vendor""date""total"],     }, }  resp = client.messages.create(     model="claude-opus-4-8", max_tokens=800,     tools=[INVOICE_TOOL],     tool_choice={"type""tool""name""save_invoice"},   # 强制填表,别写作文     messages=[{"role""user""content": [         {"type""document",           # PDF 直接当「文档块」发,模型自己看版面"source": {"type""base64""media_type""application/pdf""data": pdf_b64}},         {"type""text""text""把这张发票的关键字段抽出来,调用 save_invoice。"},     ]}], )  data = next(b.input for b in resp.content if b.type == "tool_use")  # 直接拿到解析好的 dictprint(data["vendor"], data["total"], data["items"])

妙就妙在这是两招合体:多模态负责「看懂这张 PDF 的版面和内容」,Tool Use 负责「把看到的塞进规定好的格子」。跑完你拿到的不是一段描述文字,而是一个能直接 insert 进数据库的 dict。发票、合同、简历、报表——换个 schema,同一套代码全能干。

🧑‍🏫 长文档塞不下怎么办?

一份 200 页的年报,整个塞进去必然超窗(004)。老办法照样管用:要么分而治之——按页/章切块、每块各抽一次、最后汇总(Map-Reduce);要么先检索再看——把文档灌进 006 的 RAG,问什么就只取相关几页喂给模型。别指望一口吞下一头大象。

09六个必踩的坑

  • 抽帧太稀漏关键、太密烧钱
    :视频理解成败全在采样率。先稀后密、按内容自适应,别无脑「每秒一帧」(第 05 节)。
  • 时间/顺序信息会丢
    :多帧塞进去,一定要明说「按时间先后」,最好带上时间戳,否则模型分不清先后因果(第 04 节)。
  • 看不清小字和密集表格
    :分辨率不够或图缩太小,「8」会看成「3」、表格行列会串。解法:关键页提高分辨率、单页单发、别把大图硬压小。
  • 幻视(hallucination)
    :图糊、字缺时,模型会「脑补」一个看似合理的数字或条款。财务、医疗、法律场景务必人工复核,并让它标注出处/置信度(呼应 018 可观测、017 安全)。
  • token 与成本爆炸
    :图片和 PDF 都是 token 大户。先 count_tokens 算账、能降采样降采样、固定前缀用提示缓存省钱(016)。
  • 只测「漂亮样本」等于没测
    :歪扫描、盖章遮字、手写批注、多语言混排才是照妖镜。上线前用这些「脏数据」做评估(024),别被 demo 的顺利骗了。

⚠️ 高风险字段,绝不盲信

合同金额、化验单数值、身份证号这类字段,模型看错一位就是大事故。铁律:让它「抽取 + 标出处」(这个数字来自第几页哪一行),再在代码侧做校验(金额加总对不对、日期格式合不合法),高风险的最后一定过人工。多模态是「超强的初筛」,不是「免检的终审」。

10今日小测验

Q1 进阶多模态的「总钥匙」是什么?视频和文档分别怎么被拆成模型能吃的东西?

答:总钥匙是「拆成一串图片,复用 019 的看图力」视频=一叠按时间排好的图片,用抽帧把上万帧压成薄薄一本「连环画」;文档=一页页图文混排的图片,把每页整个当图看进去,版面/表格/盖章一个不丢。两者进了模型都被切成 patch、变成「视觉 token」,和文字 token 混进同一条序列,用 001 那套注意力机制统一处理——所以难点不在「看」,而在「怎么拆」

Q2 为什么视频要「抽帧」而不能整段喂?三种抽帧策略各适合什么?

答:10 分钟视频按 30fps 有 1.8 万帧,全喂会贵、超窗、且相邻帧高度冗余。抽帧就是挑出少数有代表性的关键帧。三种策略:① 均匀抽帧(每隔固定秒数)——最简单,适合内容平稳的讲座/教程;② 关键帧/场景切分(只在画面变化大处抽)——帧少而精,适合电影/剪辑过的短视频;③ 事件驱动(检测到特定事件才抽)——最省,适合监控/体育/直播。核心权衡:抽得越密越不漏戏,但 token 和成本蹭蹭涨,采样率就是烧钱旋钮。

Q3 为什么说解析 PDF 是个「多模态」问题而不是纯文本?两条解析路线怎么选?

答:因为读懂文档不只靠认字,还要看懂版面:扫描件整页就是图片、一个字都复制不出;电子版复制也会丢双栏/表格/图表的空间关系。而「一个数字属于合计那一行」这类线索全藏在排版里,是视觉任务。两条路:A=OCR+版面分析流水线,便宜快、可离线,但版面一花就崩、不懂语义;B=整页/PDF 直接喂多模态模型,抗乱版面、能出语义和结构化,但较贵、可能看错小字。多数新项目从 B 起步;追求极致成本或超大批量时用 A+B 混合(OCR 兜底文字 + 模型理解版面)。

Q4 把 PDF 变成结构化 JSON,用到了前面哪两期的招?各起什么作用?

答:两招合体019 的多模态负责「看」——用 document 块把整张 PDF 发进去,让模型连版面带内容一起看懂;020 的 Tool Use 结构化输出负责「填表」——把想要的字段写进 input_schema、用 tool_choice 强制模型调用,直接拿到解析好的 dict,而不是一段作文。合起来就是:看懂 + 填格子 = 能直接入库的结构化数据。换个 schema,发票/合同/简历/报表通吃。长文档超窗则用 Map-Reduce 分块或 006 的 RAG 先检索。

11小结 & 下一站

🎯 带走这一句

进阶多模态 = 「拆」+「看」:把视频拆成抽帧后的连环画、把文档拆成一页页图,剩下的就是 019 的看图力,全都拍平成同一种「视觉 token」用 001 的注意力处理。视频的功夫全在抽帧密度——它直接换算成账单,先稀后密、按内容自适应;文档的关键是把它当看(版面才是信息),多数场景直接喂多模态模型、再叠上 020 的强制工具调用就能把 PDF 榨成结构化 JSON。当心六个坑:漏帧、丢顺序、看不清小字、幻视、token 爆炸、只测漂亮样本。记住:多模态是超强的「初筛」,高风险字段永远别免了人工终审。

  • 🧪 动手作业:① 找一段十几秒的短视频,照 videobot.py 每 2 秒抽一帧,让模型「按时间顺序讲讲发生了什么」,再把间隔改成 5 秒、10 秒,感受剧情细节和账单一起变化;② 拿一张发票/账单 PDF,用 document 块 + 强制工具调用抽成 JSON,故意换张歪扫描的试试它会不会「幻视」。
  • 📄 延伸阅读:回看 019 · 多模态看图 打底,再把 020 · 结构化输出 的强制填表接到文档解析上,一通百通。
  • 🔁 回顾:001 · 注意力机制 · 004 · 上下文窗口 · 006 · RAG · 016 · 成本与限流
  • ⏭️ 下一期预告(第 029 期):上下文工程进阶:把有限的上下文窗口用到极致——该塞什么、怎么压缩历史、如何组织信息,让模型每次都「读到重点」而不是被噪音淹没。