领域专用小型语言模型——端到端 Transformer 微调

本章涵盖:

  • 将一个非常小的语言模型专门化,使其能够生成可运行的 Manim 代码
  • Transformer 微调中的超参数调优过程
  • 评估生成代码的质量

本章将通过一个端到端示例,演示如何针对特定任务微调一个小模型,也就是 GPT-2 small。这种方法同样适用于针对生成式任务微调任何类似 GPT 的模型。

3.1 数据准备

在本章中,我们将微调 GPT-2 small 模型,使其能够根据自然语言 prompt 生成可运行的 Manim 代码。Manim 是一个开源 Python 动画引擎,用于制作解释性数学视频------它可以通过程序化方式创建精确动画。图 3.1 展示了一个 Manim 代码示例及其渲染结果。

图 3.1 ------ Manim 代码示例,底部是代码,顶部是对应渲染结果

我选择 GPT-2 small 作为基线,不仅是因为它本身不能生成 Manim 代码,也是为了说明:你可以从一个小模型和一个高质量、领域专用的数据集开始,即便这个数据集相对较小,也仍然可以将模型专门化到某个任务,并获得良好性能。配套 Colab notebook 包含本章的完整源代码。需要硬件加速,也就是 GPU,免费层即可。

我们将使用 Hugging Face Hub 上的 manim_python 数据集。它有两个字段:instruction,也就是自然语言 prompt;以及 output,也就是对应的 Manim Python 代码。该数据集包含两个 split:train,有 599 个样本;test,有 51 个样本。

我们将使用第 2 章介绍过的 Hugging Face Datasets 库来处理数据集。首先需要下载它:

ini 复制代码
from datasets import load_dataset

dataset = load_dataset("Edoh/manim_python")

下面的代码片段展示了训练 split 中的一个样本:

rust 复制代码
{'instruction': "Create a new scene named 'MyScene'.", 'output': 'from
 manim import * class MyScene(Scene): def construct(self): pass'}

接下来,我们加载模型 tokenizer:

ini 复制代码
from transformers import GPT2Tokenizer

model_name = "openai-community/gpt2"
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

该数据集还不能直接用于微调和测试模型,因此我们将实现一个自定义预处理函数:

ini 复制代码
def preprocess_data(examples):
    inputs = [
        f"Instruction: {instr}\nOutput: {out}"
        for instr, out in zip(examples["instruction"], examples["output"])
    ]

    tokenized = tokenizer(inputs, truncation=True, max_length=512,
 ➥padding="max_length")

    tokenized["labels"] = tokenized["input_ids"].copy()
    return tokenized

preprocess_data 函数会把 instructionoutput 两列拼接起来,然后对结果进行分词。它会被应用到数据集中的每个样本,包括 train 和 test 两个 split:

ini 复制代码
tokenized_datasets = dataset.map(preprocess_data, 
                        batched=True, 
                        remove_columns=dataset["train"].column_names)

原始列会被删除,因为它们已经不再需要。

现在数据已经准备好了,我们可以开始准备模型训练。

3.2 微调

为了微调模型,我们将使用第 2 章介绍过的 Hugging Face Transformers 库,并结合 Optuna 进行超参数搜索。这个过程会系统性探索最合适的一组超参数,以提升模型性能。超参数不同于模型参数,它们并不会在训练阶段被模型自动学习。相反,它们是预先设定的。它们的精确配置会深刻影响模型性能,决定模型只是普通,还是表现出色。

Optuna 是一个与机器学习框架无关的开源 Python 库,用于自动超参数调优。它允许你使用标准 Python 条件语句、循环和语法来搜索最优超参数。由于 Optuna 得到了 Hugging Face 库的完整支持,我们将把它作为搜索后端使用,而不直接操作它的 API。

我们将定义一个自定义函数 model_init,用于超参数搜索,使每一次 trial 都从一个全新初始化的模型开始:

python 复制代码
from transformers import GPT2LMHeadModel

def model_init():
    return GPT2LMHeadModel.from_pretrained(model_name, device_map='auto')

接下来,在开始超参数搜索之前,我们配置训练参数:

ini 复制代码
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./gpt2-manim-python-finetuned",
    eval_strategy="epoch",
    save_strategy="epoch",
    logging_strategy="steps",
    logging_steps=100,
    save_total_limit=2,
    load_best_model_at_end=True,
    metric_for_best_model="eval_loss",
    greater_is_better=False,
    fp16=True, 
    report_to="none",
)

在前面的代码片段中,我们做了以下事情:

  • 指定用于保存 checkpoint 候选项的根目录路径
  • 选择 epoch 作为评估和保存策略
  • 选择 steps 作为日志策略,也就是每 100 step 报告一次指标
  • 设置单个 trial 中 checkpoint 保存数量的最大值
  • 选择评估准确率作为选择最佳模型的关键指标
  • 启用混合精度训练,也就是 FP16

因为我们设置了评估策略,所以会使用一部分训练数据进行评估。我们将拆分训练集,并保留 10% 的样本用于评估:

scss 复制代码
train_val_split =
➥tokenized_datasets["train"].train_test_split(test_size=0.1)
tokenized_datasets["train"] = train_val_split["train"]
tokenized_datasets["validation"] = train_val_split["test"]

现在,我们可以使用训练参数以及已经分词的训练和评估数据初始化 Trainer

ini 复制代码
from transformers import (
   Trainer, 
   DataCollatorForLanguageModeling,
   EarlyStoppingCallback
)

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, 
                                mlm=False)

trainer = Trainer(
    model_init=model_init,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    tokenizer=tokenizer,
    data_collator=data_collator,
    callbacks=[EarlyStoppingCallback(early_stopping_patience=2)],
)

我们还添加了一个 callback:如果某个训练 trial 连续两步没有改善,就停止该 trial。

我们将按如下方式定义超参数调优的搜索空间:

css 复制代码
def hp_space(trial):
    return {
        "learning_rate": trial.suggest_float("learning_rate", 
        ➥1e-5, 5e-4, log=True),
        "per_device_train_batch_size":
        ➥ trial.suggest_categorical("per_device_train_batch_size",
        ➥ [2, 4, 8]),
        "weight_decay": trial.suggest_float("weight_decay", 0.0, 0.3),
        "num_train_epochs": trial.suggest_int("num_train_epochs", 3, 6),
        "warmup_steps": trial.suggest_int("warmup_steps", 0, 500),
        "gradient_accumulation_steps":
        ➥ trial.suggest_categorical("gradient_accumulation_steps",
        ➥ [1, 2, 4]),
    }

我们定义了一个函数,它接收一个 trial 对象作为输入,Optuna 将使用它来采样超参数值。在 hp_space 函数体中,我们指定取值范围来定义搜索空间。这里的超参数包括学习率、每个设备上的 batch size、权重衰减、训练 epoch 数、warmup steps,以及梯度累积步数。其中一些范围是为了适配 Colab 免费层虚拟机中可用的计算能力,也就是单张 NVIDIA Tesla T4。在其他硬件环境下,一些超参数,例如 batch size 或梯度累积步数,可能需要不同的值或范围。

注意

梯度累积步数是指,在模型权重更新之前,梯度会跨多少个 mini-batch 进行累积。这可以在不超过 GPU 内存限制的情况下实现更大的有效 batch size,尤其适合 GPU 内存紧张的情况。

现在可以运行超参数搜索了。这里我们将指定后端引擎,本例中为 Optuna;trial 数量;以及用于比较 trial 的指标,本例中是 evaluation loss:

ini 复制代码
best_run = trainer.hyperparameter_search(
    direction="minimize",
    backend="optuna",
    n_trials=10,
    hp_space=hp_space,
    compute_objective=lambda metrics: metrics["eval_loss"],
)

搜索完成后,我们可以使用存储在 best_run 变量中的最佳超参数配置 trainer:

ini 复制代码
for key, value in best_run.hyperparameters.items():
    setattr(training_args, key, value)

trainer = Trainer(
    model_init=model_init,
    args=training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets.get("validation"),
    tokenizer=tokenizer,
    data_collator=data_collator,
    callbacks=[EarlyStoppingCallback(early_stopping_patience=2)],
)

最后,我们可以开始调优模型:

scss 复制代码
trainer.train()

训练完成后,我们会把微调后的模型和 tokenizer 保存到磁盘,这样就可以重新加载它们,用于测试或生成 Manim 代码:

arduino 复制代码
trainer.save_model("./gpt2-manim-python-finetuned")
tokenizer.save_pretrained("./gpt2-manim-python-finetuned")

接下来,我们将看看如何测试模型。

3.3 测试微调后的模型

为了在测试集上测试微调后的模型,我们首先从磁盘加载它,并放入 GPU 内存:

ini 复制代码
import torch

model_dir = "./gpt2-manim-python-finetuned"
tokenizer = GPT2Tokenizer.from_pretrained(model_dir)
model = GPT2LMHeadModel.from_pretrained(model_dir)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

model.eval()

接下来,我们定义一个自定义函数 generate_output。我们会在测试数据集中每个 prompt,也就是 instruction 列上运行它,以生成期望的 Manim 代码,而 output 列就是我们的 ground truth。该函数将 prompt 作为第一个参数,同时接受最大生成长度,以及 temperature、解码策略等可选参数,本例中包括 beam search 和 nucleus sampling:

ini 复制代码
def generate_output(instruction, max_length=150, num_beams=5,
➥ temperature=0.7, top_p=0.9, repetition_penalty=1.2):

prompt 被分词之后:

ini 复制代码
prompt = f"Instruction: {instruction}\nOutput:"
input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)

从输入文本生成 Manim 代码的过程如下:

ini 复制代码
generated_ids = model.generate(
        input_ids,
        max_length=max_length,
        num_beams=num_beams,
        temperature=temperature,
        top_p=top_p,
        repetition_penalty=repetition_penalty,
        do_sample=True,
        pad_token_id=tokenizer.eos_token_id,
        eos_token_id=tokenizer.eos_token_id,
        early_stopping=True,
        no_repeat_ngram_size=2,
    )

生成的 token 会被解码,并跳过特殊 token:

ini 复制代码
generated_text = tokenizer.decode(generated_ids[0], 
            skip_special_tokens=True)

最后,生成文本会返回给调用者:

ini 复制代码
output_start = generated_text.find("Output:")
if output_start != -1:
    output_text = generated_text[output_start + len("Output:"):].strip()
else:
    output_text = generated_text.strip()

    return output_text

我们会把测试结果持久化到 CSV 文件中,以便后续更容易分析:

ini 复制代码
import csv

output_csv = "gpt2_manim_python_test_outputs.csv"
with open(output_csv, mode="w", newline="", encoding="utf-8") as csvfile:
    writer = csv.DictWriter(csvfile, 
        fieldnames=["instruction", "reference_output", "generated_output"])
    writer.writeheader()

我们会遍历测试样本,调用 generate_output 函数为每条 instruction 生成 Manim 代码,并把结果连同 prompt 和 ground truth 一起追加到 CSV 文件中:

css 复制代码
for example in dataset['test']:
    instruction = example["instruction"]
    reference_output = example["output"]

    generated_output = generate_output(instruction)

    writer.writerow({
        "instruction": instruction,
        "reference_output": reference_output,
        "generated_output": generated_output,
    })

下面分析一些有意义的例子。这些例子是在按前述方式完成两轮超参数搜索和微调后,由微调模型在测试时生成的。

给定如下 prompt:

bash 复制代码
Scale the triangle by a factor of 2 in 2 seconds.

模型生成了如下 Manim 代码:

scss 复制代码
from manim import *
 
class MyScene(Scene): 
  def construct(self): 
    triangle = RegularPolygon(n=3, radius=2, color=YELLOW) 
    self.add(triangle) 
    self.play(triangle.animate.scale(2), run_time=2) 
    rectangle.move_to((UP * 1. 5))

前面的代码除了最后一行之外都是正确的。删除最后一行后,代码就会按 prompt 指定的方式运行:它创建一个 Manim scene,添加一个三角形,并在 2 秒内把三角形尺寸放大一倍。

再看另一个例子。考虑如下 prompt:

css 复制代码
Create a point at coordinates (0, 0) and color it pink.

模型生成了如下 Manim 代码:

arduino 复制代码
from manim import *
 
class MyScene(Scene): 
  def construct(self): 
    point = Point(width=4, height=2) color=PINK) 
    self.add(point)

前面的代码可以运行,并且满足 prompt 的要求,除了该点被放在了 (4, 2),而不是要求的 (0, 0)。在这个实验的多次测试中,微调模型经常难以为 Manim 对象设置数值参数。

在这个实验中,整体幻觉率较低,并且主要限于刚才描述的两种情况。我们可以通过运行更多超参数搜索 trial 来降低,甚至消除这些问题,至少 3 次;对 manim_python 数据集来说,5 次可能最好。也可以尝试不同采样策略,降低最大生成 token 数,或者两者一起使用。即便如此,这个例子仍然说明,一个在经过整理的领域专用数据集上训练的小模型,可以很快获得某个领域或任务中的专业能力。

3.4 领域专用评估

许多统计指标都可以在测试阶段评估语言模型的性能。这些指标能为通用领域任务提供有用洞察,尤其是那些只基于非结构化自然语言的任务。但当你在特定领域进行 benchmark 时,无论模型是大是小,是否经过微调,只要输出偏离人类语言,你仍然需要领域专家,也就是 SME,进行定性审查,有时还需要外部策略和工具。基于本章讨论过的 Manim 数据集微调模型,我们将看看一些下游策略,用于自动化评估生成代码,并减轻 SME 的负担。

在这个案例中,一个完整评估流水线可以包括以下自动化步骤:

  • 语法检查------生成内容是 Python 代码,因此必须确保它在语法上有效。
  • Manim API 使用的静态分析------解析生成代码,检查预期的 Manim class、method 或 function call。
  • 在确认生成的 Manim 代码语法有效之后,需要验证它是否能够按预期完成执行。
  • 可选项------验证渲染结果是否正确,因为 Manim 输出始终是图像或动画。

你可以使用 Python 内置的 ast 包,通过自定义函数实现语法检查:

python 复制代码
import ast

def is_syntax_valid(code_str):
    try:
        ast.parse(code_str)
        return True, ""
    except SyntaxError as e:
        return False, str(e)

注意

Python 的 ast 包,也就是 Abstract Syntax Trees,可以让你处理 Python 的抽象语法树。我们将在第 7 章中,在使用 SLM 生成 Python 代码的语境下介绍它。

is_syntax_valid 函数会调用 ast.parse。如果生成代码是有效 Python 语法,就返回 True;否则返回 False。它只检查语法,不检查代码是否使用了某个特定 API,但这是一个有用的第一步,可以在运行更复杂检查之前捕获无效模型响应。

下一步是针对 Manim API 的静态分析。它仍然会使用 ast,但会针对被测试的包进行定制。为此,我们可以创建一个专用类,继承 ast 模块中的 NodeVisitor

ini 复制代码
from ast import NodeVisitor

class ManimCodeAnalyzer(ast.NodeVisitor):
    def __init__(self):
        self.imports_manim = False
        self.scene_subclass_names = []
        self.play_calls = 0
        self.create_calls = 0
        self.errors = []

这个类包含几个 utility function,它们会覆盖父类的 visit 方法,并且必须以 visit_ 前缀命名。第一个函数会验证生成代码中是否直接导入了 Manim 包:

ruby 复制代码
def visit_Import(self, node):
    for alias in node.names:
        if alias.name == "manim":
            self.imports_manim = True
    self.generic_visit(node)

另一个函数会检查是否通过 from ... import 的方式导入了 Manim 元素:

ruby 复制代码
def visit_ImportFrom(self, node):
    if node.module and node.module.startswith("manim"):
        self.imports_manim = True
    self.generic_visit(node)

Manim 程序通常被定义为一个 Scene 类,或者其子类。因此添加一个函数来检查生成的类是否是 SceneScene 的子类,会很有用:

python 复制代码
def visit_ClassDef(self, node):
    for base in node.bases:
        if isinstance(base, ast.Name) and base.id == "Scene":
            self.scene_subclass_names.append(node.name)
        elif isinstance(base, ast.Attribute):
            if base.attr == "Scene":
                self.scene_subclass_names.append(node.name)
    self.generic_visit(node)

最后,我们需要一个函数来统计 Scene 子类中对 play 方法的调用,以及 Manim Create 初始化的调用次数:

python 复制代码
def visit_Call(self, node):
    if isinstance(node.func, ast.Attribute):
        if (isinstance(node.func.value, ast.Name) and 
        ➥node.func.value.id == "self"
            and node.func.attr == "play"):
            self.play_calls += 1

    if isinstance(node.func, ast.Name) and node.func.id == "Create":
        self.create_calls += 1

    self.generic_visit(node)

现在可以定义一个使用 ManimCodeAnalyzer 类的函数:

scss 复制代码
def analyze_manim_code(code_str):
    analyzer = ManimCodeAnalyzer()

analyze_manim_code 首先检查输入代码的 Python 语法:

python 复制代码
try:
    tree = ast.parse(code_str)
except SyntaxError as e:
    return {
        "syntax_valid": False,
        "syntax_error": str(e),
        "imports_manim": False,
        "scene_subclass_names": [],
        "play_calls": 0,
        "create_calls": 0,
    }

如果 Python 代码无效,执行会停止,并且函数会返回关于所发现问题的洞察。如果代码有效,函数会使用自定义 analyzer 对它进行 Manim API 解析:

arduino 复制代码
analyzer.visit(tree)

最后,它返回所执行静态分析的摘要:

python 复制代码
return {
        "syntax_valid": True,
        "syntax_error": None,
        "imports_manim": analyzer.imports_manim,
        "scene_subclass_names": analyzer.scene_subclass_names,
        "play_calls": analyzer.play_calls,
        "create_calls": analyzer.create_calls,
    }

举例来说,给定如下 Manim 代码片段:

python 复制代码
from manim import *

class MyScene(Scene):
    def construct(self):
        circle = Circle()
        self.play(Create(circle))

analyze_manim_code 函数会返回如下结果:

vbnet 复制代码
syntax_valid: True
syntax_error: None
imports_manim: True
scene_subclass_names: ['MyScene']
play_calls: 1
create_calls: 1

执行生成代码可以遵循如下工作流:

  1. 将生成代码保存到 .py 文件中。
  2. 运行 Manim CLI 来渲染 scene。
  3. 检查渲染是否成功。

你可以通过定义一个自定义函数来覆盖这三个步骤。该函数会把生成的 Manim 代码片段和 Scene 类名称作为输入:

ini 复制代码
def evaluate_manim_code(code_str, scene_class_name="CustomScene"):

首先,输入的 Manim 代码,也就是 code_str,被保存到文件中:

python 复制代码
import os
import tempfile

with tempfile.TemporaryDirectory() as tmpdir:
        code_path = os.path.join(tmpdir, "generated_scene.py")
        with open(code_path, "w") as f:
            f.write(code_str)

接下来,设置运行 Manim CLI 的命令:

ini 复制代码
cmd = [
        "manim",
        "-ql",
        code_path,
        scene_class_name,
]

它会执行该 Python 文件以渲染 scene:

ini 复制代码
import subprocess

try:
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
    success = result.returncode == 0
    output = result.stdout + "\n" + result.stderr
except subprocess.TimeoutExpired:
    success = False
    output = "Timeout expired during rendering."

在 CLI 配置中,我们使用了 -ql,也就是 low quality 选项,以降低计算负担并加快测试速度。

接下来,使用语法检查步骤中相同的 Manim 片段,验证生成代码是否可以正确运行:

ini 复制代码
generated_code = """
from manim import *

class MyScene(Scene):
    def construct(self):
        circle = Circle()
        self.play(Create(circle))
"""

我们使用 evaluate_manim_code 验证生成的 Manim 片段是否可以正确运行。以下代码:

lua 复制代码
success, output = evaluate_manim_code(generated_code, 
              scene_class_name="MyScene")
print("Render success:", success)
print("Output:", output)

会产生类似如下输出:

vbnet 复制代码
Render success: True
Output: Manim Community v0.19.0

[08/04/25 14:03:42] INFO     Animation 0 : Using cached
cairo_renderer.py:89
                             data (hash : 
                             1185818338_3789429803_22313245 
                             7) 
                    INFO     Combining to Movie file.
 scene_file_writer.py:739
                    INFO                                
scene_file_writer.py:886
                             File ready at 
                             '/content/media/videos/gen 
                             erated_scene/480p15/MyScen 
                             e.mp4' 
                    INFO     Rendered MyScene
scene.py:255
                             Played 1 animations

运行 evaluate_manim_code 也会把生成的 scene 或动画保存为本地 MP4 文件。如果你的测试数据集包含参考视频或图片,你可以提取帧或缩略图,计算生成图像与参考图像之间的相似度指标,例如 SSIM 或 LPIPS,并将它们与质量阈值进行比较。这类验证超出了本章范围,但你可以通过把这些指标与自己选定的阈值进行检查来实现它。

注意

结构相似性指标,也就是 SSIM,用于预测数字电视、电影图像以及其他数字图像和视频的感知质量。它也可以衡量两张图像之间的相似性。Python 图像处理包 scikit-image 提供了可直接使用的实现。Learned Perceptual Image Patch Similarity,也就是 LPIPS,是一种感知相似度指标,使用深度网络激活来衡量图像 patch 之间的距离。它也可以作为图像优化中的感知损失。

以下是针对这类任务验证执行和渲染正确性的一些最终建议:

  • 沙箱化------运行任意生成代码是有风险的。使用沙箱工具,例如 Docker 容器或受限环境,来隔离执行。
  • 超时------设置超时,以防止挂起或无限循环。
  • 资源限制------在渲染过程中限制 CPU、GPU 和内存使用,以节省时间和金钱。

到这里,我们完成了关于如何针对特定领域和任务调优 SLM 的介绍。下一章中,我们将把重点转向本书的核心主题:在硬件受限环境中进行 SLM 优化、压缩和部署。

总结

  • GPT-2 small 模型可以用几百个样本的数据集进行领域专用任务微调。
  • Manim 是一个 Python 动画引擎,用于通过代码创建数学可视化。
  • 在为语言模型训练进行分词之前,要在预处理阶段拼接 instruction 和 output 字段。
  • Optuna 可以自动化超参数搜索,并与 Hugging Face Transformers 库集成。
  • 在搜索期间,模型初始化函数会为每个超参数 trial 创建一个全新的模型实例。
  • 训练参数会设置评估策略、日志间隔、checkpoint 保存和混合精度训练。
  • 当验证损失停止改善时,early stopping callback 会停止训练,以防止过拟合。
  • 超参数搜索空间定义学习率、batch size、权重衰减、epoch 数以及其他参数的范围。
  • 你可以使用 Python 的 ast 模块验证生成代码的语法。
  • 静态分析可以验证生成代码中特定领域 API 的使用模式。
  • 自定义 NodeVisitor 类可以扩展 ast 功能,用于分析框架专用代码结构。
  • 执行测试会通过命令行工具运行生成代码,以验证功能正确性。
  • 渲染验证会通过相似度指标,将生成输出与参考图像进行比较。
  • 在自动运行生成代码时,沙箱化和超时可以提高安全性。
  • 小型语言模型在经过整理的领域专用数据集上,能够很好地完成专门化任务。
  • 自动化评估流水线可以减少领域专家评估代码质量的负担。
相关推荐
风雨中的小七19 小时前
和AI一起搞事情#6. 如何实现图片文字元素编辑?
人工智能·llm
Komorebi_999919 小时前
LangChain Day2 课程:提示词模板 + Chain 链精讲
大模型·llm
程序员三明治20 小时前
【AI】Tika:一次文档解析引擎的工程实践
java·人工智能·大模型·llm·后端开发·rag·tika文件解析
冬奇Lab1 天前
Agent系列(四):工具调用深度解析——Agent 的手和眼
人工智能·llm
冬奇Lab1 天前
一天一个开源项目(第111篇):Understand Anything - 把代码库变成可探索知识图谱的 AI 引擎
人工智能·开源·llm
养肥胖虎1 天前
完整学习LLM(四):Token是什么
大模型·llm·token·学习路线
qcx231 天前
【系统学AI】03 LLM训练全流程:预训练→SFT→对齐五条路线
人工智能·llm·sft·预训练·奖励模型·对齐·路线
koharu1232 天前
CrewAI :多智能体开发
人工智能·llm·agent·crewai
qcx232 天前
【系统学AI】02 token机制全解:LLM如何‘读懂‘人类语言
人工智能·llm·产品经理·token·费用·deepseek