01 · TASK & CONTRACT
多模态任务:先定义输入与边界
多模态系统的第一个难题不是选择模型,而是说明多个输入如何组成一个任务对象。文本、图片和音频可能来自不同的人、设备和时间;它们可以互相补充,也可能互相矛盾。先把输入、同步、权限、输出和失败状态写成契约,后面才能判断模型究竟学到了什么。
12.1.1 多模态到底解决什么问题
多模态模型处理的是两个或更多信息通道共同参与的任务。用户说一句话,系统看一张照片并给出解释,是视觉与文本协作;会议录音、说话人时间戳和议程文档一起生成纪要,是音频、文本和结构化元数据协作。关键不在于输入数量,而在于一个答案是否必须同时使用不同模态的证据。
“多模态”与“多源数据”不是同义词。三张同格式的图片来自三个摄像头,仍可能只是视觉多源;一张图配一段描述,若描述只是文件名复制,也没有形成有意义的跨模态关系。判断一个任务是否需要多模态,可以做消融提问:去掉任一模态后,任务目标是否变得不可完成、风险显著上升,或只能得到更粗的结果。
常见任务有跨模态检索、图像问答、图文匹配、视频摘要、语音转写与总结、文档理解、图表问答和多模态生成。检索要求两个模态进入可比较的空间;问答要求模型把问题中的指代落到图像或音频证据;摘要要求保留时间、说话人和原文;生成则还要说明输出是合成内容,不能冒充事实来源。
多模态输入具有互补、冗余和冲突三种关系。照片能提供空间外观,文字能提供类别名称;照片和文字都说“红色”,属于冗余;图片上的仪表读数与用户输入的数字不一致,则是冲突。系统必须先定义冲突处理策略:显示两种证据、请求确认、调用工具复核,还是在高风险场景直接转人工。
任务合同要写清主体、输入、输出、非目标和使用场景。例如“从实验器材照片与用户问题中返回候选器材及证据区域”比“识别这是什么”可执行得多。合同还要规定是否允许推断不可见属性、是否接受模态缺失、是否保存原始媒体、结果是否触发后续动作,以及出现不确定时由谁接手。
不同模态的语义粒度也不一样。文本 Token 通常对应字符、子词或特殊标记,图像 Token 对应 Patch 或区域,音频 Token 对应一段时间内的声学特征。把它们简单拼成长序列,并不会自动获得共同语义;模型需要通过预训练、位置/时间信息和融合机制学习“某段话指向图片哪一块、某个词在音频的哪一秒”。
多模态系统的价值应由任务结果而不是演示效果衡量。图像问答回答得流畅,不代表它看到了小字;视频摘要很完整,不代表时间线没有错位;语音助手听起来像真人,不代表它正确识别了否定词。把可验证证据、错误状态和用户动作写入合同,能避免把语言表现误当作跨模态理解。
做学习项目时可以从低风险、可观察的任务开始:根据课程截图回答页面标题,依据一段短音频列出时间戳,或把产品图片与说明文本匹配。先用固定样例和假模型验证协议,再接入真实模型。这样即使模型能力不足,也能区分是输入合同、数据配对、融合结构还是评估方法出了问题。
一个实用判断是画出“最小证据集”。如果回答“图片里有几个烧杯”只需要图像区域,文本只是问题;如果回答“这台设备是否符合说明书中的额定范围”,就需要图像、OCR 文字、说明书文本和单位解析。证据集越明确,越能设计合适的工具调用与拒答,而不是让模型凭先验猜测。
多模态任务还要考虑输入顺序和交互轮次。用户可能先上传图片,下一轮才补充音频;视频可能边上传边处理;一段对话中的旧图片是否继续有效,也必须由会话状态决定。把媒体 ID、消息 ID、时间戳和权限写进事件记录,才能重放一次对话,判断错误是来自模型还是上下文拼接。
一页任务合同应该回答什么
第一,谁在什么场景下提交什么媒体?要限定文件格式、大小、时长、分辨率、语言、采样率和最大数量,并说明是否允许用户后续补发或替换。第二,系统要返回哪些字段?把自由描述拆成状态、结论、证据引用、置信提示、工具结果和下一步动作。
第三,哪些结论不在范围内?例如图片问答不等于身份识别,语音转写不等于说话人真实身份确认,图表读取不等于财务审计。非目标越具体,越能阻止模型把相关但未经验证的信息扩展成高风险判断。
第四,失败如何落地?媒体损坏、模态缺失、时间无法同步、文本与图片冲突、工具超时、内容敏感和权限不足都要有不同状态。每个状态对应用户可执行动作,例如重新上传、补充上下文、查看来源或转人工,而不是统一返回“模型出错”。
| 任务 | 输入组合 | 关键证据 | 常见失败 |
|---|---|---|---|
| 图文检索 | 图片 + 查询文本 | 共享向量、来源与权限 | 相似但无关、近重复泄漏 |
| 图像问答 | 图片 + 问题 | 区域、OCR 或检测结果 | 看不清仍强答、指代错位 |
| 视频摘要 | 视频 + 主题/议程 | 时间轴、说话人、关键片段 | 时间顺序错、漏掉否定 |
| 语音总结 | 音频 + 语言/上下文 | 转写、时间戳、说话人 | 专名错、重叠语音错 |
| 文档理解 | 页面图像 + 文本层 | 页码、版面、字段来源 | 表格串列、跨页错配 |
| 多模态生成 | 条件文本 + 参考媒体 | 生成参数、输入许可、合成标记 | 把生成结果当事实 |
多模态不是把输入堆在一起;每一步都要保留来源、坐标或时间信息,最后的结论才能回到原始证据。
from dataclasses import dataclass
from typing import Literal
Modality = Literal["text", "image", "audio"]
@dataclass
class MediaRef:
media_id: str
modality: Modality
uri: str
mime: str
duration_ms: int | None = None
width: int | None = None
height: int | None = None
@dataclass
class TaskRequest:
question: str
media: list[MediaRef]
allow_missing: bool = False
def validate_request(request: TaskRequest) -> dict:
if not request.question.strip():
return {"ok": False, "state": "empty_question"}
if not request.media and not request.allow_missing:
return {"ok": False, "state": "missing_media"}
ids = [item.media_id for item in request.media]
if len(ids) != len(set(ids)):
return {"ok": False, "state": "duplicate_media_id"}
for item in request.media:
if item.modality == "image" and not item.width:
return {"ok": False, "state": "missing_image_shape"}
if item.modality == "audio" and item.duration_ms is None:
return {"ok": False, "state": "missing_audio_duration"}
return {"ok": True, "state": "accepted", "media_count": len(ids)}
request = TaskRequest(
question="这张图中的器材叫什么?",
media=[MediaRef("img-01", "image", "uploads/img-01.jpg", "image/jpeg", width=640, height=480)],
)
print(validate_request(request))选择“看图回答课程问题”或“听录音整理会议决定”,写出用户、输入、输出、非目标、失败状态和一条可验证证据。
参考结论:去掉任一模态后是否真的影响任务;输出包含来源/区域/时间;不确定和权限不足都有明确动作。
完整实现路径:先定义媒体 ID、时间/空间范围、状态和证据 schema,再定义模型输入。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
from dataclasses import dataclass
@dataclass(frozen=True)
class MediaRequest:
request_id: str
media_id: str | None
question: str
permission: bool
def validate(r):
if not r.permission: return "permission_denied"
if not r.media_id: return "missing_media"
if not r.question.strip(): return "invalid_question"
return "ready"
assert validate(MediaRequest("r1", None, "图里是什么?", True)) == "missing_media"验收标准:合法、缺失、冲突和撤权四类请求都有确定状态。
若结果不对:先查媒体是否存在和可访问,再查对齐,最后才查模型输出。
12.1.2 数据对象、时间与缺失模态
多模态数据的基本单位不是“一个文件”,而是一个带关系的样本对象。它可以由消息、媒体、转写、标签、时间轴、区域框、来源、许可和版本组成。建议给每个媒体分配稳定 ID,再用事件或关系表描述“哪段文字来自哪张图”“哪句话对应音频的哪一秒”“哪块区域支持哪个答案”。文件名不能承担这些语义。
时间对齐是音频和视频任务的核心。音频帧、视频帧、字幕、说话人片段和用户问题可能使用不同的时钟或采样率;录音开始前的静音、网络缓冲和剪辑都会造成偏移。保存原始时间戳、采样率、时区和对齐版本,显示时再转换成统一时间轴,不能只把秒数四舍五入成整数。
空间对齐是图像、页面和视频区域的核心。原图尺寸、缩略图尺寸、裁剪窗口、旋转角度和补边偏移决定了一个框能否正确画回去。文档页面还要区分页面坐标、版面块坐标和文字基线坐标;模型返回“表格第二列”时,需要知道它在第几页、哪一个区域,而不是只给一个自然语言短语。
缺失模态很常见:用户只有截图没有原文件,会议没有单独麦克风,视频某一段没有字幕,上传过程丢了一张图片。缺失不能简单填零或用空字符串,因为这会让模型把“没有提供”误解成“内容为空”。协议中应区分 missing、unavailable、not_applicable 和 redacted,并规定哪些任务可以降级。
模态缺失的降级策略需要在训练和评估中提前演练。图文匹配可以只用文本检索候选,再提示需要图片确认;视频摘要可以返回已处理时间段并标出空洞;语音总结可以只整理可辨识的片段。降级不是悄悄减少输入,而是让用户知道证据范围缩小了,结论也相应变弱。
冲突和重复需要关系级别的标注。两张图片可能是同一场景的不同角度,一段 OCR 文本可能是图像中的内容,也可能是用户另行输入的修正。数据对象要记录来源和优先级,不要把同一事实重复计数。训练时可以保留冲突样本,让模型学习请求澄清;评估时应单独报告冲突处理准确率。
批处理会放大对齐错误。将不同长度的音频填充到同一批次,需要 attention mask;将多张图片拼成网格,需要保存每张图的边界;将多轮对话中的媒体放进同一上下文,需要记录媒体出现顺序。批处理字段应包含有效长度、位置映射和 padding 规则,避免模型看到不存在的内容。
多模态样本常带有隐私和权限差异。图片可能允许内部分析但禁止公开,转写可能包含未同意的姓名,说明书可能有授权限制。媒体本身、提取的向量、OCR 文本和模型日志不能默认共享同一权限。数据对象应提供最小可用引用,检索和工具层在返回前再次检查权限。
采样与分辨率也会改变对齐语义。低帧率视频可能跳过手势关键瞬间,低采样率音频可能损失辅音,缩小页面可能让脚注不可读。不要用“输入已成功编码”代表信息足够;质量字段应说明哪些内容在当前采样下不可见,并让模型或工具进入不确定状态。
数据对象最后要支持重放。给定样本 ID、媒体版本、预处理配置、对齐算法和模型版本,应该能重新构造当时的输入。若只保留最终提示字符串,无法知道图片是否被裁过、音频是否被截短、文本是否由 OCR 产生。可重放的对象设计,是多模态工程从演示走向实验的分界线。
缺失与冲突的状态设计
建议把状态拆成“证据状态”和“任务状态”。证据状态描述某模态是否可访问、是否完整、是否清晰;任务状态描述系统是否能回答、是否需要工具、是否应转人工。这样“图片被遮挡但仍可回答数量”与“图片完全缺失”不会混为一谈。
当文本与图像冲突时,不要让模型用置信度最大的那一方自动覆盖另一方。先返回冲突字段,引用各自来源,说明需要用户确认的内容;涉及金额、剂量、身份或安全动作时,默认停在澄清状态。冲突样本还应进入回归集,防止新模型变得更会圆场而不是更可靠。
模态缺失的评估要同时测任务质量和沟通质量。模型即使不能回答,也应准确说明缺少哪种证据、缺失影响什么、用户怎样补充。把“安全拒答”与“无帮助的拒绝”分开评分,才能鼓励系统在边界内继续提供可用的部分结果。
| 关系 | 示例 | 应保存的字段 | 不能假设 |
|---|---|---|---|
| 引用 | 问题引用图片区域 | media_id、bbox、页码、来源 | 文字短语一定对应区域 |
| 同步 | 字幕对应音频片段 | start_ms、end_ms、offset、版本 | 两个时间戳天然同钟 |
| 派生 | OCR 来自页面截图 | parent_id、算法、置信度 | OCR 文本就是原文 |
| 替换 | 用户补发高清图片 | 旧 ID、新 ID、原因、权限 | 新文件一定内容相同 |
| 冲突 | 说明书与照片读数不一致 | 双方证据、优先级、状态 | 模型应自行裁决 |
from dataclasses import dataclass
@dataclass
class Span:
media_id: str
start_ms: int
end_ms: int
source: str
def overlap(left: Span, right: Span) -> int:
if left.media_id != right.media_id:
return 0
return max(0, min(left.end_ms, right.end_ms) - max(left.start_ms, right.start_ms))
def align_transcript(transcript, events):
aligned = []
for sentence in transcript:
candidates = [
event for event in events
if overlap(sentence, event) >= 200
]
aligned.append({
"text": sentence.text,
"event_ids": [item.source for item in candidates],
"state": "aligned" if candidates else "unresolved",
})
return aligned
transcript = [type("T", (), {"text": "决定下周复核", "media_id": "rec-1",
"start_ms": 1200, "end_ms": 4200})()]
events = [Span("rec-1", 1500, 3900, "speaker-a")]
print(align_transcript(transcript, events))用一张视频截图、一段字幕和一个问题,标出媒体 ID、视频时间、原图区域与文本来源;再写一个模态缺失时的降级结果。
参考结论:时间和坐标能映射回原始媒体;缺失不是空字符串;回答会说明证据范围缩小。
完整实现路径:先定义媒体 ID、时间/空间范围、状态和证据 schema,再定义模型输入。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
from dataclasses import dataclass
@dataclass(frozen=True)
class MediaRequest:
request_id: str
media_id: str | None
question: str
permission: bool
def validate(r):
if not r.permission: return "permission_denied"
if not r.media_id: return "missing_media"
if not r.question.strip(): return "invalid_question"
return "ready"
assert validate(MediaRequest("r1", None, "图里是什么?", True)) == "missing_media"验收标准:合法、缺失、冲突和撤权四类请求都有确定状态。
若结果不对:先查媒体是否存在和可访问,再查对齐,最后才查模型输出。
12.1.3 输入契约与任务合同
输入契约描述“系统接受什么”,任务合同描述“接受后要完成什么”。二者要分开:一个合法的 MP3 文件可能不符合“只处理普通话单人录音”的任务;一张格式正确的图片可能因过暗、含敏感信息或超出类别范围而不能进入模型。分层契约让错误在最接近的地方被发现。
契约字段应尽量可执行。格式用真实解码与魔数检查,尺寸和时长用数值范围,语言用识别或用户声明,媒体关系用 ID 和时间窗口,权限用授权记录,质量用明确阈值。不要把“清晰”“相关”“适合回答”全部留给模型自由判断;它们至少要有可观察的代理指标和人工抽检。
输出合同同样重要。多模态模型可以生成一段很长的说明,但产品可能只需要状态、结论、证据列表和建议动作。为每种任务定义 schema、枚举、单位、坐标系和空值语义,再用校验器阻止未知字段、非法数字、越界框和不允许的工具调用。自由文本可以作为解释,不应成为唯一接口。
证据引用要和答案字段绑定。回答“温度是 80℃”时,引用应指向图片中的仪表区域或文档页码;回答“主持人同意延期”时,引用应指向音频时间段和转写片段。引用不是装饰链接,系统应能验证引用的媒体 ID、范围和版本仍存在,权限也仍允许当前用户查看。
任务合同要写出决策边界。低风险的课程问答可以在置信不足时给出候选并请求确认;涉及健康、金融、身份或自动控制时,应把模型限制写成硬门槛。模型不能自行改变权限、发送邮件、下单或删除媒体;这些动作必须由受控服务和用户确认承担。
契约版本要进入实验和日志。字段新增、默认值变化、坐标定义变化、音频裁剪方式变化,都会改变模型输入输出。给契约一个版本号,记录兼容策略和迁移脚本;回放旧样本时使用旧契约或明确执行迁移,不要让最新代码静默改写历史结果。
校验器要覆盖正常、边界、错误和恶意样本。测试空问题、超长文本、重复媒体、负时间戳、倒置区间、非法 MIME、超大分辨率、无权限引用、图片内指令和工具返回 NaN。每个失败都应返回稳定状态,日志记录规则名与媒体 ID,用户界面只显示必要信息。
验收不以“模型调用成功”为准,而以协议闭环为准。一个假模型可以返回合法答案、未知状态、冲突状态和非法区域,用来验证前后端是否正确处理。真实模型接入后再评估能力。将协议测试和能力测试分开,能快速定位是工程 bug 还是模型误差。
好的契约还支持成本控制。限制单次上传的媒体数量、总像素、音频时长、文本 Token 和工具次数,先做便宜的格式/质量筛选,再决定是否调用昂贵模型。成本超限也应是可解释状态,告诉用户如何缩短音频、压缩图片或分批处理,而不是服务无响应。
本小节的最终产物是一页可读的任务合同、一份机器可校验的 schema、一套代表性测试样本和一条重放命令。它们共同回答:系统看到了什么、没有看到什么、为什么得出这个结果、谁可以使用结果,以及模型失败后如何安全结束。
从合同到测试的四步
第一步写自然语言目标和非目标,邀请另一位学习者找出歧义;第二步把输入、输出和状态整理成表格,给字段类型、范围、单位和来源;第三步实现校验器与假模型,用正常/边界/冲突/缺失样本跑通;第四步再接真实模型,用固定版本和评估集验证能力与成本。
不要让 schema 追求“包容一切”。字段过多会让调用方不知道哪些是必填,字段含义含糊又会让下游误用。先定义最小可用协议,明确不支持的内容,再根据真实失败样例扩展;每一次扩展都补迁移说明、测试和文档。
检查权限时遵循最小暴露原则。模型可能只需要图片的一块区域和脱敏文本,不必读取整个原图或所有会话。日志保存能重放问题的最小字段,原始媒体和派生向量使用不同保留策略;用户撤回授权后,缓存、索引和报告引用都要能被追踪和处理。
| 层级 | 必须明确 | 验证例子 | 失败状态 |
|---|---|---|---|
| 媒体 | 类型、大小、时长、解码 | 真实 MIME、像素/采样率 | invalid_media |
| 关系 | 顺序、时间、区域、父子 ID | 时间重叠、坐标映射 | unresolved_link |
| 语义 | 问题、语言、类别、非目标 | 空问题、越界请求 | out_of_scope |
| 权限 | 使用目的、可见范围、保留期 | 撤权后再读 | permission_denied |
| 输出 | schema、单位、证据、动作 | 非法枚举/NaN/越界框 | invalid_output |
| 运营 | 成本、超时、重试、回滚 | 模拟超时与重复请求 | budget_exceeded |
import json
from dataclasses import dataclass
ALLOWED_STATUS = {"answer", "uncertain", "missing", "conflict", "blocked"}
@dataclass
class Result:
status: str
answer: str
evidence: list[dict]
next_action: str | None = None
def validate_result(result: Result, media_ids: set[str]) -> dict:
if result.status not in ALLOWED_STATUS:
return {"ok": False, "state": "invalid_status"}
if result.status == "answer" and not result.answer.strip():
return {"ok": False, "state": "empty_answer"}
for item in result.evidence:
if item.get("media_id") not in media_ids:
return {"ok": False, "state": "unknown_media"}
if item.get("kind") not in {"text_span", "time_span", "region"}:
return {"ok": False, "state": "invalid_evidence_kind"}
if result.status in {"missing", "conflict", "blocked"} and not result.next_action:
return {"ok": False, "state": "missing_next_action"}
return {"ok": True, "state": "accepted"}
sample = Result(
status="uncertain",
answer="可能是烧杯,但图片边缘被遮挡。",
evidence=[{"media_id": "img-01", "kind": "region"}],
next_action="请上传完整图片",
)
print(json.dumps(validate_result(sample, {"img-01"}), ensure_ascii=False))为一个图文或音频文本任务定义状态枚举、必填字段、证据类型和失败动作;用假模型构造一个合法结果和两个非法结果。
参考结论:合格标准:校验器能拒绝非法状态、陌生媒体、缺失证据或无动作的拒答;历史结果能根据版本重放。
完整实现路径:先定义媒体 ID、时间/空间范围、状态和证据 schema,再定义模型输入。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
from dataclasses import dataclass
@dataclass(frozen=True)
class MediaRequest:
request_id: str
media_id: str | None
question: str
permission: bool
def validate(r):
if not r.permission: return "permission_denied"
if not r.media_id: return "missing_media"
if not r.question.strip(): return "invalid_question"
return "ready"
assert validate(MediaRequest("r1", None, "图里是什么?", True)) == "missing_media"验收标准:合法、缺失、冲突和撤权四类请求都有确定状态。
若结果不对:先查媒体是否存在和可访问,再查对齐,最后才查模型输出。
本节复习与易错
易错一:把“同时输入”当成“已经融合”。模态之间仍需明确关系、位置和时间;没有配对或对齐,模型可能只依赖最容易的文本捷径。易错二:把缺失填成零。缺失、不可用和被遮蔽的语义不同,必须用状态表达。
易错三:只校验扩展名和提示词长度。真正的风险往往在解码失败、坐标错位、音频偏移、权限撤回和输出越界。易错四:让模型决定权限和动作。模型可以提出候选,服务端和用户确认才有权执行。
本节验收:提交任务合同、媒体关系表、输入/输出 schema、假模型测试和一条重放记录。读者应能解释每个答案引用了哪个模态、哪个范围、哪个版本,以及输入缺失或冲突时系统为什么没有继续猜。
02 · MODALITY REPRESENTATIONS
模态表示:文本、图像与音频
模型不能直接处理“文字、图片、声音”这些人类概念,而要把它们变成带有位置、时间和掩码的数值序列。表示决定哪些细节被保留、哪些关系容易学习,也决定不同模态能否在同一个上下文中安全地对齐。
12.2.1 文本 Token 与语义向量
文本进入多模态模型前通常要经过规范化、切分和词表映射。一个 Token 可能是汉字、英文子词、标点、数字片段或特殊标记;Token 数与字符数并不相等,也会随词表、语言和文本形式变化。对同一段说明书,换一个 tokenizer 可能改变序列长度、截断位置和模型能看到的单位。
规范化必须和任务目标一致。大小写、全角半角、Unicode 组合字符、空格、换行、表格分隔符和单位符号都可能有意义;过度清洗会把型号、负号、小数点或语言边界抹掉。建议保存原始文本、规范化文本和规则版本,模型输入只使用可追踪的派生结果。
特殊 Token 负责标记边界与来源。系统消息、用户问题、图片占位符、音频片段、文档页码和结束标记不能只靠自然语言约定;它们应有稳定 ID、合法顺序和可验证的配对关系。若图片占位符被截断或重复,模型可能把后面的文字错误地当成图片描述。
文本向量包含上下文,不是词典里的固定标签。相同词在不同句子中的表示会变化,“苹果”可以是水果、公司或按钮名称。做跨模态匹配时,池化位置、归一化方式、语言和提示模板都会影响相似度,不能把一个向量直接当成事实或概率。
上下文长度是资源预算,也是证据预算。过长的字幕、OCR 结果或说明书会把问题和关键区域挤出窗口;简单截断可能丢掉单位、否定和条件句。应按语义段、页面、时间片或检索结果切分,并在每段保留来源、顺序和重叠策略,避免模型只看到孤立句子。
文本 Token 与图像/音频 Token 的比例通常差异很大。一页扫描文档的 OCR 可能产生数千 Token,一张低分辨率图片只有几百视觉 Token,一小时录音经过压缩后又可能变成很长的声学序列。批处理时要记录各模态有效长度和成本,不能只用总 Token 数做粗略估计。
训练和推理的 tokenizer 必须一致。词表版本、特殊标记 ID、未知词处理和截断规则任何一个变化,都可能让旧权重看到不同的输入。加载模型时检查 tokenizer 配置和模型 manifest;如果必须迁移,先用固定样本对比 Token 序列,再重新评估,而不是只看接口没有报错。
文本表示还承担结构信息。Markdown、JSON、表格、代码和自然段的分隔方式,会影响模型是否能找到字段和引用。对于图表或文档任务,保留标题、页码、行列关系和单位比把所有内容拼成一段纯文本更可靠。结构化表示不是为了好看,而是为了让证据可定位。
多语言场景要报告语言切片。中文分词、英文子词、混合数字、方言转写和少数语言可能有不同长度与错误模式;把所有语言平均后,模型可能在主流语言上很强、在另一种语言上几乎不可用。评估时保存语言、脚本和转写来源,提示模型不确定时不要自动翻译成看似确定的结论。
学习时可以先用一个小词表观察 Token 化,再看 Token 经过嵌入和位置编码后如何进入 Transformer。不要把 Token 当作“一个词的真实含义”;它只是模型处理序列的接口。真正的语义来自上下文、训练目标和与其他模态建立的关系。
文本表示的三个检查点
检查长度:用实际 tokenizer 统计 Token,而不是用字符数估算;检查边界:确认标题、页码、时间戳和图片占位符没有被截断;检查语义:对否定、数字、单位、专名和代码做小型回归集。每个检查点都应输出可读样例,方便发现规则改变。
对话中要把“用户写的文字”和“OCR/ASR 产生的文字”区分开。它们的可信度、可编辑性和权限不同;模型回答时需要知道某段文字来自图片还是来自用户确认。给派生文本加来源标签,既帮助对齐,也避免把识别错误伪装成用户事实。
文本序列的掩码表达不可见内容。Padding、被删除的敏感片段、未授权文档和已经过期的上下文不能只用空白代替,否则模型无法区分“没有内容”和“内容被隐藏”。将掩码原因留在内部审计字段,用户侧只展示必要的解释。
| 层级 | 保存什么 | 适合做什么 | 典型风险 |
|---|---|---|---|
| 原始文本 | 原始字符、来源、时间 | 审计与回放 | 敏感信息扩散 |
| 规范化文本 | 规则版本、语言、结构 | 统一输入格式 | 过度清洗丢语义 |
| Token 序列 | ID、特殊标记、掩码 | 送入模型 | 词表/版本不一致 |
| 上下文向量 | 位置、层、池化方式 | 匹配与生成 | 误当成固定事实 |
| 引用片段 | 页码、字符范围、权限 | 回答证据 | 截断或权限失效 |
from dataclasses import dataclass
@dataclass
class TokenSpan:
token_id: int
text: str
start: int
end: int
source: str
def pack_text(text, vocab, source="user"):
spans = []
cursor = 0
for piece in text.split():
start = text.find(piece, cursor)
end = start + len(piece)
token_id = vocab.get(piece, vocab["<unk>"])
spans.append(TokenSpan(token_id, piece, start, end, source))
cursor = end
return spans
vocab = {"请": 1, "看": 2, "图片": 3, "<unk>": 0}
print(pack_text("请 看 图片", vocab))选择一段含数字、单位、英文型号和换行的文字,记录规范化前后、Token 数、特殊标记和引用范围;找一个清洗规则会改变含义的反例。
参考结论:Token 序列能回到原字符范围;未知词和截断可见;OCR/用户文本来源不同。
完整实现路径:分别记录文本 token、图像 patch、音频帧的生成规则,不用一个‘长度’掩盖三种尺度。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
from math import ceil
def patch_count(width, height, patch, add_cls=True):
tokens = ceil(width / patch) * ceil(height / patch)
return tokens + int(add_cls)
def frame_count(duration_ms, window_ms=25, hop_ms=10):
if duration_ms <= window_ms: return 1
return 1 + ceil((duration_ms - window_ms) / hop_ms)
assert patch_count(128, 96, 16) == 49
assert frame_count(83) == 7验收标准:边界输入的计数可以手算复核,预处理参数写入 manifest。
若结果不对:先核对原始媒体属性,再逐步执行规范化、切片和编码。
12.2.2 图像 Patch、区域与视觉 Token
图像表示常从规则 Patch 开始:把二维像素窗口展平并投影为向量,再加入二维位置。Patch 大小决定细节与计算的取舍;小 Patch 能保留文字和边缘,却会让 Token 数快速增加。视觉 Token 不是“看见了一个物体”,它只表示局部像素及其上下文,物体语义要由网络层层组合。
除了规则 Patch,模型还可以使用检测区域、分割区域、对象查询或多尺度特征。区域 Token 更集中于候选物体,适合定位和文档字段,但依赖前置检测器;规则 Patch 覆盖稳定、实现简单,却可能把小目标切碎。选择表示时应比较召回、空间精度、延迟和错误传播,而不是只看最终分数。
视觉位置必须跟着预处理变化。缩放、裁剪、补边、旋转和拼图会改变 Patch 与原图的映射;如果只传 Token 而不传有效尺寸和偏移,回答中的“左上角”无法回到原始图片。建立一个 position manifest,记录原图、输入图、Patch 网格和任何区域裁剪。
分辨率不是越高越好。高分辨率保留更多细节,但序列长度、显存和延迟上升;低分辨率成本低,却可能丢掉小字和细线。可以采用粗到细策略:先用低分辨率判断是否值得处理,再对候选区域做高分辨率编码,并把两阶段的证据关系写进结果。
视觉编码器通常输出连续向量,连接器再把它们变成语言模型能够消费的维度。连接器的投影、归一化和层选择会影响语言模型如何读取视觉信息;同一张图经过不同编码器可能产生不同的 Token 数和语义粒度。模型 manifest 应记录编码器、输入尺寸、Patch 大小、池化和连接器版本。
视觉 Token 还需要处理无效区域。补边像素、拼图空白、被打码区域和超出页面的 Patch 不应被当成真实内容。使用 attention mask 或有效区域坐标标记它们;若模型回答引用了补边或遮罩区域,应在校验层拦截,不能靠提示词提醒它“忽略空白”。
图像中的文字与自然图像细节有不同需求。文字更依赖高分辨率、方向和字符边界,颜色识别更依赖色彩一致性,空间布局更依赖二维位置。一个统一视觉 Token 序列未必在所有任务上都最优;必要时组合 OCR、检测或版面分析,并保留原图区域作为证据。
视频可以把图像表示扩展到时间。逐帧编码成本很高,抽帧又可能错过瞬间;关键帧、滑窗、运动采样和音视频同步各有误差。视频 Token 要有 frame_id 和 timestamp,模型输出的“发生过”必须能指向时间区间,而不是只给一段无来源的摘要。
视觉 Token 的压缩会带来不可逆信息损失。把几十万个像素压成几百个向量后,模型不能可靠恢复每个细节;如果任务需要读取小数点或区分相似零件,应在输入和评估阶段承认这个限制。不要用语言模型的流畅补全掩盖编码阶段已经丢失的证据。
学习实践可以先用固定图片改变 Patch 大小和位置,观察 Token 数、显存与输出差异;再用一个带小字和遮挡的测试集,比较规则 Patch、区域裁剪和 OCR 辅助。实验记录要包含原图、预处理、Token 配置和失败区域,避免只保存最终答案。
视觉 Token 的空间审计
给每个视觉 Token 保存最小空间元数据:原图 ID、输入尺寸、网格行列、有效比例、裁剪窗口和补边偏移。查询时如果模型声称“右下角有按钮”,校验器可以把语言描述关联到候选区域,并检查区域是否超出原图或落在无效 Patch。
多尺度表示需要明确层级关系。低分辨率 Token 提供全局布局,高分辨率区域提供局部细节;两者不能简单相加后假设没有重复。记录区域的父 Token、缩放比例和来源,让评估人员知道答案依赖哪一级证据。
涉及敏感图像时,空间元数据本身也可能泄露信息。日志不要保存未经授权的原图坐标或可恢复的人脸区域;使用受控引用和最小范围。删除请求应能传播到 Token 缓存、向量索引和评估报告。
| 表示 | 优势 | 代价 | 适合验证 |
|---|---|---|---|
| 规则 Patch | 覆盖稳定、结构简单 | 小目标可能被混合 | Patch 平移、小目标召回 |
| 对象区域 | 空间语义集中 | 依赖检测器、会漏候选 | 区域召回与误差传播 |
| 多尺度 Token | 兼顾全局与细节 | 序列和缓存更复杂 | 分辨率/延迟曲线 |
| 视频关键帧 | 成本可控 | 漏掉短暂动作 | 时间覆盖率与缺失段 |
| OCR 辅助 | 文字可读、可引用 | 识别错误会传递 | 字符级回放与区域 |
| 遮罩/掩码 | 表达无效或隐私区域 | 可见证据减少 | 掩码区域拒答 |
from dataclasses import dataclass
@dataclass
class Patch:
row: int
col: int
left: int
top: int
right: int
bottom: int
valid: bool = True
def make_grid(width, height, patch_size):
patches = []
for top in range(0, height, patch_size):
for left in range(0, width, patch_size):
right = min(left + patch_size, width)
bottom = min(top + patch_size, height)
patches.append(Patch(top // patch_size, left // patch_size,
left, top, right, bottom,
right - left == patch_size and bottom - top == patch_size))
return patches
for patch in make_grid(100, 70, 32):
print(patch)分别用 Patch 16 和 Patch 32 划分一张 128×96 图片,计算 Token 数并画出最后一行/列是否完整;说明补边或裁剪怎样影响区域引用。
参考结论:记录原图与输入图尺寸;无效 Patch 有 mask;答案区域能映射回原图。
完整实现路径:分别记录文本 token、图像 patch、音频帧的生成规则,不用一个‘长度’掩盖三种尺度。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
from math import ceil
def patch_count(width, height, patch, add_cls=True):
tokens = ceil(width / patch) * ceil(height / patch)
return tokens + int(add_cls)
def frame_count(duration_ms, window_ms=25, hop_ms=10):
if duration_ms <= window_ms: return 1
return 1 + ceil((duration_ms - window_ms) / hop_ms)
assert patch_count(128, 96, 16) == 49
assert frame_count(83) == 7验收标准:边界输入的计数可以手算复核,预处理参数写入 manifest。
若结果不对:先核对原始媒体属性,再逐步执行规范化、切片和编码。
12.2.3 音频帧、频谱与时间轴
音频是一条随时间变化的波形,模型通常不会逐采样点处理长录音,而是按窗口切成帧,再提取频谱、梅尔特征或学习型声学向量。窗口长度与步长决定时间分辨率和频率分辨率:窗口短能定位瞬间,窗口长能分辨低频结构,但可能把多个事件混在一起。
采样率决定可表示的最高频率,也影响计算量。语音、环境声、音乐和超声任务需要不同带宽;随意重采样可能丢失辅音、报警音或设备细节。音频 manifest 应记录原采样率、目标采样率、声道数、位深、增益和裁剪方式,任何自动降噪也要有版本。
波形、频谱和音频 Token 之间不是等价替换。频谱保留时间与频率的二维结构,波形保留相位和完整信号,离散音频 Token 更适合接入语言模型但可能丢掉细节。选择哪种表示取决于任务是转写、说话人分离、事件检测、音乐理解还是音频生成。
时间戳是音频证据的基本坐标。转写文字要对应 start_ms 与 end_ms,声学事件要说明检测窗口和重叠规则,摘要中的“后来”要能映射到原始时间轴。由于窗函数和模型下采样会产生边界偏差,评估时应允许明确的容差,并保留原始帧索引。
重叠说话、噪声和静音不能都交给语言模型。说话人分离、端点检测、降噪和语言识别可以是前置工具,但每一步都会带来错误传播。结果协议应区分“未检测到语音”“语音存在但无法辨识”“说话人冲突”和“工具未运行”,让下游知道是否可以继续。
音频批处理需要长度掩码和有效比例。把十秒录音填充到一分钟会浪费大量计算,也可能让模型误以为长时间没有声音;按桶分组可以提高效率,但要保留原始顺序和时间偏移。流式处理还要处理跨块上下文,不能把每块独立转写后简单拼接。
音频与文本对齐不等于逐词准确。ASR 可能把一个长句压成一段时间,标点由模型补出,专名和数字容易错。需要时用词级或字符级时间戳,并把置信度、语言和说话人信息随转写保存。摘要引用应指向音频区间,而不是只引用模型生成的句子。
环境声与语音有不同的标签粒度。一个片段可能同时有人说话、键盘声和警报;强迫它只属于一个类别会丢失共现关系。多标签或事件列表更合适,但输出要定义重叠、优先级和最小持续时间。训练数据的标注规则应和推理协议保持一致。
隐私在音频里尤其容易被忽略。声音可能暴露姓名、住址、健康信息和生物特征;即使删除转写,原始波形和说话人向量仍可能被识别。设定最小保存期限、访问权限和脱敏策略,必要时只保存片段哈希与统计字段,而不是默认保留整段录音。
练习时可以用一段自录、无敏感内容的短音频,改变窗口、步长和采样率,比较时间定位与文件大小。再加入静音、重叠说话和背景噪声,检查系统何时应该返回不确定。音频表示的目标不是把波形画得漂亮,而是让答案能够回到正确的时间证据。
音频表示的时间审计
至少保留四个时间坐标:原始文件时间、预处理后时间、帧索引和模型输出时间。裁剪、重采样、拼接和流式缓冲都可能引入偏移;转换函数要有单元测试,确保任意片段映射回原始时间的误差在约定范围内。
当模型回答“第 32 秒有人说延期”时,系统应返回音频 ID、区间、说话人状态和转写文本。若只能定位到一个宽窗口,就明示精度;不能把窗口中心点伪装成精确时刻。对高风险会议记录,保留人工复核和原音播放入口。
音频特征和说话人向量属于派生数据,权限不应自动继承为公开。删除原始录音后,缓存、转写、向量、日志和备份引用都要进入删除回放;训练数据中的音频也要有可追溯的来源和撤回流程。
| 表示 | 保留信息 | 常见用途 | 需要记录 |
|---|---|---|---|
| 波形 | 完整时序信号 | 高保真分析/生成 | 采样率、声道、位深 |
| 频谱 | 时间×频率结构 | 语音/环境声分类 | 窗长、步长、滤波器 |
| 声学向量 | 压缩的局部语义 | ASR、事件检测 | 编码器、帧率、掩码 |
| 离散音频 Token | 语言模型可消费序列 | 语音语言模型 | 词表、码率、版本 |
| 转写文本 | 可读语义与来源 | 搜索、总结 | 时间戳、语言、置信度 |
from dataclasses import dataclass
@dataclass
class Frame:
index: int
start_ms: int
end_ms: int
valid: bool
def frame_audio(duration_ms, window_ms=25, hop_ms=10):
frames = []
index = 0
start = 0
while start < duration_ms:
end = min(start + window_ms, duration_ms)
frames.append(Frame(index, start, end, end - start == window_ms))
index += 1
start += hop_ms
return frames
frames = frame_audio(83)
print({"count": len(frames), "last": frames[-1]})用窗口 25ms、步长 10ms 处理 83ms 音频,列出最后三个帧的起止时间;说明最后一个不完整帧应该补零、丢弃还是标记无效。
参考结论:时间从原始音频开始计算;补零不等于真实声音;输出引用带容差和状态。
完整实现路径:分别记录文本 token、图像 patch、音频帧的生成规则,不用一个‘长度’掩盖三种尺度。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
from math import ceil
def patch_count(width, height, patch, add_cls=True):
tokens = ceil(width / patch) * ceil(height / patch)
return tokens + int(add_cls)
def frame_count(duration_ms, window_ms=25, hop_ms=10):
if duration_ms <= window_ms: return 1
return 1 + ceil((duration_ms - window_ms) / hop_ms)
assert patch_count(128, 96, 16) == 49
assert frame_count(83) == 7验收标准:边界输入的计数可以手算复核,预处理参数写入 manifest。
若结果不对:先核对原始媒体属性,再逐步执行规范化、切片和编码。
本节复习与易错
易错一:把字符数、Token 数、Patch 数和音频帧数当成同一种长度。它们对应不同模态和不同时间/空间粒度,必须分别统计并用 mask 管理。易错二:把向量当作可解释标签。向量需要来源、版本和相似度度量,不能脱离原始证据。
易错三:预处理后丢掉原始坐标和时间。没有映射,就无法解释“图片右下角”或“第 32 秒”的来源。易错四:用空白填充缺失模态。应记录缺失、遮蔽、无效和未采集的原因。
本节验收:提交文本 Token 审查、视觉 Patch 网格、音频帧时间轴和一张模态表示对比表;每个表示都能回答保留了什么、丢掉了什么、如何回放和何时应拒答。
模态表示的复核实验
把同一个概念分别写成中文、英文、数字和带单位的文本,观察 Token 边界、长度和截断位置;再把同一张图裁成全图、局部和低分辨率版本,记录 Patch 网格与区域映射。实验不追求某个固定数字,而是确认每个表示的粒度和损失都能被描述。
对音频先画出原始时间轴,再比较不同窗口、步长和采样率。短窗口便于定位瞬间,长窗口有利于低频结构;如果模型输出的时间范围超过原始片段,说明偏移或 padding 没有被正确处理。把帧索引、有效 mask 和原始毫秒同时保留。
跨模态批次中分别统计文本 Token、视觉 Token、音频帧和 padding 比例。一个模态长度过大可能主导显存和注意力;通过分桶、池化或分段减少预算时,记录小字、短事件和否定词是否受到影响。成本变化必须和证据质量一起解释。
表示的版本包括词表、特殊标记、图像预处理、Patch 大小、音频采样率、特征编码器和池化方式。版本变更后用固定媒体重新生成 Token 摘要,并比较引用区域/时间是否仍然有效。只比较最终回答,会漏掉表示层已经发生的变化。
对于敏感媒体,保存表示摘要而非默认保存全部原数据。向量、声纹和 OCR 文本也可能泄露内容;删除与撤权测试要覆盖派生表示。学习者应能回答哪些字段用于回放、哪些字段必须加密、哪些字段不能进入训练。
统一接口不代表信息等价;Token、Patch 和帧必须带自己的位置、时间、来源与有效掩码。
| 模态 | 输入摘要 | 位置字段 | 损失风险 |
|---|---|---|---|
| 文本 | 规范化文本/词表版本 | 字符范围/Token 索引 | 截断、清洗丢单位 |
| 图像 | 尺寸/颜色/Patch 配置 | 网格、原图框、裁剪偏移 | 缩放、池化丢小字 |
| 音频 | 采样率/声道/帧配置 | 毫秒、帧索引、块偏移 | 重采样、padding、延迟 |
| 共享 | 向量维度/归一化 | 来源 ID、mask | 权限泄露、版本不兼容 |
选择同一课程概念的文字、截图和短音频,分别记录 Token/Patch/帧摘要、来源和损失;说明下游回答如何引用它们。
参考结论:合格标准:每种表示的粒度、位置/时间和版本清楚;缺失或遮蔽不会被写成正常内容。
完整实现路径:分别记录文本 token、图像 patch、音频帧的生成规则,不用一个‘长度’掩盖三种尺度。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
from math import ceil
def patch_count(width, height, patch, add_cls=True):
tokens = ceil(width / patch) * ceil(height / patch)
return tokens + int(add_cls)
def frame_count(duration_ms, window_ms=25, hop_ms=10):
if duration_ms <= window_ms: return 1
return 1 + ceil((duration_ms - window_ms) / hop_ms)
assert patch_count(128, 96, 16) == 49
assert frame_count(83) == 7验收标准:边界输入的计数可以手算复核,预处理参数写入 manifest。
若结果不对:先核对原始媒体属性,再逐步执行规范化、切片和编码。
表示层的迁移练习
把同一段课程内容分别保存为纯文本、带页码的结构化文本、截图和音频,要求系统返回同一个事实。比较四种输入的引用形式、Token/帧预算和失败状态;如果某种表示只能给出无来源的概括,应明确它不适合作为该任务的最终证据。
尝试只修改预处理而不修改模型:改变文字规范化、图像插值、Patch 大小、音频窗口或采样率。用固定媒体回放,观察哪些字段和指标变化。任何变化都写入 manifest,并在报告中区分表示改变与模型改变。
对每种模态设计一个“最小可见单元”:文本的数字和否定词、图像的一个小字区域、音频的一段短事件。遮掉这个单元后,模型应该进入不确定或给出可解释降级;若仍给出相同答案,说明模型可能依赖其他模态或语言先验。
跨模态批处理时统计有效 Token、padding、mask、来源 ID 和权限域。把一个私密媒体与公开媒体放入同批次,检查缓存、日志和结果是否隔离;训练和推理都不应通过批处理顺序泄露媒体身份。
表示迁移的验收不只是“接口能运行”。读者应能回答每个数字来自哪个字符/区域/时间、每个缺失为何发生、每个版本如何回放,以及删除请求怎样传播到派生表示。
| 变化 | 固定项 | 需要比较 | 通过标准 |
|---|---|---|---|
| 文本规范化 | 词表/模型 | Token、引用、长度 | 单位和否定不丢 |
| 图像预处理 | 编码器/连接器 | Patch、区域、召回 | 坐标可回放 |
| 音频采样 | 模型/提示 | 帧、时间、WER | 短事件不静默消失 |
| 批处理 | 样本/模型 | mask、来源、权限 | 无跨媒体泄露 |
表示层迁移检查
当项目从截图迁移到手机照片、从普通话迁移到方言或从短文本迁移到长文档时,先重新统计 Token、Patch、帧和 padding,再检查引用是否仍能定位。不要假设表示的长度比例和质量分布保持不变。
把一条可复现样本写成“原始媒体→预处理→表示摘要→模型输入→证据引用”的记录。新版本只要其中一环发生变化,就重跑这条记录;发现小字、短音或数字丢失时,降低任务范围或切换专用工具。
本节通过标准:每种模态都有可解释粒度、位置/时间和版本;缺失与遮蔽有不同状态;派生向量、OCR 和 ASR 都能按权限删除。
表示层的最小验收
拿一条文字、一块图像区域和一段音频分别做回放,确认 Token、Patch、帧都能保留来源、位置或时间。任何 padding、遮蔽、裁剪和重采样都要带状态与版本,不能让派生表示看起来像原始证据。
通过标准:读者能解释表示保留了什么、丢失了什么、如何映射回媒体,以及删除或撤权后哪些向量、OCR、ASR 和缓存必须失效。
03 · ALIGNMENT
对齐学习:让不同模态互相找到
对齐不是把文字和图像投到同一个空间就结束了,而是让“相关的内容更接近、不相关的内容可区分”,并且知道这种关系来自哪条数据、哪种语言和哪种时间或空间证据。共享向量、样本配对、负例构造和诊断必须一起设计。
12.3.1 共享空间与对比目标
共享空间的直觉是把不同模态编码器的输出映射到同一维度,再用相似度衡量它们是否描述了同一对象。图像编码器输出视觉向量,文本编码器输出句子向量,投影头负责维度和尺度;如果正配对相似度高、负配对相似度低,检索和零样本分类就有了基础。
余弦相似度常用于比较方向而不是绝对长度。先归一化向量,再计算点积,可以减少向量范数对排名的影响;但归一化不意味着结果天然是概率。温度参数会改变 softmax 的尖锐程度,过小可能过度强调少数难例,过大又让梯度过弱,必须在固定验证集上观察。
对比目标通常把一个 batch 里的其他样本当作负例。这个假设只有在样本确实不相关时才成立;同一商品的不同图片、同一视频的相邻帧和同一主题的两种描述可能被错误当成负例。批次构造因此是学习目标的一部分,不能只由数据加载器随意打乱。
双向对齐常同时优化图像到文本和文本到图像的检索。两个方向的难度可能不同:一张图片可以对应多条描述,而一句泛化文字可能对应许多图片。只报告一个方向会掩盖不对称性;应分别测 R@1、R@5、R@10、平均排名和未知类别表现。
对齐空间学到的是训练数据中的关联,不是世界的本体。图片与“狗”配对,可能让模型学到草地、相机风格或网页标题;如果标题带有类别捷径,零样本看似准确,换场景就会下降。对齐诊断需要加入反事实:保持主体不变换背景,或保持背景不变换主体,观察相似度是否仍指向任务对象。
共享空间维度和投影头容量也会影响结果。维度太小会挤压不同概念,维度太大则更容易记住样本 ID 或来源风格。投影头可以帮助训练,但下游使用哪个层、是否保留投影前表示要明确;用于检索的向量和用于生成连接器的向量未必应该相同。
语言和文化差异会在共享空间留下不均匀结构。相同图片的中文、英文和少数语言描述可能距离不同,术语、方言和文字脚本也会改变 Token 长度。多语言对齐要有语言切片、同义改写和跨语言检索测试,不能用主流语言平均分代表所有用户。
对齐训练还涉及尺度和批次统计。跨设备训练时,某一模态的向量范数或噪声分布可能不同;梯度累积和分布式负例会改变有效 batch size。记录 batch 构成、温度、归一化、负例范围和随机种子,才能解释同一模型在不同训练配置下的差异。
当正例关系有等级时,二元相似/不相似过于粗糙。图片与精确标题是强正例,图片与同类主题描述可能是弱正例,图片与相邻页面可能是上下文正例。可以使用分级标签、软目标或多任务损失,但必须说明业务上哪些关系需要检索到前几名,哪些只需要粗分组。
学习对比目标时,不要只背公式。先画出一个小 batch 的相似度矩阵,标出对角线正例、同主题样本和疑似假负例;再改变温度和归一化,观察概率分布怎样变化。公式、数据关系和检索指标必须互相对应,才算真正理解对齐。
对比学习的读法
设图像向量为 v,文本向量为 t,归一化后相似度为 v·t,温度为 τ。图到文的损失会让正确文本在一行中排名靠前,文到图的损失会让正确图像在一列中排名靠前。实现时检查 logits 的形状、标签索引和 mask;最常见的 bug 不是公式错,而是配对顺序被打乱。
温度是优化旋钮,不是模型理解程度。较低温度会放大排名差异,训练可能更快但对噪声和假负例敏感;较高温度让分布平滑,可能降低尖锐检索收益。报告中同时给温度、训练曲线和验证集检索结果,不能只记录最终 loss。
对齐空间的相似度需要任务校准。相似度高表示编码器认为内容接近,不表示图片真的证明了文本命题,更不表示权限、时间或因果关系成立。检索返回前做来源、时间和权限过滤,并在回答中把“相似候选”和“验证证据”分开。
| 量 | 含义 | 应检查 | 异常信号 |
|---|---|---|---|
| 正配对 | 同一语义或同一事件 | 关系规则、来源、时间 | 同一对象被当负例 |
| 负配对 | 应可区分的样本 | 是否有假负例 | loss 降而检索不升 |
| 相似度 | 向量间比较值 | 归一化、度量、范围 | 范数主导排名 |
| 温度 | softmax 尖锐度 | 版本、搜索范围 | 梯度过尖/过平 |
| 方向 | 图→文或文→图 | 分别报告指标 | 一侧好一侧坏 |
| 表示层 | 投影前/后向量 | 下游用途、维度 | 检索与生成不一致 |
import math
def l2_normalize(rows):
normalized = []
for row in rows:
norm = math.sqrt(sum(value * value for value in row)) or 1.0
normalized.append([value / norm for value in row])
return normalized
def cosine_logits(left, right, temperature=0.07):
if temperature <= 0:
raise ValueError("temperature 必须大于 0")
if not left or len(left) != len(right):
raise ValueError("图像与文本必须是数量相同的非空批次")
left = l2_normalize(left)
right = l2_normalize(right)
return [
[sum(a * b for a, b in zip(x, y)) / temperature for y in right]
for x in left
]
def row_cross_entropy(logits):
losses = []
for target, row in enumerate(logits):
peak = max(row)
log_denom = peak + math.log(sum(math.exp(value - peak) for value in row))
losses.append(log_denom - row[target])
return sum(losses) / len(losses)
def symmetric_contrastive_loss(left, right, temperature=0.07):
logits = cosine_logits(left, right, temperature)
transposed = [list(column) for column in zip(*logits)]
loss = (row_cross_entropy(logits) + row_cross_entropy(transposed)) / 2
return loss, logits
images = [[1.0, 0.0], [0.0, 1.0], [0.8, 0.2]]
texts = [[0.9, 0.1], [0.1, 0.9], [0.7, 0.3]]
loss, logits = symmetric_contrastive_loss(images, texts)
print("symmetric loss:", round(loss, 4))
for row in logits:
print([round(value, 3) for value in row])用三张图片和三条文字描述构造 3×3 相似度矩阵,标出正例、假负例和一个背景捷径;说明温度变化会如何改变 softmax 排名。
参考结论:正例关系有依据;分别讨论图到文和文到图;相似度不被写成概率或事实。
完整实现路径:对嵌入做归一化,显式计算相似度矩阵、温度缩放和双向检索;困难负例必须人工核验。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
import torch
import torch.nn.functional as F
image = F.normalize(torch.tensor([[1., 0.], [0., 1.], [1., 1.]]), dim=1)
text = F.normalize(torch.tensor([[.9, .1], [.2, .8], [.7, .7]]), dim=1)
temperature = 0.07
logits = image @ text.T / temperature
image_to_text = logits.argmax(dim=1)
text_to_image = logits.argmax(dim=0)
assert image_to_text.tolist() == [0, 1, 2]
assert text_to_image.tolist() == [0, 1, 2]验收标准:正配对排名、双向召回和反事实样例都有记录。
若结果不对:先查配对 ID,再查归一化与温度,最后分析负例质量。
12.3.2 正负样本与数据配对
配对质量决定对齐上限。网页图文对可能只是标题和缩略图的弱关系,用户上传的图片与问题可能是强关系,视频字幕与画面又带有时间延迟。先给配对定义强度和来源,再决定是否进入训练、验证或只用于诊断。不要把所有“同一页面”都当作同一语义。
建立正例时记录实体、事件、时间和空间粒度。相同商品不同角度可以是实例正例,类似商品可以是类别正例,同一视频不同时间可能只有上下文关系。下游检索要找实例还是找主题,决定哪些样本应靠近;标签不清会让模型同时收到互相矛盾的梯度。
负例不能只随机抽取。随机负例很容易,模型学会粗粒度词和来源后就能区分;真正能检验理解的是难负例,例如同一场景的相似器材、同一会议的不同决定、同一页面的相邻字段。难负例要有来源和人工理由,避免把本来相关的内容误删。
假负例是多模态对比训练的核心风险。一个图片可能有多个正确标题,一段音频可能同时包含几个事件,同一问题的不同答案可能都合理。可用多正例 mask、软标签、分组采样或去除近重复的方式处理;至少要在报告里统计假负例比例和修复前后指标。
弱监督配对需要置信层级。自动 alt 文本、用户标签、OCR、ASR、人工说明和专家报告的可靠性不同。把来源、生成算法、置信度和时间写入样本,训练时可以加权或过滤,评估时用高质量人工集做基准。不要让自动生成的说明成为唯一金标准。
跨模态配对还会带来版权与隐私风险。公开图片旁边的文字可能含个人信息,会议音频与字幕可能超出原授权用途;只因为能下载,不代表可以用于训练。数据卡记录授权、用途、删除和对外共享规则,撤回后能找到所有派生样本和缓存。
多语言配对要避免翻译捷径。把英文 caption 机器翻译成中文再训练,模型可能只学到翻译风格,而非图像概念。保留原文、翻译器版本和人工抽检,加入自然产生的中文描述和跨语言同图检索;评估时检查不同语言的检索方向是否对称。
配对时间窗口也应可验证。视频字幕提前或滞后几秒,可能让动作与文字错配;直播评论可能在事件发生后才发布;照片与传感器读数的时间戳可能来自不同设备。定义允许偏移和事件持续时间,记录对齐算法,不要只按文件名和排序号关联。
数据拆分必须在配对和去重之后完成。如果同一图片的多种 caption 被分到训练和测试,或同一音频切片跨集合,检索指标会被显著高估。先按实体、事件或媒体源分组,再在组内决定训练/验证/测试;拆分规则要锁定,新增数据重新审查。
完成配对审计后,给每条样本一个“可用于什么”的状态:训练、验证、诊断、仅供人工或禁止使用。这样数据工程和模型实验共享同一语义,后续发现错误时可以准确撤回,而不是在几十个数据副本里手工寻找。
配对审查表
对每条配对问五个问题:两个模态是否描述同一对象/事件?时间窗口是否有依据?空间区域是否能定位?文本是否由自动系统生成?使用目的和权限是否覆盖训练与展示?任一问题没有答案,就降低样本权重或移到诊断集。
抽样时同时看强正例、弱正例、难负例和假负例。把原始媒体、派生文本、时间/区域和审查理由并排展示;只看表格里的 ID 容易错过图像背景、字幕延迟或语义冲突。审查记录本身也要版本化。
新增数据不应默认继承旧配对规则。页面模板、设备、语言和业务流程变化会改变关联质量;每次数据源升级都重跑去重、时间对齐、权限与质量抽检,并在报告中区分模型提升来自数据量还是配对质量。
| 样本关系 | 例子 | 训练处理 | 审查问题 |
|---|---|---|---|
| 强正例 | 同一图与人工描述 | 高权重/主训练 | 是否同一对象和事件 |
| 弱正例 | 缩略图与网页标题 | 低权重/辅助 | 标题是否只是 SEO 文本 |
| 难负例 | 同场景相似对象 | 保留/分桶 | 是否误把同主题当负例 |
| 假负例 | 同图多个正确描述 | 多正例 mask | 是否存在其他合法答案 |
| 未对齐 | 字幕时间不明 | 诊断或丢弃 | 能否定位时间窗口 |
| 敏感配对 | 人物图与姓名 | 限制或脱敏 | 授权是否覆盖用途 |
from collections import defaultdict
def group_pairs(records):
groups = defaultdict(list)
for row in records:
key = (row["entity_id"], row["event_id"])
groups[key].append(row)
return groups
def sample_negatives(records, positive, limit=3):
same_scene = [
row for row in records
if row["scene"] == positive["scene"]
and row["entity_id"] != positive["entity_id"]
]
easy = [row for row in records if row["entity_id"] != positive["entity_id"]]
candidates = same_scene + easy
seen = set()
result = []
for row in candidates:
if row["id"] in seen:
continue
seen.add(row["id"])
result.append(row)
if len(result) == limit:
break
return result
records = [
{"id": "a", "entity_id": "cup-1", "scene": "lab", "event_id": "e1"},
{"id": "b", "entity_id": "cup-2", "scene": "lab", "event_id": "e2"},
{"id": "c", "entity_id": "plate-1", "scene": "home", "event_id": "e3"},
]
print(sample_negatives(records, records[0]))为一个图文检索任务写五条强正例、五条难负例和两条可能的假负例;给每条样本添加实体、事件、时间或空间字段。
参考结论:难负例不是随便找的相似文本;假负例有明确依据;拆分不会让同一实体跨集合。
完整实现路径:对嵌入做归一化,显式计算相似度矩阵、温度缩放和双向检索;困难负例必须人工核验。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
import torch
import torch.nn.functional as F
image = F.normalize(torch.tensor([[1., 0.], [0., 1.], [1., 1.]]), dim=1)
text = F.normalize(torch.tensor([[.9, .1], [.2, .8], [.7, .7]]), dim=1)
temperature = 0.07
logits = image @ text.T / temperature
image_to_text = logits.argmax(dim=1)
text_to_image = logits.argmax(dim=0)
assert image_to_text.tolist() == [0, 1, 2]
assert text_to_image.tolist() == [0, 1, 2]验收标准:正配对排名、双向召回和反事实样例都有记录。
若结果不对:先查配对 ID,再查归一化与温度,最后分析负例质量。
12.3.3 对齐诊断与来源追踪
对齐诊断从检索开始,但不止看 Recall@K。抽取一组图片和文本,画相似度矩阵,检查正例是否集中在对角线;再看同一实体的不同语言、不同背景和不同设备是否聚在一起。矩阵能暴露塌缩、来源聚类和假负例,往往比单一分数更早发现问题。
做模态消融可以回答“模型到底依赖谁”。只给图片、只给文字、交换文字、打乱区域、替换背景或遮挡关键音频,比较输出变化。如果去掉图片分数几乎不变,说明任务可能被文本捷径解决;如果任何随机图片都得到相同答案,说明模型忽略了视觉证据。
对齐质量还要看跨模态检索的方向和距离分布。图到文召回好、文到图召回差,可能是一个文本对应多图或文本编码器容量不足;距离分布过于集中,可能是温度、归一化或数据重复造成。记录分位数、类别和语言切片,不要只报告平均排名。
来源追踪把每个向量连接回媒体和样本版本。检索结果显示图片 ID、文字来源、训练/评估状态、许可和时间;删除或撤权后,索引应能过滤并回放。向量数据库不是脱离原始数据的独立事实库,权限和生命周期要沿关系传播。
对齐模型在新领域中常见域偏移。课程截图、工业设备、医学图像、方言录音和网络照片的纹理、语言和标签粒度不同;直接使用通用模型可能在总体检索上还行,却在关键术语或小区域上失败。建立小型领域集,比较零样本、提示改写和轻量微调的收益与代价。
相似度不是证据强度。一个描述与图片很接近,可能只是因为都包含“实验室”这个词;它不能证明图片里的读数、身份或因果关系。回答层要把检索用于候选召回,随后使用区域/时间工具或原始文档核验,输出中区分“候选来源”和“已验证事实”。
对齐诊断要包含异常和攻击。恶意文字可以把图文相似度推向某个类别,带有水印的图片可能让模型识别来源而不是对象,音频中的背景音乐可能成为捷径。测试内容注入、背景替换、文本覆盖、裁剪和噪声,记录模型是否会过度依赖非任务信号。
评估样本应该固定但不能被过度调参。开发集用于发现问题,保留集用于最终判断;如果每次改提示都在同一小集上试到最好,指标会失去独立性。保存运行配置、样本顺序、随机种子和人工修订,定期更新挑战集而不是反复消费同一套。
对齐报告不仅给结论,还给下一步假设。例如“中文检索低于英文,可能是词表和配对来源差异;下一步补充自然中文描述并重跑语言切片”。把假设、证据、实验和负责人写成错误地图,避免把一次分数变化描述成确定根因。
学习者最终应能从一条检索结果反向走完链路:查询 Token/音频帧/图像区域如何编码,向量如何比较,正负关系来自哪条数据,结果为何通过权限过滤,回答用什么工具验证。能完成这条链路,才是掌握了对齐,而不是背下一个损失函数。
对齐诊断的最小报告
报告至少包含:固定查询集、正负定义、图到文/文到图指标、语言/设备/场景切片、距离分布、消融结果、近邻样例、假负例比例、向量版本与权限过滤。每个数字都附样本量与配置,失败样例提供原媒体和区域/时间引用。
做背景替换或文字交换时,保持主变量只有一个。若换背景同时改变了压缩和尺寸,就不能把差异归因于背景;若遮挡图片又改变了问题文字,也不能说模型忽略了图像。反事实实验越小越好,但必须能复现。
当模型升级或重新索引时,重新计算近邻和删除回放。旧向量与新向量不能混用;如果向量维度或归一化改变,索引需要重建。报告中记录迁移期间的双写、切换和回滚窗口,避免线上结果不可解释。
| 维度 | 问题 | 证据 | 修复方向 |
|---|---|---|---|
| 检索方向 | 图→文与文→图是否对称 | 双向 Recall@K | 多正例/编码器容量 |
| 语言 | 某语言是否系统性偏低 | 语言分位数与样例 | 自然配对/词表/微调 |
| 场景 | 背景或来源是否成捷径 | 反事实替换 | 去重/采样/增强 |
| 粒度 | 实例、类别还是主题对齐 | 实体与标签切片 | 重新定义正负 |
| 鲁棒 | 遮挡/噪声后是否仍定位 | 消融与挑战集 | 区域工具/拒答 |
| 治理 | 结果是否可追溯可删除 | 版本、权限、删除回放 | 索引重建/过滤 |
from collections import defaultdict
def recall_at_k(ranked_ids, gold_ids, k):
return int(bool(set(ranked_ids[:k]) & set(gold_ids)))
def summarize(cases, k=5):
buckets = defaultdict(list)
for case in cases:
hit = recall_at_k(case["ranked"], case["gold"], k)
buckets[case["slice"]].append(hit)
return {
name: {
"count": len(values),
"recall": sum(values) / len(values),
}
for name, values in buckets.items()
}
cases = [
{"slice": "zh", "ranked": ["img-1", "img-9"], "gold": ["img-1"]},
{"slice": "audio", "ranked": ["img-7"], "gold": ["img-2"]},
]
print(summarize(cases))从一个近邻结果出发,记录数据配对、编码、相似度、权限和回答验证五个环节;提出一个反事实实验和一个可回滚的修复。
参考结论:合格标准:结论有样例和指标;能区分假负例、捷径、域偏移和索引问题;修复不会绕过权限。
完整实现路径:对嵌入做归一化,显式计算相似度矩阵、温度缩放和双向检索;困难负例必须人工核验。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
import torch
import torch.nn.functional as F
image = F.normalize(torch.tensor([[1., 0.], [0., 1.], [1., 1.]]), dim=1)
text = F.normalize(torch.tensor([[.9, .1], [.2, .8], [.7, .7]]), dim=1)
temperature = 0.07
logits = image @ text.T / temperature
image_to_text = logits.argmax(dim=1)
text_to_image = logits.argmax(dim=0)
assert image_to_text.tolist() == [0, 1, 2]
assert text_to_image.tolist() == [0, 1, 2]验收标准:正配对排名、双向召回和反事实样例都有记录。
若结果不对:先查配对 ID,再查归一化与温度,最后分析负例质量。
本节复习与易错
易错一:把共享向量当作共同事实。对齐只表示训练数据中的可比较关系,不等于证明命题。易错二:随机负例当作充分评估。难负例和假负例更能检验模型是否理解对象、事件和时间。
易错三:只看一个 Recall@K。双向检索、语言/设备切片、距离分布和消融才能说明模型是否使用正确证据。易错四:向量升级后继续复用旧索引。维度、归一化和权限变化都可能要求重建。
本节验收:提交一个相似度矩阵、配对审查表、对齐诊断报告和删除/权限回放记录;读者能解释检索候选为何出现,并说明它还需要什么独立验证。
对齐空间的反事实实验
先建立一个小型图文和音频文本配对集,再做三组反事实:保持主体换背景、保持文本换图片、保持内容换语言。记录相似度矩阵、双向召回和近邻来源;如果相似度主要跟背景或来源变化,说明模型学到了捷径。
对同一实体的多条描述使用多正例 mask,比较单正例和多正例损失;统计假负例比例与检索变化。对事件时间不确定的音频—文本对,扩大或缩小时间窗,观察召回与错误引用如何变化。
对齐诊断必须连到权限。检索到的向量在权限过滤前后分别统计,撤权后重跑近邻与缓存命中;如果结果仍返回被撤媒体,说明索引或缓存没有接收删除事件。把删除时间和索引版本写进回放。
语言切片和设备切片要有样本量,少量样本报告不确定而非确定结论。对比不同语言的正例距离、难负例距离和未知类别拒答,找出是词表、配对质量还是文化语义造成差异。
最终将每个错误归为数据、负例、编码、温度、索引、权限或回答验证层,并为一类错误设计最小修复。修复后重跑旧集和新增挑战集,防止指标提升只是换了评估分布。
对齐结果既要看相似度,也要能通过反事实、来源与权限检查;候选近邻不是事实证明。
| 实验 | 只改变什么 | 观察指标 | 解释边界 |
|---|---|---|---|
| 背景替换 | 背景不变主体 | 相似度/召回 | 发现来源捷径 |
| 文本改写 | 语义不变措辞 | 双向 Recall | 发现模板依赖 |
| 语言切换 | 内容不变语言 | 语言切片 | 发现词表/配对差异 |
| 时间平移 | 内容不变偏移 | 时间重叠/引用 | 发现同步误差 |
| 权限撤回 | 可见性 | 过滤/缓存命中 | 验证治理链路 |
制作一个 3×3 相似度矩阵和两组反事实输入,报告双向检索、假负例、来源权限和下一步修复假设。
参考结论:合格标准:每个结论有样例/指标;修复能回滚;不把相似度写成概率或事实。
完整实现路径:对嵌入做归一化,显式计算相似度矩阵、温度缩放和双向检索;困难负例必须人工核验。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
import torch
import torch.nn.functional as F
image = F.normalize(torch.tensor([[1., 0.], [0., 1.], [1., 1.]]), dim=1)
text = F.normalize(torch.tensor([[.9, .1], [.2, .8], [.7, .7]]), dim=1)
temperature = 0.07
logits = image @ text.T / temperature
image_to_text = logits.argmax(dim=1)
text_to_image = logits.argmax(dim=0)
assert image_to_text.tolist() == [0, 1, 2]
assert text_to_image.tolist() == [0, 1, 2]验收标准:正配对排名、双向召回和反事实样例都有记录。
若结果不对:先查配对 ID,再查归一化与温度,最后分析负例质量。
对齐到下游的迁移练习
共享空间在检索任务中表现良好,不代表它能直接支持生成或敏感判断。把同一向量用于候选召回、聚类和问答,分别记录需要的证据与权限;当下游要求精确数值或身份时,明确回到原始媒体或专用工具。
做一个小型领域迁移:保持编码器和对比目标不变,只替换配对数据的设备、语言或场景。报告零样本、提示改写和轻量微调三条路线,比较关键切片、假负例和成本。不要用领域测试集反复调到最好再当成独立结论。
对齐错误通常有多种可能根因。通过背景替换、语言改写、时间平移、去水印和权限撤回五个实验逐项排除;每次只改变一个因素,保留矩阵和样例。把根因写成“证据支持的假设”,不把一次相关性说成定论。
索引升级需要双版本窗口。新旧向量并存时标记 index_version,查询结果按权限和版本过滤;切换后重跑旧回归和删除回放。若维度、归一化或正负规则变化,直接重建比隐式兼容更安全。
对齐迁移报告最后写出不能保证的内容:相似度不等于事实,候选不等于授权,跨语言相近不等于翻译正确。限制越明确,系统越不容易把向量空间的便利扩大成不当推断。
| 检查 | 证据 | 常见误判 | 正确做法 |
|---|---|---|---|
| 域偏移 | 设备/语言切片 | 总体分数没降 | 报告关键切片 |
| 假负例 | 多正例/实体分组 | loss 下降就可靠 | 修配对/软目标 |
| 捷径 | 背景/水印反事实 | 相似度高就是理解 | 去捷径再评估 |
| 索引升级 | 版本/删除回放 | 新旧向量可混用 | 重建并灰度 |
| 下游使用 | 原始证据/权限 | 向量能证明身份 | 限定用途 |
对齐结果迁移检查
把共享空间用于新语言、新设备或新场景前,先做少量人工审查和双向检索,不要直接把零样本分数当作上线证据。保留正例、难负例、假负例和未知样本,才能知道距离变化来自哪里。
检索候选进入回答前必须经过来源、时间和权限过滤;相似度只负责召回。若任务需要精确读数、身份或因果结论,回到原始图像/音频/文档或专用工具复核。
本节通过标准:能画相似度矩阵、做至少一次反事实、解释索引版本和删除回放,并明确向量不能证明的事情。
对齐层的最小验收
用一个强正例、一个难负例和一个假负例画出相似度关系,再做一次背景、语言或时间的反事实。检索候选只能进入来源与权限过滤后的证据链,不能直接成为事实。
通过标准:双向指标、假负例、索引版本和删除回放都有记录;读者能说清相似度适合支持哪种召回,不适合证明哪种结论。
04 · FUSION ARCHITECTURES
融合架构:从拼接到跨模态注意力
融合回答的是“不同模态在什么时候、以什么粒度互相读取”。早期融合把表示放在一起,晚期融合分别预测再合并,交叉注意力则让一个模态有选择地查询另一个模态。架构选择必须和证据关系、序列预算、延迟及缺失模态一起评估。
12.4.1 早期、晚期与交叉融合
早期融合把文本、图像或音频表示投影到共同维度,再按顺序拼接进一个序列。它的优点是模型可以在较早层建立细粒度关系,例如文字指向图像某个 Patch;缺点是序列变长、模态顺序敏感,且一个模态的噪声可能污染所有层。输入中每种 Token 的来源、位置和有效掩码必须明确。
晚期融合让每个模态先独立编码或预测,最后用加权、门控或小型融合网络组合结果。它更容易替换编码器、控制成本,也更容易在模态缺失时降级;但如果任务需要“这句话对应图片哪一块”或“这个声音和动作是否同时发生”,独立预测可能已经丢掉了细粒度关系。
交叉注意力位于两者之间。查询模态的 Token 生成 Query,被查询模态提供 Key/Value,注意力权重决定读取哪些证据。文本可以查询图像区域,音频事件可以查询字幕片段,图像区域也可以查询说明书句子。交叉注意力不是自动解释,它只提供信息通道;权重仍需和任务指标、遮挡实验一起验证。
融合方向很重要。图像查询文本与文本查询图像的功能不同,前者可能用于生成描述,后者可能用于找到图片候选;音频查询视频适合对齐动作,视频查询音频适合解释声音来源。单向结构便宜但可能丢掉反向约束,双向结构更强但计算和训练目标更复杂。
模态之间的长度不对称会影响融合。几百个图像 Token 与数千个 OCR Token 拼接时,文本可能主导注意力;短音频片段与长视频帧也会出现类似问题。可以分层池化、分桶、使用门控或限制每个模态的预算,确保稀有但关键的证据不被长序列淹没。
融合层还要处理顺序和类型。把图片 Token 放在问题前后可能改变生成结果,音频帧的时间顺序不能被普通位置编码替代,多个图片需要区分 image_id。使用模态类型 Embedding、段落边界、二维/时间位置和显式关系 Token,减少模型把不同来源误认为连续文本。
缺失模态要求融合函数有可解释降级。晚期融合可以跳过缺失分支;交叉注意力可以使用 mask,但 mask 的语义是“不可访问”而不是“内容为空”;早期拼接要避免用一个特殊向量掩盖缺失原因。输出状态应告诉用户系统使用了哪些模态,不能让降级结果看起来与完整输入相同。
融合的有效性可以用消融和替换测试。固定文本换图片、固定图片换问题、打乱音频时间、交换两个媒体的顺序,观察输出是否随正确证据变化。如果不同输入产生几乎相同答案,说明模型可能只依赖语言先验或某个强模态。实验要控制预处理和随机种子,否则无法归因。
选择融合位置还涉及训练稳定性。冻结编码器、只训练投影层通常更容易启动,但跨模态关系适应慢;解冻所有骨干能力强,却需要更多数据和显存,也更容易破坏原有单模态能力。逐阶段解冻、分层学习率和独立验证集可以帮助观察能力迁移。
学习架构时不要只画网络方框。为每条箭头写清张量形状、来源、掩码、位置和梯度是否通过;为每个融合层设计一个可观测测试。例如把一个区域遮住,检查回答中的区域引用是否消失;把音频片段静音,检查相应事件是否被移除。结构图要能指导实验。
如何选择融合方式
如果任务是粗粒度分类或成本优先,可以先用晚期融合建立基线;如果任务需要词与区域、事件与时间的细粒度关系,交叉注意力更合理;如果数据量大、序列预算充足且需要统一生成,早期或深层融合可以提供更强交互。不要把架构名称当作结论,先写证据关系和资源约束。
比较融合方式时固定单模态编码器、数据拆分、训练步数和评估集。报告精度之外的显存、吞吐、p95、缺失模态表现、冲突处理和可解释引用。一个总体分数略低但能安全降级、能回放证据的架构,可能更适合真实系统。
融合层的失败应有状态。维度不匹配、掩码错位、时间轴不存在、区域越界和工具返回空结果都不能静默继续;连接器应返回可读错误和版本。把张量形状与关系元数据一起记录,排查问题会比只看最终文本快得多。
| 方式 | 关系建立时间 | 优势 | 限制与适用 |
|---|---|---|---|
| 早期融合 | 输入/浅层 | 细粒度交互、统一上下文 | 序列长、噪声传播 |
| 晚期融合 | 预测前后 | 模块独立、容易降级 | 细节关系可能丢失 |
| 交叉注意力 | 中深层按需查询 | 控制读取方向与证据 | 掩码和计算更复杂 |
| 门控融合 | 层间动态加权 | 可表达模态可靠性 | 门控可能学成捷径 |
| 分层融合 | 局部→全局 | 兼顾区域/时间结构 | 实现与调参成本高 |
融合层决定信息如何流动,但不能代替任务协议;输出仍需引用具体区域或时间,并通过权限与状态校验。
from dataclasses import dataclass
@dataclass
class Stream:
name: str
tokens: list[list[float]]
mask: list[bool]
missing: bool = False
def concat_streams(streams, limit):
merged = []
source = []
for stream in streams:
if stream.missing:
continue
for token, valid in zip(stream.tokens, stream.mask):
if valid and len(merged) < limit:
merged.append(token)
source.append(stream.name)
return {"tokens": merged, "source": source}
streams = [
Stream("text", [[1, 0], [0, 1]], [True, True]),
Stream("image", [[0.2, 0.8]], [True]),
Stream("audio", [], [], missing=True),
]
print(fusion_router := concat_streams(streams, limit=4))给一个图文分类任务设计晚期融合、拼接融合和交叉注意力三个基线;列出每个基线的输入、计算预算、缺失图片时的行为和一项反事实测试。
参考结论:架构与证据关系对应;比较时固定数据和编码器;降级状态可被用户看见。
完整实现路径:把视觉编码器、投影器、查询 token、语言模型的形状写成接口合同,再比较早期、拼接和交叉注意力。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
def fusion_contract(visual, query, hidden=1024):
if visual.ndim != 3: raise ValueError("visual must be (B,N,D)")
if query.ndim != 3: raise ValueError("query must be (B,Q,H)")
if visual.shape[0] != query.shape[0]: raise ValueError("batch mismatch")
if query.shape[-1] != hidden: raise ValueError("hidden mismatch")
return {"batch": visual.shape[0], "visual_tokens": visual.shape[1],
"query_tokens": query.shape[1], "hidden": hidden}
visual = torch.randn(2, 196, 768)
query = torch.randn(2, 32, 1024)
assert fusion_contract(visual, query)["query_tokens"] == 32验收标准:缺失模态、伪模态和真实模态三种输入行为可区分。
若结果不对:逐层打印 B、token 数和 hidden size,优先定位第一处不一致。
12.4.2 连接器、查询 Token 与投影
连接器是把一个模态的表示变成另一个模态可消费表示的组件。最简单的是线性投影,把视觉向量从维度 d_v 变到语言模型维度 d_l;更复杂的连接器使用查询 Token、注意力池化或小型 Transformer,从大量视觉/音频 Token 中提取有限数量的跨模态 Token。连接器既是维度适配器,也是信息瓶颈。
投影层只解决形状,不保证语义对齐。若图像编码器和语言模型的训练目标不同,线性层需要足够的配对数据才能学会把“某个视觉模式”映射到“可生成的语言上下文”。训练时要观察连接器输出的范数、梯度和下游消融,避免它退化成总是输出相似向量。
查询 Token 允许模型主动选择证据。固定数量的查询向视觉特征提问,再把结果送入语言模型,可以把高分辨率图像压缩成较短序列;查询数量太少会漏掉小目标和多个实体,太多又会增加计算并把噪声带入生成。查询预算应和任务对象数量、空间粒度及输出需求相匹配。
连接器的训练策略通常有几种:冻结两端只训练投影,先冻结语言模型再逐步解冻,或联合训练所有组件。冻结能保护预训练能力并降低资源,联合训练能适配新域但需要更多数据和防止灾难性遗忘。每种策略都要报告单模态回归,确认多模态收益没有以文本或视觉基本能力崩溃为代价。
连接器需要传递位置与时间。把视觉特征池化成一个向量会丢掉空间,音频全局池化会丢掉顺序;若任务需要引用区域或时间,连接器必须保留坐标/时间索引,或者附带可追踪的聚合关系。一个语言 Token 可以聚合多个 Patch,但输出证据列表仍应能展开到原始范围。
多图片、多段音频和多页文档会带来连接器边界。为每个媒体加入独立的开始/结束标记和 source_id,防止模型把不同图片拼成一个对象;分页或分段后要记录父子关系,避免跨页引用错误。批处理的 padding 不能被当成真实查询结果。
连接器还可能成为安全边界。未经权限过滤的视觉 Token 或 OCR Token 一旦进入语言模型,后续层很难恢复原权限;图片内文字可能包含提示注入,连接器应在输入侧标记不可信来源,工具控制器不能仅凭模型输出执行动作。连接器传递的是信息,不是授权。
调试连接器可以采用探针和替换实验。输入固定,替换投影权重、查询数量、位置编码或归一化,观察检索和生成是否变化;把视觉 Token 换成全零或随机向量,检查模型是否仍给出相同答案。探针不是最终解释,但能发现模型是否真正使用了连接器输出。
量化、低秩适配和蒸馏可以降低连接器成本,却会改变数值范围和小信号。先在固定样本上比较量化前后向量余弦、区域召回和文本质量,再决定是否部署。记录量化配置、校准集和版本,不要把“模型还能运行”当作质量通过。
学习连接器时,建议先实现一个只处理二维小矩阵的投影与查询示例,再把它接到真实编码器。能手算维度、mask 和梯度流后,再阅读更复杂的架构;否则很容易只记住名称,却不知道信息在哪里被压缩或丢失。
连接器的形状契约
写出每个张量的形状:输入媒体数量、Token 数、隐藏维度、查询数量、输出序列位置和有效 mask。形状契约还要写来源数组,例如第 17 个查询聚合了哪些 Patch 或音频帧;没有来源数组,回答中的证据无法回放。
连接器输出最好带版本和统计摘要,如均值、范数、有效比例和缺失原因。数值异常可能来自图片解码、音频静音、投影权重或混合精度;提前记录能把“模型胡说”拆成可检查的工程问题。
如果连接器把敏感区域聚合到一个向量,删除和权限过滤仍需处理整个聚合结果。不要以为原图删掉后向量就没有风险;制定派生数据保留期,并验证索引、缓存和日志的引用都能被撤回。
| 连接器 | 输出规模 | 保留信息 | 主要风险 |
|---|---|---|---|
| 线性投影 | 与输入 Token 相同 | 局部位置完整 | 序列过长、噪声多 |
| 平均/注意力池化 | 少量向量 | 全局语义 | 空间细节丢失 |
| 查询 Token | 固定查询数 | 按需聚合区域/时间 | 查询不足或学成捷径 |
| 小型 Transformer | 可变或压缩序列 | 层级关系 | 训练与延迟更高 |
| 低秩/量化适配 | 参数或数值更小 | 大体结构 | 小信号和稳定性下降 |
def mean_pool(tokens, mask):
valid = [token for token, keep in zip(tokens, mask) if keep]
if not valid:
return None
width = len(valid[0])
return [sum(row[i] for row in valid) / len(valid) for i in range(width)]
def project(vector, matrix):
return [sum(value * weight for value, weight in zip(vector, column))
for column in zip(*matrix)]
visual_tokens = [[1.0, 0.0], [0.0, 1.0], [9.0, 9.0]]
valid = [True, True, False]
pooled = mean_pool(visual_tokens, valid)
projection = project(pooled, [[1.0, 0.2], [0.1, 0.8]])
print({"pooled": pooled, "projected": projection})设图像编码器输出 196×768,语言模型需要 1024 维,查询 Token 数为 32;写出投影前后形状、mask、来源映射和一个“查询不足”的失败测试。
参考结论:每个维度含义明确;空间来源可展开;失败不会静默变成全局向量。
完整实现路径:把视觉编码器、投影器、查询 token、语言模型的形状写成接口合同,再比较早期、拼接和交叉注意力。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
def fusion_contract(visual, query, hidden=1024):
if visual.ndim != 3: raise ValueError("visual must be (B,N,D)")
if query.ndim != 3: raise ValueError("query must be (B,Q,H)")
if visual.shape[0] != query.shape[0]: raise ValueError("batch mismatch")
if query.shape[-1] != hidden: raise ValueError("hidden mismatch")
return {"batch": visual.shape[0], "visual_tokens": visual.shape[1],
"query_tokens": query.shape[1], "hidden": hidden}
visual = torch.randn(2, 196, 768)
query = torch.randn(2, 32, 1024)
assert fusion_contract(visual, query)["query_tokens"] == 32验收标准:缺失模态、伪模态和真实模态三种输入行为可区分。
若结果不对:逐层打印 B、token 数和 hidden size,优先定位第一处不一致。
12.4.3 序列预算、缓存与路由
多模态推理的成本主要来自序列长度和每个模态的编码器开销。高分辨率图片、长音频、多个页面和长对话同时出现时,显存和延迟会快速增加。预算应按媒体数量、像素/帧数、音频秒数、文本 Token、连接器 Token 和工具次数拆开,而不是只给一个总上限。
分层预算比一刀切更有用。先用低成本处理判断媒体是否相关,再对候选图片区域、关键视频片段或音频事件做精细编码;对于文档先检索页码,再把少量页面送入融合模型。每一层都保存筛选理由和被丢弃的范围,避免用户误以为系统分析了完整媒体。
缓存能降低重复编码成本,但会引入版本和权限问题。缓存键至少包括媒体哈希、预处理、编码器版本、连接器版本和权限域;用户撤权或模型升级后,旧缓存不能继续被新请求使用。命中缓存时记录命中原因和来源,回放才能复现当时的输入。
路由器负责选择模态和模型,不负责替模型做事实判断。它可以根据时长、分辨率、任务类型和预算选择小模型、专用 OCR、ASR 或通用多模态模型;但路由规则必须可解释、可测试,并在高风险任务中设置人工确认。路由失败应返回预算或能力状态,而不是偷偷降级。
流式音频和视频需要增量上下文。每个块带序号、起止时间和前后重叠,模型输出标记已确认、暂定或需要等待更多上下文。跨块合并要避免重复和边界截断,用户看到的摘要应说明覆盖到哪个时间点。流式系统不能把未完成的片段当作完整结论。
长上下文中的媒体引用要稳定。不要只用“第一个图片”或“上一段录音”指代,因为用户可能插入新媒体;使用 media_id、message_id、frame_id 和 span。对话压缩时保留证据索引和摘要版本,删除旧媒体后不能让摘要继续引用不可见内容。
序列预算和质量有直接权衡。压缩视觉 Token 可能降低小目标召回,减少音频采样可能漏掉短事件,截断文本可能丢条件句。预算实验应画出质量、延迟、显存和成本曲线,确定在哪个点开始出现不可接受的证据损失,而不是追求单一最小成本。
路由和缓存的安全测试要覆盖跨用户泄漏。不同用户上传同名文件、相同内容但不同权限、撤权后再次请求和模型版本切换,应该得到正确隔离。缓存键不能只用文件名或 URL;日志也不能暴露另一个用户的媒体 ID。
部署时区分冷启动、热缓存和异常路径。统计每个阶段耗时,记录编码器、连接器、检索和工具的 p50/p95;超时后返回已完成的安全部分和下一步动作。把重试次数与成本上限绑定,避免一个模态反复失败拖垮整条链。
本小节的实践产物是一张资源预算表、一组路由规则、一个缓存键设计和一条流式重放样例。它们让你知道模型为什么选择某个输入、哪些内容被压缩、缓存是否可用,以及在预算或权限不足时怎样优雅结束。
资源预算的四个问题
问题一:输入最多有多少媒体、像素、秒数和 Token?问题二:哪一步最贵,能否先筛选?问题三:压缩会损失什么证据,如何评估?问题四:缓存何时失效,撤权如何传播?把问题写成表格,再用真实设备测量,不要用理论 FLOPs 代替端到端延迟。
预算不应只针对成功路径。坏图片、超长音频、重复上传、工具超时和用户快速重试都消耗资源;设置请求级、用户级和服务级上限,日志记录拒绝原因。高风险任务宁可返回“需要分段处理”,也不要为了完成一次回答无限重试。
路由规则要有版本和回滚。模型或编码器更新后,重新跑固定样本,比较选择率、质量、成本和关键切片;如果新规则把高风险请求路由到能力不足的模型,应立即回滚。
| 控制点 | 可调参数 | 收益 | 必须监控 |
|---|---|---|---|
| 文本 | 分块、重叠、检索 k | 减少上下文 | 引用完整性 |
| 图像 | 分辨率、Patch、区域 | 保留关键细节 | 小目标召回 |
| 音频 | 采样率、窗口、分段 | 降低时长成本 | 短事件覆盖 |
| 融合 | 查询数、池化、mask | 减少连接器序列 | 区域/时间证据 |
| 缓存 | 键、TTL、版本、权限 | 降低重复编码 | 命中与隔离 |
| 路由 | 模型、工具、重试 | 控制成本和延迟 | 选择率与失败状态 |
def estimate_cost(media, text_tokens, rates):
image_pixels = sum(item.get("width", 0) * item.get("height", 0) for item in media)
audio_ms = sum(item.get("duration_ms", 0) for item in media)
return (image_pixels * rates["pixel"] +
audio_ms * rates["audio_ms"] +
text_tokens * rates["text_token"])
def choose_route(cost, budget, task_risk):
if task_risk == "high" and cost > budget:
return {"model": "review_queue", "state": "needs_human"}
if cost > budget:
return {"model": "chunk_then_route", "state": "needs_split"}
return {"model": "multimodal_main", "state": "accepted"}
media = [{"width": 640, "height": 480, "duration_ms": 0},
{"width": 0, "height": 0, "duration_ms": 12000}]
print(choose_route(estimate_cost(media, 120,
{"pixel": 0.000001, "audio_ms": 0.00002, "text_token": 0.001}),
budget=1.0, task_risk="normal"))给出三个图片分辨率、两种音频分段和一个 Token 上限,估算成本并决定何时改用分段或人工;同时写出缓存失效条件。
参考结论:合格标准:质量损失和资源收益同时记录;缓存键含版本与权限;超预算有用户可理解的状态。
完整实现路径:把视觉编码器、投影器、查询 token、语言模型的形状写成接口合同,再比较早期、拼接和交叉注意力。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
def fusion_contract(visual, query, hidden=1024):
if visual.ndim != 3: raise ValueError("visual must be (B,N,D)")
if query.ndim != 3: raise ValueError("query must be (B,Q,H)")
if visual.shape[0] != query.shape[0]: raise ValueError("batch mismatch")
if query.shape[-1] != hidden: raise ValueError("hidden mismatch")
return {"batch": visual.shape[0], "visual_tokens": visual.shape[1],
"query_tokens": query.shape[1], "hidden": hidden}
visual = torch.randn(2, 196, 768)
query = torch.randn(2, 32, 1024)
assert fusion_contract(visual, query)["query_tokens"] == 32验收标准:缺失模态、伪模态和真实模态三种输入行为可区分。
若结果不对:逐层打印 B、token 数和 hidden size,优先定位第一处不一致。
本节复习与易错
易错一:认为融合越深越好。深层交互可能提高能力,也可能放大噪声、延迟和权限风险;架构要由证据关系和预算决定。易错二:把连接器当成无损适配器。投影、池化和查询都会压缩信息,必须验证空间、时间和小目标证据。
易错三:缓存命中就不需要权限检查。缓存可能跨用户或跨版本泄漏;命中前后都要验证权限、媒体哈希和模型配置。易错四:超预算后静默降级。用户需要知道系统分析了哪些模态、哪些内容被跳过,以及下一步如何补充。
本节验收:提交三种融合基线、连接器形状契约、序列预算表、缓存键、反事实测试和一次缺失模态回放。读者应能从一条回答解释信息经过哪条融合路径。
融合输出的边界审查
对同一任务实现晚期融合、拼接和交叉注意力三个小基线,固定单模态编码器和数据。比较完整输入、缺失输入、模态冲突和媒体顺序变化;除了精度,记录引用有效率、p95、显存和失败状态。结构更复杂不一定更适合。
审查融合层的 mask:padding、补边、被遮蔽区域、不可用音频和无权限 Token 必须被区别处理。让模型回答一个只在被遮蔽区域出现的问题,确认它会返回不确定而不是从语言先验补全。
对查询 Token 做数量扫描,画出 Token 数、区域召回、文字准确率和成本曲线。固定查询数可能适合粗粒度任务,不适合对象多或页面密集的任务;必要时采用区域级或分层查询,并保存来源映射。
缓存与路由加入模型/连接器/预处理版本和权限域。命中缓存时检查媒体仍可见;模型升级后不能混用旧视觉 Token。用同一媒体在两个用户权限下测试,确保缓存不会把私密内容返回给无权用户。
最终把融合选择写成任务决策,而不是论文复述:哪一种证据需要细粒度交互,哪一种可以独立预测,哪一种延迟和预算能接受。所有选择都用固定实验和失败案例支撑。
| 问题 | 可观察证据 | 通过标准 | 未通过动作 |
|---|---|---|---|
| 模态是否真正交互 | 交换/遮挡消融 | 输出随正确证据变化 | 检查捷径/连接器 |
| mask 是否正确 | padding/缺失/遮蔽样例 | 无效区域不被引用 | 阻断输出 |
| 来源是否保留 | Token→区域/时间映射 | 回答可回放 | 补元数据 |
| 缓存是否隔离 | 同内容不同权限 | 撤权后不命中 | 清理/重建 |
| 预算是否可控 | Token/显存/p95 | 低于门槛 | 分段/路由/人工 |
把视觉 Token 换成零向量、随机向量和另一张图片,比较答案、引用和状态;写出模型真正使用视觉证据的判断。
参考结论:控制其他变量;引用与内容同步变化;失败时进入不确定或阻断。
完整实现路径:把视觉编码器、投影器、查询 token、语言模型的形状写成接口合同,再比较早期、拼接和交叉注意力。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
def fusion_contract(visual, query, hidden=1024):
if visual.ndim != 3: raise ValueError("visual must be (B,N,D)")
if query.ndim != 3: raise ValueError("query must be (B,Q,H)")
if visual.shape[0] != query.shape[0]: raise ValueError("batch mismatch")
if query.shape[-1] != hidden: raise ValueError("hidden mismatch")
return {"batch": visual.shape[0], "visual_tokens": visual.shape[1],
"query_tokens": query.shape[1], "hidden": hidden}
visual = torch.randn(2, 196, 768)
query = torch.randn(2, 32, 1024)
assert fusion_contract(visual, query)["query_tokens"] == 32验收标准:缺失模态、伪模态和真实模态三种输入行为可区分。
若结果不对:逐层打印 B、token 数和 hidden size,优先定位第一处不一致。
融合失败的排查顺序
第一步检查媒体关系和 mask,再检查编码器输出的形状、范数和有效比例;第二步用零/随机/交换 Token 做消融,判断语言模型是否真正读取融合结果;第三步检查连接器来源映射,确认引用区域或时间没有被池化丢失。
对拼接序列记录每种模态的起止位置和类型标记,对交叉注意力记录 query/key/value 的来源与方向。把顺序改变、缺失一支和 padding 增加作为单独用例,避免所有错误最后都被称为“注意力不够”。
当融合性能上升但成本暴涨时,先画质量—Token—延迟曲线,再决定池化、分层查询或路由。压缩后的质量损失要按任务对象度量,例如小字、短音、细边界,而不是只看总体问答分。
缓存和路由要有可解释日志。一次请求为什么使用高分辨率、为什么跳过音频、为什么进入人工队列,都能从规则版本、质量字段和预算记录中重建;没有理由的自动选择无法安全运营。
融合迁移结束后保存旧配置、评估快照和回滚步骤。任何新连接器都先在低风险样例灰度,确认证据、权限和输出协议稳定,再扩大媒体数量或任务范围。
| 顺序 | 检查对象 | 典型症状 | 下一步 |
|---|---|---|---|
| 1 | 关系/mask | 缺失被当空内容 | 修协议/掩码 |
| 2 | 编码器输出 | 范数异常/形状错 | 查预处理/版本 |
| 3 | 连接器 | 消融无变化 | 查投影/查询 |
| 4 | 来源映射 | 引用越界 | 补坐标/时间 |
| 5 | 资源与缓存 | 延迟/泄露 | 分段/隔离/回滚 |
融合路径迁移检查
新增一个模态或替换连接器时,先写出张量形状、类型标记、mask、位置/时间和来源映射,再用缺失、交换、遮挡和顺序变化样例回归。任何没有来源映射的压缩都不能用于需要证据引用的任务。
比较融合架构时同时报告准确率、证据有效率、缺失降级、p95、显存和缓存隔离;若深层交互只增加成本而没有改善关键切片,保留更简单的基线。
本节通过标准:能解释每条信息流向、每个 mask 和每个预算选择,且新旧配置可以灰度与回滚。
05 · PRETRAINING & DATA
预训练数据与目标函数
多模态预训练把海量媒体和文本变成可迁移的表示,但数据规模本身不会自动带来跨模态理解。目标函数决定模型被奖励什么,数据工程决定它看见哪些关联,训练阶段决定这些能力怎样进入下游任务。
12.5.1 多模态目标函数
目标函数是训练中“什么算做得好”的可计算定义。图文对比目标让配对图文相近、非配对图文可区分;图文匹配目标判断两者是否相关;掩码建模恢复被遮住的 Token;自回归目标根据上下文预测下一个 Token;跨模态生成目标让模型根据图片或音频生成文字。不同目标强调不同关系,不能用一个 loss 解释所有能力。
对比目标擅长建立检索空间,但它不一定教模型按顺序描述或精确定位;自回归生成擅长形成语言输出,却可能忽略细小视觉差异;掩码目标能学习局部恢复,但恢复正确不等于理解任务。实际系统常组合多个目标,用权重控制训练信号,并在下游指标上验证每个目标是否带来可观察收益。
损失权重不是越平均越好。图片和文本数量、Token 长度、音频时长不同,直接相加会让长序列或样本多的模态主导梯度。按样本、Token 或任务归一化,使用动态权重或分阶段训练都可能更合理;报告中写清每个目标的尺度、权重和有效样本数。
目标函数对负例和掩码有假设。对比目标假设负例不相关,匹配目标假设标签可靠,掩码目标假设上下文足以恢复,生成目标假设参考文本正确。如果网页标题、自动字幕或 OCR 本身有错误,模型可能被训练去复现错误。数据质量与目标函数必须一起审查。
多模态目标可以引入空间和时间监督。图文对齐可加区域文本匹配,视频字幕可加时间片排序,音频事件可加起止边界,文档任务可加页码和版面关系。更细监督通常成本更高、标注更难,但能把“相关”变成“在哪儿、什么时候、对应哪句”,适合需要证据的任务。
教师信号和蒸馏可以降低训练成本。先用强模型生成描述、区域或伪标签,再由学生学习;但教师错误、偏见和版权限制会进入数据。伪标签要保留生成模型、提示、置信度和审查状态,验证集尽量使用人工或独立来源,不能把教师输出直接当作真值。
多任务训练还要防止任务之间互相干扰。检索需要区分细粒度实体,生成可能倾向于平滑和概括;语音识别重视字词准确率,音频分类重视事件边界。共享骨干、任务头或 adapter 的组合应通过单模态回归与梯度分析检查,避免一个任务提升掩盖另一个任务退化。
自回归多模态生成涉及输出顺序。先生成文字再调用图像工具,与先读取图像区域再生成答案,梯度和错误传播不同;多轮对话还要定义媒体占位符和结束条件。训练样本要明确哪些 Token 来自用户、工具和模型,防止模型把工具结果伪装成观察事实。
目标函数中的随机采样影响可复现。负例池、帧采样、音频裁剪、mask 位置和数据顺序都会改变梯度;分布式训练中有效 batch size 和温度也可能变化。保存种子、采样器版本、数据快照和配置,报告训练曲线和关键切片,而不是只保存最终 checkpoint。
读公式时始终问三个问题:正负关系从哪里来,模型能看到哪些证据,loss 降低后哪个用户指标会改善。如果无法回答第三个问题,目标函数可能只是优化了代理任务。学习者应先在小数据上画损失、检索和生成指标,再扩大训练规模。
目标函数的选择方法
需要检索或聚类时,先考虑对比和匹配目标;需要开放式回答时加入自回归目标;需要恢复遮挡或理解版面时加入掩码/区域目标;需要事件定位时加入时间边界损失。选择后写出不希望模型学到的捷径,并为每个风险设计消融。
目标权重应通过验证集和资源预算决定。若增加生成目标只让文本流畅度提高,却让图像检索和区域引用下降,应重新平衡或分阶段训练。不要用训练 loss 的绝对值比较不同目标,它们的尺度和归一化可能完全不同。
任何自动教师信号都要有 provenance。保存输入媒体、提示、教师版本、输出、过滤规则和人工抽检;当教师模型升级时,重新评估旧样本。伪标签适合扩大覆盖,不适合替代关键切片的独立金标准。
| 目标 | 主要学习关系 | 优势 | 盲点 |
|---|---|---|---|
| 图文对比 | 配对相似度 | 检索/零样本 | 不保证生成与定位 |
| 匹配分类 | 相关/不相关 | 判断配对 | 依赖负例质量 |
| 掩码建模 | 从上下文恢复 | 局部结构/缺失 | 恢复不等于理解 |
| 自回归生成 | 预测下一个 Token | 开放式输出 | 可能语言先验主导 |
| 区域/时间对齐 | 证据位置关系 | 可引用、可定位 | 标注成本高 |
| 蒸馏/伪标签 | 复制教师行为 | 扩大数据覆盖 | 继承教师错误 |
def weighted_total(losses, weights):
if set(losses) != set(weights):
raise ValueError("loss and weight keys differ")
total = 0.0
for name, value in losses.items():
total += value * weights[name]
return total
losses = {
"contrastive": 0.42,
"matching": 0.31,
"generation": 1.20,
"grounding": 0.18,
}
weights = {
"contrastive": 1.0,
"matching": 0.5,
"generation": 0.3,
"grounding": 1.2,
}
print({"total": weighted_total(losses, weights), "weights": weights})为图文检索、图片问答、视频事件定位各选一个主目标和一个辅助目标;说明正负关系、可能捷径和对应消融。
参考结论:目标与可观测指标对应;写出假负例/伪标签风险;不把 loss 下降当作任务完成。
完整实现路径:数据卡先于训练;主目标和辅助目标分别写出输入、损失、捷径风险和停止条件。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
from collections import Counter
def audit_pairs(rows):
ids = [r["id"] for r in rows]
sources = Counter(r["source"] for r in rows)
return {
"rows": len(rows),
"duplicate_ids": len(ids) - len(set(ids)),
"missing_license": sum(not r.get("license") for r in rows),
"source_distribution": dict(sources),
}
report = audit_pairs(rows)
if report["duplicate_ids"] or report["missing_license"]:
raise ValueError(report)验收标准:去重、许可、隐私、切分与删除链路都能审计。
若结果不对:分数异常升高时先排查近重复、模板和标签泄漏。
12.5.2 数据筛选、版权与质量
预训练数据常来自网页、公开视频、文档、用户授权素材或合成内容。来源不同,许可、质量、时间和隐私风险也不同;把它们混成一个“训练集”会让模型无法解释为何学到某种关联。数据卡记录来源、收集方法、快照日期、用途、许可、撤回路径和已知缺口。
清洗不只是删除低分图片。需要检查真实 MIME、重复与近重复、文字/图片是否配对、音频是否有静音或截断、OCR/ASR 是否可信、文本是否包含隐私和恶意指令。每个过滤器要有规则版本和保留/删除计数,避免数据规模增加却不知道有效信息是否增加。
去重应在模态内和跨模态关系上同时进行。同一图片的不同尺寸、视频相邻帧、同一音频的重编码和重复标题会泄漏到验证集;图片与说明文本被重新发布也可能形成近重复配对。按媒体哈希、感知哈希、实体和事件分组,切分前完成审查。
版权和授权不能由模型判断。可下载、可阅读和可用于训练是不同权限;输入素材、自动生成 caption、第三方字体、商标和人物肖像又有各自边界。数据卡要写明用途与地域范围,保留许可证据;不确定时限制使用或转人工咨询,不在教材中承诺绝对可商用。
质量过滤应避免把少数语言和少数场景全部当作噪声。通用清晰度、语言识别和毒性过滤可能误删方言、低清但关键的设备图片或带少数文字的页面。过滤阈值要按切片抽检,并保留被过滤样本的类别统计;质量不是越高越好,而是与任务证据相容。
网页文本和图片中的指令可能被当作训练内容。广告、提示注入、恶意代码、隐写文本和错误标签会污染模型。把“内容”与“训练指令”分开,默认把图片内文字视为不可信数据;过滤器记录命中规则,安全评估集保留部分攻击样本,不能全部清除后假装没有风险。
自动 caption 可以扩大配对,但会放大模板和偏见。相同描述反复出现会让模型学到来源风格,强模型常把不可见属性补进文字。保留原 caption、生成 caption 和审核状态,限制单一模板比例;对关键任务使用人工描述或独立文档,而不是把机器文字当作观察。
音频和视频数据还要处理说话人、背景和时间。版权音乐、未同意的对话、屏幕中的个人资料以及跨视频重复片段可能在清洗中被忽略。分离媒体、转写、说话人向量和字幕的权限;删除请求要能追到所有派生文件和训练清单。
数据配比影响模型在真实输入上的行为。网页图文可能数量巨大但关系弱,人工区域标注数量少但监督强;单一语言或单一设备会形成捷径。按来源、语言、模态组合、时长、分辨率和任务粒度做配比,报告有效样本而非只报告原始文件数。
筛选结果要可复现。保存输入快照哈希、过滤器顺序、规则参数、保留样本 ID、删除原因和统计摘要;同一快照重跑应得到相同清单。新增来源或规则升级时生成差异报告,说明哪些样本进入/退出以及评估集是否受到影响。
一条数据筛选流水线
先验证文件和关系,再做去重、权限和敏感信息检查,接着进行语言/质量/配对抽检,最后按实体或事件切分并生成 manifest。过滤顺序本身会影响结果,例如先裁剪再去重可能产生新的哈希;因此顺序写进配置而不是口头约定。
每个过滤器输出 accepted、rejected、review 和 unknown 四类状态。unknown 表示证据不足,不能直接当作噪声删除;进入人工队列后保存裁决理由。统计每个状态按语言、设备、来源和任务的分布,防止某个规则悄悄消灭关键切片。
数据删除要做回放:从原图、转写、caption、向量、缓存到训练索引逐层查找。无法从已训练权重中逐样本删除时,也要明确政策、风险和后续模型重训计划,不要声称简单删除文件就完成撤回。
| 类别 | 字段 | 抽检问题 | 失败动作 |
|---|---|---|---|
| 来源 | source、snapshot、license | 是否有用途授权 | 限制/隔离/删除 |
| 配对 | parent_id、relation、time | 是否同一对象/事件 | 降权/复核 |
| 质量 | decode、clarity、duration | 低质是否仍有任务价值 | 保留切片/拒绝 |
| 重复 | hash、cluster、entity | 是否跨集合泄漏 | 重分组/去重 |
| 隐私 | person、PII、consent | 是否需要脱敏 | 遮蔽/转人工 |
| 安全 | injection、malware | 是否含恶意指令 | 隔离/审计 |
from collections import Counter
def summarize(rows):
states = Counter(row["state"] for row in rows)
sources = Counter(row["source"] for row in rows if row["state"] == "accepted")
return {
"total": len(rows),
"states": dict(states),
"accepted_by_source": dict(sources),
"unknown_ids": [row["id"] for row in rows if row["state"] == "unknown"],
}
rows = [
{"id": "a", "state": "accepted", "source": "licensed"},
{"id": "b", "state": "review", "source": "web"},
{"id": "c", "state": "unknown", "source": "web"},
{"id": "d", "state": "accepted", "source": "licensed"},
]
print(summarize(rows))选择一个公开、低风险的小数据集,记录来源、许可、配对、去重、隐私、过滤状态和删除路径;抽查一条被拒样本,说明它为什么不能直接进入训练。
参考结论:原始和派生数据分开;过滤规则有版本;unknown 不被悄悄当作 accepted 或 rejected。
完整实现路径:数据卡先于训练;主目标和辅助目标分别写出输入、损失、捷径风险和停止条件。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
from collections import Counter
def audit_pairs(rows):
ids = [r["id"] for r in rows]
sources = Counter(r["source"] for r in rows)
return {
"rows": len(rows),
"duplicate_ids": len(ids) - len(set(ids)),
"missing_license": sum(not r.get("license") for r in rows),
"source_distribution": dict(sources),
}
report = audit_pairs(rows)
if report["duplicate_ids"] or report["missing_license"]:
raise ValueError(report)验收标准:去重、许可、隐私、切分与删除链路都能审计。
若结果不对:分数异常升高时先排查近重复、模板和标签泄漏。
12.5.3 训练阶段与资源预算
多模态模型常采用分阶段训练。先让各模态编码器学习单模态表示,再训练投影/连接器建立对齐,随后做多模态预训练或指令微调,最后针对任务校准工具、拒答和输出协议。阶段并非固定配方,但每一阶段都有不同数据、目标、冻结策略和验收指标。
单模态预训练权重带来效率和能力起点,也带来接口差异。图像编码器的输入归一化、音频编码器的采样率、语言模型的词表和位置编码必须与连接器匹配。加载权重时记录来源、版本、许可和预计输入;不要只看到模型能运行就认为模态已经对齐。
连接器训练通常先冻结骨干,减少显存和灾难性遗忘;当配对数据和资源足够时再解冻部分层。分层学习率、低秩适配、梯度累积和混合精度可以降低成本,但会改变有效 batch、数值稳定性和收敛速度。训练报告要同时给出参数、数据量、步数、设备和失败重试。
训练预算需要按媒体成本估算。图片 Token 与音频帧数量不等,长视频会让某些 batch 远贵于其他 batch;按 Token/帧分桶、动态 padding 和梯度累积可以提高利用率。设置单样本和批次上限,监控显存峰值、吞吐、p95 和异常重试,避免一条超长媒体拖垮作业。
检查点不仅保存权重,还应保存 tokenizer、编码器、连接器、数据快照、目标权重、随机种子、优化器和学习率调度器。只保存最终权重会失去中途回滚和复现实验能力。定期在固定样本上生成检索、问答和缺失模态结果,及时发现某个阶段破坏了已有能力。
训练验证不能只看总 loss。每个阶段都要测单模态回归、图文检索、区域/时间对齐、生成格式、缺失模态和关键安全切片。若连接器 loss 下降而图像问答不升,可能是配对或目标有问题;若生成变流畅但检索下降,可能是语言目标过强。
参数高效微调适合领域适配,但要明确训练了哪些参数和数据。LoRA、adapter、提示学习和全量微调的记忆风险、许可和回滚方式不同;多个 adapter 叠加时记录顺序与冲突。将领域数据与通用回归集混合评估,避免新领域能力建立在旧能力遗忘上。
分布式训练会改变负例范围和随机顺序。跨卡 all-gather 让对比目标看到更多负例,但也增加通信和显存;不同 worker 的数据来源比例可能不一致。记录 global batch、有效负例、同步方式和故障恢复,检查重启后 loss/指标是否连续。
训练过程要有停止规则。固定验证指标长期不升、关键切片下降、隐私过滤失败、数据许可证撤回或成本超出预算,都可以触发暂停。暂停不等于删除检查点;保留最后安全版本,写明继续、回滚或重新筛选数据的条件。
学习者不必从零训练巨型模型才能理解训练。先用少量合成配对和冻结编码器跑一个连接器,再用小型公开集比较目标函数和批次策略;把每次实验的配置、结果和错误写进 README。可复现的小实验比一次不可解释的大作业更有价值。
阶段验收表
表示阶段:检查单模态编码器的输入与输出、基础分类/转写和预处理一致;对齐阶段:检查双向检索、假负例和语言切片;生成阶段:检查格式、证据引用、缺失模态和工具状态;交付阶段:检查延迟、成本、权限、删除与回滚。
每次阶段切换都保存一份固定评估快照。若新阶段没有提升目标指标却破坏了单模态基线,应停止并分析,而不是继续堆训练步数。训练曲线只说明优化过程,验收指标才说明是否接近任务合同。
资源预算包含失败路径和人工时间。数据抽检、标注裁决、模型重启、指标计算和安全回放都需要成本;在项目章程中写清预算上限和负责人,防止“先训练再想怎么评估”。
| 阶段 | 主要组件 | 关键数据 | 必须通过 |
|---|---|---|---|
| 单模态 | 文本/视觉/音频编码器 | 单模态任务集 | 输入契约与基线 |
| 连接器 | 投影/查询/适配器 | 高质量配对 | 双向检索与形状 |
| 多模态预训练 | 融合层/目标函数 | 分级关系数据 | 对齐/生成/缺失 |
| 指令微调 | 对话模板/工具协议 | 人工或审查指令 | schema/拒答/引用 |
| 校准发布 | 阈值/路由/监控 | 挑战集/回归集 | 成本/安全/回滚 |
def estimate_tokens(sample):
text = sample.get("text_tokens", 0)
image = sample.get("image_tokens", 0)
audio = sample.get("audio_frames", 0)
return text + image + audio
def bucket(samples, limit):
buckets = []
current = []
total = 0
for sample in sorted(samples, key=estimate_tokens):
size = estimate_tokens(sample)
if current and total + size > limit:
buckets.append(current)
current, total = [], 0
current.append(sample)
total += size
if current:
buckets.append(current)
return buckets
samples = [{"id": "a", "text_tokens": 40, "image_tokens": 80},
{"id": "b", "text_tokens": 20, "audio_frames": 120},
{"id": "c", "text_tokens": 10, "image_tokens": 50}]
print([[row["id"] for row in group] for group in bucket(samples, 150)])用少量配对数据设计“冻结编码器→训练连接器→指令微调”的实验矩阵;每阶段写数据、目标、资源预算、固定评估和停止规则。
参考结论:合格标准:阶段切换有证据;保存完整 manifest;能解释指标变化来自数据、目标还是参数策略。
完整实现路径:数据卡先于训练;主目标和辅助目标分别写出输入、损失、捷径风险和停止条件。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
from collections import Counter
def audit_pairs(rows):
ids = [r["id"] for r in rows]
sources = Counter(r["source"] for r in rows)
return {
"rows": len(rows),
"duplicate_ids": len(ids) - len(set(ids)),
"missing_license": sum(not r.get("license") for r in rows),
"source_distribution": dict(sources),
}
report = audit_pairs(rows)
if report["duplicate_ids"] or report["missing_license"]:
raise ValueError(report)验收标准:去重、许可、隐私、切分与删除链路都能审计。
若结果不对:分数异常升高时先排查近重复、模板和标签泄漏。
本节复习与易错
易错一:把训练数据量当作有效配对量。弱标题、重复图片和错误字幕可能让大量样本只提供噪声。易错二:把预训练目标当作下游能力。对比 loss 低不代表能定位区域,自回归流畅不代表看懂图像。
易错三:伪标签没有来源和审查。教师模型的偏见会被学生放大;关键切片要用独立金标准。易错四:只保存最终权重。没有 tokenizer、连接器、数据快照和目标权重,就无法复现实验或安全回滚。
本节验收:提交目标函数说明、数据卡、过滤 manifest、阶段验收表、预算脚本和一份训练回放;读者能说明模型被奖励什么、数据许可是什么、训练失败时如何停下。
预训练数据与目标的复核实验
对同一批配对数据分别训练或计算对比、匹配和生成目标的代理指标,再在固定下游集比较。目标的 loss 下降不等于检索、定位和生成都提高;记录每个目标的有效样本、权重和假负例,解释指标差异。
按来源、语言、媒体组合和质量切分数据,比较模型是否把来源风格当成语义。去掉水印、标题和文件名后重跑,若性能大幅下降,说明存在捷径。保留这些挑战样例用于发布前回归。
伪 caption 和自动转写要保留教师版本、提示、置信度和抽检结果。对同一媒体的人工描述和机器描述分别评估,发现教师把不可见属性写进文本时,降低权重或移出金标准。
训练阶段变更时固定一组单模态和多模态回归。连接器训练可能破坏文字能力,生成目标可能让检索退化,量化可能丢小区域信息;任何退化都要进入错误地图并有停止规则。
资源报告包括有效 batch、Token/帧数量、梯度累积、检查点、失败重试、设备和能耗估算。学习目标是能复现实验,不是追求一个无法解释的巨大 checkpoint。
数据、目标和训练阶段共同决定能力;每个阶段都应留下版本和可回放样例。
| 变更 | 可能收益 | 可能损失 | 必须重跑 |
|---|---|---|---|
| 增加生成目标 | 回答更完整 | 检索/定位下降 | 双向检索+证据 |
| 扩大负例池 | 区分细节 | 假负例梯度 | 多正例/语言切片 |
| 换连接器 | 压缩成本 | 小目标丢失 | 区域/Token 消融 |
| 量化/低秩 | 显存下降 | 小信号不稳 | 数值+下游指标 |
| 更新数据源 | 覆盖增加 | 许可/捷径 | 来源/删除/安全 |
任选一个目标、数据或连接器变更,写收益假设、风险、固定评估、停止门槛和回滚版本。
参考结论:合格标准:目标与下游指标对应;数据许可可追溯;退化不会被平均分掩盖。
完整实现路径:数据卡先于训练;主目标和辅助目标分别写出输入、损失、捷径风险和停止条件。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
from collections import Counter
def audit_pairs(rows):
ids = [r["id"] for r in rows]
sources = Counter(r["source"] for r in rows)
return {
"rows": len(rows),
"duplicate_ids": len(ids) - len(set(ids)),
"missing_license": sum(not r.get("license") for r in rows),
"source_distribution": dict(sources),
}
report = audit_pairs(rows)
if report["duplicate_ids"] or report["missing_license"]:
raise ValueError(report)验收标准:去重、许可、隐私、切分与删除链路都能审计。
若结果不对:分数异常升高时先排查近重复、模板和标签泄漏。
训练日志复盘练习
挑选一次训练运行,先不看最终 checkpoint,只看数据快照、目标权重、有效样本、负例、Token/帧数量和失败重试。写出本次运行真正优化的代理目标,再对照下游指标,判断代理是否足够代表任务。
把过滤前后的来源、语言、设备、模态组合和敏感样本比例画成差异表。若清洗让某个切片几乎消失,模型总体可能更稳定,但真实用户覆盖会变差;保留这条风险并决定补采、限制范围或人工处理。
对连接器、生成目标和量化分别做单变量回归。每个变更都跑单模态基线、双向检索、区域/时间证据和拒答集;这样能看出是训练目标、数值策略还是数据变化造成退化。
检查点恢复测试要从中断状态继续,比较损失、有效 batch、随机采样和评估结果。恢复后若数据顺序或负例池改变,应在报告中说明,不能把不连续曲线当作同一次实验。
训练阶段的停止条件写成可以执行的动作:关键切片下降、隐私过滤失败、许可证撤回、成本超限或 schema 不通过时暂停;保留最后安全版本并记录下一次启动所需的修复。
| 问题 | 需要的记录 | 不能只看 | 验收 |
|---|---|---|---|
| 优化了什么 | 目标、权重、样本 | 总 loss | 下游指标对应 |
| 数据变了什么 | 过滤/来源差异 | 原始文件数 | 关键切片可见 |
| 能力是否迁移 | 单模态/多模态回归 | 训练曲线 | 无严重退化 |
| 能否恢复 | checkpoint/随机/批次 | 最终权重 | 中断后可重放 |
| 何时停止 | 门槛/负责人/动作 | 主观感觉 | 可执行 runbook |
训练数据迁移检查
数据源、语言、媒体比例或教师模型变化后,重新生成来源/许可/配对/去重统计,并与旧快照做差异。新增规模不等于新增有效监督,关键是强正例、难负例、假负例和敏感样本的比例是否合理。
目标函数或权重变化时,固定单模态基线、双向检索、区域/时间证据和拒答集。若代理 loss 下降但下游退化,停止训练并检查目标、配对或梯度是否被某个模态主导。
本节通过标准:训练可以分阶段回放,自动教师有 provenance,检查点包含能恢复实验的全部配置,许可证撤回或关键切片下降能触发暂停。
训练层的最小验收
复盘一次小实验的目标、数据、负例、权重、检查点和下游指标;如果代理 loss 下降而关键切片退化,停止并定位数据、目标或梯度问题。自动 caption、ASR 或伪标签必须带来源和审查状态。
通过标准:阶段可以恢复,数据许可和删除路径可追溯,连接器/目标/量化变更都有固定回归与回滚条件。
06 · MULTIMODAL LANGUAGE MODELS
视觉语言与语音语言模型
当视觉或音频表示通过连接器进入语言模型,系统就能用对话形式回答问题、解释证据和调用工具。但语言流畅度很容易掩盖观察错误。本节把模型组件、语音时间轴、对话状态和证据协议放在一起学习。
12.6.1 视觉语言模型的组件
视觉语言模型通常由视觉编码器、连接器、语言模型和输出控制层组成。编码器把像素变成视觉特征,连接器把特征投影或压缩成语言模型可读的 Token,语言模型依据问题和上下文生成文本,控制层负责 schema、引用、拒答与工具权限。任何一个组件都可能成为瓶颈,不能只看语言模型规模。
视觉编码器可能是卷积网络、Vision Transformer 或多尺度检测器;它的输入尺寸、颜色、Patch、池化和预训练数据决定视觉信息粒度。连接器可以是线性投影、查询 Transformer 或交叉注意力;语言模型还需要认识图片占位符、区域标记和结束条件。加载预训练权重时,组件版本和接口必须一一匹配。
视觉语言模型的能力层次可以区分为描述、问答、定位、文档理解、图表推理和行动规划。描述只需要概括画面,定位需要区域坐标,文档理解需要页码与版面,行动规划还要把观察结果交给工具。一个模型在描述上优秀,并不代表在计数、读小字或空间关系上可靠。
多图片输入需要明确顺序和引用。用户说“比较这两张图”,模型必须知道 image-a 和 image-b,而不是根据上传顺序猜;同一图片在多轮对话中重复出现,也要使用稳定媒体 ID。回答“左图更亮”时,证据应引用两张图的比较区域和预处理版本。
图像问答中的问题类型影响证据要求。颜色、数量、位置、文字读取、因果解释和常识判断不能使用同一个评估标准。数量需要检测或区域计数,文字需要高分辨率 OCR,因果解释通常只能给出可见线索而不能证明现实原因。任务协议应要求模型说明观察与推断的边界。
视觉语言模型常出现语言先验。即使图片被遮住,模型也可能根据问题中的常见答案继续生成;图片内的文字可能包含“忽略此前指令”的提示,模型若把它当系统指令就会越权。将媒体文字标记为不可信数据,输入控制器分离系统/用户/媒体内容,输出校验器只允许预定义动作。
区域引用可以通过边界框、点、掩码、页面块或文字范围表达。不同表示的精度和成本不同,协议要定义坐标系、原点、归一化和容差。模型生成的“左上角”自然语言不能替代可验证坐标;如果没有可靠区域,回答应说证据不足并提供补图或工具动作。
视觉语言模型的微调数据应包含正面、边界和拒答示例。正面样本展示如何引用区域和回答问题,边界样本展示低分辨率、遮挡、未知物体和冲突输入,拒答样本展示敏感推断、权限不足和图片内注入。只有正面问答会鼓励模型在看不清时也给出完整句子。
生成输出应与观察状态分开。可以先输出 observation、evidence、uncertainty,再生成面向用户的 explanation;这样后处理能检查观察字段是否有引用。不要让模型在一段自由文本中同时完成检测、事实判断、权限决定和外部动作。
学习时先用假视觉 Token 或固定区域测试语言层协议,再接入真实视觉编码器。对比“只改变图片”和“只改变问题”的输出,检查模型是否引用正确媒体。记录视觉输入、提示、采样参数、模型版本和完整响应,才能复现一次看似合理但实际看错的回答。
视觉语言模型的组件验收
验收编码器:输入契约与预训练配置一致,尺寸/颜色/位置可回放;验收连接器:输出维度、Token 数、mask 和来源映射正确;验收语言层:图片占位符、媒体顺序、停止条件和结构化输出稳定;验收控制层:引用、权限、拒答与工具白名单不由模型自行改变。
做最小消融:零视觉 Token、随机视觉 Token、交换两张图片、遮挡证据区域、替换问题中的实体。若答案不随视觉变化,说明模型可能只使用语言先验;若答案变化但引用区域不对,说明融合有作用但 grounding 仍不可靠。
把描述、计数、定位、OCR、比较和拒答分成不同评估集。一个统一的“视觉问答准确率”会掩盖小目标、文字、空间和安全任务的差异。每个集合保存原图、问题、证据标注、允许状态和人工裁决。
| 组件 | 输入/输出 | 主要责任 | 常见问题 |
|---|---|---|---|
| 视觉编码器 | 像素→视觉特征 | 保留外观与空间 | 分辨率/颜色不匹配 |
| 连接器 | 视觉特征→语言 Token | 维度与信息瓶颈 | 压缩丢细节/来源丢失 |
| 语言模型 | Token→文本/结构 | 组合上下文与生成 | 语言先验/幻觉 |
| 引用层 | Token/候选→证据 | 区域/页码/版本 | 引用不存在或越界 |
| 控制层 | 请求→工具/状态 | 权限、预算、拒答 | 模型绕过规则 |
语言模型负责组合和表达,不负责替代输入验证、区域校验和权限控制。
from dataclasses import dataclass
@dataclass
class Message:
role: str
content: list[dict]
def media_block(media_id, modality, source):
return {"type": modality, "media_id": media_id, "source": source}
def build_prompt(question, media):
content = [
{"type": "text", "text": question},
*[media_block(item["id"], item["type"], item["source"]) for item in media],
]
return Message(role="user", content=content)
messages = build_prompt("比较两张图的主要差异", [
{"id": "img-a", "type": "image", "source": "upload/a.jpg"},
{"id": "img-b", "type": "image", "source": "upload/b.jpg"},
])
print(messages)准备一张含小字、遮挡和两个相似对象的图片,提出计数、文字、位置和原因四个问题;分别要求模型返回答案、证据区域和不确定状态。
参考结论:问题类型对应不同证据;看不清时拒答;引用区域能回到原图。
完整实现路径:回答对象必须同时返回 answer、evidence、media span 和 uncertainty;暂定转写不能冒充确认文本。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
def grounded_answer(answer, evidence, uncertainty):
required = {"media_id", "span", "quote"}
if not evidence or any(required - item.keys() for item in evidence):
return {"status": "insufficient_evidence", "answer": None}
return {"status": "answered", "answer": answer,
"evidence": evidence, "uncertainty": uncertainty}
result = grounded_answer("右上角有 3 个图标", [{
"media_id": "img-07", "span": [0.62, 0.05, 0.96, 0.30], "quote": "3"
}], "medium")
assert result["status"] == "answered"验收标准:每个结论能定位到页、区域或时间段,低证据请求会拒答。
若结果不对:先重放同一媒体和处理器版本,再查证据定位,最后查生成参数。
12.6.2 语音与音频语言模型
语音语言模型把波形、声学特征或离散音频 Token 与文本语言模型连接起来。它可以完成语音识别、语音问答、音频描述、说话人相关任务和语音对话。与图片相比,音频最大的附加维度是连续时间:同一个词在不同秒数出现,语气、重叠和背景也会改变解释。
端到端模型可以直接从音频生成文本,也可以先由 ASR 得到转写,再让语言模型总结。前者减少接口和误差传播,后者更容易引用、编辑和审计;选择取决于任务是否需要逐词时间戳、说话人、词级置信度和可检索文本。无论哪种路径,原始音频与派生转写都要保留关系。
语音输入中的语言、方言、口音、码率和设备会影响识别。自动语言识别错了,后续 Token 化和翻译都会受影响;专名、数字、否定和单位尤其容易被语言模型纠正成“更常见”的词。对重要字段保留原音播放与时间引用,不能只相信润色后的句子。
说话人分离与身份确认是不同任务。系统可以给片段标记 speaker-1、speaker-2 来组织会议,但不能因为声纹相似就断言真实姓名。对话协议区分匿名说话人、用户确认姓名和未经验证的身份,避免把模型推断扩大成个人属性。
流式语音需要增量状态。每个音频块有序号、时间范围和重叠上下文;暂定转写可以在后续块到达后修订,已确认片段则锁定并记录版本。用户界面应显示“正在识别”“已确认”“需要复核”等状态,而不是把尚未结束的句子当成最终决定。
音频语言模型也会受到背景和媒体内文字的干扰。录音里可能有广告、电视声音或另一个人的指令;这些内容是被观察的音频,不是系统命令。工具控制器应把转写当不可信数据,外部动作必须经过用户意图、权限和 schema 校验。
音频问答常需要跨模态上下文。用户问“刚才谁同意了延期”,系统需要转写、说话人片段、时间和会议议程;只给语言模型一段摘要会丢失证据。输出引用应支持播放到对应时间,涉及争议时允许用户查看原始音频和转写差异。
语音生成与语音理解也要分开。合成语音可以作为交互输出,但不能证明某人说过某句话;克隆声音需要明确同意和标记。记录合成模型、声音来源、文本、音频版本和对外标识,避免把生成内容混入会议原录音。
音频隐私不仅是文本隐私。声纹、环境背景、室内位置和未说出口的停顿都可能暴露信息;降噪和摘要也可能保留敏感线索。设定最小存储、分层权限、删除传播和访问审计,必要时只保存经过脱敏的事件而不是整段原声。
练习时用无敏感的短录音做三次实验:不同采样率、不同分段、加入轻微噪声。分别记录转写错误、时间偏移、摘要引用和成本。你会看到“听懂内容”和“能够证明某句话在何时由谁说出”是两种不同能力。
语音语言模型的时间协议
时间字段至少包括原始音频 ID、start_ms、end_ms、帧率、切块偏移和版本。转写文本如果只对应宽窗口,就把精度写清;如果后处理合并了句子,保留原始片段列表。所有摘要结论都应能展开到一个或多个音频区间。
流式系统区分 provisional、confirmed、revised 和 blocked。后续音频到达后,旧转写可以被修订,但历史结果要保留差异;工具动作只接受 confirmed 或人工确认的字段。这样不会因为一个暂定词就自动发送错误指令。
说话人标签是组织信息的引用,不是身份认证。把匿名标签、用户确认和外部目录匹配分开,权限与保留策略也分开。删除请求需要覆盖原音频、转写、声纹向量、摘要和日志。
| 任务 | 核心输出 | 必需证据 | 安全边界 |
|---|---|---|---|
| ASR | 文字与时间戳 | 音频区间、语言、置信度 | 专名/数字需复核 |
| 会议摘要 | 决定、行动项 | 说话人标签、时间 | 不证明身份或同意 |
| 声音事件 | 事件与区间 | 帧、持续时间 | 重叠声源要保留 |
| 语音问答 | 回答与引用 | 转写/原声片段 | 媒体文字不执行指令 |
| 语音生成 | 合成音频 | 文本、模型、同意 | 必须标记合成 |
from dataclasses import dataclass
@dataclass
class Chunk:
index: int
start_ms: int
end_ms: int
text: str
state: str = "provisional"
def merge_chunks(chunks):
ordered = sorted(chunks, key=lambda item: item.start_ms)
result = []
for chunk in ordered:
if result and chunk.start_ms < result[-1].end_ms:
result[-1].text = result[-1].text.rstrip() + " " + chunk.text.lstrip()
result[-1].end_ms = max(result[-1].end_ms, chunk.end_ms)
else:
result.append(chunk)
return result
items = [Chunk(1, 0, 1200, "今天", "confirmed"),
Chunk(2, 1100, 2500, "讨论预算", "provisional")]
print(merge_chunks(items))把一段短录音分成三个重叠块,记录 provisional/confirmed 的变化;让其中一个词在后续块中被修订,并说明哪些工具动作必须等确认。
参考结论:时间区间可回放;修订保留差异;未确认文本不能触发外部动作。
完整实现路径:回答对象必须同时返回 answer、evidence、media span 和 uncertainty;暂定转写不能冒充确认文本。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
def grounded_answer(answer, evidence, uncertainty):
required = {"media_id", "span", "quote"}
if not evidence or any(required - item.keys() for item in evidence):
return {"status": "insufficient_evidence", "answer": None}
return {"status": "answered", "answer": answer,
"evidence": evidence, "uncertainty": uncertainty}
result = grounded_answer("右上角有 3 个图标", [{
"media_id": "img-07", "span": [0.62, 0.05, 0.96, 0.30], "quote": "3"
}], "medium")
assert result["status"] == "answered"验收标准:每个结论能定位到页、区域或时间段,低证据请求会拒答。
若结果不对:先重放同一媒体和处理器版本,再查证据定位,最后查生成参数。
12.6.3 多模态对话协议
多模态对话的消息不再只是 role 和 text,还包括媒体引用、区域/时间范围、工具结果、版本和状态。消息顺序决定模型看到的上下文,但媒体的生命周期可能独立于消息;因此使用 message_id、media_id、parent_id 和 expires_at 连接对象,不能只把二进制内容塞进提示词。
对话协议要定义引用范围。用户说“这张图”时,是本轮上传的图、上一轮的图还是一个页面中的区域?当用户补充“我说的是右边那个”时,系统需要解析指代并记录目标区域;解析失败应询问,而不是猜一个最相似对象。
多轮对话中的摘要会压缩证据。压缩保留任务状态、媒体 ID、关键引用和未决问题,删除无关闲聊;但不能把模型自己的猜测写成事实。摘要版本、生成模型和来源列表都要保存,回放时可重新展开原消息。
工具结果也是一种模态或派生证据。OCR、检测、ASR、网页检索和计算器的输出需要带工具名、版本、输入范围、时间、置信度和权限。语言模型可以解释工具结果,但不能修改原始结果或隐藏失败状态;若工具与图片冲突,回到冲突协议。
对话状态应区分观察、推断、建议和执行。观察来自媒体或工具,推断是模型对观察的组合,建议是下一步,执行需要用户确认或受控服务。把四类内容分开显示,用户更容易发现模型把“可能”写成“已经发生”。
媒体过期、撤回和替换会影响旧对话。旧消息引用不可见媒体时,系统应显示已过期或无权限,不要加载另一个同名文件;用户补发新媒体后,创建新 ID 并说明它替代哪一个。权限检查在每次读取和工具调用前重新执行。
对话协议还需处理并发和重复。用户快速上传两张图,网络重试导致同一消息提交两次,流式音频与文本同时到达,都会改变顺序。使用幂等键、序号和服务端时间,记录接受/丢弃/合并原因,保证同一事件不会触发两次动作。
输出长度和媒体数量受预算约束。模型应在达到 Token、像素、时长或工具次数上限前给出可用的部分结果和分段建议;超限状态带有已覆盖范围。不要截断 JSON 或引用,校验器发现不完整就返回可修复错误。
对话记录的隐私策略需要细化到消息字段。原始图片、音频、转写、向量、模型解释和用户纠错的敏感性不同;按字段加密和权限,设置保留期限。用户要求删除时,不能因为摘要仍可读就认为已经完成。
学习者可以先设计一个三轮对话:上传图→提出问题→补充音频→请求比较。为每轮画消息、媒体、工具和状态关系,再用假模型回放缺失、冲突、撤权和重复请求。协议先稳定,换模型才不会把所有问题混在一起。
多模态消息的最小字段
消息字段包括 id、role、created_at、content、media_refs、parent_id、contract_version 和 state;媒体字段包括 id、modality、hash、source、permission、preprocess_version;引用字段包括 media_id、range、kind 和 evidence_version。工具结果再加 tool、input_ref、output_schema 与 status。
对外展示可以简化字段,但内部回放要保留完整关系。任何自由文本结论都应有 observation 或 evidence_ref,无法引用时进入 uncertain。用户编辑过的文本与 OCR/ASR 派生文本分开保存,避免编辑覆盖原始证据。
协议升级时维护迁移脚本和兼容窗口。旧消息的媒体引用不能静默指向新文件;旧状态枚举需要明确映射。升级后跑正常、缺失、冲突、撤权和重复事件回归,再开放新模型。
| 层级 | 例子 | 谁产生 | 能否触发动作 |
|---|---|---|---|
| 观察 | 图片有两个圆形器材 | 媒体/工具 | 否,需校验 |
| 推断 | 可能是同一型号 | 模型组合 | 否,需证据/确认 |
| 建议 | 请补充铭牌照片 | 模型/策略 | 否,用户选择 |
| 执行请求 | 创建记录/发送通知 | 控制器 | 需授权与确认 |
| 执行结果 | 已保存/已拒绝 | 受控服务 | 可追溯回执 |
| 阻断 | 权限不足/模态缺失 | 策略层 | 停止并说明 |
from dataclasses import dataclass, field
@dataclass
class Turn:
turn_id: str
media_ids: list[str] = field(default_factory=list)
evidence: list[dict] = field(default_factory=list)
state: str = "open"
def append_evidence(turn, evidence, allowed_media):
if evidence["media_id"] not in allowed_media:
raise PermissionError("media is not available")
if evidence["kind"] not in {"region", "time_span", "text_span"}:
raise ValueError("unsupported evidence kind")
turn.evidence.append(evidence)
return turn
turn = Turn("t-03", ["img-01"])
append_evidence(turn, {"media_id": "img-01", "kind": "region"}, {"img-01"})
print(turn)为“上传图片→补充录音→比较两份说明”的对话写媒体 ID、状态、证据、工具和权限变化;模拟撤权和重复上传。
参考结论:合格标准:旧媒体不会被同名替换;观察/推断/执行分层;撤权后缓存与引用都受影响。
完整实现路径:回答对象必须同时返回 answer、evidence、media span 和 uncertainty;暂定转写不能冒充确认文本。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
def grounded_answer(answer, evidence, uncertainty):
required = {"media_id", "span", "quote"}
if not evidence or any(required - item.keys() for item in evidence):
return {"status": "insufficient_evidence", "answer": None}
return {"status": "answered", "answer": answer,
"evidence": evidence, "uncertainty": uncertainty}
result = grounded_answer("右上角有 3 个图标", [{
"media_id": "img-07", "span": [0.62, 0.05, 0.96, 0.30], "quote": "3"
}], "medium")
assert result["status"] == "answered"验收标准:每个结论能定位到页、区域或时间段,低证据请求会拒答。
若结果不对:先重放同一媒体和处理器版本,再查证据定位,最后查生成参数。
本节复习与易错
易错一:认为视觉语言模型只要大就会看图。编码器、连接器、分辨率、区域引用和数据都决定观察能力。易错二:把流畅回答当作证据。计数、文字、时间和空间任务必须有可定位引用。
易错三:把 ASR 修订后的句子当作原声事实。保留时间戳、置信度和原始音频;说话人标签也不等于身份确认。易错四:让摘要覆盖原始消息。摘要是派生状态,需要来源、版本和删除策略。
本节验收:提交视觉/音频组件图、多模态消息 schema、流式回放、观察—推断—执行状态表和一次撤权测试;读者能解释模型哪里可能错、系统如何阻止错动作。
多模态对话的证据复核
为视觉、音频和图文混合问题各准备完整、缺失、低质量和冲突样例。要求模型同时返回观察、证据、推断、限制和下一步动作;比较只给问题文本时的回答,检查语言先验是否替代媒体。
对视觉语言模型做区域交换,对语音模型做时间片交换,对多图对话做媒体顺序交换。正确回答应随证据变化,引用应仍指向对应媒体;若答案不变或引用错位,进入 grounding 错误而不是简单算作语言错误。
对流式语音保存 provisional 与 confirmed 两个版本,后续块修订时保留差异。任何写入、通知或外部动作只接受 confirmed 或人工确认字段,避免暂定识别触发副作用。
多轮对话摘要只保留任务状态、媒体引用、已验证事实和未决问题。测试删除旧媒体、替换同名文件和权限变化,确保旧摘要不会绕过当前权限。
模型输出可以是自然语言,但交付层必须先通过 schema、证据和安全校验。把“说得像观察”与“有观察证据”分开评分,结果更能指导下一步数据或连接器修复。
对话系统把观察、推断和执行分层,任何外部副作用都不能直接由语言生成触发。
| 回归 | 改变因素 | 检查内容 | 安全要求 |
|---|---|---|---|
| 区域交换 | 图片内目标 | 答案/引用是否同步 | 不引用旧区域 |
| 时间交换 | 音频片段 | 转写/摘要时间 | 不执行暂定词 |
| 媒体删除 | 权限/生命周期 | 旧消息/缓存 | 拒绝读取 |
| 顺序变化 | 消息排列 | 指代/ID | 不混用媒体 |
| 摘要压缩 | 上下文长度 | 事实/未决项 | 保留来源 |
设计三轮对话并交换、删除、补发媒体,记录模型的指代、证据和状态;指出不能由模型决定的动作。
参考结论:合格标准:媒体 ID 稳定;摘要不替代原证据;确认前不执行外部动作。
完整实现路径:回答对象必须同时返回 answer、evidence、media span 和 uncertainty;暂定转写不能冒充确认文本。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
def grounded_answer(answer, evidence, uncertainty):
required = {"media_id", "span", "quote"}
if not evidence or any(required - item.keys() for item in evidence):
return {"status": "insufficient_evidence", "answer": None}
return {"status": "answered", "answer": answer,
"evidence": evidence, "uncertainty": uncertainty}
result = grounded_answer("右上角有 3 个图标", [{
"media_id": "img-07", "span": [0.62, 0.05, 0.96, 0.30], "quote": "3"
}], "medium")
assert result["status"] == "answered"验收标准:每个结论能定位到页、区域或时间段,低证据请求会拒答。
若结果不对:先重放同一媒体和处理器版本,再查证据定位,最后查生成参数。
对话模型的跨模态验收
把模型回答拆成观察、证据、推断、限制和动作五层,逐层检查。观察必须来自媒体或工具,推断说明不确定,限制声明不可见内容,动作需要用户确认或控制器授权;混在一段自由文本里会让审计和修复都变难。
做多媒体指代回归:用户先发图 A,再发图 B,随后问“它的标题是什么”;交换上传顺序、删除 A、补发 A 的高清版本,检查媒体 ID 和引用是否稳定。模型应询问歧义,而不是按最近或同名文件猜。
流式音频测试暂定转写的修订和摘要。后续片段到达后,已确认的证据不应被悄悄覆盖;工具动作只接受 confirmed 或人工确认的字段。记录每次修订的时间和原因,用户可以看到最终依据。
视觉与语音模型的安全边界不同:图像中的文字可能是注入,音频中的命令可能来自第三方,合成语音可能被误认为原声。协议在来源、可信度和合成标记上分别表达,不能用一个通用 confidence 解决。
替换编码器或语言模型后,保留相同任务合同和挑战集,比较答案、证据、拒答、延迟和成本。迁移成功的定义是接口与安全不回退,同时能力变化有证据,而不是只看生成文本更长。
| 维度 | 回归样例 | 通过条件 | 失败处理 |
|---|---|---|---|
| 观察 | 遮挡/交换媒体 | 随证据变化 | 不确定/补充 |
| 证据 | 区域/时间/页码 | 有效可定位 | 阻断交付 |
| 指代 | 多轮/删除/补发 | ID 稳定无猜测 | 询问/拒答 |
| 流式 | 暂定→确认→修订 | 外部动作等确认 | 暂停/回放 |
| 合成 | 生成语音/图像 | 清楚标记来源 | 限制展示 |
对话模型迁移检查
替换视觉或音频编码器后,保持消息 schema、媒体 ID、证据类型和状态枚举不变,先跑指代、缺失、冲突、流式修订和权限回归。能力提升必须伴随引用和拒答不退化。
把模型生成的观察、推断、限制和动作分开渲染。任何需要外部副作用的动作只接受控制器确认;合成语音/图像清楚标记来源,不能混入原始媒体证据。
本节通过标准:读者能从多轮回答追到正确媒体和时间/区域,并说明何时补充输入、何时转人工、何时拒绝。
对话层的最小验收
对一条回答分别检查观察、证据、推断、限制和动作,确保它们不是一段无法审计的自由文本。交换、删除或遮挡媒体后,引用和状态应同步变化;流式语音的暂定结果不能触发外部动作。
通过标准:多轮媒体 ID 稳定,合成内容有标记,无法观察时会说明缺少什么并提出下一步,而不是用语言先验补全。
对话层通过标准
检查模型替换、媒体顺序、区域/时间引用、流式修订、缺失和撤权五类回归;模型可以表达候选与不确定,但不能伪造观察或直接执行动作。合成图像/语音与原始证据必须有清楚的来源标记。
验收时从一条回答追到消息、媒体、表示、工具和版本,任何一环无法追溯就进入修复或人工队列。
07 · INTERACTION & TOOLS
指令、工具与多模态代理
多模态模型擅长把图像、音频和文本组织成自然语言,但可靠系统通常还需要 OCR、ASR、检测、计算、检索和业务 API。指令负责描述任务,grounding 负责绑定证据,工具负责执行可验证操作,控制器负责权限、预算与停止。
12.7.1 多模态提示与 grounding
多模态提示不只是把“请看图”写得更长,而是明确任务、媒体、区域/时间、输出字段、证据要求和不确定处理。一个好的提示让模型知道哪些内容是用户指令、哪些内容是媒体中的不可信文字、哪些工具可以调用以及何时必须停止。提示越具体,评估越容易;但提示不能替代校验器。
grounding 的核心是把语言断言绑定到可观察证据。图像回答引用区域,视频回答引用帧区间,语音总结引用音频时间和转写,文档回答引用页码与文本范围。先让模型返回结构化 evidence,再生成自然语言解释,可以检查引用是否存在、是否越界、是否有权限。
区域提示可以使用归一化坐标、像素框、点、掩码或页面块 ID;时间提示使用毫秒区间或帧编号。格式必须与编码器和前端一致,不能让模型自己猜坐标原点。对区域裁剪后的图片,保存裁剪窗口与原图映射;对音频切块,保存块偏移和重叠。
指令和媒体文字的优先级必须隔离。图片里可能出现一张写着“忽略系统规则”的纸,网页截图可能包含按钮文字,录音里可能有人说“发送邮件”。这些是被观察的内容,不是授权指令。输入层给媒体内容标记 untrusted,控制器只接受系统策略、用户明确确认和白名单工具。
grounding 也要表达不确定。模型可以返回多个候选区域、区间和置信提示,而不是强迫选一个。若证据被遮挡、分辨率不足或引用冲突,状态应是 uncertain 或 conflict,并给出补图、补录音、放大区域或人工复核动作。用户看见不确定,比看见一个没有依据的确定答案更有帮助。
多模态提示的上下文顺序会影响结果。把问题放在图片前后、先给 OCR 再给原图、把多个媒体插在同一段文字中,都可能改变注意力。固定模板和媒体顺序,建立提示版本;比较模板时一次只改一项,并报告不同语言、任务和媒体数量的影响。
引用必须与输出字段建立关系。回答中有三个数字,就应有三个证据引用或明确说明某个数字来自计算工具;一句泛泛的“如图所示”不能通过审计。对复杂回答,可以返回 claim_id、claim、evidence_refs、uncertainty 和 next_action,再由渲染层生成用户可读文本。
提示注入不只存在于文字聊天。图片、PDF、音频转写、字幕和工具返回都可能携带恶意指令;OCR 或 ASR 会把它们变成更容易被语言模型读取的文本。把来源标签作为上下文的一部分,并在控制器中屏蔽媒体内容对系统消息、工具参数和权限的影响。
grounding 评估不能只看答案是否包含区域。区域可能是大致相关但漏掉关键文字,时间可能覆盖了事件前后的无关片段,文本引用可能来自错误页面。使用 IoU、时间重叠、字符范围准确率和人工证据充分性评分,分别评估定位和解释。
学习时从两个简单任务开始:让模型为图片中的一个按钮返回坐标,让模型为一段录音返回一句决定的时间区间。先用人工标注和假模型验证 schema,再测试低质量、多个候选、冲突和媒体内指令。每次回答都问“我能否回到原始证据”,不能就进入不确定。
grounding 输出模板
一个可审计的 claim 结构可以包含 claim_id、text、evidence_refs、basis、confidence_hint、limitations 和 status。evidence_ref 再包含 media_id、kind、range、source_version 和 permission_scope。这样“模型说了什么”和“依据在哪里”是两个可独立检查的对象。
提示中写“请给证据”不够。后处理要验证引用的媒体 ID、区域/时间是否在范围内、权限是否仍有效、是否与任务要求的模态相符。验证失败就返回 invalid_evidence,不要把引用删除后继续显示答案。
多个证据冲突时,输出双方和冲突原因;不要让模型用语言顺滑地平均掉差异。高风险任务要求人工裁决,低风险任务也要告诉用户哪一部分来自图片、哪一部分来自文本或工具。
| 证据形式 | 适合任务 | 最低字段 | 常见误用 |
|---|---|---|---|
| 区域框/点 | 物体、按钮、版面 | media_id、bbox、坐标系 | 把大框当精确文字证据 |
| 掩码 | 轮廓、面积、分割 | mask_id、分辨率、映射 | 掩码与原图错位 |
| 时间区间 | 语音、视频事件 | start/end、偏移、帧率 | 只给“刚才” |
| 文本范围 | OCR、说明书、字幕 | 页码/字符/版本 | 派生文本冒充原文 |
| 多证据链 | 比较、因果线索 | claim_id、顺序、冲突 | 缺一条关系就无法复核 |
模型可以提出断言,但 grounding、工具与控制器共同决定断言是否能被验证和执行。
def validate_claims(claims, media):
allowed = {item["id"]: item for item in media}
errors = []
for claim in claims:
refs = claim.get("evidence_refs", [])
if claim.get("status") == "answer" and not refs:
errors.append((claim["id"], "missing_evidence"))
for ref in refs:
item = allowed.get(ref.get("media_id"))
if item is None:
errors.append((claim["id"], "unknown_media"))
continue
if ref.get("kind") == "region" and item["type"] != "image":
errors.append((claim["id"], "region_on_non_image"))
if ref.get("kind") == "time_span" and item["type"] != "audio":
errors.append((claim["id"], "time_on_non_audio"))
return errors
claims = [{"id": "c-1", "status": "answer",
"evidence_refs": [{"media_id": "img-1", "kind": "region"}]}]
media = [{"id": "img-1", "type": "image"}]
print(validate_claims(claims, media))为“这张图中的数字是多少”设计 claim/evidence schema,要求能引用图像区域、OCR 结果和不确定状态;再写一个错误引用的测试。
参考结论:观察与解释分开;引用有坐标/版本;媒体文字不会改变系统指令。
完整实现路径:工具调用采用输入/输出 schema、幂等键、超时和状态机;模型不能把工具计划当成工具结果。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
ALLOWED = {
"queued": {"running", "cancelled"},
"running": {"succeeded", "failed", "timed_out", "cancelled"},
}
def transition(job, target):
if target not in ALLOWED.get(job["state"], set()):
raise ValueError(f"illegal transition: {job['state']} -> {target}")
job["state"] = target
return job
job = {"id": "ocr-17", "state": "queued", "idempotency_key": "abc"}
assert transition(job, "running")["state"] == "running"验收标准:超时、重复提交、权限失败和用户撤权均能回退。
若结果不对:按 request_id 串起模型、工具和用户确认日志,定位第一次状态跳变。
12.7.2 OCR、ASR 与专用工具
OCR、ASR、检测、分割、表格解析、单位换算和计算器是多模态系统的专用工具。它们把难以直接生成的证据转成结构化结果,降低语言模型凭印象补全的机会;但工具也会错,必须带版本、置信度、输入范围和失败状态。工具输出不是天然真值。
OCR 适合读取文字、页码和表格字段,但对低分辨率、倾斜、手写、反光和复杂版面敏感。调用前可以裁剪、旋转或提高分辨率,调用后检查字符置信度、区域和语言;数字、单位、负号和小数点需要单独回归集,不能只看整段文字相似度。
ASR 将波形转成文字和时间戳,受噪声、口音、重叠说话和专名影响。会议摘要应引用原音区间和转写版本,重要决定需要人工确认;语音中的“不要”“不是”“两百”被错听会改变含义,后处理不能擅自纠正成常见表达。
检测和分割工具提供空间候选,适合计数、定位和面积测量;它们可能漏小目标、重复同一物体或把遮挡部分分开。后处理的阈值、NMS、坐标映射和类别词表要版本化,输出协议区分 no_result、uncertain 和 invalid,而不是把空列表解释成“场景中不存在”。
工具选择可以由路由器决定。图表问题先调用 OCR/版面解析,语音问题先调用 ASR,精确数值先调用计算器,通用解释再交给语言模型。路由规则应根据任务、媒体质量和预算,记录为什么选择工具;不能让模型自由决定调用一个没有权限或没有验证的 API。
工具输入要最小化。OCR 不需要读取整个用户会话,计算器不需要看到原图,检索工具不应绕过文档权限。控制器为每个工具定义输入 schema、允许字段、超时、成本、权限和最大重试;模型生成的工具参数必须先校验,再执行。
工具结果与用户文本可能冲突。用户说“图上是 80”,OCR 读成 30,系统应展示冲突并请求确认;不要用语言模型的常识自动改写。比较时保留原始工具输出、置信度和区域,必要时运行第二个工具或交人工。
工具链有错误传播。图片裁剪错会让 OCR 错,ASR 时间偏移会让摘要引用错,检测框偏移会让 VLM 描述错。每一步保留 parent_ref 和版本,评估时既看最终答案,也看中间证据;否则无法知道哪一个工具造成了问题。
专用工具还可能改变隐私风险。声纹识别、OCR 姓名、文档解析和人脸检测产生的派生数据不一定与原媒体权限相同。工具日志最小化保存,敏感字段脱敏,删除请求传播到结果、缓存和索引。不能因为工具是内部服务就跳过审计。
学习实践可以用三个假工具:返回固定 OCR、ASR 和检测结果,先验证路由、schema、冲突、超时和权限;再替换真实工具。这样你会看到,代理系统的可靠性很大部分来自工具协议和控制器,而不是模型会不会说一句漂亮的话。
工具卡片应该包含什么
工具卡记录名称、用途、输入 schema、输出 schema、版本、时延/成本、权限、数据保留、失败状态和适用范围。例如 OCR 卡片要写支持的语言、最小字号、区域坐标和置信度语义;ASR 卡片要写采样率、语言、时间戳精度和重叠说话限制。
工具返回结果必须可回到输入范围。OCR 字符带页面/区域,ASR 词带音频时间,检测候选带原图框,计算器带表达式和单位。模型可以总结工具结果,但不能丢掉原始结果或把工具置信度转换成事实确定性。
每个工具都要有异常演练:输入损坏、超时、空结果、非法数字、权限不足、版本不兼容和重复调用。异常应被控制器捕获并映射到任务状态,用户看到可行动提示,日志保留追踪 ID。
| 工具 | 输入 | 输出证据 | 必须验证 |
|---|---|---|---|
| OCR | 图片区域/页面 | 字符、框、置信度 | 语言、数字、区域 |
| ASR | 音频片段 | 文字、时间、说话人 | 偏移、专名、否定 |
| 检测 | 图片/视频帧 | 类别、框、分数 | 坐标、重复、阈值 |
| 计算器 | 表达式、单位 | 数值、公式 | 单位、范围、舍入 |
| 检索 | 查询、权限域 | 文档片段、版本 | 来源、时间、权限 |
| 人工队列 | 状态、证据链 | 裁决、理由 | 身份、时限、回执 |
from dataclasses import dataclass
@dataclass
class ToolResult:
tool: str
version: str
status: str
input_ref: str
output: dict
error: str | None = None
def accept_tool(result: ToolResult, allowed_tools):
if result.tool not in allowed_tools:
return {"state": "blocked", "reason": "tool_not_allowed"}
if result.status not in {"ok", "empty", "uncertain", "failed"}:
return {"state": "blocked", "reason": "bad_tool_status"}
if result.status == "failed":
return {"state": "tool_failed", "error": result.error or "unknown"}
return {"state": "accepted", "tool": result.tool, "output": result.output}
print(accept_tool(ToolResult("ocr", "v2", "ok", "img-1", {"text": "80"}),
{"ocr", "asr"}))为 OCR、ASR 或计算器选一个工具,写输入/输出 schema、版本、权限、超时、失败状态和一条冲突案例。
参考结论:工具结果带来源与范围;参数先校验;失败不会被模型语言掩盖。
完整实现路径:工具调用采用输入/输出 schema、幂等键、超时和状态机;模型不能把工具计划当成工具结果。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
ALLOWED = {
"queued": {"running", "cancelled"},
"running": {"succeeded", "failed", "timed_out", "cancelled"},
}
def transition(job, target):
if target not in ALLOWED.get(job["state"], set()):
raise ValueError(f"illegal transition: {job['state']} -> {target}")
job["state"] = target
return job
job = {"id": "ocr-17", "state": "queued", "idempotency_key": "abc"}
assert transition(job, "running")["state"] == "running"验收标准:超时、重复提交、权限失败和用户撤权均能回退。
若结果不对:按 request_id 串起模型、工具和用户确认日志,定位第一次状态跳变。
12.7.3 代理编排与权限边界
多模态代理把感知、检索、规划、工具调用和交付串成循环。一个安全的循环通常是:解析任务→验证媒体→选择工具/模型→获得候选证据→校验结果→询问或执行→记录回执。模型可以提出下一步,但每个动作由控制器判断是否满足权限、预算、状态和用户确认。
规划与执行要分开。模型可以提出“先 OCR 再计算”,控制器检查图片是否允许处理、工具是否在白名单、表达式是否来自可信字段、计算结果是否需要人工。不要让模型直接生成任意 URL、SQL、邮件内容或文件路径并执行;参数化接口和显式 schema 才能降低风险。
权限应按媒体、工具、动作和用户分层。用户可能允许分析一张公开图片,却不允许读取另一张私密照片;允许 OCR,却不允许声纹识别;允许生成草稿,却不允许自动发布。授权记录、目的、时限和撤回动作进入请求上下文,控制器每一步重新检查。
代理状态包含观察、计划、等待、调用、验证、执行、完成和阻断。出现工具超时、证据冲突、预算不足或用户撤权时,状态机应停止或转人工;不能靠模型自行决定“继续试试”。每个状态有可见消息和内部日志,重试需要幂等键。
多模态代理的循环可能被媒体内容诱导。图片中的二维码、文档里的链接、音频中的指令和检索文档里的 Prompt 都是不可信输入。把它们当数据传给模型,不把它们升级成系统策略;外部动作必须经过用户意图和服务端规则,不能由模型文字触发。
预算限制包括 Token、像素、音频时长、工具次数、网络请求、存储和人工队列。代理在接近上限时可以请求用户缩小范围或分批处理;超限后返回已完成的安全证据和未完成原因。无限重试既浪费资源,也会放大错误和越权风险。
并发和回滚是代理工程的一部分。两个工具同时修改同一记录时,需要锁、版本或用户确认;工具结果迟到时不能覆盖更新后的媒体;模型升级后若 schema 改变,旧任务应按旧协议回放。所有外部动作返回回执,失败时有补偿或撤销路径。
代理日志要记录输入引用、计划版本、工具参数摘要、权限决策、结果状态、用户确认和最终回执,避免保存不必要的原图或音频。敏感字段脱敏,日志访问分层;审计人员能看到决策依据,但普通用户只看到与任务相关的说明。
人机协作不是“模型失败后才找人”。高风险动作、低质量证据、冲突结论和身份/健康/金融推断应在一开始就进入人工门槛。代理把证据包、问题、候选和建议动作交给人,人返回裁决和理由,再由控制器执行或拒绝。
完成本小节后,应该能画出一台有限状态机,并指出哪些节点只能由服务端决定。代理的评分重点不是步骤越多越聪明,而是职责清楚、权限可审计、失败可停止、动作可回放。
代理控制器的硬边界
模型负责意图解析、候选计划和自然语言解释;控制器负责身份、权限、预算、schema、工具白名单、幂等、审计和最终执行。将这些责任写成接口,而不是放在系统提示词中。提示可以帮助模型遵守,但不能作为唯一安全机制。
每次工具调用前检查四件事:参数是否来自允许字段,媒体/文档权限是否覆盖,预算与风险是否通过,调用是否需要用户确认。调用后检查状态、版本、输出 schema、证据范围和副作用回执;任一项失败,进入阻断或人工。
代理需要可恢复状态。保存任务 ID、当前步骤、已验证证据、待确认动作和版本;重启后从安全检查点继续,避免重复发送或重复写入。回滚不仅切模型,还要恢复路由、阈值、工具版本和权限配置。
| 步骤 | 模型可以做 | 控制器必须做 | 失败动作 |
|---|---|---|---|
| 解析 | 提取意图/候选字段 | 验证用户与 schema | 请求澄清 |
| 规划 | 提出工具顺序 | 白名单、预算、权限 | 阻断计划 |
| 调用 | 生成参数草稿 | 校验并执行 API | 重试/人工 |
| 观察 | 总结候选证据 | 验证来源/范围/版本 | 标记不确定 |
| 执行 | 解释结果 | 确认副作用/幂等 | 补偿或回滚 |
| 交付 | 生成说明 | 过滤敏感/写回执 | 安全结束 |
from enum import Enum
class State(Enum):
READY = "ready"
WAIT_CONFIRM = "wait_confirm"
RUN_TOOL = "run_tool"
BLOCKED = "blocked"
DONE = "done"
def transition(state, event, policy):
if state is State.READY and event == "needs_external_action":
return State.WAIT_CONFIRM
if state is State.WAIT_CONFIRM and event == "confirmed":
return State.RUN_TOOL
if event in {"permission_denied", "budget_exceeded", "invalid_output"}:
return State.BLOCKED
if state is State.RUN_TOOL and event == "tool_ok":
return State.DONE
return state
state = State.READY
for event in ["needs_external_action", "confirmed", "tool_ok"]:
state = transition(state, event, {})
print(state.value)为“图片 OCR→计算→写入学习笔记”画出状态、权限、用户确认和回滚;加入工具超时、OCR 冲突和重复请求。
参考结论:合格标准:模型不能直接执行;每个状态有停止动作;外部写入有幂等和回执。
完整实现路径:工具调用采用输入/输出 schema、幂等键、超时和状态机;模型不能把工具计划当成工具结果。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
ALLOWED = {
"queued": {"running", "cancelled"},
"running": {"succeeded", "failed", "timed_out", "cancelled"},
}
def transition(job, target):
if target not in ALLOWED.get(job["state"], set()):
raise ValueError(f"illegal transition: {job['state']} -> {target}")
job["state"] = target
return job
job = {"id": "ocr-17", "state": "queued", "idempotency_key": "abc"}
assert transition(job, "running")["state"] == "running"验收标准:超时、重复提交、权限失败和用户撤权均能回退。
若结果不对:按 request_id 串起模型、工具和用户确认日志,定位第一次状态跳变。
本节复习与易错
易错一:把 grounding 当作一句“请引用图片”。引用必须有媒体 ID、区域/时间/版本,并经过校验。易错二:把工具结果当真值。OCR、ASR、检测和计算都有输入范围、版本和失败状态。
易错三:把权限写进 Prompt。权限、预算和外部副作用只能由控制器和服务端执行。易错四:代理遇到错误就无限重试。重试需要上限、幂等和停止/人工路径。
本节验收:提交 grounding schema、工具卡片、路由与状态机、权限测试和一次工具冲突回放;读者能指出模型、工具和控制器各自的职责。
工具协作的可验证回放
为同一个问题分别使用 OCR、ASR、检测和计算器工具,保存输入范围、工具版本、输出、置信度和失败状态。语言模型只总结这些结果,不能直接改写数字、坐标或时间。比较工具关闭、工具成功、工具超时和工具冲突四种路径。
对图片/音频中的文字加入“调用某工具”的诱导,确认控制器把它视为不可信内容。工具参数只能来自允许字段,媒体权限、预算、超时和幂等在执行前检查,输出回传后再校验 schema。
grounding 结果使用 claim_id 连接多个证据。一个答案同时引用 OCR 字符和原图区域时,检查两者是否来自同一版本和同一裁剪;冲突时显示双方,不让模型用一句顺滑话覆盖差异。
代理规划需要记录计划版本和状态迁移。模型可以提出先 OCR 后计算,控制器决定是否执行;若预算不足或权限撤回,状态变成 needs_split、blocked 或 review,不允许无限重试。
把人工确认设计成产品流程而非错误页面。人工看到问题、媒体、证据、候选和风险,返回裁决理由;控制器保存回执并执行或拒绝,后续评估把裁决加入回归。
模型只提出计划,工具提供派生证据,控制器和人工共同决定结果能否触发动作。
| 阶段 | 必须保存 | 典型失败 | 控制动作 |
|---|---|---|---|
| 计划 | prompt/plan_version | 工具不在白名单 | 拒绝调用 |
| 预检 | 权限/预算/参数 | 越权、超限 | blocked/分段 |
| 执行 | input_ref/版本/耗时 | 超时/空结果 | 重试上限/人工 |
| 校验 | schema/范围/置信 | 坐标或数字非法 | 丢弃并记录 |
| 交付 | 确认/回执 | 重复副作用 | 幂等/回滚 |
让 OCR 与用户文字给出不同数字,模拟工具超时、权限撤回和重复提交;写控制器的状态和用户提示。
参考结论:合格标准:冲突不被平均;参数先校验;副作用有确认、幂等和回执。
完整实现路径:工具调用采用输入/输出 schema、幂等键、超时和状态机;模型不能把工具计划当成工具结果。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
ALLOWED = {
"queued": {"running", "cancelled"},
"running": {"succeeded", "failed", "timed_out", "cancelled"},
}
def transition(job, target):
if target not in ALLOWED.get(job["state"], set()):
raise ValueError(f"illegal transition: {job['state']} -> {target}")
job["state"] = target
return job
job = {"id": "ocr-17", "state": "queued", "idempotency_key": "abc"}
assert transition(job, "running")["state"] == "running"验收标准:超时、重复提交、权限失败和用户撤权均能回退。
若结果不对:按 request_id 串起模型、工具和用户确认日志,定位第一次状态跳变。
代理拒答与人工协作练习
为每个工具写一条“可以调用”和一条“必须拒绝”的样例。拒绝原因应能归入权限、预算、输入质量、证据不足、工具不在白名单或高风险动作;用户看到下一步,而不是一串内部异常。
模拟模型提出一个合理但越权的计划,例如从图片 OCR 出姓名后自动写入公开笔记。控制器应拆解计划、阻断越权字段、保留安全的局部结果,并请求用户确认是否需要人工处理。
人工队列的任务包包含问题、媒体引用、工具结果、模型候选、风险、建议动作和截止时间。评审者只需做任务判断,不需要重新猜测模型看到了什么;裁决回写后成为可检索的回归样例。
重试和幂等要在工具层验证。网络超时后重复提交同一请求不能产生两条记录;媒体或权限变化后旧计划不能继续执行。保存 request_id、idempotency_key 和回执,回滚时可撤销或补偿。
代理的学习成果是职责分离:模型善于提出候选和解释,工具善于提供专门证据,控制器善于限制和执行。任何一层失职都应在测试和日志中可见。
| 原因 | 例子 | 用户动作 | 控制器日志 |
|---|---|---|---|
| 权限 | 私密音频无权读取 | 申请授权/换媒体 | scope、媒体 ID |
| 预算 | 音频过长/工具过多 | 分段/缩小范围 | 预算明细 |
| 证据 | 低清无法读字 | 补图/人工 | 质量指标 |
| 策略 | 身份/高风险推断 | 说明边界 | 规则版本 |
| 工具 | 超时/空结果 | 稍后重试/人工 | 回执/幂等 |
08 · EVALUATION & OPERATIONS
评估、安全与运营
多模态系统的可靠性不能由一张演示图或一个平均分证明。评估要覆盖任务指标、证据定位、模态消融和关键切片;安全要覆盖缺失、冲突、隐私、注入与越权;运营要能监控漂移、成本和错误,并在必要时快速暂停和回滚。
12.8.1 指标、切片与人工评审
指标必须从任务合同推导。图文检索可以报告 Recall@K、平均排名和双向一致性;图像问答需要答案正确性、区域 IoU、OCR 字符准确率和拒答质量;视频摘要要看事件覆盖、时间定位和事实一致性;语音任务还要看词错误率、时间戳误差和说话人混淆。一个总分不能替代这些任务特有指标。
多模态输出往往同时有内容和位置。回答文字正确但引用区域错误,或检测框正确但类别说明错误,都应被拆开计分。建立 claim-level 评估,把每条断言与证据引用、工具结果和人工判断关联;这样能知道问题是观察错、对齐错、推理错还是表达错。
检索指标受正例定义影响。一个图片可能有多个合理描述,一段音频可能包含多个事件;使用单一金标准会把合理候选算成错误。保存多正例、分级相关和未知状态,报告严格/宽松两种指标,避免指标设计反过来惩罚正确的多样答案。
生成质量需要人和自动指标配合。文字流畅、图像相似或语音自然不等于事实正确;人工评审要有任务指南、证据核对和盲审,自动指标作为筛选和回归而不是唯一裁决。保存分歧、裁决理由和评审者一致性,发现指南含糊时先修指南。
切片是评估的主线。按语言、设备、分辨率、时长、光照、噪声、场景、对象尺寸、媒体数量、缺失类型、冲突类型和权限状态切片,报告样本量和置信区间。切片变量要有任务和风险依据,不为了追求好看的分数而挑选。
模态消融回答模型是否使用正确证据。去掉图片、静音关键音频、交换两张图片、打乱字幕时间、遮住文字区域或替换背景,观察内容、引用和拒答变化。消融结果要固定其他输入和随机种子;若输出几乎不变,说明模型可能依赖文本先验或来源捷径。
对比基线帮助解释能力来源。比较单模态模型、晚期融合、通用多模态模型、工具增强和人工规则,保持数据、预算和评估集一致。报告不仅写“多模态更好”,还要说明在哪些任务和切片有收益、在哪些场景成本增加却没有证据改善。
人工评审要关注证据和沟通。评审者检查回答是否引用正确模态、是否把不确定说清、是否给出可行动的补充请求、是否泄露不应展示的信息。把“安全拒答”“无帮助拒绝”“幻觉强答”分成不同类别,才能指导修复。
评估集要版本化并防止过拟合。开发集用于调参,保留集用于发布,挑战集用于攻击和边界;提示模板、阈值和工具版本改变后重新运行。定期加入新错误但保留旧回归集,避免模型只适应最新样例。
评价结果最终要支持决策。报告写明达到/未达到的门槛、关键失败、适用范围、成本、人工比例和下一步实验;不要用平均分掩盖高风险失败。若证据不足,结论写“未知”,并说明需要什么数据或人工判断。
评估报告的最小结构
第一部分说明任务、用户、非目标、媒体和版本;第二部分说明数据切分、正例/负例、工具、预处理和随机种子;第三部分给总体指标、关键切片、置信区间、消融、失败样例和人工一致性;第四部分给发布门槛、限制、监控与回滚动作。
每条结论都要能回到样例或统计。样例给媒体 ID、区域/时间、模型和协议版本;统计给样本量、分母、缺失处理和置信区间。只给一个百分比没有审计价值,也无法判断改动是否真的改善了问题。
自动指标和人工评审可能冲突。自动相似度升高但证据定位下降,或人工认为表达更清楚但数值错误增多时,保留冲突并按任务风险决定优先级。不要为了让表格更整齐而删掉分歧。
| 任务 | 核心指标 | 证据/切片 | 常见误读 |
|---|---|---|---|
| 跨模态检索 | R@1/5/10、MRR | 语言/实体/来源 | 高召回等于事实正确 |
| 图像问答 | 答案、区域 IoU、拒答 | 小字/遮挡/未知 | 流畅度等于观察 |
| 视频理解 | 事件 F1、时间 IoU | 短事件/长视频 | 摘要完整等于无遗漏 |
| 语音任务 | WER、时间误差、分离 | 口音/噪声/重叠 | 转写润色等于原声 |
| 文档任务 | 字段 F1、页码/框准确 | 表格/跨页/低清 | 文本正确等于版面正确 |
| 代理任务 | 工具成功、越权率、回执 | 缺失/冲突/撤权 | 调用成功等于安全 |
指标、人工和运营是连续链路;任何一层失败都应能阻止不安全结果继续流向用户。
from collections import defaultdict
def hit_at_k(ranked, gold, k):
return int(bool(set(ranked[:k]) & set(gold)))
def grouped_recall(cases, k=5):
groups = defaultdict(list)
for case in cases:
groups[case["slice"]].append(
hit_at_k(case["ranked"], case["gold"], k)
)
return {
name: {"count": len(values), "recall": sum(values) / len(values)}
for name, values in groups.items()
}
cases = [
{"slice": "clean", "ranked": ["a", "b"], "gold": ["a"]},
{"slice": "missing_audio", "ranked": ["c"], "gold": ["d"]},
]
print(grouped_recall(cases))为图文检索、图像问答或语音总结选择主指标、证据指标、三个切片、一个消融和一个人工评审问题。
参考结论:指标来自任务合同;切片有样本量;能区分答案正确与证据正确。
完整实现路径:按任务质量、证据正确、切片鲁棒、安全和系统成本五层评估,并设计缺失/冲突模态回放。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
def slice_report(rows):
groups = {}
for row in rows:
key = (row["modality_state"], row["language"])
groups.setdefault(key, []).append(row)
return {str(k): {
"n": len(v),
"answer_accuracy": sum(x["answer_ok"] for x in v) / len(v),
"evidence_accuracy": sum(x["evidence_ok"] for x in v) / len(v),
} for k, v in groups.items()}
report = slice_report(rows)
assert all(v["evidence_accuracy"] >= .90 for v in report.values())验收标准:总体分数与关键切片同时过门,P0 事故可按 runbook 回滚。
若结果不对:先区分输入问题、工具问题、融合问题和生成问题,再归因。
12.8.2 缺失模态、鲁棒性与隐私
真实输入很少完美。图片会模糊或被裁,音频会有噪声或重叠说话,文本会截断或包含错别字,模态之间会失去同步。鲁棒性评估不是随机加噪后看一个分数,而是列出任务中可能发生的变化、它们应保持的语义和可接受的降级状态。
缺失模态测试要区分自然缺失、上传失败、权限遮蔽和工具不可用。对每种情况,系统可以降级、请求补充或阻断,但行为要可预期。把图片置空、音频静音和文本删掉可能得到不同含义,测试集要记录缺失原因,不能把所有情况合成一个 missing 标签。
冲突测试同时改变两个来源的说法。图片读数、OCR、用户文字和说明书可能不一致;模型应展示冲突、引用双方并请求确认。若系统在高风险场景自动选择一个来源,必须有明确的业务规则和人工门槛,不能把模型置信度当作裁决权。
扰动覆盖压缩、低光、模糊、遮挡、旋转、背景替换、采样率变化、口音、混响、字幕延迟、文字覆盖、媒体顺序和超长上下文。每项扰动保存参数、原始媒体、输出、证据和指标;否则无法判断失败来自扰动还是数据管道。
对抗输入可能出现在图像、音频、字幕、OCR 和检索文档中。图片文字诱导工具调用,音频命令诱导发送消息,网页内容要求泄露系统提示;这些都应当被标记为不可信数据。控制器验证权限、工具参数和输出,模型不能单独解除限制。
隐私评估覆盖原媒体和派生数据。检查人脸、声纹、姓名、地址、屏幕、医疗或财务信息是否被不必要保存、返回或记录;验证脱敏后的区域、转写和向量是否仍可识别。最小化收集、分层权限和删除传播要用测试证明,而不是写在说明里。
数据许可和内容安全是模型运行时问题。输入素材可能只允许内部分析,生成结果可能需要标记,工具检索可能返回过期或未授权文档。评估集加入许可状态、敏感类别、撤权和过期样例,确保模型在边界外进入 blocked 或 review。
鲁棒性与公平性要按切片看。不同语言、口音、设备、分辨率和媒体组合可能出现系统性差异;样本少时报告不确定范围,不能用平均分掩盖。修复可以补数据、改路由、降低适用范围、增加人工或拒答,不一定是换更大模型。
删除回放要覆盖训练前、服务中和评估后。删除原图后,缩略图、OCR、ASR、向量、缓存、日志和报告引用是否仍可见?如果权重无法逐样本删除,说明限制、风险与重新训练周期,避免误导用户以为删除文件就完全消失。
安全测试的结果要映射到动作。关键隐私泄露、越权工具调用和错误高风险建议触发阻断;轻微质量下降可以降级或提示。每个门槛有负责人、证据、通知和回滚,运营团队才能在新模型或新数据上线时迅速响应。
缺失模态的测试矩阵
完整输入作为基线,再分别移除图片、音频、文本,制造上传损坏、权限遮蔽、工具超时和低质量;记录答案、证据、状态、成本与用户动作。不要只比较准确率,还要检查系统是否诚实说明覆盖范围。
对冲突样本,固定一个来源逐步改变另一个来源,观察模型是否识别冲突而不是顺从最后一句话。涉及数字、身份、医疗或外部动作时,默认人工;低风险任务也要显示双方来源。
隐私回放使用假数据和受控权限。检查原图、裁剪、转写、向量和日志的访问;撤权后重新请求、缓存命中和报告下载都应失败或返回已删除状态。
| 场景 | 扰动/风险 | 期望状态 | 证据 |
|---|---|---|---|
| 低光/压缩 | 视觉质量下降 | uncertain 或降级 | 质量分、区域、提示 |
| 噪声/口音 | ASR 错误 | 复核或请求重录 | 音频区间、WER |
| 缺失媒体 | 上传/权限问题 | missing/blocked | 原因、补充动作 |
| 跨模态冲突 | 图文/音频说法不同 | conflict/人工 | 双方引用 |
| 媒体注入 | 图片/字幕恶意指令 | 不执行、记录 | 来源标签、控制器日志 |
| 敏感内容 | 人脸/声纹/PII | 遮蔽/限制/删除 | 权限和保留证明 |
| 模型漂移 | 设备/语言变化 | 告警/回滚 | 切片趋势、版本 |
REQUIRED = {"quality", "privacy", "permission", "cost", "rollback"}
def release_gate(report):
missing = REQUIRED - report.keys()
if missing:
return {"release": False, "state": "missing_evidence", "fields": sorted(missing)}
hard_fail = (
report["privacy"] < 1.0
or report["permission"] < 1.0
or not report["rollback"]
)
if hard_fail:
return {"release": False, "state": "blocked"}
if report["quality"] < 0.85 or report["cost"] > 1.0:
return {"release": False, "state": "needs_tuning"}
return {"release": True, "state": "approved"}
print(release_gate({
"quality": 0.88, "privacy": 1.0, "permission": 1.0,
"cost": 0.70, "rollback": True,
}))用同一问题制作完整、缺图片、低清、OCR 冲突、权限撤回五个样例;写出每个状态、用户提示和是否允许工具调用。
参考结论:缺失原因不同状态不同;冲突不会被平均;隐私/权限失败直接阻断。
完整实现路径:按任务质量、证据正确、切片鲁棒、安全和系统成本五层评估,并设计缺失/冲突模态回放。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
def slice_report(rows):
groups = {}
for row in rows:
key = (row["modality_state"], row["language"])
groups.setdefault(key, []).append(row)
return {str(k): {
"n": len(v),
"answer_accuracy": sum(x["answer_ok"] for x in v) / len(v),
"evidence_accuracy": sum(x["evidence_ok"] for x in v) / len(v),
} for k, v in groups.items()}
report = slice_report(rows)
assert all(v["evidence_accuracy"] >= .90 for v in report.values())验收标准:总体分数与关键切片同时过门,P0 事故可按 runbook 回滚。
若结果不对:先区分输入问题、工具问题、融合问题和生成问题,再归因。
12.8.3 发布门槛、监控与回滚
发布门槛是可执行的最小安全条件。质量门槛可以是关键任务和切片达到最低指标,证据门槛要求引用有效且可定位,安全门槛要求权限、注入和隐私测试通过,运营门槛要求 p95、吞吐和单次成本在预算内,恢复门槛要求旧版本与 runbook 已演练。
门槛要区分硬条件和可优化指标。越权、隐私泄露、非法工具参数和无法回滚属于硬失败,不能被总体分数抵消;文字风格或少量延迟波动可能进入优化队列。表格中给出阈值、分母、证据、负责人和未通过动作,避免上线时临时讨论。
监控不应保存所有原媒体。记录媒体类型、大小、质量、版本、处理阶段、耗时、状态、工具调用、用户纠错和成本;敏感输入只保留受控哈希或匿名统计。监控指标按语言、设备、场景、模态组合和缺失状态切片,才能识别漂移。
输入漂移包括分辨率、光照、音频噪声、语言、文件格式、媒体数量和问题类型变化;标签漂移包括类别比例、用户目标和人工纠错变化。监控分布与质量,不要只等用户投诉。告警阈值有冷启动期和季节性考虑,避免过多误报导致团队忽略真正风险。
错误预算要覆盖模型和工具。ASR 失败、OCR 低置信、检测空结果、连接器超时、结构化输出无效和权限拒绝分别统计;同一任务多次重试要算成本。异常聚类按版本和媒体来源生成错误地图,修复后回归并保留旧样例。
回滚需要恢复一整套兼容配置:模型权重、编码器、连接器、tokenizer、预处理、阈值、工具版本、路由和索引。只切换语言模型可能导致 Token 或视觉维度不匹配;只切换阈值可能让旧评估失效。runbook 写明顺序、检查点、负责人和用户通知。
灰度发布让新模型先处理低风险、低比例流量,比较关键切片、拒答、工具错误、延迟和成本。出现硬门槛失败立即停止并回滚,不能等待平均分稳定。灰度样本和用户反馈要有版本标记,避免新旧结果混在一起。
回滚后做回放,而不是只看服务恢复。重新运行正常、缺失、冲突、撤权、注入、工具超时和高风险样例,检查引用、状态、权限和副作用。若回滚版本的数据契约不同,执行兼容迁移并在报告中说明差异。
运营团队需要明确责任。谁接收告警、谁决定暂停、谁联系用户、谁审查数据许可、谁批准重新上线?将角色和时限写进 runbook;没有负责人的门槛等于没有门槛。每次事故后更新错误地图和测试集。
本节最终产物是一份评估报告、一张安全场景矩阵、一套发布门槛、监控字段、告警规则和回滚演练记录。它们共同把“模型似乎不错”变成可审查、可暂停、可恢复的工程判断。
发布前后检查顺序
发布前先锁定数据/模型/协议版本,运行完整评估、关键切片、缺失/冲突/注入/权限测试,确认预算和回滚;灰度期间按版本切片监控质量、拒答、工具、成本和用户纠错;发布后定期重跑回归和删除回放,出现硬失败立即暂停。
监控字段遵循最小化原则。可以记录处理阶段、耗时、状态、版本和匿名切片,不默认保存原图/原声。需要复盘时使用受控媒体 ID 和短期访问;用户撤回后,监控系统也要清理可追溯引用。
回滚演练至少覆盖模型、连接器、预处理、阈值、工具、路由和索引。演练结束给出成功/失败、耗时、遗留差异和后续修复,不要把“切回旧容器”当作完整回滚。
| 门槛 | 最低证据 | 硬失败示例 | 动作 |
|---|---|---|---|
| 质量 | 总体+关键切片+置信区间 | 关键切片低于底线 | 阻断/补数据 |
| 证据 | 区域/时间/页码可追溯 | 引用越界或不存在 | 阻断 |
| 安全 | 权限/注入/隐私回归 | 越权或泄露 | 立即暂停 |
| 协议 | schema/状态/幂等 | 非法输出触发动作 | 阻断 |
| 性能 | p50/p95/吞吐/成本 | 超预算或雪崩 | 限流/降级 |
| 恢复 | 旧版本/runbook/回放 | 无法重放或回滚 | 不发布 |
from collections import Counter
def summarize(events):
states = Counter(item["state"] for item in events)
by_model = Counter(item["model"] for item in events)
hard_errors = [
item for item in events
if item["state"] in {"permission_denied", "invalid_output", "privacy_blocked"}
]
return {
"count": len(events),
"states": dict(states),
"models": dict(by_model),
"hard_error_count": len(hard_errors),
"p95_ms": sorted(item["latency_ms"] for item in events)
[max(0, int(len(events) * 0.95) - 1)],
}
events = [
{"state": "answer", "model": "mm-v1", "latency_ms": 420},
{"state": "uncertain", "model": "mm-v1", "latency_ms": 900},
{"state": "permission_denied", "model": "mm-v2", "latency_ms": 700},
]
print(summarize(events))为新连接器上线设计灰度、监控、暂停、回滚和回放步骤;至少加入一个权限失败和一个工具超时。
参考结论:合格标准:恢复组件版本完整;硬失败立即阻断;回滚后重新验证证据、状态、权限和成本。
完整实现路径:按任务质量、证据正确、切片鲁棒、安全和系统成本五层评估,并设计缺失/冲突模态回放。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
def slice_report(rows):
groups = {}
for row in rows:
key = (row["modality_state"], row["language"])
groups.setdefault(key, []).append(row)
return {str(k): {
"n": len(v),
"answer_accuracy": sum(x["answer_ok"] for x in v) / len(v),
"evidence_accuracy": sum(x["evidence_ok"] for x in v) / len(v),
} for k, v in groups.items()}
report = slice_report(rows)
assert all(v["evidence_accuracy"] >= .90 for v in report.values())验收标准:总体分数与关键切片同时过门,P0 事故可按 runbook 回滚。
若结果不对:先区分输入问题、工具问题、融合问题和生成问题,再归因。
本节复习与易错
易错一:用总体平均分代表多模态可靠性。必须拆任务、证据和切片。易错二:把自动指标当事实裁决。人工评审、引用核对和失败样例不可省略。易错三:只测试完整输入。缺失、冲突、低质、注入和撤权才会暴露系统边界。
易错四:把隐私写成政策而不测试派生数据。OCR、ASR、向量、缓存和日志同样需要权限和删除回放。易错五:回滚只切权重。连接器、预处理、阈值、工具、路由和索引必须保持兼容。
本节验收:提交指标矩阵、缺失/安全挑战集、发布门槛、监控字段、灰度计划和回滚演练;读者能说明什么情况必须停、谁来停、停后如何证明恢复。
评估安全的端到端复核
建立一张固定挑战集,覆盖完整、缺失、冲突、低质、错位、媒体注入、权限撤回、工具超时、长上下文和预算超限。每个样例写预期状态、证据和不应发生的动作,回归时同时统计能力与协议。
指标报告按任务和切片展开,并给分母、样本量、置信区间和人工一致性。将答案正确、证据正确、引用有效、拒答合适和工具安全分列,避免一个综合分掩盖某类硬失败。
模拟输入分布变化:设备更新、语言比例改变、音频噪声增加、图片分辨率下降、问题更长或媒体更多。监控分布、延迟、成本、拒答和用户纠错,制定告警与灰度规则。
隐私回放从原图/原声到 OCR、ASR、向量、缓存、日志和报告逐层检查。撤权或删除后,任何派生引用都应被过滤或标记已删除;无法逐样本清理权重时,向用户说明限制和重训计划。
回滚演练不只切换模型容器,还恢复编码器、连接器、预处理、阈值、工具、路由和索引。回滚后重新跑完整、缺失、冲突和高风险样例,确认安全路径仍然有效。
| 级别 | 例子 | 是否阻断 | 后续动作 |
|---|---|---|---|
| P0 | 越权动作/隐私泄露 | 立即阻断 | 暂停、通知、回滚 |
| P1 | 关键证据错引/高风险强答 | 阻断高风险 | 人工、修复、回归 |
| P2 | 关键切片性能下降 | 灰度/限制 | 补数据、调路由 |
| P3 | 风格或轻微延迟波动 | 不必阻断 | 进入优化队列 |
| 未知 | 证据不足无法判断 | 保守停止 | 补样本/人工审查 |
为一个多模态助手写质量、证据、权限、隐私、成本和回滚六项门槛;模拟一个 P0 事故并按 runbook 回放。
参考结论:合格标准:硬失败不能被平均分抵消;门槛有人负责;回滚后有重新验证。
完整实现路径:按任务质量、证据正确、切片鲁棒、安全和系统成本五层评估,并设计缺失/冲突模态回放。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
def slice_report(rows):
groups = {}
for row in rows:
key = (row["modality_state"], row["language"])
groups.setdefault(key, []).append(row)
return {str(k): {
"n": len(v),
"answer_accuracy": sum(x["answer_ok"] for x in v) / len(v),
"evidence_accuracy": sum(x["evidence_ok"] for x in v) / len(v),
} for k, v in groups.items()}
report = slice_report(rows)
assert all(v["evidence_accuracy"] >= .90 for v in report.values())验收标准:总体分数与关键切片同时过门,P0 事故可按 runbook 回滚。
若结果不对:先区分输入问题、工具问题、融合问题和生成问题,再归因。
指标解读与阈值练习
先为一个任务写主指标、证据指标、拒答指标、资源指标和安全指标,再确定分母和切片。主指标回答“能力如何”,证据指标回答“依据是否正确”,拒答指标回答“边界是否诚实”,资源和安全指标回答“能否上线”。
阈值校准要在独立验证集完成,最终保留集只用于发布判断。改变阈值后重新计算混淆矩阵、区域/时间覆盖、拒答和人工比例;不要只挑一个看起来最优的点。
当自动指标与人工评审冲突时,先检查任务定义和证据指南。文本相似度高可能仍然错读数字,区域 IoU 高可能覆盖了错误对象;记录冲突样例并更新评审规则,而不是强行平均。
运营监控按版本、语言、设备、媒体组合和缺失状态切片。性能下降可能来自数据漂移、工具升级、相机变化或路由错误;把告警关联到错误地图和负责人,减少盲目重训。
发布后定期执行删除、权限、注入、超时和回滚回放。评估不是一次性报告,而是上线期间持续验证输入、模型、工具和政策是否仍与任务合同一致。
| 指标层 | 回答的问题 | 证据形式 | 未通过动作 |
|---|---|---|---|
| 能力 | 任务能否完成 | 固定集/切片 | 调参或限制范围 |
| 证据 | 答案依据是否正确 | 区域/时间/来源 | 阻断/人工 |
| 边界 | 何时会拒答 | 缺失/冲突/未知集 | 修协议/提示 |
| 运营 | 成本和延迟可控吗 | p95/吞吐/重试 | 限流/分段 |
| 安全 | 会不会越权泄露 | 权限/注入/删除 | 暂停/回滚 |
评估运营迁移检查
每次模型、连接器、工具或策略升级都重跑固定集、关键切片、缺失/冲突/注入/撤权挑战集,并比较答案、证据、状态、成本和延迟。指标下降时先定位层级,再决定修复、限流或回滚。
门槛要有分母、样本量、负责人和动作。越权、隐私泄露、非法参数和无法回滚属于硬失败;风格或轻微延迟属于优化项,不能混为一个平均分。
本节通过标准:报告可以支持发布/阻断决定,监控不保存不必要原媒体,删除和回滚回放都能证明安全路径仍有效。
运营层的最小验收
把能力、证据、边界、性能和安全门槛分开记录,给出分母、样本量、负责人和动作。上线前跑完整、缺失、冲突、注入、撤权、超时和回滚样例;上线后按版本、语言、设备和模态组合监控。
通过标准:硬安全失败立即阻断,监控不保存不必要的原媒体,回滚后能重新验证证据、权限、状态和成本。
运营层通过标准
把一次告警完整回放到用户动作:输入分布或工具状态变化如何被监控发现,谁决定暂停,如何通知,回滚哪些组件,回滚后怎样重新验证。指标、证据、隐私和成本的门槛必须有分母、负责人和时间限制。
验收时不能只展示仪表盘截图;要用固定样例证明缺失、冲突、注入、撤权和超时会进入正确状态,且监控与日志不会保存不必要的原媒体。
评估层最终验收
用一条真实但低风险的请求完成从固定评估、证据核对、权限检查、监控记录到回滚回放的闭环;报告必须列出样本量、版本、阈值、失败状态、负责人和下一步动作。只有当硬安全门槛通过,质量指标才有资格进入发布讨论。
评估层回放记录
回放记录把固定集、挑战集和线上抽样按同一 schema 保存,分别统计答案、证据、拒答、工具、延迟和成本。任何指标都写清分母与切片,不能用少量成功样例代表全量用户。
当新模型通过质量门槛却改变了拒答或权限行为时,安全门槛优先;先暂停或灰度,再分析能力收益。发布结论必须能被另一位同学用 runbook 重复执行。
发布结论
证据充分再发布,证据不足先暂停。
可复现报错:媒体批次没有一一对齐
多模态项目常见的错误不是模型太弱,而是第 7 张图配到了第 8 条文本。不要依赖列表位置,必须用稳定 ID 显式连接。
images = [{"id":"m1"}, {"id":"m2"}]
texts = [{"media_id":"m2", "text":"第二张"}]
image_ids = {x["id"] for x in images}
text_ids = {x["media_id"] for x in texts}
missing_text = image_ids - text_ids
missing_image = text_ids - image_ids
if missing_text or missing_image:
raise ValueError(
f"modality alignment failed: missing_text={missing_text}, "
f"missing_image={missing_image}"
)排错流程
- 冻结请求和媒体哈希,确认不是文件在重试时被替换。
- 检查媒体 ID、页码/时间段、预处理版本和 batch 重排。
- 单独运行编码器,确认问题发生在表示、对齐、融合还是生成阶段。
- 加入缺失、重复、乱序、撤权四个回归样例。
09 · CAPSTONE
综合项目:交付一个多模态助手
把本章知识落到一个小而完整的项目:用户上传课程截图、配套文字或短音频,助手返回页面要点、证据区域/时间、来源和不确定状态。项目不追求接入最大的模型,而追求输入可验证、职责清楚、结果可复现、权限可审计、失败可停止。
12.9.1 项目范围与输出协议
项目选题先控制风险和规模。推荐任务是“课程资料多模态检索与问答”:输入可以是一张公开课程截图、一段公开讲义文字和一段无敏感的讲解音频;输出是与问题相关的页面/段落/时间片、简短回答和引用。不要把默认项目扩展成身份识别、医疗判断、金融决策或自动发布。
用户、媒体和知识库都有明确边界。数据卡写出截图和文字的来源许可、音频是否自录、是否含个人信息、允许保存多久;输入契约限制文件类型、像素、时长和数量;任务合同说明支持的语言、问题类型、非目标和人工复核条件。
输出协议至少包含 status、answer、evidence_refs、media_summary、model_manifest、quality_flags、review_required 和 limitations。status 可取 answer、uncertain、missing、conflict、blocked;evidence_ref 区分 page、text_span、region 和 time_span。结构化结果先过校验,再渲染成用户可读文字。
项目将职责拆成输入、解析、表示、检索/融合、工具、生成、校验和交付。模型只提出候选回答;服务端验证媒体、权限、证据、预算和动作。这样更换视觉或音频模型时,协议和安全测试不必全部重写。
为了体现多模态价值,设计三个问题层级:只需文字即可回答的问题,必须看图/听音频的问题,以及需要跨模态比较的问题。用消融验证每一层是否真正使用了相应证据;如果模型只依赖文字,项目就应诚实标为文本系统,而不是宣称多模态理解。
为每个答案设计最小证据。页面标题引用文字范围,图中按钮引用区域,音频决定引用时间,跨模态比较引用双方。证据不充分时返回补图、补音频或转人工,不允许模型用常识填空。引用显示来源版本,用户可以展开查看。
交互流程至少有上传、验证、预览、提问、检索/工具、回答、纠错和删除。上传后先展示媒体 ID、质量与权限状态;处理失败可以重新上传或缩小范围;回答页显示状态、证据和限制;删除后旧链接和缓存都不可继续读取。
项目要定义成本和时间预算。限制单次图片数量、总像素、音频时长、文本 Token、工具次数和响应 p95;先做便宜的格式/质量检查,再调用模型。预算超限返回分段建议,而不是无限重试或静默截断。
安全与隐私在项目开始就加入。截图可能有姓名和账号,讲义可能有版权限制,音频可能暴露声音;使用脱敏示例、分层权限和短期存储。图片/文本里的“请调用工具”是被观察内容,不是系统命令。
最终项目章程写清成功、失败和不适用。成功是回答有证据并能重放;失败是可解释、可停止、可转人工;不适用是明确拒绝或建议换任务。评分重点是职责和证据,不是代码行数或模型参数量。
项目输出协议示例
answer 状态需要至少一个有效证据;uncertain 状态说明缺失原因和下一步;missing 状态列出需要补充的媒体;conflict 状态引用冲突双方;blocked 状态说明权限、敏感或策略原因。所有状态都包含 model_manifest 和 request_id,便于回放。
media_summary 记录每个媒体的类型、尺寸/时长、质量、权限和处理版本;limitations 说明未覆盖的语言、分辨率、音频噪声或任务范围。不要把限制放在页面底部的小字里,结果与限制要一起呈现。
结构化协议与自然语言分开测试。先用 JSON 校验器拒绝非法字段、陌生媒体、越界区域、无效时间和未授权工具,再让渲染层生成句子。模型输出再流畅,无法通过协议就不能交付。
| 问题 | 本项目选择 | 不做什么 | 理由 |
|---|---|---|---|
| 媒体 | 公开截图/短文字/自录短音频 | 真人身份与敏感录音 | 低风险可复现 |
| 任务 | 检索、问答、跨模态比较 | 自动决策与外部写入 | 证据可核对 |
| 模型 | 冻结编码器+小连接器/假模型 | 从零训练大模型 | 资源可控 |
| 工具 | OCR/ASR/计算器白名单 | 任意网络或系统命令 | 权限清楚 |
| 输出 | 状态+回答+引用+限制 | 无来源长文本 | 便于审计 |
| 运营 | 固定集、灰度、回滚 | 直接面向高风险用户 | 可恢复 |
项目把模型能力限制在可验证候选内,所有外部影响由协议、权限和用户复核控制。
ALLOWED_STATUS = {"answer", "uncertain", "missing", "conflict", "blocked"}
ALLOWED_EVIDENCE = {"page", "text_span", "region", "time_span"}
def validate(response, media_ids):
if response.get("status") not in ALLOWED_STATUS:
return False, "invalid_status"
for ref in response.get("evidence_refs", []):
if ref.get("media_id") not in media_ids:
return False, "unknown_media"
if ref.get("kind") not in ALLOWED_EVIDENCE:
return False, "invalid_evidence"
if response["status"] == "answer" and not response.get("answer", "").strip():
return False, "empty_answer"
if response["status"] != "answer" and not response.get("next_action"):
return False, "missing_next_action"
return True, "accepted"
sample = {
"status": "uncertain",
"answer": "可能是第二页的标题。",
"evidence_refs": [{"media_id": "page-2", "kind": "page"}],
"next_action": "请上传清晰截图",
}
print(validate(sample, {"page-2"}))提交项目用户、输入、输出、非目标、状态、证据和预算;用三个正常样例和三个失败样例验证 schema。
参考结论:任务低风险;状态有动作;证据可定位;模型不能直接改变外部系统。
完整实现路径:从一次失败回答反推数据、编码、对齐、融合、工具、评估和回滚接口,交付可复放项目。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 1:先复制运行,再替换为自己的数据
from pathlib import Path
import json
required = {"contract", "media_manifest", "model_revision",
"gold_requests", "evaluation", "rollback"}
release = json.loads(Path("release.json").read_text(encoding="utf-8"))
missing = required - release.keys()
if missing: raise ValueError(f"release missing {sorted(missing)}")
for request in release["gold_requests"]:
assert {"request", "expected_status", "evidence"} <= request.keys()
print("multimodal release is replayable")验收标准:第二位同学只看 README 能重现一次成功、拒答和删除回放。
若结果不对:保存最小失败包:请求、媒体哈希、版本、日志和预期状态。
12.9.2 数据、实验与错误地图
项目数据包按关系组织:media/ 保存受控媒体,text/ 保存原始与规范化文字,transcripts/ 保存带时间的转写,relations/ 保存配对和引用,splits/ 锁定切分,cases/ 保存正常/边界/失败样例,reports/ 保存指标和人工裁决。目录结构本身表达数据生命周期,README 说明每个文件能否公开。
建立固定样本集时覆盖完整输入、只文字、只图片、只音频、图文冲突、音频噪声、低清截图、媒体注入、权限撤回和重复上传。每个样本写预期状态、关键证据和不应发生的动作;没有期望状态的样本很难用于回归。
实验矩阵一次只改一个主要变量:编码器、连接器、查询 Token 数、提示模板、工具开关、阈值或分段策略。固定媒体、问题、切分、随机种子和预算,记录结果、耗时、成本和失败。把“模型换了”与“输入变了”分开。
评估同时看能力和协议。能力包括检索、问答、区域/时间定位、转写和摘要;协议包括 schema 通过率、引用有效率、缺失/冲突识别、越权阻断和幂等。一个答案正确但引用错误,仍应在交付指标中记为失败。
错误地图按层级记录:数据来源/许可、媒体质量、时间空间对齐、模态表示、配对、融合、工具、生成、输出校验、安全和界面。每条错误包含样本 ID、版本、复现命令、根因假设、修复、回归和负责人。不要只记“模型幻觉”。
错误分析先问输入与证据,再问模型。图片区域是否正确,音频时间是否偏移,OCR/ASR 是否读错,检索是否返回无权限内容,连接器是否丢 Token,模型是否把推断写成观察。分层检查能避免盲目微调。
数据版本与模型版本一起发布。修改配对、删除样本、更新 OCR/ASR、改变裁剪或提示模板,都要生成差异报告;旧结果保留 request_id 和 manifest,重放时明确使用旧或新协议。没有版本的实验无法比较。
人工评审采用盲审与分歧记录。评审者先看任务和证据,再判断回答、定位、不确定和用户动作;不必知道模型名称。争议样例进入指南和挑战集,下一次评估检查修复是否真正改善。
成本与延迟也进实验表。记录每种媒体的编码时间、融合 Token、工具调用次数、缓存命中和 p95;把人工复核时间单独列出。只看模型 API 费用会低估真实交付成本。
错误地图的修复要有回归范围。改 OCR 可能影响图文对齐,改提示可能影响语言切片,换连接器可能影响文本能力;每个修复至少重跑直接相关任务、关键安全集和一个单模态基线。修复结果写回报告,不用口头记忆。
项目实验记录模板
每次运行记录 run_id、日期、代码/环境、数据快照、契约、模型/连接器/工具版本、输入配置、随机种子、预算、结果、失败样例和人工裁决。输出目录按 run_id 隔离,避免实验互相覆盖。
对比实验表包含主变量、固定变量、总体/切片指标、schema 通过率、证据有效率、p95、成本、人工比例和结论。结论写成“在某数据和预算下,某变更改善了某切片”,不要泛化成“模型更聪明”。
错误地图中的根因是可验证假设。为每条假设设计最小实验,例如替换背景、静音时间段、绕过 OCR、恢复旧连接器或关闭缓存;实验结果支持、否定或保留未知。这样复盘会产生下一步,而不是堆描述。
| 维度 | 指标例子 | 最低证据 | 未通过动作 |
|---|---|---|---|
| 任务 | 检索/问答/定位 | 固定集+关键切片 | 修复/限制范围 |
| 协议 | schema 通过、引用有效 | 正常/失败回归 | 阻断交付 |
| 缺失冲突 | 识别率、动作正确 | 模态消融/冲突集 | 增加拒答/人工 |
| 安全 | 越权率、隐私失败 | 注入/撤权回放 | 立即阻断 |
| 工程 | p95、成本、重试 | 目标设备测量 | 限流/分段 |
| 复现 | 重放一致性 | run_id/manifest | 补版本与脚本 |
from collections import defaultdict
def add_error(errors, case_id, layer, hypothesis, owner):
errors.append({
"case_id": case_id,
"layer": layer,
"hypothesis": hypothesis,
"owner": owner,
"status": "open",
})
def group_by_layer(errors):
grouped = defaultdict(list)
for error in errors:
grouped[error["layer"]].append(error)
return dict(grouped)
errors = []
add_error(errors, "case-07", "alignment", "字幕比音频提前", "audio-team")
add_error(errors, "case-12", "protocol", "引用区域越界", "api-team")
print(group_by_layer(errors))从一个错误答案开始,分别检查数据、对齐、表示、工具、融合、输出和安全层;写一个最小反事实实验和一个回归样例。
参考结论:合格标准:根因是可检验假设;修复带版本和负责人;没有用“模型幻觉”结束分析。
完整实现路径:从一次失败回答反推数据、编码、对齐、融合、工具、评估和回滚接口,交付可复放项目。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 2:先复制运行,再替换为自己的数据
from pathlib import Path
import json
required = {"contract", "media_manifest", "model_revision",
"gold_requests", "evaluation", "rollback"}
release = json.loads(Path("release.json").read_text(encoding="utf-8"))
missing = required - release.keys()
if missing: raise ValueError(f"release missing {sorted(missing)}")
for request in release["gold_requests"]:
assert {"request", "expected_status", "evidence"} <= request.keys()
print("multimodal release is replayable")验收标准:第二位同学只看 README 能重现一次成功、拒答和删除回放。
若结果不对:保存最小失败包:请求、媒体哈希、版本、日志和预期状态。
12.9.3 交付、演练与迁移
交付包至少包含 README、数据卡、任务合同、输入/输出 schema、模型 manifest、工具卡、代码、测试、固定集、评估报告、错误地图、监控字段和 runbook。README 从干净目录开始,写安装、数据获得、运行、预期输出、限制、隐私、删除和故障排查。
代码按职责拆成 media_io、preprocess、alignment、encoders、fusion、tools、protocol、evaluate 和 app 层。每个函数写输入、输出、异常和副作用;模型模块不直接访问数据库或发送请求,工具模块不决定用户权限,协议模块不修改原始证据。
演练正常、只文字、只图片、只音频、模态缺失、冲突、低质量、注入、权限撤回、工具超时、重复请求、预算超限和模型回滚。对每个样例保存日志、用户可见状态、证据和下一步动作;成功截图不是验收标准。
发布前用假模型验证协议,再接真实模型。假模型故意返回正确、空、非法区域、陌生媒体、超时和重复回执,检查系统是否阻断或安全降级。真实模型能力变化时,协议测试仍应稳定通过。
灰度阶段只开放低风险任务和小比例用户,监控答案、证据、拒答、工具、延迟、成本和用户纠错。硬安全失败立即暂停;质量波动进入人工复核。保留旧模型、旧连接器、预处理、阈值、工具和索引,以便完整回滚。
删除演练覆盖原图、缩略图、OCR/ASR、向量、缓存、日志和报告引用。撤权后再次请求应失败或显示已删除;对已训练权重不能逐样本删除的部分,明确政策和重新训练计划。
用户沟通需要说明模态范围和限制。结果页面显示使用了哪些媒体、哪些证据、是否经过工具、是否不确定;生成内容标记合成,检索结果显示来源和时间。不要用拟人化语气暗示模型拥有人的观察或授权。
迁移到下一章时,保留本章接口:输入契约、媒体关系、证据引用、状态、权限、预算、评估和回滚。行业/科学模型可能换数据和风险,但这些工程责任仍然存在。
项目总结区分事实、推断和未知。列出已通过的测试、仍失败的切片、下一步数据、待确认的许可和暂不支持的请求。把限制写清楚不会削弱项目,而是证明你理解多模态系统的边界。
评分重点不是代码长短,而是职责是否清楚:至少拆出“读取媒体”“建立关系”“调用模型/工具”“校验证据”“保存结果”五类职责;不要把所有逻辑写在一个循环里。代码应有示例、测试、可读错误和可恢复运行命令。
交付前的最终问题
别人能否在没有口头说明的情况下运行?数据许可和模型版本是否能追溯?回答中的每个关键断言能否回到图片区域、音频时间或文字范围?模态缺失、冲突、低质量和权限撤回是否会安全停止?
如果只替换一个编码器,哪些接口和回归必须保持?如果用户要求删除,哪些派生数据会被清理?如果成本超限,系统如何分段或转人工?如果新模型导致关键切片下降,谁有权限暂停并回滚?
把这些问题的答案写进 README、schema、测试和 runbook,而不是只写在总结段落。可交付的学习项目应该让读者看见证据、复现结果、理解限制,并能在失败后恢复。
| 交付物 | 内容 | 验收动作 |
|---|---|---|
| README | 安装、运行、限制、排错 | 干净目录执行 |
| 数据卡 | 来源、许可、切分、删除 | 抽查媒体关系 |
| 协议 | schema、状态、证据 | 非法输出回归 |
| 模型清单 | 编码器/连接器/版本/预处理 | 重放固定样例 |
| 工具卡 | 输入输出、权限、超时 | 模拟空/错/超时 |
| 评估包 | 指标、切片、人工、错误地图 | 发布门槛判断 |
| runbook | 监控、暂停、回滚、删除 | 演练并记录耗时 |
import json
from pathlib import Path
def replay(path, runner):
case = json.loads(Path(path).read_text(encoding="utf-8"))
result = runner(case["input"])
assert result["status"] == case["expected_status"]
assert result["contract_version"] == case["contract_version"]
for ref in case.get("required_evidence", []):
assert ref in result.get("evidence_refs", [])
return {"case_id": case["id"], "state": "passed"}
def fake_runner(value):
return {
"status": "uncertain",
"contract_version": "mm-1",
"evidence_refs": ["page-2"],
}
case = {
"id": "case-07", "input": "sample",
"expected_status": "uncertain", "contract_version": "mm-1",
"required_evidence": ["page-2"],
}
Path("case.json").write_text(json.dumps(case), encoding="utf-8")
print(replay("case.json", fake_runner))提交项目代码、数据卡、协议、表格/图示、评估、错误地图、README、监控和回滚演练;再写一段迁移说明,说明下一章换模型时哪些接口保持不变。
参考结论:合格标准:可复现、可审查、可拒答、可删除、可回滚;能解释每个函数职责和每个结论的证据。
完整实现路径:从一次失败回答反推数据、编码、对齐、融合、工具、评估和回滚接口,交付可复放项目。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 3:先复制运行,再替换为自己的数据
from pathlib import Path
import json
required = {"contract", "media_manifest", "model_revision",
"gold_requests", "evaluation", "rollback"}
release = json.loads(Path("release.json").read_text(encoding="utf-8"))
missing = required - release.keys()
if missing: raise ValueError(f"release missing {sorted(missing)}")
for request in release["gold_requests"]:
assert {"request", "expected_status", "evidence"} <= request.keys()
print("multimodal release is replayable")验收标准:第二位同学只看 README 能重现一次成功、拒答和删除回放。
若结果不对:保存最小失败包:请求、媒体哈希、版本、日志和预期状态。
本章总复习与易错
易错一:把多模态当成输入越多越好。模态必须对任务有证据价值,并且关系、时间、空间和权限清楚。易错二:把连接器或融合层当作无损过程。Token 压缩会丢细节,必须用消融和引用评估。
易错三:把检索相似度、OCR/ASR 结果或模型流畅话语当作事实。它们都是候选或派生证据,仍需来源、范围、版本和人工边界。易错四:把工具和代理交给模型自由控制。控制器负责 schema、权限、预算、幂等和回滚。
易错五:只评估完整、清晰、配对的样例。缺失、冲突、低质、注入、撤权、超时和重复请求才会暴露真实边界。易错六:只保存最终答案。没有媒体关系、manifest、错误地图和 runbook,就无法复现或修复。
本章验收:读者能够从一条回答追到多个模态的原始证据,说明哪些信息被表示和压缩,解释融合与工具的职责,判断何时拒答或转人工,并在模型升级、数据删除和预算变化后完成回放或回滚。
项目交付的独立验收
请一位没有参与开发的同学从干净目录运行项目,只给 README 和允许的数据。观察他能否找到环境变量、数据许可、启动命令、预期输出、失败状态和删除入口;任何需要口头补充的步骤都写回文档。
用假模型返回合法答案、无证据答案、越界区域、未知媒体、工具超时和重复回执。协议、控制器和界面应分别做出正确反应;真实模型接入后再加入能力评估,避免把工程错误归因于模型。
交付报告展示至少一条完整证据链和一条拒答链:输入媒体、表示/工具、融合、claim、状态、用户提示和日志版本都能对应。展示失败比只展示成功更能证明项目理解了边界。
把代码职责拆开并为每类写测试:读取媒体、建立关系、调用模型/工具、校验证据、保存结果、监控和回滚。不要把所有逻辑塞进一个循环;职责清楚比代码行数更重要。
迁移时只替换一个组件,例如视觉编码器或 ASR,重跑接口、切片、权限和回放;记录哪些结果变化是预期能力差异,哪些是协议回归。下一章的行业/科学模型会换数据和风险,但本章的契约和验收方式继续有效。
综合项目以协议和证据为中心,模型只是其中一个可替换组件。
| 提问 | 需要看到什么 | 不通过怎么办 |
|---|---|---|
| 能从零运行吗 | README、环境、命令 | 补文档/脚本 |
| 答案有证据吗 | 区域/时间/页码/版本 | 阻断/人工 |
| 缺失会怎样 | 状态、提示、降级 | 补回归 |
| 工具安全吗 | 白名单、参数、回执 | 撤销权限 |
| 能删除吗 | 派生数据/缓存/日志回放 | 清理/说明限制 |
| 模型能替换吗 | 接口、manifest、回归 | 适配/回滚 |
让同学只看 README 完成一次正常、一次拒答和一次删除回放;记录他遇到的歧义并修改项目包。
参考结论:合格标准:干净目录可运行;证据、权限、状态和回滚均可追溯;没有口头隐藏步骤。
完整实现路径:从一次失败回答反推数据、编码、对齐、融合、工具、评估和回滚接口,交付可复放项目。 本题不要只交一段说明:应同时提交输入样例、可运行实现、输出或断言,以及失败时的明确状态。
# 对应本节练习 4:先复制运行,再替换为自己的数据
from pathlib import Path
import json
required = {"contract", "media_manifest", "model_revision",
"gold_requests", "evaluation", "rollback"}
release = json.loads(Path("release.json").read_text(encoding="utf-8"))
missing = required - release.keys()
if missing: raise ValueError(f"release missing {sorted(missing)}")
for request in release["gold_requests"]:
assert {"request", "expected_status", "evidence"} <= request.keys()
print("multimodal release is replayable")验收标准:第二位同学只看 README 能重现一次成功、拒答和删除回放。
若结果不对:保存最小失败包:请求、媒体哈希、版本、日志和预期状态。
项目 README 与交付自检
README 首页先写任务、用户、支持的媒体、非目标和快速运行命令;随后给目录结构、数据许可、环境变量、模型/工具版本、预期输出和失败状态。读者不应先猜项目是分类、检索、问答还是代理。
示例结果同时放一条完整回答、一条不确定、一条缺失、一条冲突和一条阻断。每条结果展示状态、证据、限制和下一步,避免 README 只展示最漂亮的成功截图。
测试命令按职责分组:媒体解码与时间/空间映射、Token/Patch/帧、协议 schema、工具白名单、权限撤回、幂等、评估和回滚。每个测试有输入、期望状态和失败信息,运行后能定位责任模块。
交付前让同学复现一次模型替换:保持任务合同不变,只换一个编码器或连接器。记录接口、质量、证据、成本和安全变化;如果必须修改协议,写迁移脚本和兼容窗口,而不是直接覆盖历史结果。
项目总结写已证实、未知和下一步。列出哪些结论来自固定数据,哪些只在小样本上观察,哪些场景明确不支持;这种诚实边界是进入行业/科学模型章节前最重要的工程能力。
| 检查 | 最小证据 | 通过条件 | 不通过动作 |
|---|---|---|---|
| 可运行 | 干净目录命令 | 他人可复现 | 补 README/环境 |
| 可解释 | claim+区域/时间/页码 | 每条结论可回放 | 阻断/补工具 |
| 可拒答 | 缺失/冲突/越权集 | 状态与动作正确 | 修协议/控制器 |
| 可删除 | 原/派生/缓存回放 | 撤权后不可见 | 清理/说明限制 |
| 可迁移 | 替换组件回归 | 接口和安全不退化 | 适配/回滚 |
交付包迁移检查
把项目交给另一位同学或换一台机器运行,记录所有需要口头补充的步骤并写回 README。清晰写出数据获得方式、许可、环境、模型/工具版本、预算、输出状态和删除入口。
只替换一个组件完成迁移实验,比较接口、证据、切片、拒答、成本和回滚。若需要改协议,提供迁移脚本和兼容窗口,不覆盖历史结果。
本节通过标准:项目可从干净目录运行,可审查、可拒答、可删除、可回滚,并能把事实、推断和未知分开。