更新 @2026-09-30:本系列对着 transformers 5.17.0(2026-09-09 发布)、tokenizers 0.23.2、datasets 5.0.1、peft 0.21.0、trl 1.13.0 的源码读,模型用本地缓存的 Qwen2.5-0.5B。文中的文件路径、类名、函数名以这组版本为准,不引用行号;读别的版本请以本地源码对照——找入口的方法不变。

内容简介

《读 Hugging Face 源码》是一组共四篇的系列文章,是《AI 算法工程师学习地图》 L4–L5 两层的深入篇。工具箱第五篇用六行代码组装了一次 LoRA SFT,并在末尾列了六个读源码的入口。本系列沿这六个入口,把源码读一遍。

它回答的问题是:

AutoModelForCausalLM.from_pretrained、model.generate、tok.apply_chat_template、load_dataset(...).map、get_peft_model、SFTTrainer / DPOTrainer / GRPOTrainer——工具箱第五篇那六行代码的每一行,执行时经过了哪些文件的哪些函数?L0 的公式与 L4 / L5 的结构图,各对应源码里的哪几行?[^q0]

Hugging Face 的五个库加起来几十万行,本系列不通读,只沿一次 LoRA SFT 会走到的调用链读:模型怎么加载、一次前向怎么走到 loss、generate 的循环长什么样、文本怎么变成 input_ids、数据怎么从 Arrow 文件到一个 batch、LoRA 怎么挂到 nn.Linear 上、SFT 的 -100 在哪一步填、DPO 与 GRPO 的公式各是哪十几行。每一篇的写法相同:先用一张调用链图给出骨架,再逐段贴源码——只贴那一段真正在做事的几行,其余用文件名 + 函数名指路——最后把它与本地图前面各层的公式或结构图对上。

举一个例子说明这个系列的取法。「LoRA 是给权重加一个低秩增量 \(BA\)」是 《算法工程师的数学(03):正交与旋转、特征值与 SVD——从 RoPE 到 LoRA》的数学;到了源码里它是三件可查验的事:

  1. inject_adapter 用 named_modules 逐个匹配 target_modules,命中的 nn.Linear 被 setattr 换成 lora.Linear,all-linear 会排除 lm_head;
  2. update_layer 建 lora_A(kaiming)与 lora_B(全零),scaling = α/r——所以训练开始时增量恰好为零;
  3. forward 只多一行 result + lora_B(lora_A(dropout(x))) * scaling,先算瘦的那一半,永远不物化 \(BA\)。

Qwen2.5-0.5B 上 r=16 挂七类线性层,可训练参数 8.8M(1.8%),state_dict 里只有 adapter。

系列覆盖的范围是那六行代码的四段,每段一篇:

系列四段与各篇覆盖的源码
段 主题 读的源码 篇
第一段 模型
  • from_pretrained 的六步
  • 一个 DecoderLayer 的代码
  • attention 注册表
  • KV cache
  • ForCausalLMLoss
第一篇
第二段 生成
  • GenerationConfig 的三层优先级
  • LogitsProcessor 一串
  • StoppingCriteria
  • _sample 的 while 循环
第二篇
第三段 数据
  • tokenizer.json 的五段
  • chat template 与 assistant mask
  • Arrow、map、fingerprint
  • collate_fn
第三篇
第四段 微调与后训练
  • get_peft_model 与 lora.Linear
  • SFT 的 labels 与 packing
  • dpo_loss
  • GRPO 的优势、裁剪与归一
第四篇

缩写

本系列的 85 个缩写(展开)
缩写全称一句话解释
accelerate — HF 的设备分配与分布式训练库
actor — RL 里被训练、负责生成回答的策略模型
AdaLoRA Adaptive LoRA 按重要性给各层自适应分配秩的 LoRA 变体
Adam adaptive moment estimation 按梯度一阶、二阶矩自适应步长的优化器
AdamW Adam with decoupled weight decay 带解耦权重衰减的 Adam 优化器,每个参数存两份矩
adapter — 挂在冻结基座上的一小组可训参数,这里就是 LoRA 的 A、B
assistant mask — 0/1 列表,标出哪些 token 属于 assistant 回合
beam search — 每步保留得分最高的若干条前缀
bf16 bfloat16 16 位浮点,指数位与 fp32 一样、尾数只有 8 位
bitsandbytes — 做 4 / 8 bit 量化的库,Linear4bit 来自它
BPE byte-pair encoding 字节对编码:反复合并最常见的相邻 pair 得到词表
ByteLevel — 把每个字节映射成一个可见字符的预分词 / 解码方式
cDPO conservative DPO 给 DPO 加标签平滑,对标注噪声更鲁棒
chunked prefill — 把长 prompt 切块依次前向,降低显存峰值
clip-higher — DAPO 的做法:裁剪上界 ε_high 设得比下界大
continuous batching — 一条序列结束就把位置让给新请求的动态调度
CTRL Conditional Transformer Language Model Salesforce 的可控生成模型,重复惩罚出自其论文
CUDA graph — 把一串 kernel 录成图一次提交,省 CPU 发射开销
DAPO Decoupled Clip and Dynamic sAmpling Policy Optimization GRPO 的改进:clip-higher、过滤全对 / 全错组等
DDP Distributed Data Parallel 数据并行:每卡一份完整模型,梯度做 all-reduce
decode — prefill 之后每步只喂一个新 token 的生成阶段
DoLa Decoding by Contrasting Layers 用深层与浅层 logits 之差来解码
DoRA Weight-Decomposed Low-Rank Adaptation 把权重拆成幅度和方向,方向用 LoRA 更新
DPO Direct Preference Optimization 直接用好 / 差回答对训模型,不单独训奖励模型
Dr. GRPO GRPO Done Right 去掉按长度、按组标准差归一的 GRPO 修正版
dynamic sampling — DAPO 的做法:丢掉全对 / 全错的组再补采
eos end of sequence 序列结束 token,生成到它就停
FFN feed-forward network 前馈网络,block 里 attention 之后那个子层
FlashAttention — 分块计算、不物化 T×T 矩阵的 attention kernel
fp32 32-bit floating point 单精度浮点
FSDP Fully Sharded Data Parallel 把参数、梯度、优化器状态切到各卡的数据并行
GQA grouped-query attention 多个 q 头共用一组 k、v 头,KV cache 更小
GRPO Group Relative Policy Optimization 同一 prompt 采一组回答,组内归一化奖励当优势
GSPO Group Sequence Policy Optimization 重要性比按整条序列算,而不是按 token
hinge — DPO 的变体,用 relu(1 − βΔ) 代替 logsigmoid
IA3 Infused Adapter by Inhibiting and Amplifying Inner Activations 只学几个逐元素缩放激活的向量
Jinja — Python 模板引擎,chat template 用它写
k3 — Schulman 给的一种 KL 估计式,非负、方差小
kaiming — Kaiming(He)初始化:按输入维度缩放的随机初始化
KL Kullback–Leibler divergence KL 散度,衡量两个分布差多远
Liger — LinkedIn 的融合 kernel 库,省 logits 显存
logits — softmax 之前、每个词表位置一个的未归一化分数
LoRA Low-Rank Adaptation 给权重加一个低秩增量 BA,只训 A、B 两个小矩阵
meta device — 只记形状和 dtype、不分配存储的虚拟设备
meta 设备 — 只记形状和 dtype、不分配存储的虚拟设备
micro-batch — 梯度累积里每次前向的那一小份 batch
min-p — 去掉概率低于「最大概率 × p」的 token
MLP multi-layer perceptron 多层感知机,这里指每层 attention 之后的前馈子层
mmap memory map 把文件映射进内存,读到哪页才从磁盘加载哪页
n-gram — 连续 n 个 token 的片段
nanoGPT — Karpathy 写的极简 GPT 训练代码
NER named entity recognition 命名实体识别:标出人名、地名这类片段
NFC Normalization Form C Unicode 规范化形式:组合字符合成一个码位
OOV out of vocabulary 词表外:输入里有词表表示不了的部分
packing — 把多条短样本拼进一条序列,省掉 padding
PagedAttention — vLLM 按固定大小块管理 KV cache 的 attention
parquet — Apache Parquet 列式存储文件,Hub 上的数据集常用
peft Parameter-Efficient Fine-Tuning HF 的参数高效微调库,LoRA 就在这里
PiSSA Principal Singular values and Singular vectors Adaptation 用 W 的前 r 个奇异向量初始化 A、B
PPO Proximal Policy Optimization 把新旧策略的概率比裁剪在 1±ε 内的策略梯度算法
pre-norm — 先归一化再进子层、残差绕过归一化的写法
prefill — 生成前把整段 prompt 一次前向、填满 KV cache
prompt tuning — 只训一段拼在输入前面的软提示向量
QA question answering 问答;抽取式 QA 从原文里划出答案
REINFORCE — 最基本的策略梯度:loss = −A·log π
repetition penalty — 降低已出现过的 token 的分数
RMSNorm Root Mean Square Layer Normalization 只除以均方根、不减均值的归一化
rollout — 用当前策略生成回答的阶段
RoPE rotary position embedding 旋转位置编码:按位置把 q、k 的每对维度旋转一个角度
RPO Regularized Preference Optimization DPO loss 再加一项 chosen 的 SFT loss
rsLoRA Rank-Stabilized LoRA scaling 用 α/√r 代替 α/r,秩大时更稳
safetensors — HF 的权重格式:JSON 头 + 原始字节,可 mmap
SDPA scaled dot-product attention PyTorch 的融合 attention 函数,自选 kernel
SFT supervised fine-tuning 有监督微调:拿标注好的回答训模型
SiLU Sigmoid Linear Unit 激活函数 x·sigmoid(x),又叫 Swish
SSE Server-Sent Events HTTP 上服务端单向推送的流式协议
SVD singular value decomposition 奇异值分解
SwiGLU Swish-Gated Linear Unit 用 SiLU 做门控的 FFN:两路相乘再降维
top-k — 只保留分数最高的 k 个 token
top-p — nucleus 采样:只留累计概率达到 p 的那些高分 token
trl Transformer Reinforcement Learning HF 的后训练库:SFT、DPO、GRPO 的 Trainer
value head — 模型上额外加的输出标量价值的头,PPOTrainer 要用
varlen variable-length 按每段长度切开拼接序列算 attention 的接口
verl Volcano Engine Reinforcement Learning 字节开源的 LLM 强化学习训练框架
vLLM — 开源的 LLM 推理引擎

为什么写这个系列?

六行代码,四个黑盒

工具箱第五篇的六行里,from_pretrained 进去一个 Hub 名字、出来一个 nn.Module;get_peft_model 进去一个模型、出来一个只有 1.8% 参数可训的模型;SFTTrainer(...).train() 进去一个 Dataset、出来一个训好的模型;generate 进去一段 prompt、出来一段文本。会用它们不需要知道里面是什么;但调不出、报错、结果与预期差 \(10^{-4}\) 或差 1.0 的时候,每一个问题的答案都在某个具体的文件里。

代码比论文准,比教程全

L4 讲了 RoPE 的公式、L5 推了 DPO 的 loss、L0 写了 GRPO 的 \((R - \text{mean}) / \text{std}\)。论文写的是想法,代码写的是实际做法:apply_rotary_pos_emb 用的是前后两半配对而不是相邻两维;dpo_loss 的 loss_type 列表一眼看出 IPO、hinge 各改了哪一行;GRPO 的 loss_type 决定按什么归一,正是后训练系列讨论的长度偏差在代码里的位置。这些细节教程通常不写,读源码是唯一的来源。

这几个库的骨架很稳,只是名字在变

transformers 有 518 个模型目录,但 decoder-only 语言模型几乎全是 modeling_llama.py 的变体;generate 四千行,骨架只有「配置 → 处理器 → 停止条件 → 循环」四段;trl 一年三十多个版本,SFTTrainer 的数据契约改过数次,但 -100、compute_loss 往下追的路没变。本系列教的是找入口的方法:从 Auto 类的表、从 compute_loss、从 forward 里那一行注册表调用往下追。

系列的整体主线

四篇沿着一次 LoRA SFT 的数据流走:模型先加载起来(第一篇),它的两端是整数 input_ids——出的一端是 generate(第二篇),进的一端是 tokenizer 与 datasets(第三篇)——最后把 LoRA 挂上去、把三种 loss 算出来(第四篇)。

%%{init: {"flowchart": {"wrappingWidth": 260}}}%%
%% 图:四篇沿一次 LoRA SFT 的数据流:第三篇的 tokenizers / datasets 造 input_ids 与 labels,第一篇的 transformers 模型算前向与 loss,第四篇的 peft 挂 LoRA、trl 给三种 Trainer,第二篇的 generate 是推理侧的出口
flowchart LR
    D["03 tokenizers · datasets<br/>messages → input_ids / labels<br/>Arrow 文件 → collate_fn"] --> M["01 transformers 模型侧<br/>from_pretrained → nn.Module<br/>forward → logits → ForCausalLMLoss"]
    M --> G["02 generate<br/>LogitsProcessor 一串<br/>_sample 的 while 循环"]
    P["04 peft · trl<br/>lora.Linear 的一行<br/>SFT 的 -100 · dpo_loss · GRPO"] --> M
    D --> P

贯穿四篇的几条设计线,在系列总结里单独拎出来:

  1. 注册表 + 字符串键:model_type → 模型类、_attn_implementation → attention 函数、loss_type → loss 函数、target_modules → 要换的层——模型代码不改,实现随便换。
  2. Rust / Arrow 内核 + Python 壳:tokenizers 与 datasets 的 Python 源码里要知道哪一行之下是另一种语言。
  3. -100 一个约定串起三个库:tokenizers 的 assistant mask、trl 的 build_labels、transformers 的 ForCausalLMLoss。
  4. 一个上下文省一份模型:peft 的 disable_adapter() 让 DPO 与 GRPO 不用再加载参考模型。

章节结构与分章导读

1. transformers 模型侧:from_pretrained 怎么把三个文件变成 nn.Module,forward 怎么走到 loss

第一篇读 models/auto/、modeling_utils.py、models/llama/modeling_llama.py 与 loss/loss_utils.py。加载的六步:AutoConfig 读 model_type → modeling_auto.py 的表选类 → 解析 safetensors → torch.device("meta") 下建骨架 → safe_open mmap 按名字填 → tie_weights、初始化缺失键、eval()。前向:一个 DecoderLayer 的 RMSNorm、RoPE、MLP、两条残差与 《Transformer 原理与实现(01):Transformer 长什么样——从一句话到下一个 token》的公式逐行对应;attention 是 ALL_ATTENTION_FUNCTIONS[config._attn_implementation] 的一次函数指针调用;loss 不在建模文件里,按类名分派到 ForCausalLMLoss。最后读 modular_qwen2.py,看 518 个模型怎么维护。

它回答的问题:from_pretrained 加载 8B bf16 模型时峰值内存为什么约等于最终的 16 GB 而不是 32 GB?attn_implementation="sdpa" 与 "eager" 的 logits 为什么在 fp32 下差 \(10^{-4}\)、bf16 下差 1.0?labels 传进去之后,-100 与 num_items_in_batch 各在哪一行起作用?

2. generate:一次采样的完整调用链

第二篇读 generation/ 目录:utils.py、logits_process.py、stopping_criteria.py、configuration_utils.py。骨架四段:GenerationConfig 三层优先级(kwargs > 仓库的 generation_config.json > 全局默认)→ _get_logits_processor 按非空字段装配一串 LogitsProcessor(processor 在前、warper 在后,顺序由代码决定)→ 三个停止条件 → _sample 的 while 循环(prefill 与 decode 是同一个 forward,第一次 logits_to_keep=1,之后每步只喂最后一个 token)。temperature、top_k、top_p、repetition_penalty 各是十行代码。

它回答的问题:不传 do_sample 只传 temperature=0.2,输出是随机的吗?top_k=50, top_p=0.9 同时设置,保留的是交集还是并集、顺序能换吗?batch 里先停的那条序列在后面的步里发生了什么?

3. tokenizers 与 datasets:从 messages 到 input_ids,从 Arrow 文件到 collate_fn

第三篇读两条流水线。tokenizer.json 的五段(normalizer → pre_tokenizer → BPE model → post_processor → decoder)由 Rust 库执行,Python 壳 PreTrainedTokenizerFast 只做加载对齐、__call__ → encode_batch、padding;chat template 是沙箱 Jinja,{% generation %} 标记给出 assistant mask——SFT loss mask 的第一种来源,Qwen2.5 的模板没有它。load_dataset 把文件转成 .arrow 缓存,Dataset 是 mmap 视图;map 逐批取、调函数、写新文件,文件名是 fingerprint;shuffle / select 只改索引;变长 padding 在 collate_fn 里、纯 Python。

它回答的问题:len(tok)、tok.vocab_size、config.vocab_size 三个数为什么不同(151665 / 151643 / 151936)?"12345" 为什么是 5 个 token 而 "hello" 是 1 个?改了 map 函数里一个 print 为什么整个 10 分钟重跑?

4. peft 与 trl:LoRA 怎么挂上去,SFT / DPO / GRPO 的 loss 各在哪一行

第四篇读 peft/tuners/lora/ 与 trl/trainer/ 的三个 Trainer。peft:get_peft_model 返回 PeftModel(LoraModel(原模型)),原模型被原地改;lora.Linear 的 forward 一行;merge 把 \(\frac{\alpha}{r}BA\) 加进 weight.data。trl:SFT 三种数据格式,labels 在 _prepare_dataset 的 build_labels 这一次 map 里造,collator 只 pad,padding_free 时把每段首 token 置 -100;dpo_loss 的 sigmoid 分支就是 \(-\log\sigma(\beta\Delta)\),参考模型有三种来源;GRPO 的采样、组内优势、PPO 裁剪与 KL、四种 loss_type 的归一。

它回答的问题:target_modules=["q_proj", "v_proj"] 与 "q_proj|v_proj" 哪一种能匹配到 model.layers.3.self_attn.q_proj?LoRA 只训 8.8M 参数,反向为什么仍要经过冻结的 base_layer,省下的到底是什么?GRPO 一组 8 条全对的回答对梯度贡献多少、beta=0 时 trl 省掉了什么?

5. 系列总结与通关自测

第五篇把四篇压成一张「问题 → 文件 → 函数」的索引表,回顾贯穿五个库的四条设计线,再给一套三段式自测。

贯穿全系列的源码阅读线

每一篇都从工具箱第五篇的一行代码进入,读到它与本地图前面各层的对应处为止:

六行代码各自的入口、终点与对应的前置篇
六行里的哪一行 入口文件 读到哪里为止 对应的前置篇 篇
AutoModelForCausalLM.from_pretrained(name) models/auto/auto_factory.py → modeling_utils.py → models/llama/modeling_llama.py ForCausalLMLoss(loss/loss_utils.py) 《Transformer 原理与实现(01):Transformer 长什么样——从一句话到下一个 token》的结构图、《算法工程师的数学(05):从最大似然到交叉熵——第一个要会推的 loss》的交叉熵 01
model.generate(**inputs, do_sample=True, top_p=0.9) generation/utils.py _sample 循环里的 multinomial 与 stopping_criteria 《算法工程师的数学(05):从最大似然到交叉熵——第一个要会推的 loss》「七、softmax」的采样公式 02
tok.apply_chat_template(messages) / load_dataset(name).map(fn) tokenization_utils_tokenizers.py、utils/chat_template_utils.py / arrow_dataset.py、load.py Rust 的 encode_batch / ArrowWriter 与 collate_fn 预训练第二篇的 BPE 03
get_peft_model(model, LoraConfig(...)) peft/mapping_func.py → tuners/lora/model.py → tuners/lora/layer.py Linear.forward 的一行 《算法工程师的数学(03):正交与旋转、特征值与 SVD——从 RoPE 到 LoRA》的 \(W + BA\) 04
SFTTrainer(...).train()(及 DPO / GRPO) trl/trainer/sft_trainer.py、dpo_trainer.py、grpo_trainer.py build_labels、dpo_loss、_compute_loss 后训练系列第一、三、四篇 04

每篇给出的输出——Loading weights: 290/290、装配出的 LogitsProcessorList、cache-<fingerprint>.arrow 的文件名、trainable params: 8.8M——都来自 Qwen2.5-0.5B 上的一次实际运行,脚本在 ai-learning-labs/hf-source-reading;正文已给出读懂所需的全部代码,读本系列不需要它。

前置要求与说明

前置要求

读本系列需要的前置
需要 到什么程度 在哪里学
工具箱第五篇的六行代码 跑过一次,知道 config.json / tokenizer.json / safetensors 各是什么 工具箱第五篇
nn.Module、state_dict、DataLoader 知道键名从属性路径来、collate_fn 做什么 工具箱第三篇
Transformer 的结构 能画出一个 decoder block,知道 RoPE、GQA、KV cache 各在哪 《Transformer 原理与实现(01):Transformer 长什么样——从一句话到下一个 token》
SFT / DPO / GRPO 的目标函数 认得 \(-\log\sigma(\beta\Delta)\) 与 \((R - \text{mean}) / \text{std}\) 后训练系列第一、三、四篇

第二篇只依赖第一篇;第三篇独立;第四篇用到前三篇(ForCausalLMLoss、apply_chat_template、map)。

版本与基线

transformers 5.17.0、tokenizers 0.23.2、datasets 5.0.1、peft 0.21.0、trl 1.13.0;模型 Qwen/Qwen2.5-0.5B,数据集 HuggingFaceH4/no_robots。引用只给文件路径与类名 / 函数名,不给行号——这几个库的目录结构改得快,行号几个月就失效,找入口的方法不会。

章节目录

章节目录
篇 标题 读它需要
01 transformers 模型侧:from_pretrained 怎么把三个文件变成 nn.Module,forward 怎么走到 loss 《Transformer 原理与实现(01):Transformer 长什么样——从一句话到下一个 token》
02 generate:一次采样的完整调用链 01
03 tokenizers 与 datasets:从 messages 到 input_ids,从 Arrow 文件到 collate_fn 工具箱第五篇
04 peft 与 trl:LoRA 怎么挂上去,SFT / DPO / GRPO 的 loss 各在哪一行 01–03、后训练系列
05 系列总结与通关自测 01–04

最终目标

读完四篇,对工具箱第五篇的六行代码,你应该能:

  1. 说出 from_pretrained 的六步,并解释为什么 8B 模型的加载峰值内存等于权重大小;
  2. 在 modeling_llama.py 里指出 L4 结构图上每个方框对应的类,并说出 attention 实现是怎么切换的;
  3. 写出 temperature / top_k / top_p 各自的十行代码,说出 _sample 循环每步做的六件事;
  4. 解释 tokenizer.json 的五段与 map 的 fingerprint,判断一次 map 为什么命中或没命中缓存;
  5. 从 get_peft_model 追到 lora.Linear.forward 的那一行,从 SFTTrainer 追到 -100 被填的那一次 map,从 dpo_loss 与 _compute_loss 里各指出论文公式对应的几行。

之后遇到任何一个新的 Trainer、新的 LogitsProcessor、新的模型目录,方法相同:找到注册表、找到 compute_loss、找到 forward 里那一行调用,往下追。

[^q0]: 每行一条调用链,各对应一篇:from_pretrained → AutoConfig 读 config.json 查架构映射 → 构造 Qwen2ForCausalLM → safetensors 按 state_dict 键名加载(L4 结构图的七样东西就是 modeling_qwen2.py 里的七个类);model.generate → GenerationMixin.generate 的循环:prepare_inputs_for_generation → forward 取最后位置 logits → LogitsProcessor → 采样 → 更新 KV cache(DynamicCache)(对应 《Transformer 原理与实现(02):一个 token 的旅程——训练侧与推理侧》的 decode);apply_chat_template → Jinja 模板渲染成字符串 → tokenizer 的预分词 + BPE 合并 → input_ids;load_dataset(...).map → Arrow 文件 → Dataset.map 的批处理 / 多进程 / 缓存指纹;get_peft_model → inject_adapter 匹配 target_modules 把 nn.Linear 换成 lora.Linear(scaling = α/r、lora_B 全零,对应 《算法工程师的数学(03):正交与旋转、特征值与 SVD——从 RoPE 到 LoRA》);SFTTrainer 在 DataCollator 里填 -100 做 loss mask,DPOTrainer 的 dpo_loss 十几行是 $$-\log\sigma(\beta(\log\pi/\pi_{ref} w - \log\pi/\pi{ref} _l))$$,GRPOTrainer 的组内相对优势与裁剪比率各十几行(对应 L5 的公式)。
这篇对你有用?

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/reading-hugging-face-source-code.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×