跳转至

腾讯微信大模型算法一面

简单介绍你的多模态Agent整体项目架构。

我们的多模态Agent项目旨在构建一个能够理解图像、文本等多模态输入,并调用多种工具完成复杂任务的智能助手。整体架构可分为四层:

  • 感知层:以多模态大模型(如Qwen-VL-Chat)为核心,负责统一理解图文输入,生成自然语言推理结果和工具调用指令。视觉部分使用ViT-large作为视觉编码器,文本部分使用Qwen-7B的Transformer解码器,中间通过一个跨模态连接器(Resampler/Linear Projection)将视觉特征投影到语言模型的词嵌入空间。

  • 记忆层:包含短期记忆(对话历史的KV Cache和token序列)和长期记忆(基于向量数据库的多模态RAG)。多模态RAG支持图文混合检索,将文本和图像分别编码后存入统一向量空间或双路召回后融合,为Agent提供外部知识支撑。

  • 执行层:Agent的核心编排模块。采用ReAct范式,模型输出“思考-行动-观察”的循环。我们定义了一套工具调用协议,模型可以生成JSON格式的function call,支持调用多种工具:OCR、目标检测、搜索引擎、计算器、数据库查询、图像编辑等。工具执行结果会作为新的多模态上下文追加到对话中,形成闭环。

  • 服务层:提供API网关、推理加速(vLLM/TGI)、多轮会话管理、自动标注评估等基础设施。模型通过LoRA微调来适配特定领域的标注任务。

这一架构将多模态感知、工具调用、知识检索和交互式推理融为一体,具有较强的通用性和可扩展性。

image.png


基于Qwen多模态模型做LoRA微调,遇到过哪些问题?会话滑动窗口切分,如何规避训练集、测试集数据泄露?

遇到的问题:

  • 梯度不平衡:视觉编码器和文本LLM的收敛速度差异大。视觉部分使用预训练的ViT,如果完全解冻容易破坏视觉表示,但若完全冻结,下游任务可能对齐不足。需要分层设置学习率,对视觉部分设置较小的学习率或不微调,主要微调跨模态连接器和LoRA注入的LLM部分。

  • 显存溢出:多模态输入中图像特征占用大量显存。我们通过将图像分辨率限制在448×448,并利用模型的视觉Resampler将变长的图像token压缩到固定数量(如256个),降低序列长度。

  • 数据格式与对齐:训练数据需要严格符合Qwen-VL的对话模板,包括<img>...</img>标签和特定的角色标识。构造数据时容易遗漏系统提示或图片路径,导致训练时模型无法正确解析输入。

  • 灾难性遗忘:在单一领域数据上微调后,模型在其他通用能力上会有所退化。我们混合了一部分通用指令数据来缓解,同时使用较小的LoRA秩(r=8或16)来限制参数变化幅度。

会话滑动窗口切分与数据泄露规避:

  • 对于长会话数据,我们采用滑动窗口将对话切分成多个训练样本。具体做法是:从对话起始位置开始,设定窗口大小为2048个token,滑动步长为窗口大小的一半,每次滑动截取一个子对话作为训练样本。对于每个截取的窗口,需保证其开头是用户或系统的完整轮次,不能从某个句子中间切分。

  • 避免数据泄露的核心原则:测试集和训练集必须按会话级别隔离,而不是按切分后的片段。如果一个会话被切成了多个片段,所有片段要么全部分配到训练集,要么全部分配到测试集。此外,验证集/测试集的构建使用完全独立的数据源或时间切分(例如用最新采集的一批数据作为测试集)。对于包含多轮交互的会话,我们保证不将同一会话中前几轮作为训练、后几轮作为测试,防止模型在训练中看到未来的用户意图和上下文,从而虚高评测分数。


多模态任务中,文本和图片特征怎么做模态对齐?

模态对齐是多模态模型的核心,目的是让视觉特征和文本特征处于相同的语义空间中,以便LLM能够“读懂”图像。目前主流方法有以下几种:

  • 基于可学习连接器的对齐(如Qwen-VL、LLaVA):首先使用预训练的视觉编码器(如ViT)提取图像特征,得到一系列patch特征向量。然后通过一个轻量的连接模块(如线性层或Q-Former风格的Resampler)将这些视觉token投影到LLM的词嵌入维度。这个连接器在预训练或指令微调阶段与LLM一起训练,从而将视觉特征对齐到文本空间。为了压缩序列长度,通常会将视觉token数量控制在64-256个。

  • 两阶段训练:第一阶段对齐预训练,在大量图文对上,只训练视觉连接器,冻结视觉编码器和LLM,使图像特征初步具备语言表征能力。第二阶段指令微调,解冻LLM(或部分)和连接器,使用多模态指令数据训练,进一步提升对齐质量和指令遵循能力。

  • 对比学习对齐(如CLIP):对于检索类任务,采用双塔结构,分别将文本和图像编码到同一向量空间,通过对比损失拉近匹配的图文对、推远不匹配的。在多模态大模型中,有些会使用CLIP的视觉编码器作为初始化,因为其特征已经和文本粗略对齐。

  • 交叉注意力对齐(如Flamingo):在LLM的层之间插入交叉注意力模块,视觉特征通过交叉注意力直接注入到LLM的每一层,让文本token在生成时动态地关注图像特征。这种方法对齐更紧密,但需要修改LLM结构。

我们的项目采用LLaVA/Qwen-VL风格的对齐方式:使用ViT-G作为视觉编码器,通过一个由cross-attention构成的Resampler将可变长度的图像特征压缩为256个固定长度的视觉token,再送入LLM。训练时,先做对齐预训练,冻结视觉编码器和LLM,只训练Resampler;再做LoRA微调,将Resampler和LLM的LoRA参数一起训练,实现高效的模态对齐。


你做的多模态自动标注Agent,如何保证识别精准度?整体工具调用逻辑是怎么设计的?

保证识别精准度的措施:

  • 强基座模型+专项微调:以Qwen-VL-Max作为底座,针对标注任务进行LoRA微调,注入领域知识。标注场景下,我们对图像中的目标区域、文本行、关系等都有明确的schema定义,通过构造高质量、高覆盖的微调数据,让模型学习到符合规范的输出格式和判断标准。

  • 工具链与交叉验证:Agent并不完全依赖单一模型的生成结果。在需要精确OCR时,调用专用的OCR工具(如PaddleOCR);在需要目标检测时,调用Grounding DINO或OWL-ViT。多工具的结果可以相互校验:比如模型声称某区域有文本,但OCR工具未能识别出文字,则标记为低置信度,交由人工复核或重新推理。

  • 自省与校准机制:Agent在输出标注结果后,会自我反问:“这个标注是否符合给定的类别体系?坐标是否在图像范围内?是否存在漏标?”通过生成自检报告,将不确定的case自动上报给人工审核队列。

  • 人在回路反馈闭环:人工修正的结果会作为新的训练数据,通过持续微调不断提升Agent的标注精度。我们还设计了主动学习策略,优先选择模型置信度低、工具间存在分歧的样本供人工标注。

工具调用逻辑设计:

  • Agent采用ReAct模式,核心循环是:观察→思考→行动→观察。

  • 系统提示中定义了所有可用工具的名称、功能、参数schema。模型在思考后,如果需要使用工具,会输出特定格式的function call JSON,例如:

{
  "tool": "ocr",
  "parameters": {"image_id": "img_01", "region": [100, 200, 300, 400]}
}
  • 调度器解析JSON,调用对应的工具API,获取结果后,将结果以观察文本的形式追加到对话历史中(例如“OCR结果:检测到文本‘Hello World’,置信度0.98”)。然后模型继续思考下一步,直至最终输出标注结果。

  • 为控制调用次数和成本,设置最大工具调用轮数为5。若超过轮数仍未完成,则强制模型基于当前信息给出最佳结果。


项目中的多模态RAG如何实现?图文混合检索采用的是双路召回还是统一向量空间?

我们的多模态RAG采用双路召回+融合重排序的架构,兼顾了效率与准确性。

  • 向量化:文本和图像使用不同的编码器分别向量化。文本使用BGE-M3多语言文本编码器;图像使用CLIP-ViT-L的视觉编码器。两者输出的向量维度不同,分别存入独立的向量索引(文本索引用FAISS,图像索引用Milvus)。

  • 双路召回:

  • 文本检索:将用户query通过文本编码器编码,在文本向量库中召回Top-K1个最相似的文本段落。
  • 图像检索:将用户query中的图像或文本描述(如果是纯文本query则用CLIP文本编码器)编码,在图像向量库中召回Top-K2个最相似的图像。

  • 融合与重排序:

  • 对召回的两类结果,构建候选集合。如果query同时包含文本和图像,我们使用多模态重排序模型(如基于BLIP-2的图文匹配打分)对候选进行统一打分;如果是纯文本query,则根据候选的来源类型赋予不同的先验权重,并通过文本相似度进行排序。
  • 最终选择Top-N个最相关的文档/图像作为上下文,连同原始query一起送入生成模型。

  • 为什么不使用统一向量空间? 统一空间需要将图文向量对齐到一个共享嵌入空间(如使用CLIP的双塔对齐),但在专业领域(如工业图纸、医学影像),通用对齐模型的性能往往不佳。双路召回允许我们分别使用领域内表现最好的文本和图像编码器,灵活性更高。此外,双路召回便于独立扩展和增量更新。

  • 后期改进:我们正在尝试引入多模态大模型作为重排序和答案生成的后端,利用其强大的多模态理解能力来直接对召回的图文列表进行信息整合和回答。


LoRA低秩适配的底层原理是什么?为什么只训练少量低秩矩阵,就能微调模型且不破坏原权重?

底层原理: LoRA(Low-Rank Adaptation)基于一个假设:模型在适应下游任务时,权重的更新量 ΔW 是低秩的,即可以用两个小矩阵的乘积来近似。

image.png

为什么少量低秩矩阵就能微调?

  • 大模型在预训练阶段已经学习到了丰富的通用知识表示,下游任务通常只需要对这些知识进行重新组合或微小的方向调整。这种任务特定的调整在参数空间中往往集中在一个低维子流形上,因此一个低秩矩阵就足以捕捉主要的适应方向。

  • 语言模型中的全秩微调产生的参数变化矩阵,其有效秩远低于矩阵维度,这一点已被经验验证。因此用低秩分解来近似 ΔW 是合理且高效的。

为什么训练后可以合并?

image.png

额外好处:

  • 显存占用极低:只需存储两个小矩阵的梯度和优化器状态,不需要存储全量权重的梯度。

  • 多任务切换灵活:可以为不同任务训练不同的LoRA模块,共享同一个基座模型,通过热插拔快速切换。

  • 无推理延迟:合并后与原始模型结构完全一致。


多模态模型训练时,视觉编码器和文本编码器容易出现梯度不平衡、训练不同步,怎么理解这个问题、如何解决?

理解问题:

多模态模型通常由三个部分组成:视觉编码器(如ViT)、跨模态连接器(Resampler/Projector)、以及大型语言模型(LLM)。这三者参数量、预训练程度、学习动态差异巨大。

  • 梯度不平衡:LLM拥有数十亿参数,其产生的梯度范数通常很大;视觉编码器虽然参数也不少,但其预训练任务(图像分类/对比学习)与多模态指令任务差距大,直接联合训练可能导致LLM主导梯度更新,视觉部分得不到有效训练,或者视觉部分的梯度爆炸扰乱LLM的稳定表征。此外,跨模态连接器作为中间的桥梁,训练初期梯度可能极大,因为它需要从头学习对齐。

  • 训练不同步:视觉编码器一般是预训练好的,而跨模态连接器是随机初始化的。如果所有参数以相同学习率训练,连接器可能还处于欠拟合状态时,LLM已经过拟合了下游数据,导致整体模型无法有效收敛。另外,视觉部分在初期可能需要较大学习率来快速对齐,而LLM只需要微小的调整,统一学习率会导致视觉欠训练或LLM灾难性遗忘。

解决方案:

image.png

  • 两阶段训练:第一阶段只训练连接器(冻结视觉和LLM),使用大量图文对进行对齐预训练,直到连接器能够有效将视觉特征映射到LLM空间。第二阶段再解冻LLM(或使用LoRA)进行指令微调。这从根本上解决了不同步问题。

  • 梯度裁剪与归一化:设置全局梯度裁剪阈值(如max_norm=1.0),防止某一部分的梯度尖峰破坏训练。同时监控各模块的梯度范数,在出现明显不平衡时及时调整学习率。

  • 适当的冻结策略:在许多高效微调场景中,我们直接冻结视觉编码器,只训练跨模态连接器和LLM的LoRA参数。因为预训练视觉编码器已经具有很强的语义提取能力,只需要通过连接器对齐即可。

  • 动态调整:利用Warmup和Cosine退火学习率调度,让学习率在训练初期缓慢上升,中期保持较高水平,后期逐渐下降,帮助各模块协调收敛。


手撕:给定多个区间,计算需要移除的最少区间数量,让剩下所有区间完全无重叠。

问题:给定多个区间 [start, end),求最少移除区间数,使剩余区间互不重叠。

思路:经典贪心算法,等价于“最多不重叠区间个数”问题。按照区间结束时间从小到大排序,然后贪心选择最早结束的不重叠区间。最少移除数 = 总区间数 - 最多可选的不重叠区间数。

算法步骤:

  1. 将所有区间按结束端点升序排序。

  2. 初始化last_end = -infcount = 0(记录最多可选不重叠区间的数量)。

  3. 遍历每个区间[start, end)

  4. 如果start >= last_end,说明该区间与已选区间不重叠,选择它,更新last_end = endcount += 1

  5. 答案 = n - count

代码实现(Python):

def eraseOverlapIntervals(intervals):
    if not intervals:
        return 0
    # 按结束时间排序
    intervals.sort(key=lambda x: x[1])
    count = 1  # 至少选第一个区间
    last_end = intervals[0][1]
    for i in range(1, len(intervals)):
        start, end = intervals[i]
        if start >= last_end:  # 不重叠
            count += 1
            last_end = end
    return len(intervals) - count

示例: 输入 [[1,2],[2,3],[3,4],[1,3]],排序后 [[1,2],[2,3],[1,3],[3,4]]。贪心选择 [1,2],接着 [2,3],跳过 [1,3],最后选 [3,4]。最多不重叠区间数为3,总区间数4,移除最少1个。返回1。

复杂度:排序 O(n log n),遍历 O(n)。是解决该问题的最优算法。