梯度消失和梯度爆炸

梯度消失(Gradient Vanishing)和梯度爆炸(Gradient Exploding)是深度学习(尤其是深度神经网络和循环神经网络 RNN)训练中常见的两个核心问题。它们都会导致模型无法有效训练,但表现形式相反。

简单来说,这两个问题都源于反向传播(Backpropagation)*中的*连乘效应


1. 核心机制:为什么会出现这个问题?

在神经网络中,我们通过反向传播算法来更新参数。为了计算靠近输入层(浅层)参数的梯度,需要利用链式法则(Chain Rule),将后面所有层的梯度乘起来。

$$\frac{\partial Loss}{\partial w_1} = \frac{\partial Loss}{\partial y} \cdot \frac{\partial y}{\partial h_n} \cdot ... \cdot \frac{\partial h_2}{\partial h_1} \cdot \frac{\partial h_1}{\partial w_1}$$

想象你有一长串数字相乘:

  • 梯度消失:如果这些数字大部分都小于 1(例如 0.9),乘得越多,结果越接近 0
  • 梯度爆炸:如果这些数字大部分都大于 1(例如 1.1),乘得越多,结果就会趋向 无穷大

2. 梯度消失 (Gradient Vanishing)

现象

  • 在深层网络中,靠近输入层(浅层)的参数几乎不更新,而靠近输出层的参数更新正常。
  • 模型看起来在训练,但实际上前几层只是在做随机特征提取,导致整体模型无法收敛或性能很差。

主要原因

  1. 激活函数选择不当:使用了 SigmoidTanh 函数。
    • Sigmoid 的导数最大值只有 0.25。当网络很深时,多个小于 0.25 的数相乘,梯度会以指数级衰减。
  2. 网络太深:层数越多,连乘链条越长,衰减越严重。

解决方法

  • 更换激活函数:使用 ReLU (Rectified Linear Unit) 及其变体(Leaky ReLU)。ReLU 在正区间的导数恒为 1,解决了连乘导致的衰减问题。
  • Batch Normalization (BN):通过规范化每一层的输入,强行将数据拉回到激活函数的敏感区间,防止梯度变小。
  • 残差结构 (ResNet):引入 “Shortcut Connection”(捷径),让梯度可以通过“高速公路”直接传到浅层,不再完全依赖层层相乘。

3. 梯度爆炸 (Gradient Exploding)

现象

  • Loss 震荡:损失函数(Loss)忽大忽小,甚至变成 NaN(非数字)。
  • 权重剧变:模型参数更新幅度过大,直接飞出合理范围。
  • 多见于 RNN (循环神经网络) 处理长序列数据时。

主要原因

  1. 权重初始化过大:初始参数值太大,导致每一层的输出和梯度都成倍放大。
  2. 网络结构问题:在 RNN 中,同一个权重矩阵在时间步上被反复相乘,极易导致数值溢出。

解决方法

  • 梯度裁剪 (Gradient Clipping):简单粗暴但有效。如果梯度的范数(Norm)超过某个阈值(比如 5),就强行把它截断(缩放)到这个阈值以内。
  • 改善权重初始化:使用 XavierHe Initialization,根据每层的神经元数量科学地设置初始权重的范围。
  • 使用 LSTM/GRU:在处理序列数据时,LSTM 通过“门控机制”专门设计了梯度的传输通道,缓解了长序列中的梯度问题。

总结对比

特性 梯度消失 (Vanishing) 梯度爆炸 (Exploding)
本质 连乘项 < 1,梯度趋近于 0 连乘项 > 1,梯度趋近于无穷
后果 浅层参数不更新,模型学不到东西 权重数值溢出 (NaN),无法收敛
高发场景 深层网络 (Deep CNN/MLP),使用 Sigmoid 循环神经网络 (RNN),深层网络
核心解法 ReLU, BatchNorm, ResNet Gradient Clipping, 权重正则化

安装

PyTorch 在 PyPI 上的包名是 torch,而不是 pytorch

1
2
# 仅安装 CPU 版本
uv add torch

安装 GPU 版本的 PyTorch 需要指定 CUDA 版本的索引。

1
2
# CUDA 12.1 版本(推荐,适用于较新的显卡)
uv add torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

检查你的 NVIDIA 驱动支持的 CUDA 版本:

1
nvidia-smi

如何知道对应的cuda版本索引

访问 PyTorch 官网: https://pytorch.org/get-started/locally/

torchvisiontorchaudio 是 PyTorch 生态系统中的两个官方扩展库:

torchvision - 计算机视觉工具包:

  • 预训练模型(ResNet、VGG、YOLO 等)
  • 图像数据集(CIFAR-10、ImageNet、COCO 等)
  • 图像转换和增强功能
  • 图像读取和处理工具

torchaudio - 音频处理工具包:

  • 音频数据集
  • 音频转换和预处理
  • 音频特征提取(MFCC、梅尔频谱等)
  • 音频读取和保存

张量与向量

1
2
3
from torch import nn
linear = nn.Linear(5, 3)
linear.state_dict()
1
2
3
4
5
OrderedDict([('weight',
tensor([[ 0.3763, -0.3488, 0.4359, 0.1161, 0.3337],
[ 0.2588, 0.1844, 0.1083, -0.1958, 0.2706],
[-0.0392, -0.0902, 0.3593, -0.2657, 0.3799]])),
('bias', tensor([-0.4142, -0.0444, -0.2487]))])

linear = nn.Linear(5, 3) 创建了一个线性层(全连接层)

参数含义: - 5 - 输入特征数(in_features) - 3 - 输出特征数(out_features)

内部结构: 这个层包含两个可学习的参数: - 权重矩阵 W:形状为 (3, 5) - 偏置向量 b:形状为 (3,)

数学运算: y = xWT + b

'weight' (权重) —— 这是 2阶张量 (矩阵),是一个形状 (Shape) 为 3 × 5矩阵

参考资料

从零搭建神经网络,识别手写数字【PyTorch】【Transformer结构拆解】_哔哩哔哩_bilibili

介绍

FastAPI + Supabase 后端,React + Ant Design 前端的工厂管理系统。包含零件、供应商、仓库、员工、库存、采购、用户权限等模块,支持 Supabase Auth 鉴权和业务角色控制。

后端结构

1
2
3
4
5
6
7
- backend/
- main.py:FastAPI 入口,注册各路由
- api/:业务路由(factory 模块、auth、users 等)
- services/:业务服务、鉴权依赖
- schemas/:Pydantic 模型
- src/db/:SQLAlchemy 模型、数据库工具、DDL/示例数据文档
- docs/:后端设计文档

鉴权功能实现解析

什么是负载均衡

负载均衡本质上是一个反向代理(Reverse Proxy)*或*数据包转发器。它位于客户端(Client)和后端服务集群(Upstream Servers)之间,通过向外暴露一个虚拟IP(VIP),屏蔽了后端具体的网络拓扑结构。

为什么我们需要负载均衡?

在计算机系统中,使用负载均衡主要有三个巨大的好处:

  1. 高可用性(High Availability / Reliability): 如果有服务器坏了(宕机),负载均衡器会立刻发现,并停止向它发送请求,转而分发给其他健康的服务器。这样用户就感觉不到服务中断。
  2. 高性能(Performance): 通过将流量分摊,避免单一服务器过载,从而保证网页打开的速度和响应时间。
  3. 可扩展性(Scalability): 如果业务突然增长(比如双11大促),你可以随时增加几台新服务器进来,负载均衡器会自动开始给它们分配任务,非常灵活。
image-20251217201102150

它是怎么分配任务的?(常见算法)

负载均衡器并不是“瞎”分配的,它有一套策略(算法)来决定把请求给谁。最常见的有这几种:

  • 轮询(Round Robin): 最简单的策略。按顺序一个一个来:请求1给服务器A,请求2给服务器B,请求3给服务器C,请求4又回到服务器A。
  • 最小连接数(Least Connections): 比较智能。它会看谁现在手头的活儿最少(连接数最少),就把新任务给谁。适合某些任务处理时间长短不一的场景。
  • 源地址哈希(IP Hash): 为了保证“从一而终”。它保证来自同一个 IP 地址的用户,总是被分配到同一台服务器上。这在需要保持登录状态(Session)的场景中很有用。

OSI 模型(Open Systems Interconnection Model)

互联网数据传输原理 |OSI七层网络参考模型_哔哩哔哩_bilibili

7. 应用层 (Application Layer)

  • 作用: 直接与用户打交道,为应用程序提供网络服务接口。
  • 关键点: 这是用户“看得到”的一层。
  • 常见协议: HTTP (网页), FTP (文件), SMTP (邮件), DNS (域名)。
  • 数据单元: Data (数据)

6. 表示层 (Presentation Layer)

  • 作用: 数据的“翻译官”。负责数据的格式化、加密/解密、压缩/解压缩。
  • 关键点: 确保一个系统的应用层发送的数据能被另一个系统的应用层读取。比如把 JSON 对象转成二进制,或者处理 SSL/TLS 加密。
  • 常见格式: JPEG, ASCII, EBCDIC, SSL/TLS。

5. 会话层 (Session Layer)

  • 作用: 建立、管理和终止应用程序之间的“会话”(Session)。
  • 关键点: 它负责断点续传、同步点。比如你从网盘下载文件,断网了,下次能接着下,就是会话层的功劳。
  • 常见协议: RPC, SQL。

4. 传输层 (Transport Layer) —— (关键层级)

  • 作用: 负责端到端(End-to-End)的数据传输,区分具体的应用程序(通过端口号)。
  • 关键点: 负载均衡中的“四层负载”就在这里。它决定了数据是“可靠传输”(TCP)还是“快速传输”(UDP)。
  • 核心设备/概念: 端口 (Port), 负载均衡器 (L4)。
  • 常见协议: TCP (可靠, 三次握手), UDP (快速, 直播/游戏)。
  • 数据单元: Segment (段)

3. 网络层 (Network Layer) —— (关键层级)

  • 作用: 负责地址寻址和路由选择(Routing)。决定数据包如何从从源地址到达目的地址(跨网络)。
  • 关键点: IP 地址在这里起作用。路由器(Router)工作在这一层。
  • 常见协议: IP, ICMP (Ping), OSPF, BGP。
  • 数据单元: Packet (包)
  • 作用: 负责节点到节点(Node-to-Node)的传输,处理物理寻址。
  • 关键点: MAC 地址在这里起作用。交换机(Switch)通常工作在这一层。它负责在同一局域网内把数据帧发给对的人。
  • 常见协议: Ethernet (以太网), VLAN, Wi-Fi (802.11)。
  • 数据单元: Frame (帧)

1. 物理层 (Physical Layer)

  • 作用: 传输比特流(0 和 1)。
  • 关键点: 真正的物理介质。把数字信号转换成电信号、光信号或无线电波。
  • 核心设备: 网线, 光纤, 集线器 (Hub), 中继器。
  • 数据单元: Bit (比特)

TCP/IP 协议栈

1. 为什么叫 TCP/IP?

虽然名字里只有 TCP 和 IP,但它其实是一个协议族(Protocol Suite),包含了几十个协议。 之所以用这两个命名,是因为它们最重要:

  • IP (Internet Protocol): 负责把数据包送到目的地(解决“路怎么走”)。
  • TCP (Transmission Control Protocol): 负责把数据可靠地传输(解决“东西别丢了”)。

2. TCP/IP 的四层模型(与 OSI 的映射)

TCP/IP 更加务实,它将 OSI 的 7 层模型压缩为了 4 层

我们由上至下来看:

第一层:应用层 (Application Layer)

  • 对应 OSI: 应用层 + 表示层 + 会话层
  • 功能: 处理特定的应用程序细节。
  • 常见协议:
    • HTTP/HTTPS: 浏览网页。
    • SSH: 远程登录服务器。
    • DNS: 域名解析(把 google.com 变成 IP 地址)。
    • FTP: 文件传输。

第二层:传输层 (Transport Layer)

  • 对应 OSI: 传输层
  • 功能: 提供端到端(Host-to-Host)的通信服务。它只关心两台主机上的进程(通过端口号区分),而不关心中间经过了多少路由器。
  • 两大主角:
    • TCP: 可靠、面向连接(打电话)。
    • UDP: 不可靠、无连接(大喇叭广播)。

第三层:网络层 (Internet Layer)

  • 对应 OSI: 网络层
  • 功能: 处理数据包在网络中的路由选择。这是互联网的核心
  • 核心协议:
    • IP (IPv4/IPv6): 核心载体。
    • ICMP: 比如 ping 命令就在这里工作,用来报错或探测。
    • ARP: 地址解析协议(知道 IP 找 MAC 地址)。

第四层:网络接口层 (Network Interface Layer)

  • 对应 OSI: 数据链路层 + 物理层
  • 功能: 处理与物理硬件的交互。TCP/IP 标准对这一层并没有严格定义,只要能传 IP 数据包就行。
  • 常见技术: 以太网 (Ethernet)、Wi-Fi。

常见的负载均衡分类

你可能会听到“四层负载”和“七层负载”这样的术语,它们的区别在于“指挥”的层级不同:

类型 对应层级 (OSI模型) 特点 典型代表
四层负载均衡 (L4) 传输层 (TCP/UDP) 速度快。只看 IP 和端口号,不看内容,直接转发数据包。 LVS, F5 (硬件)
七层负载均衡 (L7) 应用层 (HTTP/HTTPS) 更智能。它能看懂 URL、Cookie、头部信息。比如把 /image 的请求分给图片服务器,把 /api 的请求分给应用服务器。 Nginx, HAProxy, AWS ALB

正向代理和反向代理

正向代理 (Forward Proxy) —— 代表客户端

这是大家通常说的“挂代理”或“梯子”。

  • 场景: 你(客户端)想访问 Google,但直接访问不了。
  • 做法: 你连上一个代理服务器,对它说:“帮我访问 Google”。代理服务器去访问 Google,把结果拿回来给你。
  • 特点: Google 不知道是你访问的,它只看到了代理服务器。
  • 作用: 隐藏客户端身份,突破访问限制。

反向代理 (Reverse Proxy) —— 代表服务端

这是互联网公司部署在自家门口的“门卫”。

  • 场景: 你(客户端)访问 www.baidu.com
  • 做法: 你的请求其实并没有直接到达百度的核心数据库或应用服务器,而是先到达了百度的反向代理服务器。这个代理服务器决定把你的请求转给内部的哪台机器处理,处理完后再把结果给你。
  • 特点: 你(客户端)不知道具体是哪台内部服务器为你服务的,你只看到了反向代理。
  • 作用: 隐藏服务端身份,保护内部网络。

常见问题

如何解决多个服务器session一致性的问题

这是一个非常经典的分布式系统架构问题。

在单体架构(一台服务器)中,Session 保存在服务器的内存里,这没问题。 但在集群环境(多台服务器)下,默认情况是这样的:

  1. 用户在 服务器 A 登录,Session 存在 A 的内存里。
  2. 下一次请求,负载均衡器把用户分发到了 服务器 B
  3. 服务器 B 的内存里没有这个 Session,于是判断用户“未登录”,强制踢下线。

为了解决这个问题,业界主要有四种成熟的方案,我会按推荐程度从低到高为您介绍:

1. 方案一:会话保持 (Sticky Sessions / Session Affinity)

原理: 让负载均衡器“记住”用户。 如果不改变代码,这是最简单的改法。我们在负载均衡器(如 Nginx)上配置策略,保证同一个 IP 的请求永远只发给同一台服务器

  • 实现方式: Nginx 的 ip_hash 策略。
  • 优点:
    • 简单,不需要修改任何应用程序代码。
    • 不需要引入额外的存储组件。
  • 缺点:
    • 故障丢失: 如果这台服务器宕机,上面所有用户的 Session 全部丢失。
    • 负载不均: 如果某个公司几千人都用同一个公网 IP 出口,这些请求会全部压在同一台服务器上,导致负载均衡失效。

2. 方案二:Session 复制 (Session Replication)

原理: 让服务器之间“互通有无”。 当服务器 A 产生了一个 Session,它通过网络广播把这个 Session 同步给服务器 B、C、D。

  • 实现方式: Tomcat 集群自带的 Session 复制功能。
  • 优点:
    • 服务器宕机不会丢失 Session(因为其他机器也有)。
  • 缺点(致命):
    • 性能极差: 每次存取 Session 都要广播,网络风暴严重。
    • 扩展性差: 随着服务器数量增加,同步数据的成本呈指数级上升。通常不建议在生产环境使用。

3. 方案三:集中式 Session 存储 (Centralized Session Storage) —— 【最推荐】

原理: Session 也不存 A,也不存 B,而是存到一个公共的“保险柜”里。 所有的服务器在处理请求时,都去这个公共的地方读写 Session。

  • 实现方式: 使用 Redis 或 Memcached 作为 Session 仓库。
    • Spring Boot 项目中只需引入 spring-session-data-redis 依赖,几行配置就能搞定。
  • 优点:
    • 无状态化: 应用服务器变得“无状态”(Stateless),可以随意增加或减少服务器,不影响用户体验。
    • 高可用: 某台应用服务器挂了,用户被切到另一台,Session 依然在 Redis 里,用户无感知。
    • 速度快: Redis 是基于内存的,读写速度极快。
  • 缺点:
    • 引入了新的组件(Redis),需要保证 Redis 的高可用(如使用 Redis Cluster)。

4. 方案四:客户端存储 (Token / JWT) —— 【现代架构主流】

原理: 服务器根本不存 Session。 服务器生成一个加密的令牌(Token),交给客户端(浏览器/App)自己保存。客户端每次请求都带着这个令牌,服务器解密验证身份。

  • 实现方式: JWT (JSON Web Token)
  • 优点:
    • 极致的服务器无状态: 服务器不需要查数据库,也不需要查 Redis,只要通过 CPU 计算验签即可。
    • 适合微服务: 一个 Token 可以在多个不同的微服务之间通用。
  • 缺点:
    • 不可撤销: 一旦 Token 发出去,在过期前都有效。如果用户想改密码或被封号,服务器很难强制让 Token 立刻失效(除非引入黑名单机制,但这又变回了方案三)。
    • 带宽占用: Token 通常比 Session ID 长,每次请求都要带,稍微增加一点流量。

如何保证负载均衡器的高可用性

如果我们只部署了一个负载均衡器(比如一台 Nginx),虽然它帮后端服务器分担了压力,但它自己就成了整个系统的单点故障(SPOF - Single Point of Failure)。一旦这台 Nginx 宕机,整个网站就彻底瘫痪了,后端有再多服务器也没用。

为了解决这个问题,业界通用的标准方案是:高可用架构(High Availability Architecture),核心思想是“冗余”“自动故障转移(Failover)”

最主流的实现方式有以下几种:

1. 主备模式 (Active-Passive) —— 最经典方案

这是最简单、最常用,也是中小企业首选的方案。通常使用 Keepalived 软件配合 VRRP 协议 来实现。

架构设计:

  • 准备两台 LB: 一台作为主节点(Master),一台作为备节点(Backup)
  • VIP(虚拟 IP): 两台机器对外只暴露一个虚拟 IP (Virtual IP)。正常情况下,这个 VIP 绑定在主节点上。用户只访问这个 VIP。
  • 心跳检测: 备节点会每隔几秒向主节点发送“心跳包”确认它还活着。

工作流程:

  1. 正常状态: 所有的流量都流向拥有 VIP 的主节点,备节点闲置待命。
  2. 故障发生: 主节点挂了(比如断电、进程崩溃),备节点收不到心跳包。
  3. IP 漂移 (IP Failover): 备节点立刻通过 VRRP 协议把 VIP“抢”过来,绑定到自己身上。
  4. 恢复: 对用户来说,只是网络卡顿了一瞬间,请求马上就能被备节点处理,服务未中断。
  • 优点: 简单稳定,配置方便。
  • 缺点: 资源有一半是浪费的(备节点平时不干活)。

2. 双主模式 (Active-Active) —— 高性能方案

如果你觉得有一台机器闲置太浪费,可以使用双主模式。

架构设计:

  • 准备两台 LB: 这里的两台都是 Master。
  • 两个 VIP: 配置两个不同的虚拟 IP(VIP_A 和 VIP_B)。
  • DNS 轮询: 在域名解析(DNS)层面,将域名同时解析到这两个 VIP 上。

工作流程:

  • 用户 A 解析到了 VIP_A,流量走了 LB 1。
  • 用户 B 解析到了 VIP_B,流量走了 LB 2。
  • 互为备份: LB 1 是 LB 2 的备用,LB 2 也是 LB 1 的备用。
  • 故障发生: 如果 LB 1 挂了,Keepalived 会把 VIP_A 漂移到 LB 2 上。此时 LB 2 身上同时挂着 VIP_A 和 VIP_B,独自承担所有流量。
  • 优点: 资源利用率最大化,两台机器都在工作。
  • 缺点: 架构稍复杂,需要设计好容量,确保一台机器能扛住两倍的流量(万一另一台挂了)。

3. 全局负载均衡 (GSLB) —— 异地多活

如果整个机房(数据中心)都停电了或者光缆被挖断了,上面的方法都得死。这时候需要更高层级的DNS 负载均衡

  • 原理: 在 DNS 服务器上配置策略。
  • 实现:
    • 北京的用户解析到北京机房的负载均衡器 IP。
    • 上海的用户解析到上海机房的负载均衡器 IP。
  • 高可用: 如果北京机房挂了,DNS 服务器检测到后,会自动把北京用户的流量引导到上海机房。

Nginx

Nginx入门必须懂3大功能配置 - Web服务器/反向代理/负载均衡_哔哩哔哩_bilibili

Caddy

全自动HTTPS加密,开箱即用,Caddy基础入门,反向代理,负载均衡,网站托管全流程_哔哩哔哩_bilibili

参考资料

什么是负载均衡?不就是加台服务器么?_哔哩哔哩_bilibili

什么是消息队列

用一个最简单的生活类比:去餐厅吃饭

  • 没有 MQ (同步通信):你(客户端)点完菜,必须站在厨房门口盯着厨师(服务端)把菜做好,端走后才能去干别的事。如果厨师动作慢,你就被“卡”住了。
  • 有 MQ (异步通信):你把点菜单交给服务员(MQ)。服务员把单子贴在后厨的墙上(队列)。你就可以回座位玩手机了。厨师做完一道菜,就从墙上撕下一个单子继续做。

技术上的定义: 消息队列是一个存放消息的容器

  1. 生产者 (Producer):发送消息的程序(比如:点餐系统)。
  2. 消费者 (Consumer):从队列中读取并处理消息的程序(比如:后厨系统)。
  3. Broker:消息队列的服务端本身,负责接收、存储和转发消息。

消息队列(Message Queue, MQ) 是一种进程间通信(IPC)*或*服务间通信的中间件机制。它通过提供异步通信协议,允许发送者(Producer)和接收者(Consumer)在不同的时间、不同的进程甚至不同的网络环境下进行数据交换。

为什么要用 MQ?

MQ 主要是为了解决三个问题:

1. 解耦 (Decoupling)

  • 场景:系统 A 下单后,需要通知系统 B(库存)、系统 C(积分)、系统 D(短信)。
  • 问题:如果不用 MQ,A 必须调用 B、C、D 的接口。如果 D 挂了,A 也会报错;如果后面加个系统 E,A 又要改代码。
  • MQ 方案:A 下单后,往 MQ 扔一条消息“有人下单了”,然后就不管了。B、C、D 自己去 MQ 里监听这条消息。哪怕 D 挂了,A 也不受影响。
image-20251217163458817

2. 异步 (Asynchronous)

  • 场景:用户注册,需要写数据库(50ms) + 发邮件(50ms) + 发短信(50ms)。总共耗时 150ms。
  • MQ 方案:写完数据库(50ms)后,往 MQ 发个消息(5ms)就直接告诉用户“注册成功”。邮件和短信服务自己在后台慢慢消费消息去发送。响应时间从 150ms 降到了 55ms。
image-20251217163433856

3. 削峰 (Peak Shaving / Load Leveling)

  • 场景:秒杀活动,平时每秒 10 个请求,秒杀时每秒 5000 个请求。数据库只能抗 2000 个,直接崩了。
  • MQ 方案:把 5000 个请求全部打入 MQ(MQ 的写入性能通常极高)。后台系统按照自己的能力(比如每秒处理 2000 个)慢慢从 MQ 里拉取处理。就像水库蓄水一样,保护下游系统不被冲垮。
image-20251217163517525

市面上主流的 MQ 选型

特性 RabbitMQ RocketMQ Kafka
主要特点 稳定、功能全 金融级可靠、高吞吐 极高吞吐、大数据
开发语言 Erlang Java Scala/Java
单机吞吐量 万级 十万级 百万级
消息延迟 微秒级 (极快) 毫秒级 毫秒级
适用场景 中小型公司,对实时性要求高,数据量没那么大。 阿里出品,适合复杂的业务系统(如电商交易),高可靠。 日志收集、大数据实时计算、用户行为追踪。
缺点 Erlang 语言难维护,吞吐量相对低。 社区主要在国内。 某些配置下可能丢数据,不适合极其严苛的金融交易。

Exchange(交换器)

在消息队列(特别是基于 AMQP 协议 的实现,如 RabbitMQ)中,交换器 (Exchange) 是核心组件之一。

如果说 Queue(队列)是存储消息的“仓库”,那么 Exchange(交换器)就是负责分拣和投递的“路由器”。

在专业的 AMQP 架构中,生产者 (Producer) 绝不会直接把消息发送到队列中,而是发送给交换器。交换器根据既定的路由规则 (Routing Key),将消息分发到一个或多个队列中。

核心机制:Binding 与 Routing Key

理解交换器,必须先理解两个概念:

  • Binding (绑定):这是连接 Exchange 和 Queue 的纽带。它告诉交换器:“如果你收到了消息,请把它按这条路径转给这个队列。”
  • Routing Key (路由键):生产者发送消息时带的一个“标签”。交换器会拿着这个标签,去和 Binding 规则做匹配。

数据流向:

Producer –> Message + RoutingKey –> Exchange –> (匹配逻辑) –> Queue –> Consumer

RabbitMQ 的工作模式

第一类:基础队列模式 (点对点)

这两种模式主要利用队列“存储转发”的特性,通常不需要显式配置复杂的 Exchange(交换机)。

1. 简单模式 (Simple / Hello World)

  • 架构P (生产者) -> Queue (队列) -> C (消费者)
  • 机制:最原始的模式。一个生产者对应一个消费者。
  • 场景:简单的“短信发送”任务。程序 A 产生内容,程序 B 发送,两者不需要同时在线。

2. 工作队列模式 (Work Queues)

  • 架构P -> Queue -> C1, C2
  • 机制竞争消费。一个队列对应多个消费者,但一条消息只能被一个消费者抢到
  • 核心逻辑
    • 轮询 (Round-robin):默认情况下,RabbitMQ 会依次把消息分给每个消费者(你一条,我一条)。
    • 公平分发 (Fair Dispatch):通过设置 prefetch=1,让“忙碌”的消费者不接新单,把消息给“空闲”的消费者(即:谁处理得快谁多干活)。
  • 场景集群削峰。比如大促期间的订单处理,启动 100 个订单处理服务(Worker)去消费同一个订单队列,加快处理速度。

第二类:高级发布订阅模式 (Publish/Subscribe)

这类模式引入了 Exchange (交换机) 的概念,实现了“一次发送,多处接收”。区别在于路由规则的不同。

3. 发布/订阅模式 (Publish/Subscribe - Fanout)

  • 架构P -> Exchange (Fanout) -> Queue A, Queue B -> C1, C2
  • 机制广播。生产者把消息发给交换机,交换机把它复制给所有绑定到它身上的队列。
  • 特点:速度最快,因为它完全忽略 Routing Key,闭着眼转发。
  • 场景数据同步日志广播。比如“修改密码”事件,既要发给“短信队列”通知用户,又要发给“审计队列”记录日志。

4. 路由模式 (Routing - Direct)

  • 架构P -> Exchange (Direct) -> Queue A (error), Queue B (info)
  • 机制精准匹配。发送消息时携带 Routing Key(比如 “error”),交换机只把消息投递给绑定了 “error” Key 的队列。
  • 场景日志分级存储
    • 消费者 A 只想接收 error 级别的日志写磁盘(绑定 key=“error”)。
    • 消费者 B 想接收所有级别的日志打印控制台(绑定 key=“info”, “warning”, “error”)
image-20251217182529481

5. 主题模式 (Topics - Topic)

  • 架构P -> Exchange (Topic) -> Queue
  • 机制通配符匹配。这是最灵活的模式。
    • #:匹配 0 个或多个单词。
    • *:匹配 1 个单词。
  • 例子
    • 发送 Key:usa.news
    • 队列 A 绑定:usa.# (接收美国的所有消息)
    • 队列 B 绑定:#.news (接收全世界的新闻)
  • 场景复杂业务路由。比如外卖系统,按区域(北京.海淀)、按品类(食品.奶茶)进行多维度的消息分发。
image-20251217182552430

Quorum 队列

1. 为什么要发明 Quorum 队列?(历史背景)

在 Quorum 队列出现之前,RabbitMQ 想要实现“一台机器挂了数据不丢”,用的是 镜像队列 (Mirrored Queues)

老镜像队列的致命痛点:

  1. 同步风暴:当一个新节点加入集群时,它需要从老节点把所有数据复制过来。这个过程会导致整个集群卡顿(Stop-the-world),甚至导致集群崩溃。
  2. 效率低下:它采用的是“链式复制”或者简单的广播,一条消息要在所有节点间转圈圈,性能随着节点数增加而剧烈下降。
  3. 即将被废弃:RabbitMQ 官方已经宣布,在未来的版本(4.0)中将彻底删除镜像队列。

所以,Quorum 队列就是为了“接班”而来的。


2. Quorum 队列的核心原理:Raft 算法

“Quorum”这个词的本意是“法定人数”(也就是多数派)。

它的核心逻辑不再是“所有人都必须收到消息”,而是“只要大多数人收到消息,这事儿就成了”。它基于著名的分布式一致性算法 Raft

工作机制图解:

假设你的集群有 3 个节点(Node A, Node B, Node C)。

  1. Leader 选举:三个节点通过投票,选出 Node A 作为 Leader,B 和 C 是 Follower
  2. 写消息
    • 生产者把消息发给 Leader (A)。
    • A 把消息写入自己的日志,并同时发给 B 和 C。
    • 关键点:只要 B 或者 C 其中有一个 回复“我收到了”(加上 A 自己,就是 2 票,满足 3 票中的多数派),A 就认为这条消息写入成功
    • A 返回 ACK 给生产者。
  3. 故障切换
    • 如果 Leader (A) 挂了。
    • B 和 C 发现老大不在了,迅速发起新一轮投票。
    • 因为 B 和 C 都是活着的(2 > 3/2),它们能立刻选出新的 Leader,继续工作。
image-20251217190353627

常见问题

如果重启rabbitmq,出现消息丢失问题如何解决

核心原因是默认情况下 RabbitMQ 是将数据存储在内存中的。一旦进程关闭或服务器重启,内存数据就会被清空。

要解决这个问题,必须配置 “持久化” (Persistence)

但这不仅仅是改一个配置那么简单。要保证消息绝对不丢,你需要同时满足 三个层面的持久化(缺一不可):

1. 交换器的持久化 (Exchange Durability)

如果你只持久化了队列和消息,但交换器没持久化。重启后,交换器没了,生产者发消息时找不到交换器,消息就会直接报错或丢弃。

  • 如何设置:在声明交换器时,将 durable 参数设为 True

  • 代码示例 (Python)

    1
    2
    3
    4
    5
    6
    import pika

    # durable=True 是关键
    channel.exchange_declare(exchange='my_exchange',
    exchange_type='direct',
    durable=True)

2. 队列的持久化 (Queue Durability)

如果队列不持久化,重启后队列元数据会消失,依附于该队列的消息(无论消息本身是否持久化)都会一起消失。

  • 如何设置:在声明队列时,将 durable 参数设为 True

  • 代码示例 (Python)

    1
    2
    # durable=True 告诉 RabbitMQ 重启后恢复该队列
    channel.queue_declare(queue='my_queue', durable=True)

3. 消息的持久化 (Message Persistence)

这是最容易被遗忘的一步。即便队列还在,如果消息本身是“瞬态”的,重启后队列是空的。

  • 如何设置:在发送消息(Publish)时,设置 delivery_mode = 2(1 是非持久化,2 是持久化)。

  • 代码示例 (Python)

    1
    2
    3
    4
    5
    6
    7
    8
    channel.basic_publish(
    exchange='my_exchange',
    routing_key='my_queue',
    body='Hello World',
    properties=pika.BasicProperties(
    delivery_mode=2, # 关键点:2 代表消息持久化
    )
    )

进阶:这样就 100% 安全了吗?

不是的。 即便你做到了以上三点,依然存在两个极端情况会导致丢失:

  • 漏洞 1:消息刚到内存,还没来得及刷盘 RabbitMQ 为了性能,不会每收到一条消息就立马写硬盘(fsync),而是先存缓存区。如果这时候断电了,缓存区里的几条消息就丢了。
    • 解决方案发布确认机制 (Publisher Confirms)。 生产者开启 Confirm 模式。只有当 RabbitMQ 明确告诉你“我已经把这条消息存入硬盘了”(Handle Ack),你才算发送成功。如果超时未收到 Ack,生产者需要重发。
  • 漏洞 2:磁盘坏了 / 物理机报废 如果单台机器硬盘物理损坏,持久化也没用。
    • 解决方案镜像队列 (Mirrored Queues)仲裁队列 (Quorum Queues)。 这是集群层面的高可用。将消息复制到 3 台不同的机器上。挂掉一台,另外两台还有数据。

如何解决同一个消息被消费多次的问题

这是一个非常经典且必须解决的分布式系统问题。在专业术语中,解决这个问题的方法叫做实现接口的“幂等性” (Idempotency)

简单来说,幂等性意味着:无论我对同一个消息处理多少次,最终的结果都和处理一次是一样的。

在 RabbitMQ(以及大多数 MQ)的设计中,为了保证消息不丢,默认采用的是 “至少投递一次” (At-Least-Once) 策略。

  • 场景还原:消费者把钱扣了,正准备告诉 MQ “我办完了(ACK)”,结果网线断了进程崩了
  • 后果:MQ 没收到 ACK,以为你没办完,于是把消息重新发给另一个消费者。结果:扣了两次钱

要解决这个问题,不能依赖 MQ,必须由消费者(Consumer)在业务逻辑层面来保证。以下是三种最主流的工程实现方案:

方案一:利用数据库的唯一约束 (最强硬方案)

这是最简单、最可靠的方法,适用于新增数据(Insert)的场景。

  • 原理:利用数据库(MySQL/Oracle)的主键(Primary Key)或唯一索引(Unique Key)约束。
  • 做法
    1. 每条消息必须携带一个全局唯一的 ID(比如 message_id 或者业务上的 order_id)。
    2. 消费者尝试向数据库插入数据。
    3. 如果插入成功 -> 处理结束,发送 ACK。
    4. 如果插入失败(报 DuplicateKeyException) -> 说明已经处理过了,直接忽略,发送 ACK

方案二:利用 SQL 的条件更新 (状态机方案)

适用于更新数据(Update)的场景,比如更新订单状态。

  • 原理:利用 SQL 的 WHERE 条件作为乐观锁,防止回退。

  • 错误做法

    1
    UPDATE orders SET status = 'PAID' WHERE id = 1001;

    风险:如果你执行两次,它就更新两次,虽然状态看起来一样,但如果有触发器或日志,就会重复。

  • 正确做法 (带前置条件)

    1
    2
    UPDATE orders SET status = 'PAID' 
    WHERE id = 1001 AND status = 'UNPAID'; -- 关键在这里
    • 第一次执行:找到 ID=1001 且状态是 UNPAID 的记录,更新成功,影响行数 = 1。
    • 第二次执行:虽然 ID=1001 还在,但这时的状态已经是 PAID 了,不满足 status = 'UNPAID',所以影响行数 = 0。业务逻辑判断影响行数为 0,即视为重复消费,直接 ACK。

方案三:Redis 去重表 (最高性能方案)

如果你的业务不涉及数据库,或者并发量极高,可以用 Redis 做“去重记录表”。

  • 做法

    1. 消息到达,先拿着 message_id 去 Redis 查一下:EXISTS message_id
    2. 如果有:说明处理过了,直接丢弃,ACK。
    3. 如果无:开始处理业务。
    4. 业务处理完,把 message_id 写入 Redis(通常设置一个过期时间,比如 24 小时)。

    注意:这里存在原子性问题(先查后写中间可能并发),通常使用 SETNX (Set if Not Exists) 命令或者 Lua 脚本来保证原子性。

如何处理消息乱序的问题

在 RabbitMQ 中,单个队列由单个消费者消费时,是严格保证先进先出(FIFO)的。

但是,为了提升性能,我们通常会开启多个消费者(Competing Consumers Pattern)同时消费同一个队列,或者发生消息重试(Nack/Requeue)。这时候,顺序就乱了。

场景举例: 生产者依次发了三条关于“订单 A”的消息:

  1. INSERT (创建订单)
  2. UPDATE (支付订单)
  3. DELETE (删除订单)

如果有两个消费者 C1 和 C2。 C1 拿到了 INSERT,C2 拿到了 UPDATE。 C2 的网速很快,先处理完 UPDATE。结果数据库报错“找不到订单”,操作失败。然后 C1 才把 INSERT 做完。 结果:数据不一致,业务崩盘。

解决这个问题的核心思路是:我们不需要“全局有序”,只需要“局部有序”(即:保证同一个 ID 的消息是有序的即可,不同 ID 之间的顺序无所谓)。

对于 Python 开发者以及大多数分布式系统来说,解决消息乱序最稳健、最通用的方案就是:拆分 Queue + 一致性 Hash (Queue Sharding)

核心方案:拆分 Queue + 一致性 Hash

这个方案的核心逻辑是:我们不需要“全局有序”,只需要保证“同一业务 ID 的消息有序”

只要保证同一个订单(例如 Order_1001)的所有操作(下单、支付、发货)都严格进入同一个队列,并且被同一个消费者处理,那么顺序就绝对不会乱。

1. 架构设计图解

我们要把原来的“一个大队列”拆分成 N 个“小队列”。

  • 原来的模型(会乱序)Producer -> Queue -> Consumer A, Consumer B (并发抢单,顺序错乱)
  • 现在的模型(保证有序)Producer -> Exchange -> Queue_1 -> Consumer A (只负责 Queue_1) Producer -> Exchange -> Queue_2 -> Consumer B (只负责 Queue_2) Producer -> Exchange -> Queue_3 -> Consumer C (只负责 Queue_3)

2. 具体实现步骤

这个方案分为三个关键环节:

第一步:生产者负责“路由分发” 在发送消息时,生产者必须根据业务 ID(如 order_id)决定这条消息发往哪个队列。通常使用 Hash 取模 算法。

  • 逻辑index = hash(order_id) % N (N 是队列的总数量)。
  • 例子:假设有 3 个队列。
    • Order_1001 的 Hash 模 3 结果是 0 -> 发往 Queue_0
    • Order_1002 的 Hash 模 3 结果是 1 -> 发往 Queue_1
    • Order_1001后续状态(如支付)Hash 结果肯定还是 0 -> 依然发往 Queue_0

第二步:RabbitMQ 队列配置 你需要创建 N 个队列(如 order_sub_queue_0, order_sub_queue_1…)。

  • 进阶技巧:RabbitMQ 有一个官方插件叫 rabbitmq_consistent_hash_exchange。你只需要把消息发给这个交换机,带上 routing_key(设为 order_id),交换机会自动帮你根据 Hash 值均匀分发到绑定的队列中,连生产者的代码都不用改太复杂。

第三步:消费者“独占”队列 (关键) 这是最重要的一点:每个小队列,同一时刻只能有一个消费者在监听。

  • Consumer A 专门监听 Queue_0
  • Consumer B 专门监听 Queue_1

因为 RabbitMQ 的单个队列是先进先出 (FIFO) 的,而 Consumer A 是单线程顺序处理 Queue_0 的,所以 Order_1001 的“下单”一定比“支付”先被处理。

如何处理消息处理失败的情况

在分布式系统中,消息处理失败是常态(比如数据库挂了、网络抖动、代码 bug)。

如果处理失败,绝不能简单地忽略,否则会导致数据丢失;也不能死板地无限重试,否则会死循环拖垮系统。

处理失败通常有三道防线,层层递进:

第一步:判断异常类型(是“病”还是“命”?)

try...except 捕获到异常时,不能盲目重试,先看是什么错:

  1. 致命错误(Fatal Error)
    • 例如:JsonDecodeError(格式不对)、KeyError(缺字段)、NullPointerException(空指针)。
    • 决策:这种错误重试一万次也没用。跳过重试,直接进死信队列
  2. 临时错误(Transient Error)
    • 例如:Timeout(连接超时)、Deadlock(数据库死锁)、503 Service Unavailable
    • 决策:这种病能治。进入重试流程

第二步:带策略的重试(Retry)—— 关键缓冲

既然决定要救,也不能瞎救(比如立即原地无限重试,那是“毒药”)。我们需要“有节制、有延迟”的重试。

  • 检查重试次数: 从消息 Header 中读取 retry_count
  • 逻辑
    • 如果 count < 3(假设最大重试3次):
      1. count + 1
      2. 等待一会儿(Backoff):不要立即重试,而是把消息发到一个 “延迟队列”(或者用代码 sleep 一会儿,但 Python 中不建议阻塞主线程,推荐用延迟插件 rabbitmq_delayed_message_exchange)。
      3. 重新发布这条消息(Publish)。
      4. 对当前失败的这条消息进行 ACK(因为它已经生成了新的替身去排队了)。
    • 如果 count >= 3
      • 说明救不活了,放弃治疗。
      • 进入第三步

第三步:死信队列(DLQ)—— 最终兜底

这是最后一道防线。当重试次数耗尽,或者遇到致命错误时,才轮到它出场。

  • 操作:调用 basic_nack(delivery_tag, requeue=False)
  • 结果
    • RabbitMQ 会根据配置,自动把这条消息“踢”到死信交换机。
    • 死信交换机把它路由到 死信队列
  • 后续
    • 开发/运维人员配置报警脚本,监听死信队列。
    • 一旦有消息进来,发钉钉/邮件报警
    • 人工排查原因(比如发现是数据库挂了),修复后,手动把死信队列里的消息取出来再发回业务队列(或者写脚本批量重发)。

Kafka

消息队列Kafka是什么?架构是怎么样的?5分钟快速入门_哔哩哔哩_bilibili

1. 核心思维转变:从“队列”到“日志”

这是理解 Kafka 最重要的一步。

  • RabbitMQ (队列模型):就像“收件箱”。你把信拿出来,信就没了(Delete)。它的目标是让消息越快被处理完越好,堆积消息是异常状态。
  • Kafka (日志模型):就像“船长的航海日志”
    • 消息是追加写入 (Append-only) 的。
    • 消费者读消息,不会删除消息,只是在自己的笔记本上记一下:“我读到了第 100 行”。
    • 这意味着:消息可以被多个不同的消费者重复读取,甚至可以“倒带”回去重读历史数据。

2. 为什么 Kafka 快得离谱?(架构设计)

Kafka 单机可以轻松抗住 每秒几十万甚至上百万 的写入,它是怎么做到的?

A. 顺序写磁盘 (Sequential Write)

RabbitMQ 尽量用内存,而 Kafka 直接写磁盘。 你可能会问:“写磁盘不是慢吗?” 随机写确实慢,但顺序写极快。Kafka 强制所有数据只能追加到文件末尾。在现代操作系统中,顺序写磁盘的速度(600MB/s+)甚至可以超过随机写内存的速度。

B. 零拷贝 (Zero-Copy)

还记得你之前感兴趣的底层原理吗?Kafka 是利用 OS sendfile 系统调用的教科书级案例。

  • 传统方式:磁盘 -> 内核 Buffer -> 用户态 Buffer (Application) -> 内核 Socket Buffer -> 网卡。
  • Kafka 方式:磁盘 -> 内核 Buffer -> 直接传给网卡
    • 数据完全不经过应用程序(Kafka JVM),CPU 也就不用瞎忙活。

C. 分区 (Partitioning) —— 扩展性的核心

Kafka 将一个 Topic (主题) 拆分成了多个 Partition (分区)

  • 每个 Partition 是一个独立的物理日志文件。
  • 不同的 Partition 可以分布在不同的服务器上。
  • 结果:并发读写能力随着机器数量线性扩展。

3. Kafka 的核心组件

1. Broker

Kafka 的服务器节点。

2. Topic & Partition

  • Topic 是逻辑分类(比如 logs)。
  • Partition 是物理存储。Topic A 可以分为 Partition 0, 1, 2。
  • 注意:Kafka 只保证 Partition 内部的消息有序,不保证整个 Topic 全局有序。

3. Producer (生产者)

生产者决定把消息发给哪个 Partition(通常轮询或 Hash)。

4. Consumer Group (消费者组) —— Kafka 的神来之笔

这是 Kafka 区别于 RabbitMQ 的最大特色。

  • 机制:一个 Topic 可以被多个 Group 消费。
  • 组内 (Queue 模式):同一个 Group 里的消费者,互相竞争。Partition 0 给消费者 A,Partition 1 给消费者 B。一个 Partition 只能被组内的一个消费者消费(防止乱序)。
  • 组间 (Pub/Sub 模式):Group A 消费了一遍数据,Group B 可以再消费一遍同样的数据,互不干扰。

5. Offset (偏移量)

消费者读到哪了?RabbitMQ 是 Server 记,Kafka 是 消费者自己记(或者提交给 Kafka 的内部 Topic __consumer_offsets)。

  • 你可以随时修改 Offset,让消费者从昨天的数据开始重新跑一遍(用于修复 Bug 后重算数据)。
image-20251217193104605

RocketMQ

消息队列RocketMQ是什么?和Kafka有什么区别?架构是怎么样的?7分钟快速入门_哔哩哔哩_bilibili

参考资料

什么是消息队列?不就是排个队么?_哔哩哔哩_bilibili

RabbitMQ是什么?架构是怎么样的?_哔哩哔哩_bilibili

Claudeskills

Claudeskills是一组指令、脚本和资源的文件夹,Claude 会动态加载它们以提升在特定任务上的性能。技能教会 Claude 如何以可重复的方式完成特定任务,无论是创建符合公司品牌指南的文档、使用组织特定的流程分析数据,还是自动化个人任务。

skills的结构

image-20251217085141312

最简单来说,一个技能是一个包含 SKILL.md 文件的目录。这个文件必须以 YAML 前文开始,其中包含一些必需的元数据:namedescription。启动时,智能体会将所有已安装技能的 namedescription 预加载到系统提示中。

这是第一级渐进式披露元数据:它仅提供足够的信息,让 Claude 知道何时应使用每个技能,而无需将所有内容加载到上下文中。该文件的实际主体是第二级的详细程度。如果 Claude 认为该技能与当前任务相关,它将通过读取完整的 SKILL.md 将其加载到上下文中。

image-20251217090930032

随着技能复杂性的增加,它们可能包含过多上下文而无法放入单个 SKILL.md 中,或者只有特定场景下才相关的上下文。在这些情况下,技能可以在技能目录中捆绑额外的文件,并通过 SKILL.md 中的名称引用它们。这些额外的链接文件是第三级 (以及更高级别)的详细程度,Claude 可以根据需要选择导航和发现。

在下面的 PDF 技能中,SKILL.md 指向了两个额外的文件(reference.mdforms.md),这些文件由技能作者选择与核心的 SKILL.md 一起打包。通过将填写表单的说明移至单独的文件(forms.md),技能作者能够保持技能的核心部分简洁,并相信 Claude 只有在填写表单时才会读取 forms.md

image-20251217090952141
image-20251217091013052

三种 Skill 内容类型,三个加载级别

Skills 可以包含三种类型的内容,每种在不同时间加载:

第 1 级:元数据(始终加载)

内容类型:指令。Skill 的 YAML 前置数据提供发现信息:

1
2
3
4
---
name: pdf-processing
description: 从 PDF 文件中提取文本和表格、填充表单、合并文档。在处理 PDF 文件或用户提及 PDF、表单或文档提取时使用。
---

Claude 在启动时加载此元数据并将其包含在系统提示中。这种轻量级方法意味着您可以安装许多 Skills 而不会产生上下文成本;Claude 只知道每个 Skill 的存在以及何时使用它。

第 2 级:指令(触发时加载)

内容类型:指令。SKILL.md 的主体包含程序知识:工作流、最佳实践和指导:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# PDF 处理

## 快速入门

使用 pdfplumber 从 PDF 中提取文本:

```python
import pdfplumber

with pdfplumber.open("document.pdf") as pdf:
text = pdf.pages[0].extract_text()
```

有关高级表单填充,请参阅 [FORMS.md](FORMS.md)。

当您请求与 Skill 描述匹配的内容时,Claude 通过 bash 从文件系统读取 SKILL.md。只有这样,此内容才会进入上下文窗口。

第 3 级:资源和代码(按需加载)

内容类型:指令、代码和资源。Skills 可以捆绑其他材料:

1
2
3
4
5
6
pdf-skill/
├── SKILL.md (主要指令)
├── FORMS.md (表单填充指南)
├── REFERENCE.md (详细 API 参考)
└── scripts/
└── fill_form.py (实用脚本)

指令:包含专业指导和工作流的其他 markdown 文件(FORMS.md、REFERENCE.md)

代码:Claude 通过 bash 运行的可执行脚本(fill_form.py、validate.py);脚本提供确定性操作而不消耗上下文

资源:参考资料,如数据库架构、API 文档、模板或示例

Claude 仅在引用时访问这些文件。文件系统模型意味着每种内容类型都有不同的优势:指令用于灵活指导,代码用于可靠性,资源用于事实查询。

级别 加载时间 令牌成本 内容
第 1 级:元数据 始终(启动时) 每个 Skill 约 100 个令牌 YAML 前置数据中的 namedescription
第 2 级:指令 触发 Skill 时 不到 5k 个令牌 包含指令和指导的 SKILL.md 主体
第 3 级+:资源 按需 实际上无限制 通过 bash 执行的捆绑文件,不将内容加载到上下文中

渐进式披露确保任何给定时间只有相关内容占据上下文窗口。

claudecode使用skills

Agent Skills - Claude Code Docs

个人 Skills

个人 Skills 在您的所有项目中都可用。将它们存储在 ~/.claude/skills/ 中:

1
mkdir -p ~/.claude/skills/my-skill-name

使用个人 Skills 的场景

  • 您的个人工作流和偏好
  • 您正在开发的实验性 Skills
  • 个人生产力工具

项目 Skills

项目 Skills 与您的团队共享。将它们存储在项目中的 .claude/skills/ 中:

1
mkdir -p .claude/skills/my-skill-name

使用项目 Skills 的场景

  • 团队工作流和约定
  • 项目特定的专业知识
  • 共享的实用程序和脚本

项目 Skills 被检入 git 并自动对团队成员可用。

skills示例代码仓库

anthropics/skills: 技能公共存储库 — anthropics/skills: Public repository for Skills

./skills: 创意与设计、开发与技术、企业与沟通以及文档技能的示例

./spec: Agent Skills 规范

./template: 技能模板

仓库包含以下主要skill类别:

🎨 创意与设计类 (Creative & Design)

  • algorithmic-art - 使用 p5.js 创建生成艺术,支持种子随机性、流场和粒子系统
  • canvas-design - 使用设计哲学创建美观的视觉艺术,输出 .png 和 .pdf 格式
  • slack-gif-creator - 创建针对 Slack 大小限制优化的动画 GIF

💻 开发与技术类 (Development & Technical)

  • artifacts-builder - 使用 React、Tailwind CSS 和 shadcn/ui 组件构建复杂的 claude.ai HTML artifacts
  • mcp-builder - 创建高质量 MCP 服务器的指南,用于集成外部 API 和服务
  • webapp-testing - 使用 Playwright 测试本地 Web 应用程序,进行 UI 验证和调试

🏢 企业与沟通类 (Enterprise & Communication)

  • brand-guidelines - 将 Anthropic 的官方品牌颜色和排版应用到 artifacts
  • internal-comms - 编写内部沟通文档,如状态报告、新闻通讯和常见问题解答
  • theme-factory - 使用 10 个预设专业主题为 artifacts 设置样式,或即时生成自定义主题

🛠️ 元技能类 (Meta Skills)

  • skill-creator - 创建有效扩展 Claude 能力的技能指南
  • template-skill - 用作新技能起点的基础模板

📄 文档技能 (Document Skills)

document-skills/ 子目录包含 Anthropic 开发的用于帮助 Claude 创建各种文档文件格式的技能: README.md:45-47

  • docx - 创建、编辑和分析 Word 文档,支持跟踪更改、注释、格式保留和文本提取 README.md:49
  • pdf - 综合 PDF 操作工具包,用于提取文本和表格、创建新 PDF、合并/拆分文档以及处理表单 README.md:50
  • pptx - 创建、编辑和分析 PowerPoint 演示文稿,支持布局、模板、图表和自动幻灯片生成 README.md:51
  • xlsx - 创建、编辑和分析 Excel 电子表格,支持公式、格式化、数据分析和可视化 README.md:52

参考资料

Claude Agent Skills - 全新的技能包_哔哩哔哩_bilibili

【手把手教程】开发自己的Claude Agent Skills_哔哩哔哩_bilibili

Agent Skills - Claude Docs

用 Agent Skills 为代理赋能  Anthropic — Equipping agents for the real world with Agent Skills  Anthropic

三种指令类型 (R, I, J)

MIPS 指令集的设计非常整齐,所有的指令都是 32位(bits) 长。但是,这 32 个格子怎么分配,取决于指令的类型。

(1) R-type (Register type, 寄存器型)

  • 用途:纯粹的数学运算。
  • 特点:所有的操作数都在寄存器里。
  • 例子add $t0, $t1, $t2 (把 t1 和 t2 里的数拿出来相加,结果存到 t0)。
  • 格局:因为它需要 3 个寄存器(2个源,1个目标),所以它的空间被切得很细,最后剩下了 6 位给 func

(2) I-type (Immediate type, 立即数型)

  • 用途:涉及常数运算,或者读写内存。
  • 特点:操作数里包含一个具体的数字(立即数)。
  • 例子
    • ori $t0, $t1, 100 (把 t1 和数字 100 做或运算)。
    • lw $t0, 4($t1) (去内存取数,地址是 t1 + 4)。
  • 格局:为了放那个数字(比如 100),它牺牲了 rdfunc 的位置,腾出了 16位 的空间来存这个数。

(3) J-type (Jump type, 跳转型)

  • 用途:程序跳转(就像代码里的 goto)。
  • 特点:跳得很远。
  • 例子j 1000 (直接跳到地址 1000 去执行)。
  • 格局:除了开头的 op,剩下的 26位 全用来存目标地址,保证能跳得足够远。
指令类型 全称 (中文名) 包含的常见指令 (加粗为你课件中的) 核心特征 指令格式结构 (32位)
R-Type Register Type (寄存器型) add (加法) sub (减法) and, or, slt, nor 纯运算。 操作数全在寄存器里,不做数据传输,不涉及立即数。 op(6) rs(5) rt(5) rd(5) shamt(5) func(6) (特点:op全为0,靠 func 区分)
I-Type Immediate Type (立即数型) ori (或立即数) lw (加载字) sw (存储字) beq (相等分支) addi, andi, bne 带常数/地址偏移。 计算或访存时,需要用到一个具体的数字(16位)。 op(6) rs(5) rt(5) immediate(16) (特点:最后16位是常数/地址)
J-Type Jump Type (跳转型) jump (无条件跳转) jal (跳转并链接) 长距离跑路。 不需要计算,直接跳到程序代码的另一个位置。 op(6) address(26) (特点:后面26位全是目标地址)

opfunc 是什么?

这两个是指令的身份证

  • op (Opcode, 操作码):指令的高 6 位 (31-26 bit)。
    • 作用:它决定了这到底是一条什么指令(大类)。
    • 比喻:就像身份证上的“省份”。控制器一看 op,就知道你是 lw 还是 sw 还是 beq
    • 特殊情况:对于所有的 R-type 指令,它们的 op 都是 000000。这就尴尬了,控制器光看 op 分不清你是加法还是减法。
  • func (Function code, 功能码):指令的低 6 位 (5-0 bit)。
    • 作用:专门用来区分 R-type 指令的具体操作。
    • 比喻:就像身份证上的“名字”。
    • 逻辑:当 op000000 时,控制器就会去查 func
      • func = 100000 -> 做加法 (add)
      • func = 100010 -> 做减法 (sub)
    • 注意:只有 R-type 有 func。I-type 和 J-type 的这最后 6 位已经被那个常数或者地址占用了,所以它们没有 func 字段。
image-20251212164727970

为什么对于所有的 R-type 指令,它们的 op 都是 000000

设计者回头看了一眼 R-type 指令的格式。他们发现了一个惊喜:

R-type 指令只需要存 3 个寄存器编号(rs, rt, rd)和 1 个移位量(shamt)。

  • op: 6位
  • rs: 5位
  • rt: 5位
  • rd: 5位
  • shamt: 5位
  • 总共使用6 + 5 + 5 + 5 + 5 = 26 位。

32 - 26 = 6 位!

R-type 指令的末尾,正好空出了 6位 没地方用。

天才的设计思路来了:

既然 R-type 指令末尾空着 6 位,那为什么不把 Opcode 统一设为 000000(意思是:这是一条运算指令),然后利用末尾这空闲的 6 位(也就是 func 字段)来具体区分是加法还是减法呢?

  • 这样一来,add, sub, and, or, slt 等几十条运算指令,在 Opcode 表里只占用 1 个位置(就是 000000)。
  • 这就把剩下的 63 个 Opcode 宝贵名额,留给了那些没有空闲位置的 I-type 和 J-type 指令(因为它们后面被立即数填满了,没有 func 字段可用)。

控制信号的含义都有哪些

这些信号就是控制器发出的“命令”,控制数据通路里的多路选择器 (Mux)使能开关 (Enable)

我们把它们分为三类来记:

第一类:选路信号(Mux Selectors)

这些信号决定数据走哪条路。你可以理解为火车轨道的道岔。如果是 0 走左边,1 走右边。

  1. RegDst (Register Destination)
    • 含义运算结果存到哪个寄存器编号里?
    • 背景
      • R-type 的目标寄存器编号在指令的第 15-11 位 (rd 字段)。
      • I-type (如 lw) 的目标寄存器编号在指令的第 20-16 位 (rt 字段)。
      • 1: 选 rd (R-type)。
      • 0: 选 rt (I-type)。
  2. ALUSrc (ALU Source)
    • 含义ALU 的第二个操作数来自哪里?
      • 0: 来自寄存器堆的读出数据(两个寄存器做运算,如 R-type)。
      • 1: 来自指令里的立即数(寄存器和数字做运算,如 ori, lw, sw)。
  3. MemtoReg (Memory to Register)
    • 含义写回寄存器的数据来源是谁?
      • 1: 来自内存的数据(只有 lw 指令是这样,把数据从仓库搬回寄存器)。
      • 0: 来自 ALU 的计算结果(绝大多数指令,如 add, ori)。

第二类:开关信号(Enables)

这些信号是安全锁。只有设为 1,才允许动作。

  1. RegWr (Register Write)
    • 含义允不允许修改寄存器的值?
      • 1: 允许写。像 add, lw, ori 这种需要保存结果的指令。
      • 0: 禁止写。像 sw (只是往内存存数),beq (只是比较),j (只是跳转),绝对不能改写寄存器里的数据。
  2. MemWr (Memory Write)
    • 含义允不允许修改内存的值?
      • 1: 允许写。只有 sw (Store Word) 指令是 1
      • 0: 禁止写。其他所有指令。

第三类:功能控制信号

  1. ExtOp (Extension Operation)
    • 含义16位的立即数怎么变成32位?
    • 背景:指令里的数字只有16位,但计算器是32位的。
      • 1 (Signed): 符号扩展。保持正负号不变(比如 -2 扩展后还是 -2)。用于 lw, sw, beq(算地址偏移量)。
      • 0 (Unsigned): 零扩展。高位直接补0。用于逻辑运算如 ori
  2. Branch
    • 含义这是一个条件分支指令吗?
    • :如果是 beq 指令,这个信号是 1。它会结合 ALU 的“零标志位”来决定要不要跳。
  3. Jump
    • 含义这是一个无条件跳转指令吗?
    • :如果是 j 指令,这个信号是 1。直接强行修改 PC 指针。
  4. ALUctr (ALU Control)
    • 含义ALU 到底做什么数学题?
    • :这是个多位信号(通常3位或4位)。
      • Add: 加法 (用于 add, lw, sw)
      • Sub: 减法 (用于 sub, beq 比较是否相等)
      • Or: 逻辑或 (用于 ori)

什么是控制器 (Controller)?

在 CPU 这个“大工厂”里,主要分两部分:

  1. 数据通路 (Datapath):这是干活的工人机器。包括寄存器(用来记账的小本子)、ALU(算盘/计算器)、内存(大仓库)。它们负责搬运数据、做加减法。
  2. 控制器 (Controller):这是工厂的指挥官

控制器的作用: 它不直接干活(不存数、不算数),它的任务是看懂指令,然后对着数据通路里的各种开关“发号施令”。

  • 输入:它看的是指令(Instruction)。比如指令说“把 A 和 B 加起来”,这串 0101 的机器码传给控制器。
  • 输出:控制器根据指令,把相应的控制信号线拉高(置1)或拉低(置0)。

一句话总结:控制器就是 CPU 的大脑,它通过查表(真值表)来告诉身体的各个部位(ALU、寄存器、内存)在当前这一刻该干什么。

什么是数据通路 (Datapath)?

它的任务:根据指挥官(控制器)的命令,把数据从仓库(寄存器/内存)里搬出来,送到加工车间(ALU)算一下,再搬回去。

核心组成

  1. 仓库:寄存器堆 (Registers)、数据存储器 (Data Memory)。
  2. 车间:算术逻辑单元 (ALU)、加法器 (Adder)。
  3. 交通枢纽:多路选择器 (Mux)、扩展器 (Ext)。

什么是单周期处理器 (Single-Cycle Processor)?

核心定义:CPI (Cycles Per Instruction) = 1。

通俗解释

  • 时钟信号“哒”(上升沿)一下,CPU 开始取指令。
  • 在时钟“哒”下一响之前,CPU 必须把这条指令的所有工作(取指、翻译、运算、读写内存、写回结果)全部做完。

单周期处理器包含哪些功能?

为了让程序能跑起来,处理器通常需要支持三大类功能(指令):

A. 算术逻辑运算 (R-Type)

  • 功能:做数学题。
  • 指令例子add (加), sub (减), and (与), or (或), slt (比较大小)。
  • 流程:读寄存器 -> ALU 算 -> 写回寄存器。

B. 数据传输/访存 (I-Type)

  • 功能:搬运数据。CPU 的寄存器太小,数据多了要放内存里。
  • 指令例子
    • lw (Load Word):从内存搬到寄存器。
    • sw (Store Word):从寄存器搬到内存。
  • 流程:算地址 -> 读/写内存 -> (如果是读)写回寄存器。

C. 条件分支与跳转 (Branch & Jump)

  • 功能:改变程序执行顺序(也就是改变 PC 的值)。
  • 指令例子
    • beq (Branch if Equal):如果两个数相等,就跳到某处去。
    • j (Jump):无条件直接飞到某处。
  • 流程:比较 -> 计算新地址 -> 修改 PC。

单周期处理器包含哪些组成部分?

我们可以把它分为“肉体”(数据通路)和“灵魂”(控制单元)两大部分。

第一部分:数据通路 (Datapath) —— “干活的”

这部分你之前的实验已经做了一大半了,现在需要补全:

  1. 取指单元 (Instruction Fetch) —— 这是新面孔
    • PC (Program Counter):一个 32 位的寄存器,存着“现在执行到哪一行代码了”。
    • 指令存储器 (Instruction Memory):存放你写的机器码的大仓库。
    • 加法器 (+4):让 PC 自动加 4,指向下一条指令。
  2. 寄存器堆 (Register File)
    • CPU 的“口袋”,这里有 32 个格子 ($0 - $31),用来存放临时数据。
  3. 算术逻辑单元 (ALU)
    • CPU 的“计算器”,负责算加减法、逻辑运算。
  4. 数据存储器 (Data Memory)
    • CPU 的“大仓库”,用来做 lwsw
  5. 扩展单元 (Extender)
    • 把 16 位的立即数变成 32 位。
  6. 多路选择器 (Mux) —— 交通警察
    • 关键组件!比如 ALUSrc 这个 Mux,决定了 ALU 的输入是来自寄存器还是立即数。

第二部分:控制单元 (Control Unit) —— “发号施令的”

在你上一个实验里,“你”就是控制器(手动在波形里设 0 或 1)。现在要写一个模块来替代你。

  1. 主控制器 (Main Control)
    • 输入:指令的高 6 位 (Opcode)。
    • 输出:所有的控制信号 (RegDst, ALUSrc, MemWr 等)。
    • 作用:看到 lw 指令,它就自动把 MemtoReg 拉高,把 RegWr 拉高。
  2. ALU 控制器 (ALU Control)
    • 输入:主控制器的信号 + 指令的低 6 位 (Funct)。
    • 输出:给 ALU 的 ALUctr (3位或4位)。
    • 作用:专门告诉 ALU 该做加法还是减法。

浏览器的跨域预检

INFO: 127.0.0.1:54365 - “OPTIONS /usersrole=admin HTTP/1.1” 200 OK INFO: 127.0.0.1:54365 - “GET /usersrole=admin HTTP/1.1” 200 OK 为什么每次调用这个接口会有两个

• 这是浏览器的跨域预检 + 实际请求各一次:

  • 前端带 Authorization 头、跨域调用 /users?role=admin 时,浏览器会先发 OPTIONS 预检,看服务器是否允许该方法 头。你启用了 CORS,所以预检返回 200。
  • 预检通过后才发真正的 GET /users?role=admin,所以日志里看到两条。

这是正常行为;非浏览器(如 curl/postman)或同源、无自定义头的请求则不会有预检。

如何解决跨源的问题

我们推导出了两套针对不同环境的最佳实践方案,核心逻辑都是“利用中间人(代理)实现同源”

💻 开发环境 (Development)

  • 工具: Vite (server.proxy)
  • 原理: 利用 Vite 启动的 Node.js 服务转发请求。
  • 效果: 浏览器只跟 Vite 打交道(同源),Vite 跟后端打交道(服务器间无 CORS 限制)。完美消除预检和跨域报错。

🚀 生产环境 (Production)

  • 工具: Nginx (反向代理)
  • 原理: 浏览器所有请求(页面 + 接口)统一发给 Nginx(比如 80 端口)。
    • / -> Nginx 返回静态 HTML/JS 文件。
    • /api -> Nginx 转发给后端服务(8000 端口)。
  • 效果: 在浏览器看来,它始终只访问了一个域名(同源),因此不需要 CORS 配置,也没有预检请求,性能最高

Authorization 头是什么

Authorization 头(Header)是 HTTP 协议中用来验证用户身份的一个标准字段。

1. 标准格式

Authorization 头的值并不是随便填写的,它遵循一个严格的语法结构:

1
Authorization: <认证类型> <凭证数据>
  • 认证类型 (Schema): 说明后面跟的是哪种类型的凭证(比如是“密码”还是“令牌”)。常见的有 BearerBasic
  • 空格: 中间必须有一个空格分隔。
  • 凭证数据 (Credentials): 具体的加密字符串或 Token。

2. 最常用的两种类型

A. Bearer (最常见,用于 JWT)

这是目前现代 Web 应用和 API 最主流的方式,通常配合 JWT (JSON Web Token) 使用。

  • 含义: “Bearer” 的意思是“持有者”。意思是:“谁持有这个令牌,谁就有权限。”

  • 场景: 用户登录后,服务器发给用户一个 JWT。用户下次请求时,把这个 JWT 放在这里。

  • HTTP 请求示例:

    HTTP

    1
    2
    3
    GET /api/user/profile HTTP/1.1
    Host: example.com
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

    注意: 这里的乱码字符串 eyJ... 就是上一条回答中提到的 JWT。

B. Basic (最基础,用于用户名密码)

这是 HTTP 协议内置的最古老的认证方式。

  • 含义: 直接把“用户名:密码”拼接,然后进行 Base64 编码传给服务器。

  • 场景: 内部系统、简单的测试环境、或者某些传统的网关认证。

  • 原理:

    1. 用户名 admin,密码 123456
    2. 拼接:admin:123456
    3. Base64 编码:YWRtaW46MTIzNDU2
  • HTTP 请求示例:

    1
    2
    POST /api/login HTTP/1.1
    Authorization: Basic YWRtaW46MTIzNDU2

常用mcp

supabase

模型上下文协议 (MCP) | Supabase 文档 — Model context protocol (MCP) | Supabase Docs

codex mcp login supabase

context7

context7/i18n/README.zh-CN.md at master · upstash/context7

claude mcp add –transport http context7 https://mcp.context7.com/mcp –header “CONTEXT7_API_KEY: ctx7sk-7f2ea2a0-d309-4ba7-97d0-af20a783540f”

[mcp_servers.context7] url = “https://mcp.context7.com/mcp” http_headers = { “CONTEXT7_API_KEY” = “ctx7sk-7f2ea2a0-d309-4ba7-97d0-af20a783540f” }

vercel

Use Vercel’s MCP server

1
2
3
4
5
6
7
8
9
10
11
[mcp_servers.vecel]
command = "cmd"
args = [
"/c",
"npx",
"-y",
"mcp-remote@latest",
"https://mcp.vercel.com"
]
env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" }
startup_timeout_ms = 60_000

Chrome DevTools

ChromeDevTools/chrome-devtools-mcp: Chrome DevTools for coding agents

langchain docs

https://docs.langchain.com/mcp

1
2
3
4
"Docs by LangChain": {
"url": "https://docs.langchain.com/mcp",
"type": "http"
}
1
2
[mcp_servers.docs_by_langchain]
url = "https://docs.langchain.com/mcp"

vscode mcp配置文档

Use MCP servers in VS Code

codex mcp配置文档

模型上下文协议 — Model Context Protocol

其他

figma

stripe

shadcn

MCP Server - shadcn/ui

semgrep

semgrep/cli/src/semgrep/mcp at develop · semgrep/semgrep

mcp市场

Awesome MCP Servers

参考

用过上百款编程MCP,只有这15个真正好用,Claude Code与Codex配置MCP详细教程_哔哩哔哩_bilibili

0%