大模型八股 & 经验
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是稳定组合 - 若切换到Lion或Muon等二阶优化器,可能需要降低 \(\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 训练往往面临几个实际问题:
- Rank 选择困境:设小了(如 r=8/16)表达能力受限,复杂任务拟合不足;设大了(如 r=128)训练不稳定,容易过拟合或破坏预训练知识。
- lr错误动量积累:前期冷启动阶段(B=0
)的高方差梯度噪声被 Adam 的 momentum 和 variance
持续记忆,导致后期即使学习率降低,优化器仍带着错误惯性震荡,无法在新初始化的优质子空间内精细收敛。
- 初始化敏感:标准 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 叠加。
实施建议
- 必做前两阶段:阶段一(rsLoRA 探索)+ 阶段二(DoRA
精修)是性价比最高的组合,能解决 90% 的场景。
- 阶段三视情况:仅在数据分布不均(如长尾任务)或追求极致精度时启用,会增加约
30% 训练时间。
- 合并操作不可省:这是将"探索知识"固化为"模型本能"的关键,跳过合并直接阶段二等于从头训练。
- 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 层
分类体系变化下的方案
问题
新增类别时,神经网络的全局耦合性会导致两个后果:
- 灾难性遗忘:新类训练梯度扭曲特征空间,旧类样本的相对位置漂移,决策边界失效。
- 决策边界冲突:如果新旧类别语义相近(如"手机"与"智能手机"),扁平 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 能力
- 精确的 in-context retrieval(如复制、引用前文细节)
这种设计被验证为接近理论最优: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]