大模型八股 & 经验

LoRA

初始化

LoRA的公式是这样的,输入先过A矩阵,后过B矩阵:

\[W' = W + \frac{\alpha}{r} \cdot BA\]

AB = 0可以保证初始时模型保持原性能。那么可以通过A=0,也可以通过B=0来实现。原文选择的是B=0来实现。为什么呢?

先从方差讲起。设输入 \(x_j\) 满足 \(\text{Var}(x_j) = 1\),且 \(A_{ij} \sim \mathcal{N}(0, \sigma_A^2)\) 独立于 \(x\)

计算 \(z = Ax\)**:

\(z_i = \sum_{j=1}^{d_{in}} A_{ij} x_j\)

先算单个乘积项的方差(乘法):

\[\text{Var}(A_{ij} x_j) = \mathbb{E}[A_{ij}^2]\mathbb{E}[x_j^2] = \sigma_A^2 \cdot 1 = \sigma_A^2\]

再算求和后的方差(加法,各项独立):

\[\text{Var}(z_i) = \sum_{j=1}^{d_{in}} \text{Var}(A_{ij} x_j) = d_{in} \cdot \sigma_A^2\]

那么这个时候方法变成了 \(d_{in}\) 倍。

再计算 \(y = Bz\)**:

\(y_k = \sum_{i=1}^{r} B_{ki} z_i\),其中 \(B_{ki} \sim \mathcal{N}(0, \sigma_B^2)\) 独立于 \(A\)(从而独立于 \(z\))。

同样先算乘积项方差(乘法):

\[\text{Var}(B_{ki} z_i) = \sigma_B^2 \cdot \text{Var}(z_i) = \sigma_B^2 \cdot d_{in}\sigma_A^2\]

再算求和方差(加法):

\[\text{Var}(y_k) = \sum_{i=1}^{r} \text{Var}(B_{ki} z_i) = r \cdot \sigma_B^2 \cdot d_{in}\sigma_A^2\]

为了使整个计算过程,各层的方差都能稳定,不容易出现梯度爆炸或者消失的情况,方差就要保持恒定。

为使输出方差 \(\text{Var}(y_k) = \Theta(1)\)(不随维度爆炸或消失),需要:

\[r \sigma_B^2 d_{in} \sigma_A^2 = 1\]

  • 如果 \(\sigma_A^2 = 1/d_{in}\)(标准 He/Xavier),则必须 \(\sigma_B^2 = 1/r\)
  • 如果反过来让 \(B\) 随机且用 \(1/d_{out}\) 的小方差,则 \(\text{Var}(y_k) = r/d_{out} \ll 1\),信号严重衰减;而\(\sigma_B^2 = 1/r\)又太大(为了补偿从低维 r 到高维输出的映射,必须用大方差),导致初始梯度方差过大,对学习率极其敏感,训练不稳定。
候选方案 随机侧所需方差 对梯度的影响
方案 A\(A\) 随机,\(B=0\) \(\sigma_A^2 = 1/n\)(极小,如 \(1/4096\) \(Ax\) 方差可控(\(\Theta(1)\)),梯度稳定
方案 B\(B\) 随机,\(A=0\) \(\sigma_B^2 = 1/r\)(大 256 倍,如 \(1/16\) \(B\) 方差大,梯度 \(\frac{\partial \mathcal{L}}{\partial A} = B^T\delta\) 初始噪声强

缩放系数

\[W' = W + \frac{\alpha}{r} \cdot BA\]

公式里\(\frac{\alpha}{r}\)是缩放系数,一般是设置为2。

LoRA论文与工程实践(特别是Hugging Face PEFT库)中,alpha/r = 2 的默认设定源于方差守恒(Variance Scaling)的考量:

  • 初始化时\(A\) 的初始化标准差通常为 \(\sigma_A\)\(B\) 初始化为零
  • 前向传播初期\(B \cdot A\) 的输出的方差约为 \(r \cdot \sigma_A^2\)(假设独立同分布)
  • 为了保持与预训练权重 \(W_0\) 相当的梯度更新幅度,需要引入缩放因子

当设置 \(\frac{\alpha}{r} = 2\) 时:
- 实际等效的"学习率"或"更新步长"被放大2倍
- 这补偿了低秩分解(rank通常很小,如8、16、32)带来的表达能力限制
- 同时避免了 \(\alpha/r\) 过大导致的训练不稳定(梯度爆炸)或过小导致的欠拟合

为什么一般两倍就足够?按道理r比d要小很多。因为实际更新时,d变化的rank其实也不高,真实的rank也不是d,而是远小于d。

另外,前向后向都分别有系数,相当于有效梯度放大倍数是2的平方=4。

实际训练中,这个比例的选择还需考虑:

配置 特性 适用场景
\(\alpha = 2r\) 标准保守配置,梯度更新适中 通用指令微调,数据量中等(10K-100K样本)
\(\alpha = r\) 更新幅度减半,更稳定 数据量小(<10K)或需要强正则化时
\(\alpha = 4r\) 或更高 更新激进,拟合能力强 数据量大(>500K)或复杂任务,但需配合更低的学习率

值得注意的是,\(\alpha/r\) 的最优值与基础学习率优化器选择强相关:

  • 使用AdamW时,\(\alpha/r = 2\) 配合 lr=1e-4 是稳定组合
  • 若切换到LionMuon等二阶优化器,可能需要降低 \(\alpha/r\) 到1,因为这些优化器本身具有更大的有效步长
  • QLoRA(4-bit量化训练)中,由于量化噪声的存在,通常保持 \(\alpha=2r\) 或略微提高到 \(\alpha=3r\) 以补偿梯度精度损失

为何Adam需要更大的系数?

Adam的二阶矩特性

Adam维护动量 \(m\) 和方差 \(v\) 两个状态,更新公式为: \[\theta_{t+1} = \theta_t - \eta \cdot \frac{m_t}{\sqrt{v_t} + \epsilon}\]

关键问题:LoRA的初始化(\(A\) 为Kaiming初始化,\(B=0\))导致早期梯度具有特殊结构——\(B\) 的梯度 \(\nabla B \propto A^T\),而 \(A\) 的梯度 \(\nabla A = 0\)(因为 \(B=0\))。这种不对称性使得: 1. 梯度方差估计偏低:Adam的 \(v\) 项在训练初期会严重低估LoRA参数的真实梯度方差,因为 \(B\) 的梯度依赖于 \(A\) 的随机投影。 2. 有效学习率被抑制:由于 \(\sqrt{v_t}\) 的分母效应,Adam对LoRA参数的实际更新步长比SGD更小。

LoRA+的理论洞察: LoRA+论文严格证明,为了达到与全量微调相当的收敛速度,需要: - \(A\) 的学习率:\(\eta_A = \eta_{base}\) - \(B\) 的学习率:\(\eta_B = \Theta(r) \cdot \eta_A\)(实践中通常设为16倍)

由于标准LoRA使用统一学习率,通过增大 \(\alpha\)(即增大等效学习率)来补偿Adam在二阶矩估计上的保守性

具体数值: - 全量微调(AdamW):学习率 \(2 \times 10^{-5}\) - LoRA(AdamW):学习率 \(1 \times 10^{-4}\)5倍于全量微调

微调经验 - 多阶段微调

大部分使用LoRA微调的场景,数据量其实都比较小(数据量很大的直接全量问题微调就行了)。

在使用 LLaMA Factory + PEFT微调时,传统的单阶段 LoRA 训练往往面临几个实际问题:

  1. Rank 选择困境:设小了(如 r=8/16)表达能力受限,复杂任务拟合不足;设大了(如 r=128)训练不稳定,容易过拟合或破坏预训练知识。
  2. lr错误动量积累:前期冷启动阶段(B=0 )的高方差梯度噪声被 Adam 的 momentum 和 variance 持续记忆,导致后期即使学习率降低,优化器仍带着错误惯性震荡,无法在新初始化的优质子空间内精细收敛。
  3. 初始化敏感:标准 LoRA 使用随机高斯初始化,在低资源场景(<10k 样本)下收敛速度慢,最终性能依赖随机种子。

第一个问题好理解。

第三个问题:

在小数据场景下,训练步数有限(如只有 500-1000 步)。由于 B 从零开始,前几十步的更新完全依赖于 A 的随机初始值:
- A 的随机矩阵实际上定义了低维子空间的初始随机基(random basis)。
- 优化器只能在这个随机定义的子空间内进行搜索。
- 如果随机种子不好,A 的初始方向恰好与任务需要的特征方向正交,模型需要花费大量步数"旋转"这个子空间,而小数据场景下没有足够步数完成这个调整。

在大数据场景(>100k 样本)中,充足的迭代次数允许优化器逐渐"修正"初始的随机方向。但在小数据下,就很看随机种子了。

第二个问题:

类似地,由于步数不够,初始积累的动量对后续的影响很大。如果数据量很多,那么可以通过大量的后续step来纠正,但是如果步数不够,就可能纠正不过来。

这些问题本质上是在单一训练阶段内试图解决探索利用的矛盾。借鉴非凸优化中的多尺度策略,我们可以将训练拆分为多个阶段,每个阶段专注于特定目标。

因此分段方案整个训练流程分为三个阶段,分别对应"粗调-精修-补漏":

阶段 目标 核心技术 Rank 设置 学习率
1 全局探索 rsLoRA + NEFTune 高(128)→ 快 高(2e-4)→ 快
2 结构精修 DoRA 中(32)→ 准 中(5e-5)→ 稳
3 边界强化 Hard Example Mining 低(16)→ 狠 低(1e-5)→ 细

阶段间通过权重合并(Weight Merge)实现知识固化,避免灾难性遗忘。

阶段一:全局探索(rsLoRA + NEFTune)

rsLoRA(Rank-Stabilized LoRA)是 2024 年提出的改进,核心是将缩放因子从 \(\alpha/r\) 改为 \(\alpha/\sqrt{r}\)。这使得我们可以安全使用更大的 rank(如 128 甚至 256)而不会导致训练崩溃或破坏预训练权重。

NEFTune(Noisy Embedding Fine-Tuning)则是在输入嵌入层加入均匀噪声(\(\text{Uniform}[-0.5, 0.5] \times \alpha\)),迫使模型学习更鲁棒的特征表示,减少过拟合。(LlamaFactory暂不支持)

这个阶段的参数:
- rank=128 + rsLoRA:传统 \(\alpha=2r\) 的线性缩放会导致高秩时梯度过大,rsLoRA 的 \(\alpha/\sqrt{r}\) 让高秩训练稳定。
- epoch=1:防止在探索阶段过拟合,只学粗粒度特征。
- NEFTune:相当于在嵌入空间做数据增强,提升泛化能力。

阶段二:结构精修(DoRA)

DoRA(Weight-Decomposed Low-Rank Adaptation)将权重更新解耦为幅度(Magnitude)方向(Direction)两个分量:

\[W = W_0 + \underbrace{m}_{\text{幅度向量}} \cdot \underbrace{\frac{BA}{\|BA\|}}_{\text{方向矩阵}}\]

这种分解使得低秩(如 r=32)的表达能力接近传统 LoRA 的 r=64,同时训练更稳定(方向更新与幅度更新解耦)。

阶段二与阶段一的差别:

维度 阶段一(探索) 阶段二(精修)
缩放机制 \(\alpha/\sqrt{r}\) (rsLoRA) \(\alpha/r\) (标准)
更新方式 标准 BA 乘积 幅度-方向解耦
目标模块 All(全层) 仅 Attention
噪声 有(NEFTune)
学习率 2e-4(激进) 5e-5(保守)

阶段三:边界强化(Hard Example Mining)

利用阶段二模型在训练集上的 loss 分布,筛选出困难样本(预测误差大的 20%),进行针对性强化。这类似于 Boosting 中的残差补偿思想。

对比

  • 单阶段大 rank(128)反而效果差,因为没有 rsLoRA 导致训练不稳定。
  • 三阶段方案通过阶段一的 NEFTune 和阶段二的 DoRA,实现了更好的泛化(验证集 Acc 提升 6.3%)。
  • 阶段二合并操作减少了推理时的 adapter 层数,实际部署速度优于持续多 LoRA 叠加。

实施建议

  1. 必做前两阶段:阶段一(rsLoRA 探索)+ 阶段二(DoRA 精修)是性价比最高的组合,能解决 90% 的场景。
  2. 阶段三视情况:仅在数据分布不均(如长尾任务)或追求极致精度时启用,会增加约 30% 训练时间。
  3. 合并操作不可省:这是将"探索知识"固化为"模型本能"的关键,跳过合并直接阶段二等于从头训练。
  4. DoRA 兼容性:DoRA 与 QLoRA 兼容,可在阶段二同时开启 quantization_bit: 4 进一步节省显存。

实践经验

用LlamaFactory跑DoRA就是配置里加一个use_dora: true就行。但是用Qwen3.5跑DoRA的时候发现有问题,loss一直是0,grad norm一直是nan。

后来发现Qwen3.5是 Hybrid Attention-Mamba 架构(前24层混合了 linear_attention + full_attention),里面的in_proj_a, in_proj_b, in_proj_z, in_proj_qkv这几个层是能使用DoRA,因为他们不是标准的nn.Linear,而是状态空间的选择性投影层。

因此需要把lora_target从all改成仅包含这七个就可以:
- q_proj, k_proj, v_proj, o_proj — 标准 Attention 投影 - gate_proj, up_proj, down_proj — MLP 层

分类体系变化下的方案

问题

新增类别时,神经网络的全局耦合性会导致两个后果:

  1. 灾难性遗忘:新类训练梯度扭曲特征空间,旧类样本的相对位置漂移,决策边界失效。
  2. 决策边界冲突:如果新旧类别语义相近(如"手机"与"智能手机"),扁平 softmax 会强制它们互斥竞争,最终要么新类吃掉旧类,要么旧类压制新类。

方案一:冻结 Backbone + 独立 Sigmoid 二分类头

核心思路:特征空间不动,只在新类头上找参数空间。

  • Backbone 完全冻结:权重和 BN 统计量全部冻住,旧类样本的特征坐标恒定。
  • 独立 Sigmoid:每个类别一个二分类头,旧类头权重冻结,新增类别 = 新增一个可训练的 sigmoid 神经元。新类的梯度不会回传到旧类头。
  • 为什么不用 Softmax:softmax 的归一化分母包含所有类别,新增类必然结构性挤压旧类概率;sigmoid 每个神经元独立输出,互不影响。
  • 余弦分类器进阶:如果新旧类样本量极度不平衡(新类 10 万 vs 旧类 1 千),新类头权重范数会被拉得很大,导致模型偏向预测新类。此时对特征和权重做 L2 归一化,用余弦相似度替代点积,消除范数不平衡的偏置。
  • 新类头初始化:不要用随机初始化。取新类样本特征的均值方向初始化权重,偏置设负值,让模型从"保守拒识"开始学,避免上线初期就把旧类样本错分给新类。

方案二:双模型/模块架构(物理隔离)

核心思路:旧模型负责旧世界,新模块负责新世界,上层融合。

  • 共享冻结 Backbone,但旧类头和新类头参数不共享。旧类头从旧模型直接拷贝后,一个字节都不更新;新类头独立训练。
  • 推理时旧类概率取旧头,新类概率取新头,拼接融合。如果新旧类在同一语义层竞争(如"手机" vs "智能手机"),可做层级路由:旧头先判"是否手机",再进入新头判"智能手机 vs 功能机"。
  • 天然支持影子测试:新模块并行运行但不参与实际决策,观察一周后旧类指标无漂移再切换融合模式。
  • 适用于金融风控、医疗诊断等对旧类准确率零容忍的场景。

要注意的工程细节

细节 处理
BN 陷阱 只冻权重不够,必须把 backbone 设为 eval 模式,彻底冻住 running mean/var,否则旧类特征分布会隐性漂移
多标签重叠 一个样本可同时属于多个类别时,必须用 sigmoid + BCE,softmax 的互斥假设会直接破坏体系
语义重复 新增类别前,先算类别名与现有类别的 embedding 余弦相似度,>0.92 就触发别名映射,避免无意义膨胀
Hard Negative 回放 不存原始旧数据,只存旧类样本在冻结 backbone 上的特征向量。训练新类时,挑出与新类中心最接近的 Top-100 旧特征,强迫新类头对它们输出低概率,守住旧类边界
旧类回归测试 上线前必须跑 Backward Compatibility Test,单类 F1 下降 >2% 告警,>5% 禁止上线

三条原则

表示层冻结:Backbone 不动,旧类样本在特征空间的坐标就不漂移。

决策层隔离:独立 sigmoid 替代共享 softmax,新增类别只增参数,不改旧参数。

冲突靠层级:语义冲突的类别不要在扁平空间竞争。粗分类器冻结(如电子产品 vs 服装),细分类器局部增量(如智能手机 vs 老人机),把冲突爆炸半径限制在单个父节点内。

关于MCP (Model Context Protocol)

MCP是基于 Anthropic 2024.11 发布的开放协议,当前已成为 AI Agent 基础设施的事实标准之一。

定义

MCP 是 AI 应用(Host)与外部工具/数据源(Server)之间的「USB-C 协议层」。

它标准化了:工具如何被描述、发现、调用、以及结果如何回传。模型本身永远只使用自己原生训练过的 Function Call 格式,协议翻译由 MCP Client 完成。

核心架构(三层解耦)

┌─────────────┐
│    Host     │  ← 用户直接使用的应用(Claude Desktop / Cursor / VSCode / Cherry Studio)
│  (MCP       │
│   Client)   │  ← 负责:协议翻译、工具发现、生命周期管理、安全隔离
└──────┬──────┘
       │ MCP Protocol (JSON-RPC / stdio / SSE)
       ▼
┌─────────────┐
│ MCP Server  │  ← 负责:暴露工具 schema、执行业务逻辑、返回结果
│  (你开发的)  │     只讲 MCP 协议,不关心底层模型
└─────────────┘
层级 职责 开发者
Host 运行 AI 助手,集成 MCP Client Cursor / Anthropic / VSCode 等团队
MCP Client 将 MCP 协议 ↔︎ 模型 Native API 双向翻译 Host 团队内置
MCP Server 连接外部系统(DB / API / 文件),暴露工具能力 工具开发者

MCP 到底解决了什么问题?

生态碎片化:N × M 问题(核心)

没有 MCP 之前: - N 个 Host(Cursor、Claude、VSCode...)× M 个工具(GitHub、MySQL、Slack...) - 每个工具要为每个 Host 单独写适配代码 - 工具开发者需要给 Cursor 提 PR、给 Anthropic 发邮件、给 VSCode 写插件...

有了 MCP 之后: - 工具开发者写 一次 Server,所有支持 MCP 的 Host 自动接入 - Host 接 一次 MCP Client,自动获得整个工具生态 - 成本从 N×M 收敛为 N+M

协议翻译:屏蔽模型差异

不同 LLM 的 Native Function Call API 在协议层存在差异: - Schema 字段:OpenAI 用 parameters,Claude 用 input_schema - 响应结构:OpenAI tool_calls[] vs Claude content[].type=tool_use - 结果回传:OpenAI role: tool vs Claude role: user + tool_use_id - 流式语义:OpenAI 增量 delta.tool_calls vs Claude 原子 content_block_stop

MCP 的处理方式: - Server 只暴露统一的 MCP schema - Client 负责将 MCP 转译为当前模型的 Native 格式 - 模型永远说自己的母语,不需要知道 MCP 的存在

超出 Tool Call 的能力范畴

MCP 不仅解决「调用工具」,还标准化了:

MCP 能力 说明 与 Prompt-based 的区别
Resources URI 寻址的外部资源(file://db://),支持订阅和增量更新 Prompt 只能手动贴文本,无生命周期管理
Tools 带权限控制的函数调用(需用户批准) Prompt-based 无原生权限边界
Prompts 预编写的模板,帮助用户完成特定任务 应用层自行管理,无标准发现机制
Sampling Server 反向请求模型推理(代理 LLM 调用) 传统架构不支持

安全与隔离

  • MCP Server 运行在独立进程(stdio/SSE)
  • 与 Host 进程隔离:Server 崩溃不影响 Host,权限独立可控
  • 所有调用走标准化协议,便于审计和日志

MCP vs Function Calling:面试考点

误区澄清

「MCP 是一种新的 Function Call 格式,所有模型都要学它」→ 错误。

模型永远只输出自己原生训练过的格式(OpenAI 的 tool_calls、Claude 的 tool_use)。MCP 是Server 与 Client 之间的协议,模型根本不参与。

对比表

维度 Native Function Calling MCP
定位 模型能力(模型层) 系统架构(协议层)
标准化对象 模型输出格式 工具描述 + 调用协议
解决什么问题 让模型学会调用工具 让工具被所有 Host 复用
模型是否感知 是(训练 special token) 否(通过 Client 翻译)
生态范围 单 Host 单模型 跨 Host 跨模型
Prompt-based 能替代吗 简单场景可以,复杂场景可靠性远低于 Native Prompt-based 无法替代,因为涉及资源管理、发现机制、安全隔离

关键理解

MCP 不解决「模型会不会用工具」,它解决的是「工具怎么被接入到各个 Host」。

模型能力问题(理解 schema、遵循格式)靠模型训练解决; 系统架构问题(工具发现、复用、隔离)靠 MCP 解决。

工作流程(完整链路)

1. 用户提问
        ↓
2. Host 通过 MCP Client 向所有已连接的 Server 请求 tools/list
        ↓
3. Client 将 MCP schema 转译为当前模型的 Native schema
        ↓
4. 请求发给 LLM(附带可用工具列表)
        ↓
5. LLM 判断是否需要调用工具:
   ├─ 不需要 → 直接返回文本结果
   └─ 需要   → 返回 Native tool_call(如 OpenAI 的 tool_calls 数组)
        ↓
6. Client 将 Native tool_call 翻译回 MCP JSON-RPC 请求
        ↓
7. 发给对应的 MCP Server 执行
        ↓
8. Server 执行业务逻辑,返回结果
        ↓
9. Client 将结果转译为模型要求的结果格式,回传给 LLM
        ↓
10. LLM 基于工具结果生成最终回答
        ↓
11. Host 展示给用户

面试 Q&A

Q1:MCP 和 Function Calling 有什么区别?

答: 两者处于不同抽象层。 - Function Calling 是模型能力,解决「模型如何理解并调用外部函数」,由模型训练决定(special token、post-training)。 - MCP 是系统协议,解决「外部工具如何被标准化地接入到各种 AI 应用」。 - 关系:MCP Client 会把 MCP 协议描述的工具,转译成模型支持的 Function Call 格式。模型感知不到 MCP。

Q2:既然现在模型很强,prompt 里定义好 JSON 格式就能做工具调用,为什么还要 MCP?

答: 分两个层面: 1. 可靠性层面:Prompt-based 在简单场景下确实可用,但在复杂 schema、多轮并行调用、长参数列表时,错误率显著高于 Native Function Call(后者有 constrained decoding 或 special token 加持)。这是模型能力问题,MCP 不解决,但也不依赖它。 2. 架构层面:即使所有模型都能完美输出 JSON,「工具如何被 Host 发现、管理、复用、隔离」仍然是独立问题。MCP 让工具开发者写一次 Server,所有 Host 自动接入,解决 N×M 生态碎片化。

Q3:开发 MCP Server 时,需要自己写 Client 吗?

答: 不需要。Server 开发者只写 Server(暴露 schema 和业务逻辑),只讲 MCP 协议。Client 由 Host 团队(Cursor、Anthropic 等)内置,负责将 MCP 协议转译为各自接入模型的 Native API。

Q4:如果所有模型厂商统一了 Function Call API 标准,还需要 MCP 吗?

答: 模型 API 统一会消除 MCP Client 的「翻译层」价值,但 MCP 的核心价值不依赖于此: - 工具发现tools/list 让 Host 动态发现工具,而非人工集成 - 资源管理:Resources 的 URI 寻址、订阅、增量更新 - 安全隔离:Server 独立进程、权限边界 - 生态复用:一次开发,全 Host 接入

类比:即使所有数据库的 C API 统一,SQL 作为查询抽象层仍然必要。

Q5:MCP Server 和 API 有什么区别?为什么不能直接调 API?

答: API 是「机器与机器通信」,MCP Server 是「AI 与工具通信」。 - MCP Server 提供自描述能力(schema + 语义描述),让 LLM 理解「这个工具能做什么」 - 提供标准化生命周期(初始化、发现、调用、关闭) - 提供上下文注入机制(Resources),而不仅是单次请求-响应 - 普通 API 没有这些语义层,LLM 无法直接理解。

Q6:MCP 的局限性是什么?

答: 1. 模型依赖:MCP 只是协议层,最终 tool use 效果仍取决于模型本身的 Native Function Call 能力。非 Claude 模型在复杂场景下的体验可能不如原生 Claude。 2. 冷启动:需要 Host 支持。如果某个 Host 不接 MCP,该 Host 用户无法使用 MCP Server(但主流 Host 已广泛支持)。 3. 状态管理:MCP 本身是无状态的(Server 每次调用独立),复杂的多步状态需要 Server 自行维护。 4. 性能:stdio 通信有进程间开销,高频调用场景下延迟高于直接库调用。

MCP Client 开发详解:以天气查询为例

通过一个完整可运行的示例,展示 MCP Server 开发后,Client 侧具体需要做什么。

Server 侧代码(工具开发者编写)

Server 只暴露一个「查询天气」的工具,完全不知道最终用户会用什么模型。

# weather_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

@mcp.tool()
async def get_weather(city: str, date: str = "today") -> str:
    """
    查询指定城市的天气情况。

    Args:
        city: 城市名称,如 "深圳"、"Beijing"
        date: 日期,默认为今天
    """
    # 模拟调用外部 API
    weather_data = {
        "深圳": "晴天,25°C",
        "Beijing": "多云,18°C",
        "Shanghai": "小雨,22°C"
    }
    result = weather_data.get(city, "未知城市")
    return f"{city} {date} 天气:{result}"

if __name__ == "__main__":
    # 通过 stdio 与 Host 通信
    mcp.run(transport="stdio")

Server 开发者的工作到此结束。 他只定义了: - 工具名称:get_weather - 输入参数 schema:city (string, required), date (string, optional) - 业务逻辑:查天气,返回字符串


Client 侧代码(Host 团队编写)

Client 需要完成:启动 Server → 发现工具 → 转译 schema → 调用模型 → 执行工具 → 回传结果

# weather_client.py
import asyncio
import json
from typing import Any
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 假设 Host 接的是 OpenAI(实际可以是任意模型)
import openai

class WeatherMCPClient:
    def __init__(self):
        self.session: ClientSession | None = None
        self.tools: list[dict] = []  # 存储从 Server 发现的工具
        self.openai_client = openai.AsyncOpenAI(api_key="your-key")

    async def connect_server(self):
        """Step 1: 启动 Server 进程,建立 stdio 通信"""
        server_params = StdioServerParameters(
            command="python",
            args=["weather_server.py"],
            env=None
        )

        # stdio_client 会启动子进程,建立双向管道
        async with stdio_client(server_params) as (read, write):
            async with ClientSession(read, write) as session:
                self.session = session

                # Step 2: 初始化 MCP 会话(握手)
                await session.initialize()
                print("[Client] MCP 会话初始化完成")

                # Step 3: 向 Server 请求工具列表(Capability Discovery)
                tools_result = await session.list_tools()
                self.tools = tools_result.tools
                print(f"[Client] 发现 {len(self.tools)} 个工具:{[t.name for t in self.tools]}")

                # 进入交互循环
                await self.chat_loop()

    # ==================== 核心:协议翻译层 ====================

    def translate_to_openai(self, mcp_tools: list) -> list[dict]:
        """
        Step 4: 将 MCP schema 转译为 OpenAI 的 Native Function Call 格式

        这是 Client 的核心工作之一:协议翻译。
        MCP 的 schema 和 OpenAI 的 JSON Schema 基本兼容,但字段名和结构有差异。
        """
        openai_tools = []
        for tool in mcp_tools:
            openai_tools.append({
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema  # MCP 的 inputSchema 直接兼容 JSON Schema
                }
            })
        return openai_tools

    def parse_openai_tool_call(self, tool_call: Any) -> tuple[str, dict]:
        """
        Step 6: 解析 OpenAI 的 tool_calls,提取函数名和参数

        OpenAI 格式:
        {
            "id": "call_abc123",
            "type": "function",
            "function": {"name": "get_weather", "arguments": '{"city":"深圳"}'}
        }
        """
        name = tool_call.function.name
        # arguments 是 JSON 字符串,需要解析
        args = json.loads(tool_call.function.arguments)
        return name, args

    # ==================== 核心:交互主循环 ====================

    async def chat_loop(self):
        """Step 5-11: 完整的对话 + 工具调用链路"""

        user_question = "深圳今天天气怎么样?"
        print(f"[User] {user_question}")

        # Step 5: 调用 LLM,附带工具列表
        # 注意:模型看到的永远是它自己训练过的 OpenAI 格式,不知道 MCP 存在
        openai_tools = self.translate_to_openai(self.tools)

        messages = [{"role": "user", "content": user_question}]

        response = await self.openai_client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=openai_tools,          # 模型看到的 Native 工具列表
            tool_choice="auto"
        )

        assistant_msg = response.choices[0].message

        # 检查模型是否决定调用工具
        if assistant_msg.tool_calls:
            print(f"[LLM] 决定调用工具:{[t.function.name for t in assistant_msg.tool_calls]}")

            # Step 6-7: 将 OpenAI 的 tool_calls 翻译为 MCP 请求,发给 Server 执行
            tool_results = []
            for tool_call in assistant_msg.tool_calls:
                name, args = self.parse_openai_tool_call(tool_call)

                # Step 7: 通过 MCP 协议调用 Server
                # 这是 JSON-RPC 请求:{"method": "tools/call", "params": {"name": "...", "arguments": {...}}}
                result = await self.session.call_tool(name, arguments=args)
                print(f"[Server] 工具 {name} 返回:{result.content}")

                # Step 8: 将 Server 结果包装成 OpenAI 要求的格式回传
                # OpenAI 要求 tool 结果用 role="tool" + tool_call_id 关联
                tool_results.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": str(result.content)
                })

            # Step 9: 将工具结果加入对话历史,再次发给 LLM
            messages.append({
                "role": "assistant",
                "content": assistant_msg.content or "",
                "tool_calls": [
                    {
                        "id": tc.id,
                        "type": "function",
                        "function": {"name": tc.function.name, "arguments": tc.function.arguments}
                    }
                    for tc in assistant_msg.tool_calls
                ]
            })
            messages.extend(tool_results)

            # Step 10: LLM 基于工具结果生成最终回答
            final_response = await self.openai_client.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                tools=openai_tools
            )

            print(f"[LLM] 最终回答:{final_response.choices[0].message.content}")
        else:
            print(f"[LLM] 直接回答:{assistant_msg.content}")


if __name__ == "__main__":
    client = WeatherMCPClient()
    asyncio.run(client.connect_server())

Client 执行流程拆解

步骤 Client 动作 对应代码 为什么需要
1. 进程管理 启动 Server 子进程,建立 stdio 管道 stdio_client(server_params) Server 独立进程运行,Client 负责生命周期
2. 协议握手 发送 initialize 请求 session.initialize() MCP 要求版本协商和能力声明
3. 工具发现 调用 tools/list 获取 schema session.list_tools() 动态发现:Server 可随时增删工具,Host 无需硬编码
4. Schema 转译 MCP inputSchema → OpenAI parameters translate_to_openai() 协议翻译:字段名、结构、语义映射
5. 模型调用 将转译后的 tools 发给 LLM openai_client.chat.completions.create(tools=...) 模型只认 Native 格式
6. 响应解析 从 OpenAI tool_calls 提取 name + arguments parse_openai_tool_call() OpenAI 返回 JSON 字符串,需反序列化
7. MCP 执行 将提取的参数通过 MCP 协议发给 Server session.call_tool(name, args) JSON-RPC 调用:统一入口
8. 结果回传 Server 结果 → OpenAI role: tool 格式 tool_results.append({role: "tool", ...}) OpenAI 要求特定结果回传结构,Claude 则是另一种
9. 多轮对话 将工具结果加入 messages,再次请求 LLM messages.extend(tool_results) 让模型基于外部数据推理
10. 错误处理 工具调用失败时的重试、超时、降级 (示例省略) 生产级 Client 需处理 Server 崩溃、网络超时

有无 MCP 的对比

没有 MCP 的世界(硬编码接入)

class HardcodedWeatherClient:
    def __init__(self):
        self.openai_client = openai.AsyncOpenAI(api_key="your-key")

    def get_tools(self):
        # Host 开发者必须手动把工具 schema 写死在代码里
        return [{
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查询指定城市的天气情况",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "城市名称"},
                        "date": {"type": "string", "description": "日期"}
                    },
                    "required": ["city"]
                }
            }
        }]

    async def execute_tool(self, name: str, args: dict):
        # Host 开发者必须直接调用外部 API 或实现业务逻辑
        # 这意味着工具逻辑和 Host 代码耦合在一起
        if name == "get_weather":
            return f"{args['city']} 天气:晴天,25°C"

没有 MCP 的问题: 1. Schema 硬编码:工具描述写在 Host 代码里,Server 改了字段,Host 必须发版更新 2. 业务耦合:天气查询逻辑直接写在 Host 里,Host 越来越臃肿 3. 无法复用:另一个 Host(比如 VSCode 插件)想用这个功能,得把代码复制一遍 4. 无动态发现:Host 不知道 Server 有哪些工具,除非人工维护列表

有了 MCP 之后

  • Schema 由 Server 自描述(tools/list),Host 动态获取
  • 业务逻辑在 Server 内,Host 只做协议翻译
  • 任何支持 MCP 的 Host 都能零成本接入
  • Server 增删工具,Host 自动感知

总结

Client 是「双语翻译官 + 外交管家」: - 对内(对模型):说模型的 Native 方言(OpenAI / Claude / Gemini 格式) - 对外(对 Server):说 MCP 普通话(JSON-RPC) - 管理:负责 Server 进程、工具发现、调用编排、错误处理

Server 开发者写一次 weather_server.py,所有 Host 的 Client 都能通过同一套 MCP 协议调用它——这就是 N×M → N+M 的具体体现。

FDE(forward deployed engineer)

这个概念最早由 Palantir 开创,OpenAI 将其用于描述一种驻扎在客户现场、负责将前沿模型从原型推进到生产环境的混合技术角色。

FDE 不是传统意义上的售前或售后支持,而是一个兼具技术深度、客户现场执行力和业务敏锐度的角色。用 OpenAI 自己的描述,FDE 是"技术负责人、顾问和产品经理的混合体"——需要写生产级代码,同时也要坐在客户现场理解决策如何实际发生,并在真实环境中验证原型。

维度 传统 SWE 传统 Solutions Architect / 咨询 FDE
代码参与 核心工作 较少 核心工作,要求生产级代码
客户现场 通常不驻场 驻场但偏方案讲解 长期嵌入,与业务同频决策
交付物 产品功能 PPT / 架构图 生产系统 + 可复用模式
反馈闭环 通过 PM 间接传递 通常不传递 直接影响 Research / Product 路线图
模糊度容忍 需求相对明确 需求模糊但交付偏规划 在高度模糊中快速构建并验证

Mamba

https://www.bilibili.com/video/BV1d31NBJEVJ/?spm_id_from=333.337.search-card.all.click&vd_source=2604eea3e471f292d1b24c3cb82e9a13

SSM, Structured State Space Models, 状态空间模型

Gated DeltaNet(GDN)线性注意力层

Qwen3.5使用了较多的GDN,比例约为 3:1(24 层中 18 层 GDN,6 层 Full Attention)。

Gated DeltaNet 通过线性注意力 + 门控循环状态替代 softmax attention,核心优势在于:

维度 纯 Transformer (Full Attention) Gated DeltaNet
序列复杂度 \(O(n^2 \cdot d)\) \(O(n \cdot d^2)\),线性于序列长度
KV-Cache 每 token 存 K/V,随长度线性增长 只维护一个固定大小的状态矩阵 \(S_t\)
长上下文内存 10GB+ (128K, 40层) 约为纯 Transformer 的 **25%**
推理吞吐 Baseline 2-3x 提升

纯 Gated DeltaNet(或纯 Mamba)虽然快,但有一个致命弱点:精确内容检索(exact recall)能力弱于 softmax attention。线性/循环机制擅长"变换和压缩表示",但在需要精确定位某一历史 token 时表现不足。

Qwen3.5 的 3:1 混合设计(每 4 层中 3 层 GDN + 1 层 Full Attention)是一种功能分层策略:

  • GDN 层(75%):承担主要的表示变换、长距离信息压缩、特征提取。利用线性复杂度处理"大海量"上下文。
  • Full Attention 层(25%):作为"精确检索锚点",在关键深度位置恢复全局 softmax attention,提供:
    • 精确的 in-context retrieval(如复制、引用前文细节)
    • 训练稳定性(防止循环误差累积)
    • 更强的 in-context learning 能力

这种设计被验证为接近理论最优:Wang et al. 对 72 个混合模型的系统分析表明,线性层与 attention 层的最优比例在 3:1 到 6:1 之间(https://arxiv.org/html/2605.01106v1)

RAG

https://zhuanlan.zhihu.com/p/2024154319985885819

Ndcg@k

RRF,Reciprocal Rank Fusion,是一种将多个不同排序结果列表合并为单个最优排序列表的算法。

Reinforcement Fine-Tuning, RFT

Rejection Sampling Fine-Tuning, RFT

多模态

ViT,patch,2D-RoPE

Vision-Language Adaptor

pixel shuffle

动态分辨率,子图,全景图

deepseek-vl:Hybrid Vision Encoder

长上下文

claude code

(https://zhuanlan.zhihu.com/p/2025176118068621451)[https://zhuanlan.zhihu.com/p/2025176118068621451]

预训练