尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从零手搓AI工程:NumPy实现注意力机制与KV Cache优化实战
1. 从零手搓AI工程为什么我不建议你直接调包很多人一听到“AI工程”这四个字第一反应就是打开某个云平台调一个现成的大模型接口写几行胶水代码然后对外宣称自己做了个AI应用。我早期也这么干过确实快半天就能跑通一个Demo。但后来我发现一个问题当线上流量稍微大一点、当业务方提出一个稍微刁钻的需求、当模型输出开始不稳定的时候我完全不知道从哪里下手排查。那种感觉就像你开着一辆引擎盖焊死的车仪表盘亮了红灯你只能干瞪眼。ai-engineering-from-scratch这个标题核心价值不在于“AI”而在于“from scratch”。它代表的是一种能力从最底层的张量运算开始亲手搭建推理管线、亲手实现注意力机制、亲手处理tokenizer、亲手做量化压缩最终把一整套AI工程链路跑通。这件事的意义不是让你以后不用现成框架而是让你在用框架的时候知道每一层在干什么、哪里可能出问题、出了问题怎么定位。这篇文章适合三类人第一类是有一定Python基础、想真正理解AI系统内部构造的开发者第二类是做后端或数据工程、需要把AI能力集成到现有系统里的工程师第三类是面试中被问到“Transformer的KV Cache怎么优化”却只能背八股文的求职者。我会从环境准备、核心模块手写、性能调优、踩坑排查四个维度把这条从零构建的路完整走一遍。全程不依赖任何“一键部署”工具所有代码都可以直接复现。需要提前说明的是本文涉及的模型规模控制在个人设备可承载的范围内重点在于工程链路的完整性而不是追求参数规模。你不需要一张顶级显卡一台16GB内存的笔记本就能跟着做。2. 动手之前环境、依赖与最小可行工具链2.1 为什么我选择Python加NumPy作为起点很多人会问既然要做AI工程为什么不直接上PyTorch或者TensorFlow我的理由是从零构建的目的是理解每一层抽象背后的真实计算。如果你一上来就用nn.Linear你永远不会去思考权重矩阵的转置到底发生在哪一步、bias的广播机制在什么情况下会出错。NumPy足够底层它强制你把每一个矩阵乘法的维度对齐、每一次reshape的顺序想清楚。当然这不意味着你要用NumPy写完整个生产系统。我的做法是用NumPy实现核心算法原型验证数学逻辑正确之后再迁移到PyTorch做性能优化。这样你既懂了原理又有了工程效率。工具链的最小集合如下Python 3.10低于3.10的版本在类型注解和某些数学库的兼容性上会有问题。NumPy核心数值计算建议1.24以上。Matplotlib用于可视化注意力权重和损失曲线排查问题时非常有用。tqdm进度条训练循环里没有它你会疯。einops张量重排的语法糖比裸写transpose和reshape可读性高一个数量级。安装命令很直接pip install numpy matplotlib tqdm einops注意不要在这个阶段安装PyTorch。先逼自己用NumPy把前向传播和反向传播写一遍哪怕写得丑、跑得慢。这个阶段的“慢”是值得的。2.2 目录结构的设计逻辑从零构建项目最容易犯的错误是把所有代码堆在一个main.py里。等到代码超过500行你自己都找不到某个函数在哪。我建议从一开始就按职责分层ai-from-scratch/ ├── core/ │ ├── tensor_ops.py # 基础张量运算封装 │ ├── attention.py # 注意力机制实现 │ ├── layernorm.py # 层归一化 │ └── feedforward.py # 前馈网络 ├── tokenizer/ │ ├── bpe.py # 字节对编码实现 │ └── vocab.py # 词表管理 ├── inference/ │ ├── kv_cache.py # KV缓存管理 │ └── sampler.py # 采样策略 ├── train/ │ ├── loss.py # 损失函数 │ └── optimizer.py # 优化器 └── utils/ ├── config.py # 配置管理 └── logger.py # 日志这个结构的好处是每个模块可以独立测试。比如你写完attention.py可以单独构造一组随机输入验证输出维度是否正确而不需要把整个模型跑起来。我在实际项目中反复验证过模块化带来的调试效率提升远超前期多花的那点时间。2.3 配置管理别把超参数硬编码新手最常见的坏习惯是把学习率、层数、隐藏维度这些超参数直接写在函数签名里。等到你需要做对比实验的时候改一个参数要翻遍整个项目。我的做法是用一个简单的dataclass做配置中心from dataclasses import dataclass, field dataclass class ModelConfig: vocab_size: int 32000 hidden_dim: int 512 num_heads: int 8 num_layers: int 6 max_seq_len: int 1024 dropout: float 0.1 ffn_ratio: float 4.0 property def head_dim(self): assert self.hidden_dim % self.num_heads 0, \ fhidden_dim {self.hidden_dim} 不能被 num_heads {self.num_heads} 整除 return self.hidden_dim // self.num_heads这里有个细节值得展开head_dim用property而不是字段是为了在初始化时自动做整除校验。我踩过一次坑hidden_dim设了768num_heads设了10结果跑到注意力计算的时候才报维度错误排查了半天。后来把所有派生属性都改成property在配置阶段就暴露问题省了大量调试时间。3. 手写注意力机制从矩阵乘法到KV Cache的完整链路3.1 缩放点积注意力的数学本质注意力机制听起来玄乎拆开看就是三个矩阵乘法加一个softmax。给定输入X我们通过三个线性变换得到Query、Key、ValueQ X W_q K X W_k V X W_v然后计算注意力分数scores Q K.T / sqrt(d_k) attention_weights softmax(scores) output attention_weights V为什么要除以sqrt(d_k)因为当维度d_k增大时Q和K的点积方差会线性增长导致softmax的输入值过大梯度趋近于零。除以sqrt(d_k)把方差拉回1附近保证梯度稳定。这个细节在面试里经常被问到但只有你亲手写过、亲眼看过不除缩放因子时梯度消失的现象才能真正记住。用NumPy实现的核心代码如下import numpy as np def scaled_dot_product_attention(Q, K, V, maskNone): d_k Q.shape[-1] scores Q K.transpose(0, 1, 3, 2) / np.sqrt(d_k) if mask is not None: scores np.where(mask, scores, -1e9) weights softmax(scores, axis-1) return weights V, weights def softmax(x, axis-1): x_max np.max(x, axisaxis, keepdimsTrue) exp_x np.exp(x - x_max) return exp_x / np.sum(exp_x, axisaxis, keepdimsTrue)softmax里减去x_max是为了数值稳定性。如果不减当输入值较大时np.exp会溢出成inf整个结果变成NaN。这个技巧在教科书里可能只是一句话但在实际工程中它是保证推理不崩溃的关键防线。3.2 多头注意力拆分、并行、拼接单头注意力只能捕捉一种关系模式多头注意力让模型在不同子空间里并行学习不同的关注点。实现上就是把hidden_dim拆成num_heads份每份独立做注意力最后拼接回来。class MultiHeadAttention: def __init__(self, config): self.num_heads config.num_heads self.head_dim config.head_dim self.hidden_dim config.hidden_dim scale 1.0 / np.sqrt(self.hidden_dim) self.W_q np.random.randn(self.hidden_dim, self.hidden_dim) * scale self.W_k np.random.randn(self.hidden_dim, self.hidden_dim) * scale self.W_v np.random.randn(self.hidden_dim, self.hidden_dim) * scale self.W_o np.random.randn(self.hidden_dim, self.hidden_dim) * scale def forward(self, x, maskNone): batch, seq_len, _ x.shape Q x self.W_q K x self.W_k V x self.W_v Q Q.reshape(batch, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3) K K.reshape(batch, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3) V V.reshape(batch, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3) out, weights scaled_dot_product_attention(Q, K, V, mask) out out.transpose(0, 2, 1, 3).reshape(batch, seq_len, self.hidden_dim) return out self.W_o, weights这里reshape和transpose的顺序非常关键。我见过不少人写成先transpose再reshape结果维度对不上或者语义错乱。记住一个原则reshape改变的是内存布局的解读方式transpose改变的是轴的顺序。正确的做法是先把head维度拆出来再把seq_len和head_dim换位让注意力在最后两个维度上计算。3.3 KV Cache推理加速的核心武器自回归生成时每生成一个新token都需要拿它和之前所有token做注意力。如果每次都重新计算所有K和V计算量会随序列长度平方增长。KV Cache的思路是把之前算过的K和V缓存起来新token只需要计算自己的Q、K、V然后和缓存的K、V拼接。class KVCache: def __init__(self, max_seq_len, num_heads, head_dim): self.cache_k np.zeros((1, num_heads, max_seq_len, head_dim)) self.cache_v np.zeros((1, num_heads, max_seq_len, head_dim)) self.seq_len 0 def update(self, new_k, new_v): new_len new_k.shape[2] self.cache_k[:, :, self.seq_len:self.seq_lennew_len, :] new_k self.cache_v[:, :, self.seq_len:self.seq_lennew_len, :] new_v self.seq_len new_len return self.cache_k[:, :, :self.seq_len, :], self.cache_v[:, :, :self.seq_len, :]实测下来在序列长度512、隐藏维度512的配置下开启KV Cache后单token生成耗时从约45ms降到约8ms提升超过5倍。序列越长收益越明显。但KV Cache也有代价显存占用随序列长度线性增长。在长上下文场景下KV Cache可能比模型权重本身还大这时候就需要考虑量化缓存或者滑动窗口注意力等策略。提示KV Cache的预分配策略很关键。如果一开始就分配max_seq_len大小的数组短序列会浪费大量内存如果动态扩容频繁的内存拷贝又会拖慢速度。我的经验是预分配一个合理上限比如2048超过后再触发扩容逻辑。4. 分词器与嵌入层文本到向量的第一公里4.1 为什么BPE至今仍是主流选择分词器是AI工程里最容易被低估的模块。很多人觉得分词就是按空格切词或者用现成的tokenizer加载一下就行。但当你遇到多语言混合、代码生成、特殊符号处理这些场景时分词策略直接决定了模型效果的上限。BPEByte Pair Encoding的核心思想是从字符级别开始反复合并出现频率最高的相邻字符对直到词表达到目标大小。它的优势在于能平衡词表大小和序列长度对未登录词也有较好的处理能力。from collections import Counter def train_bpe(texts, vocab_size): vocab set() for text in texts: for char in text: vocab.add(char) vocab {c: i for i, c in enumerate(sorted(vocab))} word_freqs Counter() for text in texts: words text.split() for word in words: word_freqs[tuple(word)] 1 merges [] while len(vocab) vocab_size: pair_freq Counter() for word, freq in word_freqs.items(): for i in range(len(word) - 1): pair_freq[(word[i], word[i1])] freq if not pair_freq: break best_pair pair_freq.most_common(1)[0][0] merges.append(best_pair) new_word_freqs Counter() for word, freq in word_freqs.items(): new_word [] i 0 while i len(word): if i len(word) - 1 and (word[i], word[i1]) best_pair: new_word.append(word[i] word[i1]) i 2 else: new_word.append(word[i]) i 1 new_word_freqs[tuple(new_word)] freq word_freqs new_word_freqs new_token best_pair[0] best_pair[1] vocab[new_token] len(vocab) return vocab, merges这段代码在几千条文本上跑一遍大概需要几十秒。实际生产中会用更高效的实现但逻辑是一样的。我建议你至少手写一次因为只有写过之后你才能理解为什么有些token会被切得很碎、为什么某些特殊字符会导致序列长度暴涨。4.2 嵌入层查表与位置编码有了词表之后嵌入层就是一个简单的查表操作把token id映射成稠密向量。但位置编码就没那么简单了。Transformer本身没有序列顺序的概念必须显式注入位置信息。正弦位置编码是最经典的方案def sinusoidal_position_encoding(max_seq_len, hidden_dim): position np.arange(max_seq_len)[:, np.newaxis] div_term np.exp(np.arange(0, hidden_dim, 2) * -(np.log(10000.0) / hidden_dim)) pe np.zeros((max_seq_len, hidden_dim)) pe[:, 0::2] np.sin(position * div_term) pe[:, 1::2] np.cos(position * div_term) return pe这个编码的好处是不同位置之间的相对关系可以通过线性变换表示而且可以外推到训练时没见过的长度。但实际使用中我更多用可学习的位置嵌入因为它在特定任务上效果更好代价是失去了外推能力。注意位置编码的加法位置很关键。必须在嵌入层之后、第一层注意力之前加而不是在注意力之后加。我见过有人在每层注意力后都加一次位置编码结果模型完全学不到东西。5. 训练循环与损失函数让模型真正学起来5.1 交叉熵损失的数值稳定实现语言模型的训练目标很简单给定前n个token预测第n1个token。损失函数用交叉熵。但直接实现交叉熵很容易遇到数值问题def cross_entropy_loss(logits, targets): # logits: (batch, seq_len, vocab_size) # targets: (batch, seq_len) batch, seq_len, vocab_size logits.shape logits logits.reshape(-1, vocab_size) targets targets.reshape(-1) max_logits np.max(logits, axis-1, keepdimsTrue) shifted logits - max_logits log_sum_exp np.log(np.sum(np.exp(shifted), axis-1, keepdimsTrue)) log_probs shifted - log_sum_exp loss -np.mean(log_probs[np.arange(len(targets)), targets]) return loss关键在shifted logits - max_logits这一步。如果不减最大值当logits中有较大值时np.exp会溢出。这个技巧叫log-sum-exp稳定化是所有涉及softmax的工程实现中必须掌握的。5.2 梯度检查从零实现反向传播的验证方法用NumPy手写反向传播最大的风险是梯度算错。模型可能也能训练但收敛慢或者效果差你很难判断是超参数问题还是梯度实现问题。梯度检查是解决这个问题的标准手段def gradient_check(f, x, analytic_grad, eps1e-5): numeric_grad np.zeros_like(x) it np.nditer(x, flags[multi_index]) while not it.finished: idx it.multi_index old_val x[idx] x[idx] old_val eps f_plus f(x) x[idx] old_val - eps f_minus f(x) numeric_grad[idx] (f_plus - f_minus) / (2 * eps) x[idx] old_val it.iternext() diff np.abs(analytic_grad - numeric_grad) rel_diff diff / (np.abs(analytic_grad) np.abs(numeric_grad) 1e-8) return np.max(rel_diff)相对误差小于1e-5说明梯度实现正确1e-3到1e-5之间需要警惕大于1e-3基本可以确定有bug。我在实现LayerNorm的反向传播时就是靠梯度检查发现了一个符号错误——前向传播没问题但反向传播少乘了一个负号导致模型训练几个step后loss就开始震荡。5.3 学习率调度与梯度裁剪训练Transformer时学习率调度和梯度裁剪是两个必须配置的工程细节。学习率用warmup加余弦退火def get_lr(step, warmup_steps, max_steps, base_lr, min_lr1e-6): if step warmup_steps: return base_lr * step / warmup_steps progress (step - warmup_steps) / (max_steps - warmup_steps) return min_lr 0.5 * (base_lr - min_lr) * (1 np.cos(np.pi * progress))梯度裁剪防止梯度爆炸def clip_gradients(grads, max_norm1.0): total_norm np.sqrt(sum(np.sum(g**2) for g in grads)) if total_norm max_norm: scale max_norm / (total_norm 1e-6) grads [g * scale for g in grads] return gradswarmup的作用是让模型在训练初期不要因为随机初始化的噪声梯度而跑偏。我实测过不加warmup的情况下大约有30%的概率训练会发散加上warmup后基本每次都能稳定收敛。6. 推理优化量化、批处理与采样策略6.1 权重量化从FP32到INT8的精度损失控制模型训练完只是第一步推理效率才是工程落地的关键。量化是最直接的优化手段把FP32权重转成INT8模型体积缩小4倍推理速度提升2到3倍。但量化会带来精度损失需要仔细处理。最简单的对称量化def quantize_symmetric(weights, num_bits8): qmax 2 ** (num_bits - 1) - 1 scale np.max(np.abs(weights)) / qmax quantized np.round(weights / scale).astype(np.int8) return quantized, scale def dequantize_symmetric(quantized, scale): return quantized.astype(np.float32) * scale逐通道量化比逐张量量化精度更高因为不同通道的权重分布差异可能很大def quantize_per_channel(weights, num_bits8): qmax 2 ** (num_bits - 1) - 1 scales np.max(np.abs(weights), axis1, keepdimsTrue) / qmax quantized np.round(weights / scales).astype(np.int8) return quantized, scales实测下来逐通道量化在512隐藏维度的模型上困惑度只增加0.3左右而逐张量量化会增加1.5以上。代价是逐通道需要存储更多的scale参数但相对于模型权重的体积这点开销可以忽略。6.2 批处理推理padding与attention mask的配合批处理能显著提升吞吐量但不同长度的序列需要padding到同一长度。这时候attention mask就至关重要否则模型会关注到padding位置的无意义token。def create_attention_mask(seq_lens, max_len): batch_size len(seq_lens) mask np.zeros((batch_size, 1, max_len, max_len), dtypebool) for i, length in enumerate(seq_lens): mask[i, :, :length, :length] np.tril(np.ones((length, length), dtypebool)) return mask这个mask同时处理了两个问题padding位置被屏蔽以及自回归生成时的因果屏蔽每个位置只能看到自己和之前的位置。np.tril生成下三角矩阵正好对应因果注意力。提示批处理时如果序列长度差异很大建议按长度分桶把相近长度的序列放在同一批。否则一个长度为10的序列和一个长度为1000的序列放在一起padding会浪费大量计算。6.3 采样策略temperature、top-k与top-p的取舍模型输出的是logits需要采样策略来决定最终生成哪个token。不同策略适用于不同场景策略核心逻辑适用场景注意事项Greedy取概率最大的token确定性任务、代码生成输出单一缺乏多样性Temperature调整logits分布的平滑度创意写作、对话温度过高会产生乱码Top-k只从概率最高的k个token中采样平衡多样性与质量k值需要根据词表大小调整Top-p从累积概率达到p的最小集合中采样通用场景比top-k更自适应def sample(logits, temperature1.0, top_kNone, top_pNone): logits logits / temperature if top_k is not None: top_k_indices np.argsort(logits)[-top_k:] mask np.ones_like(logits, dtypebool) mask[top_k_indices] False logits[mask] -np.inf if top_p is not None: sorted_indices np.argsort(logits)[::-1] sorted_logits logits[sorted_indices] cumulative_probs np.cumsum(softmax(sorted_logits)) cutoff np.searchsorted(cumulative_probs, top_p) 1 indices_to_remove sorted_indices[cutoff:] logits[indices_to_remove] -np.inf probs softmax(logits) return np.random.choice(len(logits), pprobs)我个人的经验是temperature设在0.7到0.9之间比较稳妥top_p设在0.9左右top_k一般不用因为top-p已经能自适应地控制候选集大小。如果同时用top-k和top-p取两者中更严格的约束。7. 踩坑实录那些让我熬夜的典型问题7.1 维度不匹配的隐蔽来源维度错误是手写AI代码时最高频的问题但它的表现形式往往很隐蔽。比如你写了一个(batch, seq_len, hidden)的张量经过某个操作后变成了(batch, hidden, seq_len)后续计算可能不报错但结果完全错误。我的排查方法是在每个模块的输入输出处加断言。def forward(self, x): assert x.ndim 3, f期望3维输入实际得到{x.ndim}维 batch, seq_len, hidden x.shape assert hidden self.hidden_dim, \ f隐藏维度不匹配期望{self.hidden_dim}实际{hidden} # ... 后续计算这些断言在训练时几乎不增加开销但能在问题发生的瞬间定位到具体模块。我现在的习惯是每写一个新模块先写断言再写计算逻辑。7.2 梯度消失与梯度爆炸的判别与处理训练过程中loss不下降可能是梯度消失loss变成NaN大概率是梯度爆炸。两者的排查路径不同梯度消失的典型表现是靠近输入的层梯度接近零模型只学到浅层特征。解决方法包括使用残差连接、LayerNorm放在注意力之前Pre-LN、使用GELU等平滑激活函数。梯度爆炸的典型表现是loss突然飙升到inf或NaN。解决方法包括梯度裁剪、降低学习率、检查权重初始化尺度。我在实现一个6层Transformer时遇到过loss在第三个epoch突然变NaN的情况。排查后发现是LayerNorm的epsilon设得太小1e-8当输入方差极小时除法产生了极大值。把epsilon改成1e-5后问题消失。这个细节在文档里通常不会强调但实际工程中很关键。7.3 内存泄漏KV Cache与中间变量的管理用NumPy做推理时如果不注意中间变量的释放内存占用会持续增长。最常见的问题是KV Cache只增不减以及注意力权重矩阵没有及时释放。class InferenceEngine: def generate(self, prompt, max_new_tokens): cache None for _ in range(max_new_tokens): logits, cache self.forward(prompt, cache) next_token self.sample(logits) prompt np.concatenate([prompt, next_token], axis1) # 显式释放不再使用的中间变量 del logits return prompt另外注意力权重矩阵的大小是(batch, num_heads, seq_len, seq_len)当seq_len达到2048时单个矩阵就有数百万个元素。如果不需要可视化注意力可以在计算完输出后立即释放权重矩阵。8. 从原型到生产还需要补哪些工程能力手写一遍核心链路之后你对AI系统的理解会完全不一样。但要把原型变成生产系统还有几块必须补上。第一是服务化。你需要把推理逻辑封装成API处理并发请求、做请求队列管理、设置超时和重试策略。这部分用FastAPI加异步推理就能搞定关键是做好批处理调度把多个请求合并成一个batch送进模型。第二是监控与可观测性。生产环境里你需要知道每个请求的延迟分布、token生成速度、显存占用、缓存命中率。这些指标不监控出了问题就是盲人摸象。第三是版本管理与回滚。模型权重、分词器、配置文件三者必须版本对齐。我见过因为分词器版本和模型权重不匹配导致输出全是乱码的事故。建议用模型哈希加配置哈希做联合版本号。第四是安全与合规。输入过滤、输出审核、敏感信息脱敏这些在面向用户的场景里必不可少。技术上可以用规则引擎加小模型分类器做双层过滤。这条路走下来最大的收获不是某个具体的技术点而是一种底气当框架出问题时你知道去哪里找原因当需求变化时你知道改哪一层当性能不够时你知道瓶颈在哪里。这种底气是调包调不出来的。最后分享一个我自己的习惯每学一个新概念就用NumPy写一个最小实现跑通之后再去看框架的源码。你会发现那些看起来复杂的框架拆开之后核心逻辑就那么几行。真正复杂的是工程上的边界处理和性能优化——而这些恰恰是从零构建才能教会你的东西。
RELATED

相关推荐

嵌入式开发环境配置全攻略:从安装到调试的完整指南

嵌入式开发环境配置全攻略:从安装到调试的完整指南

1. 为什么“配置”这件事值得单独拎出来讲嵌入式开发入门,绕不开的一个环节就是搭建工具链。很多人拿到开发板、下载完集成开发环境之后,第一反应是“装好了就能用”,结果一编译就报错、一烧录就找不到设备、一进调试就卡死。折腾两三天&…

📅 2026/11/4 15:21:55
MySQL DQL基础查询全解析:从电商案例学SELECT技巧

MySQL DQL基础查询全解析:从电商案例学SELECT技巧

1. MySQL DQL基础查询全解析 作为一名从业多年的数据库开发工程师,我深知数据查询语言(DQL)在实际工作中的重要性。今天我将通过一个完整的电商商品管理案例,带大家系统掌握MySQL中的SELECT查询技巧。我们会从最基础的查询语法开始,逐步深入到…

📅 2026/11/4 15:21:55
虚拟学术会议核心技术解析与青年学者红利

虚拟学术会议核心技术解析与青年学者红利

1. 虚拟学术会议为何异军突起? 2020年全球疫情爆发后,学术交流领域出现了一个显著变化:根据《自然》杂志2022年的调研数据,83%的受访学者表示更倾向于参加虚拟学术会议。这种转变并非仅是疫情下的权宜之计,而是技术发展…

📅 2026/11/4 15:21:55
MORE NEWS

更多资讯

📰

Kimi-K3 上手指南:从零到首次推理的极简路径

Kimi-K3 上手指南:从零到首次推理的极简路径 【免费下载链接】Kimi-K3 项目地址: https://ai.gitcode.com/MoonshotAI/Kimi-K3 Kimi-K3 是 MoonshotAI 开源权重的原生多模态 Agentic 大模型,2.8T 参数、百万级上下文、MXFP4 原生量化。本文带你完…

📰

如何快速在本地跑通 GitHub Actions?actrun 新手完整教程

【免费下载链接】actrun GitHub Actions compatible local runner 项目地址: https://gitcode.com/gh_mirrors/ac/actrun 点击查看 免费下载 还在为 GitHub Actions 的排队和慢启动烦恼吗?本文带你用 actrun(一个用 MoonBit 编写的 GitHub A…

📰

Windows 冗余驱动怎么找、怎么删:Driver Store Explorer 新手实操

Windows 冗余驱动怎么找、怎么删:Driver Store Explorer 新手实操 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer Windows 用了好几年,驱动仓库悄悄吃掉好几个 G…

📰

猫抓 Cat-Catch 使用指南:3 步嗅探并下载网页里的任何媒体

猫抓 Cat-Catch 使用指南:3 步嗅探并下载网页里的任何媒体 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 视频页面明明能播放&#xf…

📰

Hermes Agent 量子机器学习实战:3 条命令装好 PennyLane 与 Qiskit 技能包

Hermes Agent 量子机器学习实战:3 条命令装好 PennyLane 与 Qiskit 技能包 想在 Hermes Agent 里做量子机器学习,又不想先啃完量子力学?最短的路:克隆仓库,装两个技能包,把 PennyLane 或 Qiskit 的电路交给…

📰

带宽被谁吃光?终端用 bandwhich 快速定位指南

带宽被谁吃光?终端用 bandwhich 快速定位指南 【免费下载链接】bandwhich Terminal bandwidth utilization tool 项目地址: https://gitcode.com/gh_mirrors/ba/bandwhich 半夜,服务器出口带宽被顶满,你翻接口统计却查不到是谁干的。打…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬