从 LLM 到 Agent Skill,龙虾的技术基础 · ① LLM

本系列共 8 篇,从 LLM 一直讲到 Agent Skill。总览:从 LLM 到 Agent Skill:我理解的 AI 应用进化路线


这篇写给谁

你可能已经听过 LLM、Transformer、注意力 这些词,也知道大模型在「猜下一个词」,但中间这一大段——神经网络怎么一步步长成今天这个样子——有点模糊。这篇就用尽量少术语、尽量说人话的方式,帮你把这条线捋直。

读完你不需要会推公式,只要心里有一张粗线条地图:后面看别人聊架构、聊论文,不至于完全接不上。


神经网络到底在干什么

可以把它想成很多层「小开关」叠在一起。

  • 输入:比如一张手写数字图片,展平成一长串数字(像素明暗)。
  • 神经元:对上一层的数加权求和,再过一个非线性函数(你可以先理解成「把结果掰弯一下,不然多层叠起来和一层没区别」)。
  • 多层:浅层可能只看出边缘、小块;层数多了,可以组合成更抽象的东西(笔画、部件,再到「像几」)。
  • 学习:就是在调这些权重偏置,让输出越来越接近你想要的答案(比如「这是数字 7」)。

训练时常说的梯度下降、反向传播,你可以先记一句:算出差了多少,把误差一层层往回传,告诉每一层该怎么微调。 不必深究细节,知道「模型是在调参数」就够了。


MLP:最朴素的「全连接」网络

**MLP(多层感知机)**听起来唬人,其实就是:一层一层全连接,数据从入口单向流到出口,中间没有「绕圈」。

  • 全连接的意思是:这一层的每个点,都和上一层的每一个点相连。参数多,但结构简单。
  • 适合表格数据、向量特征,也常作为别的模型后面的「分类头」。

后面出现的 CNN、RNN、Transformer,本质上还是「前向算一遍、反向更新参数」,只是连接方式变了——为了更省参数、更适合某种数据(图像、序列、文本)。


CNN:给图像「省参数」的办法

图像是整整齐齐的格子(像素)。如果对每个像素都全连接,参数会爆炸。

**卷积神经网络(CNN)**用了两个很自然的想法:

  1. 只看一小块:就像用一个小窗口在图上滑,先看局部有没有边、角、纹理,再组合成更大图案。这叫局部连接,参数少很多。
  2. 同一套权重到处用:同一个「小探测器」在整幅图上滑,重复用同一组数。这叫权值共享,参数再砍一截,而且对图像平移之类的情况更稳。

图像领域 CNN 火了很久。它说明一件事:网络结构不是越复杂越好,而是要符合数据的样子。 文本后来也走过类似思路——先为序列设计结构(比如 RNN),再演进到更适合并行训练的 Transformer。


RNN / LSTM:先学会「读一串字」

文字、语音、股价,都是按顺序来的循环神经网络(RNN)的思路是:每一步不光看当前输入,还带上上一步留下的状态,像带一点「短期记忆」。

问题是:句子一长,很久以前的信息很难传到最后,训练时还容易梯度消失(可以理解为误差传回去时越变越弱)。于是有了 LSTM、GRU 这类带「门」的结构,专门缓解长距离依赖——在 Transformer 流行之前,做翻译、语音识别很多都用它们。

你只要记住:RNN 系是「一个字一个字往后啃」;后来大家发现,这种啃法并行度差、长句吃力,才更积极地去用「注意力」那一套。


NLP 几条线,串到「下一词预测」

和 LLM 关系比较大的几件事,用大白话说一下:

  • 统计语言模型:数一数词挨着出现的频率,猜下一个词——这是最朴素的「下一词」思想。
  • 词向量(如 Word2Vec):把每个词变成一串数字(向量),意思相近的词,在空间里离得近。神经网络处理文本时,终于有了一个稳定的输入格式。
  • Seq2Seq(编码器–解码器):一端读进去,一端生成出来,典型如机器翻译;后面又加上注意力,让生成时不是死背编码,而是能「回头看」原文不同位置。

这些都在解决同一件事:文本怎么变成计算机能学的表示,以及长距离信息怎么传。 搞顺了,你就容易理解为什么 Transformer 会出现。


Transformer:为什么现在大家言必称它

2017 年的论文《Attention Is All You Need》提出了 Transformer。用人话概括几个关键点:

(1)自注意力:每个位置都能「看见」其它位置

不像 RNN 那样严格按顺序传,自注意力让一句话里任意两个词可以直接建立联系(权重由内容算出来)。长距离依赖比纯 RNN 自然很多。

(2)并行训练更友好

RNN 一步一步算,GPU 很难吃满;Transformer 这种结构更容易一块算,大模型时代这点特别重要。

(3)顺序不能丢:位置编码

注意力本身不区分「谁在前谁在后」,所以必须额外告诉模型每个词的位置(正弦编码或可学习位置嵌入等)。你可以记:没有位置信息,句子就是一袋词,顺序乱了模型也不知道。

(4)多头注意力

同一层里做好几套「注意力」,再合起来,相当于让模型从多个角度看同一句话(有的偏语法,有的偏指代……不必记死,知道「多头 = 多视角」即可)。

(5)一块块叠起来

典型是一块里:注意力 → 简单的前馈网络(可以理解为每个位置各自过一个小 MLP)→ 加残差、做归一化,然后堆很多层。你听说过的 BERT 偏编码、GPT 偏解码生成,都是在这套骨架上换花样,再加大规模数据和算力。

今天说的 LLM,绝大多数就是 Transformer 这一脉 练出来的大模型——差别主要在数据、规模、对齐方式,而不是换一个完全不同的数学物种。


vLLM 采样参数详解:Temperature、Top-p、Top-k

模型算完一轮,得到的是每个候选 token 的 logits(可以理解为「原始得分」,还没变成概率)。真正吐出下一个词之前,还要经过:缩放 → 变成概率 →(可选)裁掉一批低概率候选 → 再按概率随机抽一个
Temperature、Top-p、Top-k 就是在这条链路上动手脚的三类旋钮——vLLM、OpenAI API、HuggingFace 推理里名字大同小异,直觉是共通的。

下面分开说「人话版含义」,再给一段 NumPy 演示代码,把流程跑通一遍(不是 vLLM 源码,只为理解数学过程)。
说明: 真实推理框架里,Top-k 与 Top-p 谁先谁后、是否和论文里的 nucleus 实现细节完全一致,以各库文档为准;这里演示用的是一种常见教学顺序:先 temperature + softmax,再 top-p,再 top-k,最后归一化采样

Temperature(温度)在干什么

  • logits 会先除以 temperature,再去做 softmax 变成概率。
  • 温度越低(比如 0.2、0.5):概率分布越「尖」,最高分那几个 token 更占优势,输出更稳、更像复读机,创意变少。
  • 温度越高(比如 1.2、1.5):分布更「平」,低分 token 也有机会被抽到,输出更随机、更发散,但也更容易胡说八道。
  • 温度 = 1:一般不改变 logits 的相对比例(只是除以 1),相当于「按模型原始自信程度采样」。

一句话:温度调的是「有多敢乱选」——低则保守,高则放飞。

Top-p(核采样,Nucleus Sampling)

  • 先把所有 token 按概率从大到小排队,从队首开始累加概率,直到累加和超过你设的 top_p(例如 0.9),就只在这「一小块高概率集合」里玩。
  • 直觉:不看长尾里那一堆几乎不可能的词,避免模型在很离谱的选项里抽奖;top_p 越小,圈子缩得越狠,输出越稳。
  • top_p = 1不裁,保留全体 token(在 softmax 之后、若不再配合极小 top_k,理论上仍可能抽到极低概率词)。

一句话:Top-p 是「只保留概率质量够集中的一坨」,动态决定保留几个候选。

Top-k

  • 只保留概率最高的 k 个 token,其余直接扔掉,再在这 k 个里归一化、采样。
  • k 越小:越死板、越可预测;k 越大:候选越多,多样性上来,但计算和随机性都增加。
  • 常和 Top-p 一起用:先缩小候选池,再控制随机性(具体顺序以引擎为准)。

一句话:Top-k 是「硬截断:最多只留前 k 名」。

和「贪心解码」对比

  • 贪心(greedy):每一步永远选概率最大的那个 token——没有随机性,温度、top-p、top-k 往往就失去意义(或退化成只留一个候选)。
  • 采样(sampling):按概率抽,才有「同一句上文,每次回答略不同」的体验。

演示代码(综合 temperature + top-p + top-k)

下面用一组虚构的 logits 走完整流程,你可以改 temperaturetop_ptop_k 看多轮采样的频率变化。

## vLLM采样参数详解(教学演示)

import numpy as np
from collections import Counter
import random


def demonstrate_sampling_logic(temperature=1.0, top_p=1.0, top_k=None):
    """
    演示综合采样逻辑:同时使用temperature、top_p和top_k参数

    采样流程:
    1. 应用temperature缩放logits
    2. 应用softmax得到概率分布
    3. 应用top_p过滤(核采样)
    4. 应用top_k过滤
    5. 重新归一化并采样

    Args:
        temperature: 温度参数,控制随机性(默认1.0)
        top_p: 核采样参数,累积概率阈值(默认1.0,即不限制)
        top_k: Top-k参数,保留概率最高的k个token(默认None,即不限制)
    """
    # 模拟语言模型输出的logits(未归一化的概率)
    logits = np.array([3.2, 3.1, 2.1, 2.5, 1.8, 1.2, 0.5, -0.2, -1.0, -2.5])
    token_names = ['the', 'a', 'is', 'was', 'and', 'or', 'but', 'if', 'then', 'else']

    print("=" * 60)
    print("=== 综合采样逻辑演示 ===")
    print(f"参数设置: temperature={temperature}, top_p={top_p}, top_k={top_k}")
    print("=" * 60)
    print(f"\n原始logits: {logits}")
    print(f"Token名称: {token_names}\n")

    # 步骤1: 应用temperature缩放
    print("步骤1: 应用Temperature缩放")
    print("-" * 40)
    scaled_logits = logits / temperature
    print(f"缩放后logits: {scaled_logits}\n")

    # 步骤2: 应用softmax得到概率分布
    print("步骤2: 应用Softmax得到概率分布")
    print("-" * 40)
    exp_logits = np.exp(scaled_logits)
    probabilities = exp_logits / np.sum(exp_logits)

    print("温度调整后的概率分布:")
    for i, (token, prob) in enumerate(zip(token_names, probabilities)):
        print(f"  {token}: {prob:.4f}")
    print()

    # 步骤3: 应用top_p过滤(核采样)
    print(f"步骤3: 应用Top-p过滤 (top_p={top_p})")
    print("-" * 40)

    # 按概率从高到低排序
    sorted_indices = np.argsort(probabilities)[::-1]
    sorted_probs = probabilities[sorted_indices]
    sorted_tokens = [token_names[i] for i in sorted_indices]

    # 计算累积概率
    cumulative_probs = np.cumsum(sorted_probs)

    # 找到累积概率≤top_p的token
    if top_p < 1.0:
        valid_mask = cumulative_probs <= top_p
        if not np.any(valid_mask):
            # 如果累积概率都超过top_p,至少保留概率最高的token
            valid_mask[0] = True

        filtered_probs = sorted_probs[valid_mask]
        filtered_tokens = [sorted_tokens[i] for i in range(len(sorted_tokens)) if valid_mask[i]]

        print(f"Top-p过滤后选择的token数量: {len(filtered_tokens)}")
        print(f"选择的token: {filtered_tokens}")
        print(f"累积概率: {cumulative_probs[valid_mask]}")
    else:
        # top_p=1.0时不进行过滤
        filtered_probs = sorted_probs
        filtered_tokens = sorted_tokens
        print("Top-p=1.0,不进行过滤,保留所有token")

    print()

    # 步骤4: 应用top_k过滤
    print(f"步骤4: 应用Top-k过滤 (top_k={top_k})")
    print("-" * 40)

    if top_k is not None and top_k < len(filtered_tokens):
        # 只保留前top_k个token
        final_probs = filtered_probs[:top_k]
        final_tokens = filtered_tokens[:top_k]
        print(f"Top-k过滤后选择的token数量: {len(final_tokens)}")
        print(f"选择的token: {final_tokens}")
    else:
        final_probs = filtered_probs
        final_tokens = filtered_tokens
        if top_k is None:
            print("Top-k=None,不进行过滤")
        else:
            print(f"Top-k={top_k},但过滤后的token数量({len(filtered_tokens)})≤top_k,保留所有token")

    print()

    # 步骤5: 重新归一化并采样
    print("步骤5: 重新归一化概率分布并采样")
    print("-" * 40)

    normalized_probs = final_probs / np.sum(final_probs)

    print("最终归一化概率分布:")
    for token, prob in zip(final_tokens, normalized_probs):
        print(f"  {token}: {prob:.4f}")
    print()

    # 模拟多次采样
    random.seed(42)
    num_samples = 20
    samples = np.random.choice(final_tokens, size=num_samples, p=normalized_probs)
    sample_counts = Counter(samples)

    print(f"{num_samples}次采样结果:")
    print(f"  采样序列: {list(samples)}")
    print(f"  采样频率统计:")
    for token in final_tokens:
        count = sample_counts.get(token, 0)
        freq = count / num_samples
        print(f"    {token}: {count}次 ({freq:.2%})")

    print("\n" + "=" * 60)
    print("演示完成!")
    print("=" * 60 + "\n")


if __name__ == "__main__":
    demonstrate_sampling_logic(temperature=0.4, top_p=0.9, top_k=1)

怎么玩: 把上面代码存成 .py,装好 numpy 后运行。试着改三处:

  • 只动 temperature:看概率分布变尖还是变平;
  • 固定温度,改小 top_p:看候选 token 是否被砍掉一大截;
  • top_k=3:看最终是否只剩三个词在竞争。

Transformer 推理在干什么:自回归 + KV 缓存

训练好的生成式 LLM,推理时做的是一件事:反复「猜下一个 token」,直到凑够长度或遇到结束符。这叫自回归生成——每产出一位新词,就把它拼进上下文,再算下一位。

为什么不能每次都把整段话重新算一遍

最笨的办法:第 1 步输入 5 个 prompt token,第 2 步输入 6 个,第 3 步输入 7 个……每一步都对整段序列做一遍注意力。这样第 (n) 步的计算量大致随长度平方涨,长对话会慢到不可用。

KV 缓存就是为这个问题准备的:历史 token 的 Key、Value 在每一步已经算过了,而且只要上下文不变,它们就不会变。所以可以把每一层算出来的 K、V 存下来;新一步只给模型最新那一个 token,用它的 Q 去和「缓存里所有历史 K」算注意力,再聚合 V。这样每步只算新 token 对应的投影,长度很长时加速非常明显(vLLM、llama.cpp 等都在这条思路上做工程优化)。

为什么通常只缓存 K、V,不缓存 Q

  • Q 表示「当前这个位置在查谁」。每生成 token,只出现的查询向量,必须现算
  • K、V 表示「历史每个位置能提供什么信息」。已经生成过的 token 不会再变,所以 K、V 可以增量追加进缓存。

直观类比:图书馆里书架(K/V)已经摆好了;你每问一个新问题,只更新你手里的「问题卡」(Q),不用把书架重摆一遍。

实现时要注意的一处细节:位置编码

增量解码时,每一步喂给模型的往往只有长度 1input_ids(新 token)。如果还像「整段预填」那样只用 pos_encoding[:1],新 token 会永远被当成第 0 个位置,语义就错了。正确做法是按全局下标取位置向量,例如用 pos_offset 表示「当前片段在整句里的起点」。下面代码里用 pos_offset 做了这件事(很多教学 demo 会漏掉这一点)。

下面这段代码在演示什么

用 PyTorch 写了一个迷你 Transformer(多层自注意力 + 前馈 + LayerNorm),在 MultiHeadAttention 里实现了 K/V 拼接缓存demonstrate_transformer_inference 模拟:第一步吃完整 prompt,之后每步只吃新 token,并打印每层 K 缓存的长度是否在增长。需要本机已安装 PyTorch。完整脚本与下文一致,也可直接打开同目录下的 simple_transformer_kv_demo.py 运行。

"""
简单的Transformer推理框架演示
用于理解Transformer架构和KV缓存的工作原理
"""

import math
from typing import List, Optional, Tuple

import torch
import torch.nn as nn
import torch.nn.functional as F


class MultiHeadAttention(nn.Module):
    """多头注意力:Q/K/V 线性投影 + 缩放点积注意力;推理时可传入历史 K/V 缓存。"""

    def __init__(self, d_model: int, num_heads: int, d_k: Optional[int] = None):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_k if d_k is not None else d_model // num_heads

        # 将同一输入投影为 Q、K、V(自注意力时 query/key/value 同源)
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

        self.scale = math.sqrt(self.d_k)

    def forward(
        self,
        query: torch.Tensor,
        key: torch.Tensor,
        value: torch.Tensor,
        kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
    ) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
        batch_size, seq_len, _ = query.shape

        # Q 必须用当前输入现算(新 token 的「查询」);K、V 可与缓存拼接
        Q = self.W_q(query)
        K = self.W_k(key)
        V = self.W_v(value)

        if kv_cache is not None:
            K_cache, V_cache = kv_cache
            K = torch.cat([K_cache, K], dim=1)
            V = torch.cat([V_cache, V], dim=1)

        Q = Q.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
        K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)

        scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale
        attention_weights = F.softmax(scores, dim=-1)
        context = torch.matmul(attention_weights, V)

        context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
        output = self.W_o(context)

        # 返回本步之后「全长」K、V,供下一步继续追加
        new_kv_cache = (
            K.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model),
            V.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model),
        )

        return output, new_kv_cache


class FeedForward(nn.Module):
    def __init__(self, d_model: int, d_ff: int):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(0.1)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.linear2(F.relu(self.linear1(x)))


class TransformerBlock(nn.Module):
    def __init__(self, d_model: int, num_heads: int, d_ff: int):
        super().__init__()
        self.attention = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = FeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(0.1)

    def forward(
        self,
        x: torch.Tensor,
        kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
    ) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
        attn_output, new_kv_cache = self.attention(x, x, x, kv_cache)
        x = self.norm1(x + attn_output)
        ff_output = self.feed_forward(x)
        x = self.norm2(x + ff_output)
        return x, new_kv_cache


class SimpleTransformer(nn.Module):
    def __init__(self, vocab_size: int, d_model: int, num_layers: int, num_heads: int, d_ff: int):
        super().__init__()
        self.d_model = d_model
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoding = nn.Parameter(torch.randn(1000, d_model))

        self.layers = nn.ModuleList([TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers)])
        self.output_layer = nn.Linear(d_model, vocab_size)

    def forward(
        self,
        input_ids: torch.Tensor,
        kv_cache: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
        pos_offset: int = 0,
    ) -> Tuple[torch.Tensor, List[Tuple[torch.Tensor, torch.Tensor]]]:
        batch_size, seq_len = input_ids.shape

        x = self.embedding(input_ids)
        # 增量解码:按全局位置取 pos,避免 seq_len=1 时永远用第 0 个位置编码
        x = x + self.pos_encoding[pos_offset : pos_offset + seq_len].unsqueeze(0)

        new_kv_cache: List[Tuple[torch.Tensor, torch.Tensor]] = []
        for i, layer in enumerate(self.layers):
            layer_kv = kv_cache[i] if kv_cache is not None else None
            x, layer_new_kv = layer(x, layer_kv)
            new_kv_cache.append(layer_new_kv)

        logits = self.output_layer(x)
        return logits, new_kv_cache


def demonstrate_transformer_inference(num_steps: int = 8):
    vocab_size = 1000
    d_model = 128
    num_layers = 2
    num_heads = 4
    d_ff = 512

    model = SimpleTransformer(vocab_size, d_model, num_layers, num_heads, d_ff)
    model.eval()

    print("=== Transformer 推理演示 ===\n")
    print(f"词汇表: {vocab_size}, d_model: {d_model}, 层数: {num_layers}, 头数: {num_heads}\n")

    prompt = torch.tensor([[1, 2, 3, 4, 5]])
    print(f"初始提示词: {prompt[0].tolist()}")

    kv_cache = None

    for step in range(num_steps):
        print(f"\n--- 生成步骤 {step + 1} ---")

        if step == 0:
            current_input = prompt
            pos_offset = 0
        else:
            current_input = next_token
            pos_offset = prompt.shape[1] - current_input.shape[1]

        with torch.no_grad():
            logits, new_kv_cache = model(current_input, kv_cache, pos_offset=pos_offset)

        last_logits = logits[0, -1, :]
        next_token = torch.argmax(last_logits).unsqueeze(0).unsqueeze(0)

        print(f"  本步输入长度: {current_input.shape[1]}, pos_offset: {pos_offset}")
        print(f"  logits 形状: {tuple(logits.shape)}, 下一 token: {next_token.item()}")

        prompt = torch.cat([prompt, next_token], dim=1)
        kv_cache = new_kv_cache
        if kv_cache is not None:
            print(f"  第1层 K 缓存序列长度: {kv_cache[0][0].shape[1]}")

    print(f"\n最终序列: {prompt[0].tolist()}")


if __name__ == "__main__":
    demonstrate_transformer_inference(num_steps=8)

与本地可运行脚本 逐行一致 时,请以 simple_transformer_kv_demo.py 为准(含打印格式与 num_steps);上表为便于排版略有压缩,复制运行请直接运行该 .py 文件


收束:落到「你用的那个聊天模型」

前面从 MLP、CNN、RNN 讲到 Transformer,不是为了背名词,而是让你知道:LLM 不是天上掉下来的,是在「怎么表示文本、怎么传信息、怎么并行训练」这条路上长出来的。

落到日常做产品、调应用,记住三句就够:

  1. LLM 本质:在给定上下文的情况下,预测下一个 token(词片段)。
  2. 它不是搜索引擎:事实、时效、私有数据,要靠 RAG、工具、校验 补。
  3. 体验和成本:上下文怎么裁剪、输出怎么约束,直接决定效果和账单——这就是本系列后面会展开的 Token、Context 等话题。

小结(带走这张地图)

  • 神经网络:层层加权 + 非线性,通过调参数拟合任务。
  • MLP:最简单的全连接堆叠,是理解其它结构的底板。
  • CNN:利用局部性和权值共享,特别适合图像。
  • RNN/LSTM:按顺序处理序列,曾长期统治 NLP,但长句与并行是短板。
  • Transformer:用自注意力建立全局联系、易并行,成为现代 LLM 的主干。
  • 推理采样:logits → softmax 成概率后,用 temperature 调「保守/放飞」,用 top-p 裁掉长尾,用 top-k 限制候选个数,再抽样得到下一个 token(与 vLLM 等引擎里常见参数一一对应)。
  • 推理加速:自回归生成时复用历史 KV 缓存,避免每步对整段序列重复算 K/V;增量解码时别忘了给新 token 正确的位置编码(如 pos_offset)。

下一篇:《② Token》——为什么说 token 是成本、延迟和「能记多少」的硬约束。


延伸阅读(想啃原文时)


系列:龙虾的技术基础 ①/8

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐