公司未注明

面经合集

完整信息

18 条问法 · 18 道题

本篇目录

这份面经的含金量非常高,几乎涵盖了大模型算法工程师在工业界落地的所有核心考点:RAG检索增强、大模型底层架构、微调与强化学习对齐、以及工程化推理加速。

为了便于你系统性地学习和理解,我将这两面中出现的技术和问题重新划分为五大硬核技术模块,为你逐一详细拆解。

模块一:RAG(检索增强生成)与信息检索

1. 工业界文本语义切分技术(Chunking)

  • 规则切分: 最基础的方案,基于特定的标识符(如换行符 \n、句号等)结合最大长度(Max Token Length)进行切分。通常会设置 Overlap(重叠度,如前后保留50个字符)以防止句子从中间断开导致语义丢失。
  • 语义切分: 利用小参数量的 Embedding 模型,计算相邻句子的余弦相似度。如果相似度低于某个阈值,说明语义发生了转折,就在该处进行切分。
  • LLM 辅助切分: 对于复杂的长文档(如 PDF 财报),利用大模型提取文档的大纲树(结构化切分),或者让 LLM 总结每一段的主旨,将主旨与原文绑定在一起作为 Chunk。

2. 检索方案对比:Qwen3-Embedding vs BM25 vs BGE-M3

  • 传统 BM25(稀疏检索): 基于词频-逆文档频率(TF-IDF)的变体。优点:对专有名词、特定料号、生僻词的精确匹配极好,无需 GPU,速度极快。缺点:无法理解同义词和语义,比如搜“苹果手机”,无法召回写着“iPhone”的文档。
  • Qwen3-Embedding(稠密检索/语义检索): 将文本映射到高维稠密向量空间。优点:语义理解极强,能跨越字面表达的差异进行召回。缺点:对精准关键词匹配较弱。
  • BGE-M3: 业界顶尖的检索模型,名字里的 M3 代表 Multi-lingual(多语言)、Multi-granularity(多粒度长度)、Multi-Function(多功能)。它同时输出稠密向量(Dense)、稀疏向量(Sparse,类似词频)以及多向量(ColBERT-like),是融合了 BM25 和纯 Embedding 优点的“六边形战士”。

3. RRF (Reciprocal Rank Fusion) 加权递归融合

  • 原理: 在混合检索(如 BM25 + 向量检索)中,不同检索器的打分范围完全不同,无法直接相加。RRF 放弃了具体的得分,只看文档在各个检索器中的排名(Rank)。
  • 公式: \text{Score} = \sum \frac{1}{k + \text{Rank}_i} (其中 k 通常取 60)。
  • 优势: 极度鲁棒。不需要调节权重超参数,就能平滑地将多路召回的结果融合在一起,排名越靠前的文档最终得分越高。

4. RAG 标准链路与 Query 前置优化

  • 为什么不能直接检索: 用户的原始输入往往是口语化、省略主语或带有错别字的(例如:“这俩有什么区别?”)。直接去向量库搜,大概率召回垃圾信息。
  • 前置优化(Query Rewrite):
  • 意图识别与路由: 判断是查知识库、查实时天气、还是闲聊。
  • Query 重写/指代消解: 利用 LLM 结合历史对话,把“这俩”补全为“Qwen3 和 Llama3”。
  • HyDE(假设性文档嵌入): 让 LLM 先根据 Query 瞎编一个答案,拿生成的答案去向量库检索,往往能大幅提升召回率。

5. GraphRAG 图结构构建与差异化

  • 逻辑: 传统 RAG 是把文章切碎,丢失了全局上下文。GraphRAG 利用 LLM 从文档中抽取“实体(Entity)”和“关系(Relationship)”,构建知识图谱。
  • 差异化检索: 面对“请总结这本小说的全局剧情”这类问题,传统 RAG 只能召回几个零碎片段,而 GraphRAG 可以通过图结构的社区聚类(Community Detection)和路径游走,生成涵盖全局的宏观摘要。

模块二:大模型底层架构机制

1. Transformer 架构与 \sqrt{d_k} 的底层逻辑

  • 架构简述: 包含自注意力机制(Self-Attention)、前馈神经网络(FFN)、层归一化(LayerNorm)和残差连接。
  • 为什么除以 \sqrt{d_k}: 在计算注意力得分 Q K^T 时,Q 和 K 的维度是 d_k。随着维度 d_k 变大,点积结果的方差会线性增大。方差过大会导致点积数值极大,送入 Softmax 函数后,部分概率极度接近 1,其余接近 0。这会使得 Softmax 的梯度趋近于 0(梯度消失)。除以 \sqrt{d_k} 是为了将方差缩放回 1,保证反向传播时梯度健康流动。

2. Encoder vs Decoder 架构

  • 区别: Encoder(如 BERT)是双向注意力,能同时看到上下文;Decoder(如 GPT)是单向(Causal)注意力,只能看到当前词之前的序列。
  • Decoder-only 获胜的原因:
  1. 生成式任务(自回归预测下一个词)天然匹配 Decoder。
  2. Zero-shot 和 Few-shot 提示工程更契合从左到右的生成逻辑。
  3. Decoder 架构可以完美利用 KV Cache 加速推理,而双向 Encoder 在生成任务中每次都要重算全局注意力,效率极低。

3. 位置编码盘点

  • 绝对位置编码(Sinusoidal/Learned): 直接加在 Token Embedding 上,模型难以直接感知相对距离。
  • 相对位置编码(ALiBi等): 在计算 Attention Score 时直接加上距离惩罚,外推性好,但计算复杂度较高。
  • RoPE(旋转位置编码): 结合了绝对与相对的优点。通过乘以一个旋转矩阵,使得两个 Token 向量进行内积时,结果天然包含它们的相对位置差。它是目前开源大模型(Llama, Qwen, DeepSeek)的绝对标配。

4. 多模态大模型基础架构

  • 通用范式: Vision Encoder + Projector + LLM Backbone。
  • 图像先输入到 Vision Encoder(如 ViT/CLIP)提取视觉特征 \rightarrow 通过 Projector(如 MLP 线性层或 Q-Former)将视觉特征映射到 LLM 的文本特征空间 \rightarrow 与文本 Token 拼接后送入 LLM 进行自回归生成。

模块三:对齐算法与微调 (Alignment & Fine-tuning)

1. 演进路线:PPO \rightarrow DPO \rightarrow GRPO

  • PPO(Proximal Policy Optimization): 经典 RLHF。需要 Actor、Critic、Reward、Reference 四个模型。通过 Reward 模型打分,Actor 根据分数更新策略。缺点是极其消耗显存,训练极不稳定。
  • DPO(Direct Preference Optimization): 直接偏好优化。将强化学习问题等价转化为分类问题(交叉熵损失)。不需要训练 Reward 模型,直接用人类偏好数据(Chosen/Rejected)来微调 Actor 模型。训练稳定,算力友好。
  • GRPO(Group Relative Policy Optimization): DeepSeek 提出的算法。彻底去除了 PPO 中的 Critic 网络。针对同一个 Prompt 采样多条回答,通过这组回答得分的组内均值和标准差来计算相对优势(Advantage),极大节省了显存开销。

2. DPO 损失函数(口述级推导)

  • DPO 的核心在于最大化优选回复 y_w 相对于劣选回复 y_l 的隐式奖励差。其公式为:

\mathcal{L}_{DPO} = - \mathbb{E}_{(x, y_w, y_l)} \left[ \log \sigma \left( \beta \log \frac{\pi_\theta(y_w\vert{}x)}{\pi_{ref}(y_w\vert{}x)} - \beta \log \frac{\pi_\theta(y_l\vert{}x)}{\pi_{ref}(y_l\vert{}x)} \right) \right]

  • 解释: \pi_\theta 是当前训练模型,\pi_{ref} 是冻结的参考模型。公式通过计算两个模型生成概率的比值,推动训练模型增加好数据的概率,同时降低坏数据的概率,\beta 是控制偏离参考模型程度的超参数。

3. LoRA 微调核心超参数

  • r (Rank):低秩矩阵的秩,通常设为 8 到 64。越大拟合能力越强,但也越容易过拟合。
  • lora_alpha:缩放因子。通常设置为 r 的 2 倍。它控制 LoRA 更新权重对原本模型权重的影响力度。
  • target_modules:早期只加在 Attention 的 Q、V 上,现在的最佳实践是加在所有线性层上(包括 FFN 的 Gate、Up、Down)。

模块四:工程加速与异常排查 (Inference & OOM)

1. vLLM 推理加速的核心:PagedAttention

  • 痛点: 传统推理中,KV Cache 会预先分配一大块连续的显存,导致严重的内存碎片(碎片率高达 60%)。
  • PagedAttention: 借鉴操作系统的虚拟内存分页机制,将 KV Cache 划分为固定大小的块(Blocks)。显存不再需要连续,极大地减少了碎片,显存利用率飙升,从而能够显著增加并发的 Batch Size。
  • Continuous Batching(连续批处理): 在迭代(Iteration)级别进行调度,只要有一个请求生成完毕,立刻把新的请求塞进来,不需要等待整个 Batch 的所有句子都生成完。

2. OOM (显存溢出) 排查思路与解决

  • 训练阶段:

  • 解决: 开启 Gradient Checkpointing(以计算时间换显存);使用 DeepSpeed ZeRO 策略(切分优化器状态、梯度和模型权重);降低 Batch Size 并利用 Gradient Accumulation(梯度累加)。

  • 微调阶段:

  • 解决: 引入 QLoRA(将基座模型量化为 4-bit 驻留显存);开启 FlashAttention-2 减小前向传播的显存占用峰值。

  • 推理阶段:

  • 排查: 多数是因为并发太高或用户的 prompt 太长导致 KV Cache 撑爆。

  • 解决: 限制最大上下文长度(Max Seq Len);使用 vLLM 框架;引入 KV Cache 量化(如 INT8/FP8)来压缩缓存。

模块五:手撕代码核心思路

1. 多头注意力机制 (Multi-Head Attention)

  • 核心逻辑:
  1. 输入 X 通过线性层分别映射出 Q, K, V。
  2. 对维度进行 Reshape,拆分出 num_heads。
  3. 计算 scores = matmul(Q, K.transpose) / sqrt(d_k)。
  4. 对 scores 应用 Softmax。
  5. 计算 context = matmul(softmax_scores, V)。
  6. 将所有的 head 重新拼接(Concat),最后经过一个输出线性层。

2. 最大连通域面积 (岛屿最大面积 - LeetCode 695)

  • 核心逻辑: 典型的图遍历问题,使用 DFS(深度优先搜索)或 BFS。
  • 遍历二维网格矩阵的每一个点,遇到为 1 的网格,以此为起点发起 DFS,将相连的 1 标记为已访问(如设为 0 防止重复计算),并累计面积。更新全局最大面积变量,直到遍历完整个网格。 没问题!针对腾讯面经中出现的两道经典手撕代码题,我为你整理了可以直接运行、且带有详细工程注释的完整 Python 代码。

一、 多头注意力机制 (Multi-Head Attention) 完整实现

在面试中手撕 Attention,面试官最看重的是你对张量维度变换(Reshape / Transpose)的熟练度,以及是否记得缩放因子(Scale)和掩码(Mask)的处理。以下是基于 PyTorch 的标准实现:

import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        # 必须保证模型维度能被头数整除
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # 每个头的维度

        # 定义 Q, K, V 的线性映射层
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)

        # 最后的输出线性映射层
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, q, k, v, mask=None):
        batch_size = q.size(0)

        # 1. 线性映射并切分多头
        # 维度变化: [batch_size, seq_len, d_model] 
        #        -> [batch_size, seq_len, num_heads, d_k] 
        #        -> [batch_size, num_heads, seq_len, d_k]
        Q = self.W_q(q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(k).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = self.W_v(v).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)

        # 2. 计算注意力得分 (Q 乘以 K 的转置,并除以根号 d_k)
        # K.transpose(-2, -1) 将最后两个维度翻转,即变为 [batch_size, num_heads, d_k, seq_len]
        # scores 维度: [batch_size, num_heads, seq_len, seq_len]
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

        # 3. 掩码机制 (可选,通常用于 Decoder 防止看到未来的词)
        if mask is not None:
            # 将 mask 中为 0 的位置替换为极小值,Softmax 后会趋近于 0
            scores = scores.masked_fill(mask == 0, -1e9)

        # 4. Softmax 归一化得到注意力权重
        attn_weights = torch.softmax(scores, dim=-1)

        # 5. 权重乘以 V 得到 Context 向量
        # context 维度: [batch_size, num_heads, seq_len, d_k]
        context = torch.matmul(attn_weights, V)

        # 6. 拼接多头 (Concat)
        # transpose 恢复维度顺序: [batch_size, seq_len, num_heads, d_k]
        # contiguous() 保证张量在内存中连续,否则 view() 会报错
        # view() 展平多头: [batch_size, seq_len, d_model]
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)

        # 7. 最后的线性投影
        output = self.W_o(context)

        return output

# --- 测试代码 ---
if __name__ == "__main__":
    batch_size, seq_len, d_model = 2, 10, 512
    num_heads = 8

    # 模拟输入张量
    x = torch.randn(batch_size, seq_len, d_model)

    # 实例化并前向传播
    mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
    out = mha(x, x, x) # 自注意力机制中,Q, K, V 都是输入 x

    print(f"输入维度: {x.shape}")
    print(f"输出维度: {out.shape}")

二、 最大连通域面积 / 岛屿的最大面积 (LeetCode 695)

这是一道高频的基础图论/搜索题。面试时推荐使用 DFS (深度优先搜索),代码最简洁,不易出错。核心思路是:遍历网格,遇到 1 就以此为起点进行 DFS,并把遍历过的 1 沉岛(设为 0),防止死循环重复计算。

class Solution:
    def maxAreaOfIsland(self, grid: list[list[int]]) -> int:
        # 边界条件处理
        if not grid or not grid[0]:
            return 0

        rows, cols = len(grid), len(grid[0])
        max_area = 0

        # 定义深度优先搜索函数
        def dfs(r: int, c: int) -> int:
            # Base Case:如果越界,或者当前格子是水(0),返回面积 0
            if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == 0:
                return 0

            # 关键步骤:标记当前陆地为已访问(沉岛策略,避免死循环)
            grid[r][c] = 0

            # 当前格子算 1 个面积
            area = 1

            # 递归向上下左右四个方向扩展,累加连通的面积
            area += dfs(r + 1, c) # 下
            area += dfs(r - 1, c) # 上
            area += dfs(r, c + 1) # 右
            area += dfs(r, c - 1) # 左

            return area

        # 遍历整个二维网格
        for r in range(rows):
            for c in range(cols):
                # 只有遇到陆地(1)才开始发起 DFS
                if grid[r][c] == 1:
                    # 更新全局最大面积
                    max_area = max(max_area, dfs(r, c))

        return max_area

# --- 测试代码 ---
if __name__ == "__main__":
    grid = [
        [0,0,1,0,0,0,0,1,0,0,0,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0,0],
        [0,1,1,0,1,0,0,0,0,0,0,0,0],
        [0,1,0,0,1,1,0,0,1,0,1,0,0],
        [0,1,0,0,1,1,0,0,1,1,1,0,0],
        [0,0,0,0,0,0,0,0,0,0,1,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0,0],
        [0,0,0,0,0,0,0,1,1,0,0,0,0]
    ]

    sol = Solution()
    print(f"最大岛屿面积为: {sol.maxAreaOfIsland(grid)}")  # 期望输出: 6

复杂度分析:

  • 时间复杂度: O(M \times N),其中 M 和 N 分别为网格的行数和列数。我们最多遍历网格中的每个单元格一次。
  • 空间复杂度: O(M \times N)。在最坏情况下(整个网格全是陆地),递归调用的调用栈深度会达到 M \times N。
本篇目录
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18

同作者面经