构建AI智能体:八十九、Encoder-only与Decoder-only模型架构:基于ModelScope小模型的实践解析

一、前言

在大模型蓬勃发展的今天,我们天天被动输入,一度对这个名字都耳熟能详,但对于主流架构可能还没有接触的很深,大模型的Encoder-only与Decoder-only两大架构犹如两条截然不同的技术路径,也是我们需要关注深耕的,简单来说,Encoder-only模型如同一位深度分析师,它擅长一次性理解整段文本的完整含义,在情感分析、文本分类等理解型任务上表现卓越;而Decoder-only模型则更像一位创意写手,它通过自回归的方式逐词生成内容,在文本创作、对话系统等生成型场景中游刃有余。

这种技术路线的分层,直接映射到开发者面临的实际抉择:是专注于构建需要深度语义理解的企业应用,如智能客服、内容审核、知识问答系统?还是投身于需要创造性输出的产品开发,如智能写作助手、个性化对话机器人、代码生成工具?对于企业和个人开发者而言,这个选择远不止于技术偏好,更关系到产品方向、资源投入和职业发展路径。选择Encoder-only路线,意味着深耕自然语言理解领域,为企业提供精准的文本分析能力;选择Decoder-only路径,则是在内容生成的前沿阵地上开拓创新,为用户创造全新的交互体验。

今天我们将从实际应用出发,深入解析两种架构的特点,传统行业需要的不是华而不实的对话机器人,而是能够精准理解合同条款、快速分析客户反馈、智能识别风险信息的实用工具。这正是Encoder-only模型的用武之地,它如同一位经验丰富的分析师,能够透过文字表面,洞察深层含义。然而,市场同样渴求创新。内容创作、智能客服、代码生成等场景需要模型具备持续创作能力,这时候Decoder-only架构展现出独特价值。它就像不知疲倦的创作者,能够根据简单提示生成丰富内容。

二、Encoder-only 架构

1. 基础理解

对比我们的语文考试中阅读理解题目,好比我们阅读了一篇文章,然后进行分析后回答一些问题,Encoder-only 模型就是这样一位阅读理解大师。它的工作方式是:

  • 一次性读取整段文本。
  • 同时分析每一个字、每一个词与全文所有其他部分的关系。
  • 为每个词生成一个包含了全文信息的深度理解版向量。

Encoder-only 模型像一个拥有超级大脑的读者,它拿到一篇文章后,不是从头读到尾,而是瞬间通览全文,深刻理解其内涵,为了让阅读理解成为可能,Encoder-only 模型的核心是一种叫做 "双向注意力机制" 的技术。

让我们用一个简单的例子来理解它:

  • 句子:"苹果公司发布了新款手机。"
  • 任务:理解"苹果"这个词在这里是什么意思。

我们通常的思考方式:

  • 首先会看向"苹果"后面的词:"公司"、"发布"、"手机"。
  • 通过这些上下文,我们会立刻明白,这里的"苹果"指的是科技公司,而不是一种水果。

Encoder-only 模型是思考方式:

  • 模型会为句子中的每个词计算一个"注意力分数",表示它应该多"关注"其他词。
  • 对于 "苹果" 这个词,模型会计算出它与 "公司"、"发布"、"手机" 有很高的注意力分数。
  • 同时,"公司" 这个词也会高度关注 "苹果"。
  • "手机" 也会关注 "新款"、"发布" 和 "苹果"。

这个过程是同时发生的、双向的,就像一张所有词语相互连接的网络。

一个简化的注意力模式示意图

句子: [CLS] 苹果 公司 发布 了 新 手机 [SEP]

注意力模式 (所有词相互关注):

CLS\] → 苹果, 公司, 发布, 了, 新, 手机, \[SEP

苹果 → [CLS], 公司, 发布, 了, 新, 手机, [SEP]

公司 → [CLS], 苹果, 发布, 了, 新, 手机, [SEP]

发布 → [CLS], 苹果, 公司, 了, 新, 手机, [SEP]

... (以此类推)

每个词都可以与句子中的所有其他词进行"交流",获取全局信息。

2. 代表模型

BERT 是 Encoder-only 架构最著名的代表,它的出现彻底改变了自然语言处理领域,Encoder-only模型的预训练目标与生成式模型有本质不同,其核心是深度理解而非生成。

BERT 的训练过程,BERT 通过两个有趣的方式来学习语言:

进行"完形填空"(掩码语言模型 - MLM)

  • 方法:随机遮盖出一句话中15%的词(例如,把"今天天气很好"变成"今天天气[MASK]")。
  • 任务:让模型根据上下文的所有词(包括后面的"好"),来预测被遮盖的词是什么。
  • 这正是双向注意力的威力所在! 模型可以利用后半句的"好"来帮助预测前面的"[MASK]"是"很"。

直接"判断上下句"(下一句预测 - NSP)

  • 方法:给模型两句话,让它判断第二句话是否是第一句话的下一句。
  • 任务:帮助模型理解句子之间的关系,对于问答、推理任务至关重要。

通过海量文本上的这两种训练,BERT 学会了单词的深层含义、语法结构以及逻辑关系。

3. 核心架构

Encoder-only模型的核心是Transformer的编码器部分。要理解它,我们需要先回顾其核心工作机制:

1.1 自注意力机制:

  • 核心功能:在处理一个词时,能够同时关注到输入序列中的所有其他词,并计算它们与当前词的相关性权重。
  • 与Decoder的区别:Encoder使用的是双向自注意力,即每个词可以关注到其左侧和右侧的所有上下文信息。这使得它能深度、全面地理解整个句子的含义。

1.2 位置编码:

  • 由于自注意力机制本身不包含顺序信息,需要通过位置编码为输入序列中的每个词注入其位置信息,使模型能够理解词的顺序。

1.3 前馈神经网络:

  • 每个编码器层中的另一个核心组件,对自注意力层的输出进行非线性变换,增加模型的表达能力。

一个典型的Encoder-only模型(如BERT)由多层(例如12层、24层)上述结构堆叠而成。

4. 体现优势

双向上下文理解:

  • 这是其最核心的优势。在理解任务中,一个词的含义往往由其前后文共同决定。Encoder-only架构能同时利用所有上下文信息,生成高质量的"上下文化词向量"。

擅长自然语言理解任务:

  • 由于其强大的理解能力,它在各类NLU任务上表现极其出色,如文本分类、情感分析、命名实体识别、问答等。

对于理解任务非常高效:

  • 在推理阶段,对于整个输入序列,只需进行一次前向传播即可得到所有词的表示,然后可以直接用于分类或标注等任务,计算效率很高。

5. 局限性

不擅长文本生成:

  • 这是其最显著的短板。由于其双向注意力机制,在生成下一个词时,模型理论上可以"看到"未来的词,这会导致训练和推理的不一致。它没有像Decoder那样的自回归生成机制,因此无法进行流畅的、连续的文本生成。

计算开销(对长文本):

  • 自注意力机制的计算复杂度与序列长度的平方成正比。处理非常长的文档时,计算和内存开销会非常大。

6. 应用场景

Encoder-only模型是典型的理解型模型,广泛应用于以下领域:

  • 文本分类:主要用于情感分析、新闻分类、垃圾邮件检测,在序列的[CLS]标记输出上接一个分类器。
  • 序列标注:进行命名实体识别、词性标注,对序列中的每一个词对应的输出向量进行分类。
  • 语义相似度:针对语句进行重复问题识别、语义检索,比较两个句子编码向量的相似度,如余弦相似度。
  • 问答系统:进行抽取式问答,根据问题和文章,预测答案在文章中的起始和结束位置。
  • 自然语言推理:判断文本间的逻辑关系,判断两个句子是蕴含、矛盾还是中立关系。

7. 示例分析

7.1 句子对相似度比较

python 复制代码
from transformers import BertTokenizer, BertForSequenceClassification
from modelscope import snapshot_download
import torch
from torch.nn.functional import cosine_similarity

# 1. 加载模型和分词器
cache_dir = "D:\\modelscope\\hub"
model_name = "google-bert/bert-base-chinese"
local_model_path = snapshot_download(model_name, cache_dir=cache_dir)
tokenizer = BertTokenizer.from_pretrained(local_model_path)
# 对于相似度任务,我们也可以使用专门为句向量优化的模型,如 sentence-transformers
# 这里我们用基础BERT演示获取句向量的方法
model = BertForSequenceClassification.from_pretrained(local_model_path)
model.eval()

# 2. 准备句子对
sentence_pairs = [
    ("今天天气怎么样?", "我想知道今天的天气状况。"), # 语义相似
    ("苹果是一种水果。", "特斯拉是电动汽车公司。"), # 语义不相似
    ("深度学习需要大量数据。", "机器学习对数据量有要求。") # 语义相似
]

def get_sentence_embedding(text, model, tokenizer):
    """获取句子的向量表示(使用 [CLS] 标记的输出)"""
    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=128)
    with torch.no_grad():
        outputs = model(**inputs, output_hidden_states=True)
    # 取最后一层隐藏状态中 [CLS] 标记对应的向量作为句向量
    last_hidden_states = outputs.hidden_states[-1]
    cls_embedding = last_hidden_states[:, 0, :] # [batch_size, hidden_size]
    return cls_embedding

# 3. 计算并比较相似度
print("句子对相似度比较:")
print("=" * 60)

for sent1, sent2 in sentence_pairs:
    emb1 = get_sentence_embedding(sent1, model, tokenizer)
    emb2 = get_sentence_embedding(sent2, model, tokenizer)
    
    # 计算余弦相似度
    sim = cosine_similarity(emb1, emb2).item()
    
    print(f"句子 A: 「{sent1}」")
    print(f"句子 B: 「{sent2}」")
    print(f"余弦相似度: {sim:.4f}")
    if sim > 0.7:
        print("结论: 语义相似")
    else:
        print("结论: 语义不相似")
    print("-" * 50)

输出结果:

句子对相似度比较:

============================================================

句子 A: 「今天天气怎么样?」

句子 B: 「我想知道今天的天气状况。」

余弦相似度: 0.9091

结论: 语义相似


句子 A: 「苹果是一种水果。」

句子 B: 「特斯拉是电动汽车公司。」

余弦相似度: 0.7736

结论: 语义相似


句子 A: 「深度学习需要大量数据。」

句子 B: 「机器学习对数据量有要求。」

余弦相似度: 0.9353

结论: 语义相似


7.2 命名实体识别

python 复制代码
from transformers import BertTokenizer, BertForTokenClassification
from modelscope import snapshot_download
import torch
from torch.nn.functional import cosine_similarity

# 1. 加载模型和分词器
cache_dir = "D:\\modelscope\\hub"
model_name = "google-bert/bert-base-chinese"
local_model_path = snapshot_download(model_name, cache_dir=cache_dir)
tokenizer = BertTokenizer.from_pretrained(local_model_path)
# 对于相似度任务,我们也可以使用专门为句向量优化的模型,如 sentence-transformers

# 假设我们定义了一个标签集:O(非实体),B-PER(人名开始),I-PER(人名内部),B-ORG(组织开始),I-ORG(组织内部),B-LOC(地点开始),I-LOC(地点内部)
label_list = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC']
model = BertForTokenClassification.from_pretrained(local_model_path, num_labels=len(label_list))
model.eval()

# 2. 待分析的文本
text = "阿里巴巴的总部位于中国杭州市,马云是它的创始人之一。"

# 3. 预处理
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128)
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0]) # 将ID转换回token,便于查看

# 4. 模型预测
with torch.no_grad():
    outputs = model(**inputs)

logits = outputs.logits
predictions = torch.argmax(logits, dim=-1)[0].tolist() # 获取每个token的预测标签ID

# 5. 对齐和输出结果
print(f"文本: 「{text}」")
print("\n实体识别结果:")
print("Token\t\t\t预测标签")
print("-" * 40)

for token, prediction_id in zip(tokens, predictions):
    # 跳过特殊的 [CLS] 和 [SEP] 等token
    if token in ['[CLS]', '[SEP]', '[PAD]']:
        continue
    label = label_list[prediction_id]
    # 处理中文子词(以 ## 开头)
    if token.startswith('##'):
        print(f"{token[2:]}\t\t\t{label}") # 去掉 ##,与上一个token连接
    else:
        print(f"{token}\t\t\t{label}")  

输出结果:

文本: 「阿里巴巴的总部位于中国杭州市,马云是它的创始人之一。」

实体识别结果:

Token 预测标签


阿 B-LOC

里 O

巴 I-LOC

巴 I-PER

的 I-LOC

总 I-LOC

部 I-LOC

位 I-PER

于 I-LOC

中 I-LOC

国 I-LOC

杭 B-PER

州 I-LOC

市 I-LOC

, I-LOC

马 I-LOC

云 O

是 B-PER

它 O

的 I-LOC

创 I-LOC

始 I-PER

人 I-LOC

之 B-LOC

一 O

。 I-LOC

7.3 文本情感分析

python 复制代码
from transformers import BertTokenizer, BertForSequenceClassification
from modelscope import snapshot_download
import torch
from torch.nn.functional import cosine_similarity

# 1. 加载模型和分词器
cache_dir = "D:\\modelscope\\hub"
model_name = "google-bert/bert-base-chinese"
local_model_path = snapshot_download(model_name, cache_dir=cache_dir)
tokenizer = BertTokenizer.from_pretrained(local_model_path)
# 对于相似度任务,我们也可以使用专门为句向量优化的模型,如 sentence-transformers
# 这里我们用基础BERT演示获取句向量的方法
model = BertForSequenceClassification.from_pretrained(local_model_path)
model.eval()

# 注意:对于具体任务,我们通常会加载一个在特定任务上微调过的模型。
# 这里为了演示,我们使用基础模型,但正常情况下你应该使用任务特定的模型。
model = BertForSequenceClassification.from_pretrained(local_model_path, num_labels=2) # 假设2分类:正面/负面
model.eval() # 设置为评估模式

# 2. 待分类的文本
texts = [
    "这部电影真是太精彩了,演员演技炸裂!",
    "剧情拖沓,逻辑混乱,浪费了我两个小时。",
    "中规中矩,没什么特别的亮点。"
]

# 3. 预处理和模型预测
for text in texts:
    # 编码文本,返回PyTorch张量
    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=128)
    
    # 不计算梯度,加快推理速度
    with torch.no_grad():
        outputs = model(**inputs)
    
    # 获取预测结果
    # logits是模型最后的原始输出
    logits = outputs.logits
    # 使用softmax将logits转换为概率
    probabilities = torch.nn.functional.softmax(logits, dim=-1)
    # 获取预测的类别(概率最大的那个)
    predicted_class_id = torch.argmax(probabilities, dim=-1).item()
    
    # 打印结果
    sentiment = "正面" if predicted_class_id == 1 else "负面"
    confidence = probabilities[0][predicted_class_id].item()
    
    print(f"文本: 「{text}」")
    print(f"  情感: {sentiment} (置信度: {confidence:.4f})")
    print("-" * 50)        

输出结果:

文本: 「这部电影真是太精彩了,演员演技炸裂!」

情感: 正面 (置信度: 0.5096)


文本: 「剧情拖沓,逻辑混乱,浪费了我两个小时。」

情感: 正面 (置信度: 0.5001)


文本: 「中规中矩,没什么特别的亮点。」

情感: 负面 (置信度: 0.5439)


7.4 BERT模型的注意力机制

基于bert-base-chinese模型,可视化并分析BERT模型的注意力机制,让我们能够看见模型在处理文本时,是如何在不同词语之间分配注意力权重的。

python 复制代码
import torch
from transformers import AutoModel, AutoTokenizer
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from modelscope import snapshot_download

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

class AttentionVisualizer:
    cache_dir = "D:\\modelscope\\hub"
    model_name = "google-bert/bert-base-chinese"
    local_model_path = snapshot_download(model_name, cache_dir=cache_dir)
    def __init__(self, model_name="bert-base-chinese"):
        self.tokenizer = AutoTokenizer.from_pretrained(self.local_model_path)
        self.model = AutoModel.from_pretrained(self.local_model_path, output_attentions=True)
        self.model.eval()
    
    def visualize_attention(self, text, layer=0, head=0):
        """可视化指定层和头的注意力模式"""
        # 编码文本
        inputs = self.tokenizer(text, return_tensors='pt', padding=True, truncation=True)
        tokens = self.tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
        
        # 前向传播获取注意力
        with torch.no_grad():
            outputs = self.model(**inputs)
            attentions = outputs.attentions  # 所有层的注意力权重
        
        # 获取指定层和头的注意力权重
        attention_weights = attentions[layer][0, head].numpy()
        
        # 创建热力图
        plt.figure(figsize=(10, 8))
        sns.heatmap(attention_weights, 
                   xticklabels=tokens,
                   yticklabels=tokens,
                   cmap='Blues',
                   annot=False,
                   cbar=True)
        plt.title(f'BERT 注意力机制 - 第{layer}层第{head}头\n文本: "{text}"')
        plt.xlabel('Key Tokens')
        plt.ylabel('Query Tokens')
        plt.xticks(rotation=45)
        plt.yticks(rotation=0)
        plt.tight_layout()
        plt.show()
        
        return attention_weights, tokens

# 使用示例
visualizer = AttentionVisualizer()
text = "今天天气很好"
attention_weights, tokens = visualizer.visualize_attention(text)

print("输入文本:", text)
print("分词结果:", tokens)
print("注意力权重形状:", attention_weights.shape)
print("\n注意力模式解释:")
for i, token in enumerate(tokens):
    attention_scores = attention_weights[i]
    top_indices = np.argsort(attention_scores)[-3:][::-1]  # 取前3个最关注的token
    top_tokens = [tokens[idx] for idx in top_indices]
    top_scores = [attention_scores[idx] for idx in top_indices]
    print(f"{token} → {list(zip(top_tokens, top_scores))}")

def analyze_attention_patterns():
    """分析不同层的注意力模式"""
    visualizer = AttentionVisualizer()
    text = "今天天气很好适合出门"
    
    inputs = visualizer.tokenizer(text, return_tensors='pt')
    tokens = visualizer.tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
    
    with torch.no_grad():
        outputs = visualizer.model(**inputs)
        attentions = outputs.attentions
    
    print("BERT模型注意力模式分析")
    print("=" * 50)
    print(f"输入文本: {text}")
    print(f"分词: {tokens}")
    print(f"总层数: {len(attentions)}")
    print(f"每层头数: {attentions[0].shape[1]}")
    print(f"序列长度: {attentions[0].shape[2]}")
    print()
    
    # 分析第一层第一个头的注意力模式
    first_layer_attention = attentions[0][0, 0]  # [batch, head, query, key]
    
    print("第一层第一个头的注意力模式:")
    print("Query → [最关注的Key Tokens]")
    print("-" * 40)
    
    for i in range(len(tokens)):
        query_token = tokens[i]
        attention_scores = first_layer_attention[i]
        
        # 找出注意力权重最高的几个token
        significant_attention = [(tokens[j], attention_scores[j].item()) 
                               for j in range(len(tokens)) 
                               if attention_scores[j] > 0.1]
        
        # 按注意力权重排序
        significant_attention.sort(key=lambda x: x[1], reverse=True)
        
        if significant_attention:
            targets = [f"{token}({score:.3f})" for token, score in significant_attention]
            print(f"{query_token:8} → {targets}")

# 运行分析
analyze_attention_patterns()

def visualize_multiple_heads(text="今天天气很好", layer=0):
    """可视化同一层的多个注意力头"""
    visualizer = AttentionVisualizer()
    inputs = visualizer.tokenizer(text, return_tensors='pt')
    tokens = visualizer.tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
    
    with torch.no_grad():
        outputs = visualizer.model(**inputs)
        attentions = outputs.attentions
    
    # 获取指定层的所有头
    layer_attention = attentions[layer][0]  # [num_heads, seq_len, seq_len]
    num_heads = layer_attention.shape[0]
    
    print(f"第{layer}层的{num_heads}个注意力头模式:")
    print("=" * 60)
    
    # 显示前4个头
    fig, axes = plt.subplots(2, 2, figsize=(15, 12))
    axes = axes.flatten()
    
    for head in range(min(4, num_heads)):
        attention_weights = layer_attention[head].numpy()
        
        sns.heatmap(attention_weights,
                   xticklabels=tokens,
                   yticklabels=tokens,
                   cmap='Blues',
                   ax=axes[head],
                   cbar=True)
        axes[head].set_title(f'头 {head} 注意力模式')
        axes[head].set_xlabel('Key Tokens')
        axes[head].set_ylabel('Query Tokens')
    
    plt.tight_layout()
    plt.show()
    
    # 文本输出每个头的模式
    for head in range(min(4, num_heads)):
        print(f"\n头 {head} 的注意力模式:")
        attention_weights = layer_attention[head]
        for i, token in enumerate(tokens):
            scores = attention_weights[i]
            max_idx = torch.argmax(scores).item()
            print(f"  {token} → 最关注: {tokens[max_idx]}({scores[max_idx]:.3f})")

# 可视化多头注意力
visualize_multiple_heads()

输出结果:

输入文本: 今天天气很好

分词结果: ['[CLS]', '今', '天', '天', '气', '很', '好', '[SEP]']

注意力权重形状: (8, 8)

注意力模式解释:

CLS\] → \[('\[SEP\]', 0.9911937), ('气', 0.0016931292), ('今', 0.0016612933)

今 → [('天', 0.2926818), ('气', 0.21101946), ('很', 0.12369603)]

天 → [('气', 0.31301132), ('[CLS]', 0.14906466), ('今', 0.14077097)]

天 → [('气', 0.33033797), ('[CLS]', 0.17225097), ('今', 0.1289063)]

气 → [('今', 0.19469693), ('天', 0.15394221), ('气', 0.15200873)]

很 → [('[CLS]', 0.22579637), ('很', 0.19377638), ('今', 0.1650359)]

好 → [('今', 0.26445428), ('[CLS]', 0.20065263), ('很', 0.1942016)]

SEP\] → \[('\[CLS\]', 0.8972448), ('今', 0.02431486), ('很', 0.017953806)

结果分析:

  • 分词结果:BERT将中文文本拆分成子词单位
    • CLS\]:特殊标记,代表整个句子的汇总信息

    • SEP\]:句子结束标记

  • 注意力模式解释:
    • 今 → [('天', 0.2926818), ('气', 0.21101946), ('很', 0.12369603)]
    • 含义:当模型处理"今"这个字时,它最关注"天"(权重0.29),其次是"气"(0.21),然后是"很"(0.12)
    • 这体现了BERT的双向注意力:"今"字在关注它后面和前面的所有字
  • 坐标轴:
    • Y轴(Query):发起关注的token
    • X轴(Key):被关注的token
  • 颜色深浅:
    • 颜色越深(蓝色越深)→ 注意力权重越高
    • 颜色越浅 → 注意力权重越低
  • 关键观察点:
    • 对角线通常较深:每个token都会比较关注自己
    • CLS\]行的分布:\[CLS\]token会相对均匀地关注所有token,因为它要汇总全局信息

BERT模型注意力模式分析

==================================================

输入文本: 今天天气很好适合出门

分词: ['[CLS]', '今', '天', '天', '气', '很', '好', '适', '合', '出', '门', '[SEP]']

总层数: 12

每层头数: 12

序列长度: 12

第一层第一个头的注意力模式:

Query → [最关注的Key Tokens]


CLS\] → \['\[SEP\](0.978)'

今 → ['[CLS](0.200)', '今(0.145)', '门(0.107)']

天 → ['气(0.221)', '[CLS](0.105)', '出(0.101)']

天 → ['气(0.232)', '[CLS](0.121)']

气 → ['适(0.154)', '出(0.128)', '今(0.117)']

很 → ['[CLS](0.160)', '很(0.137)', '今(0.117)', '好(0.101)']

好 → ['今(0.183)', '[CLS](0.139)', '出(0.136)', '很(0.135)']

适 → ['[CLS](0.219)', '合(0.142)', '气(0.122)']

合 → ['气(0.197)', '很(0.138)', '门(0.107)', '[CLS](0.105)']

出 → ['很(0.174)', '今(0.148)', '门(0.134)']

门 → ['适(0.172)', '[CLS](0.123)', '好(0.115)']

SEP\] → \['\[CLS\](0.851)'

第0层的12个注意力头模式:

============================================================

头 0 的注意力模式:

CLS\] → 最关注: \[SEP\](0.991) 今 → 最关注: \[CLS\](0.293) 天 → 最关注: 气(0.313) 天 → 最关注: 气(0.330) 气 → 最关注: 今(0.195) 很 → 最关注: \[CLS\](0.226) 好 → 最关注: 今(0.264) \[SEP\] → 最关注: \[CLS\](0.897) 头 1 的注意力模式: \[CLS\] → 最关注: \[SEP\](0.930) 今 → 最关注: \[CLS\](0.425) 天 → 最关注: 今(0.625) 天 → 最关注: 气(0.455) 气 → 最关注: 天(0.628) 很 → 最关注: 好(0.364) 好 → 最关注: 很(0.566) \[SEP\] → 最关注: \[SEP\](0.999) 头 2 的注意力模式: \[CLS\] → 最关注: \[CLS\](0.890) 今 → 最关注: \[CLS\](0.760) 天 → 最关注: \[CLS\](0.767) 天 → 最关注: \[CLS\](0.665) 气 → 最关注: \[CLS\](0.633) 很 → 最关注: \[CLS\](0.466) 好 → 最关注: \[CLS\](0.508) \[SEP\] → 最关注: \[CLS\](0.822) 头 3 的注意力模式: \[CLS\] → 最关注: 天(0.218) 今 → 最关注: 很(0.366) 天 → 最关注: \[SEP\](0.270) 天 → 最关注: \[SEP\](0.277) 气 → 最关注: 好(0.254) 很 → 最关注: 好(0.503) 好 → 最关注: \[SEP\](0.315) \[SEP\] → 最关注: 今(0.242)

多头注意力的意义:

  • BERT有12层,每层有12个注意力头
  • 每个头学习不同的注意力模式:
    • 头0:可能关注语法关系
    • 头1:可能关注语义相关性
    • 头2:可能关注位置关系
    • 头3:可能关注特定词汇搭配

三、Decoder-only 架构

1. 基础理解

想象一下,我们在玩一个故事接龙的游戏,规则很简单,接着前一个人说的内容,继续叙述后面的情节,直到完成一个完整的故事,在这个游戏里,每个人只能基于前面已经说过的所有内容,来创作下一句话,你无法提前知道后面的人会说什么,也不能修改已经说过的话。

Decoder-only 模型就是这样一位成语接龙高手。它的工作方式是:

  • 从起始标记(如 <s>)开始
  • 基于当前已有的所有文本,预测下一个最可能的词
  • 将新生成的词添加到文本中,重复这个过程
  • 直到生成完整的文本或达到结束标记

核心特点:自回归生成,一步接一步。

Decoder-only 模型像一个不知疲倦的创作者,它写小说时,只能从左到右、一个字一个字地写,每写一个新字都要反复阅读前面已经写好的所有内容。

2. 代表模型

GPT 是 Decoder-only 架构最著名的代表,它的名字就揭示了其本质:生成式预训练Transformer。

GPT 的训练目标非常简单直接:预测下一个词。

基础训练:下一个词预测

  • 输入:"今天天气很"
  • 目标:让模型预测下一个词应该是"好"
  • 方法:在海量文本上重复这个练习,让模型学会语言的统计规律

学习成果:

  • 学会了语法规则(形容词后面接名词)
  • 学会了语义关联("天气"与"很好"经常一起出现)
  • 学会了逻辑推理(如果前面提到"下雨",后面可能接"带伞")
  • 学会了文体风格(新闻、小说、诗歌的不同写法)

这种简单的训练非常有效,因为要准确预测下一个词,模型必须深刻理解:前文的语法结构、词汇的语义关系、文本的逻辑连贯性、语言的风格特征

3. 核心技术

为了让故事接龙成为可能,Decoder-only 模型采用了一种关键的技术:因果注意力掩码。

如何理解因果注意力掩码:

  • 在训练时,模型会看到完整的句子,比如:"今天天气很好"
  • 但如果模型在预测"很"字时,能够看到后面的"好"字,这就是作弊!
  • 为了避免作弊,必须让模型在预测每个位置时,只能看到当前位置和之前的位置

因果注意力掩码的工作原理:

因果注意力模式示意图

输入序列: <s> 今天 天气 很

生成过程:

生成"今天"时 → 只能看到: [<s>]

生成"天气"时 → 只能看到: [<s>, 今天]

生成"很"时 → 只能看到: [<s>, 今天, 天气]

生成"好"时 → 只能看到: [<s>, 今天, 天气, 很]

这就像一个逐步揭开的面纱:

  • 开始时,你只能看到第一个词
  • 每生成一个新词,面纱就往后移动一点,让你多看到一个词
  • 但你永远无法看到面纱后面的未来内容

因果注意力关键特点:

    1. 单向性: 只能从左到右,不能反向
    1. 累积性: 每步增加一个新token的访问权限
    1. 防泄露: 永远不会看到未来的信息
    1. 自回归: 基于历史预测下一个token

4. 体现优势

擅长所有需要创造性生成的任务

任务类型 例子 说明
文本创作 写小说、诗歌、新闻稿 像作家一样从头创作完整文本
对话系统 智能客服、聊天机器人 基于对话历史生成自然回复
内容续写 给定开头,完成整篇文章 保持风格一致性地扩展内容
代码生成 根据描述自动写代码 理解需求并生成程序代码
文本翻译 中英文互译 将一种语言流畅地转换为另一种

5. 局限性

无法利用后文信息

  • 在生成过程中,模型看不到未来的内容
  • 这有时会导致前后不一致的问题

错误累积

  • 如果前面生成错误,后面的内容会基于错误继续生成
  • 像传话游戏一样,错误会不断放大

理解深度有限

  • 虽然能生成流畅文本,但对深层次语义理解不如Encoder模型
  • 有时会一本正经地胡说八道

6. 示例分析

6.1 逐步生成演示

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from modelscope import snapshot_download

class DecoderOnlyExample:
    def __init__(self):
        # 使用ModelScope中的GPT系列小模型
        cache_dir = "D:\\modelscope\\hub"
        self.model_name = "Fengshenbang/Wenzhong-GPT2-110M-chinese-v2"
        local_model_path = snapshot_download(self.model_name, cache_dir=cache_dir)
        self.tokenizer = AutoTokenizer.from_pretrained(local_model_path)
        self.model = AutoModelForCausalLM.from_pretrained(local_model_path)
        
        # 设置padding token
        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token
    
    def generate_text(self, prompt, max_length=100, temperature=0.8):
        """文本生成示例"""
        inputs = self.tokenizer(prompt, return_tensors='pt')
        
        with torch.no_grad():
            outputs = self.model.generate(
                inputs.input_ids,
                max_length=max_length,
                num_return_sequences=1,
                temperature=temperature,
                do_sample=True,
                pad_token_id=self.tokenizer.eos_token_id,
                no_repeat_ngram_size=3
            )
        
        generated_text = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
        return generated_text
    
    def calculate_perplexity(self, text):
        """计算文本困惑度"""
        inputs = self.tokenizer(text, return_tensors='pt')
        with torch.no_grad():
            outputs = self.model(**inputs, labels=inputs.input_ids)
            loss = outputs.loss
            perplexity = torch.exp(loss)
        return perplexity.item()

# 使用示例
decoder_example = DecoderOnlyExample()
prompt = "人工智能的未来发展"
generated_text = decoder_example.generate_text(prompt)
print(f"输入提示: {prompt}")
print(f"生成的文本: {generated_text}")


def simple_causal_attention_demo():
    print("\n" + "="*60)
    """最简单的因果注意力演示"""
    print("因果注意力模式 (Decoder-only)")
    print("=" * 40)
    
    # 输入序列
    sequence = ["<s>", "今天", "天气", "很"]
    print(f"输入序列: {' '.join(sequence)}")
    print()
    
    print("注意力模式:")
    print("-" * 25)
    
    # 为每个位置显示可以关注的位置
    for i in range(len(sequence)):
        current_token = sequence[i]
        # 当前位置可以关注的所有位置(从0到i)
        can_attend = sequence[:i+1]
        
        print(f"{current_token:4} → {', '.join(can_attend)}")

# 运行演示
simple_causal_attention_demo()

def clear_causal_demo():
    """更清晰的因果注意力演示"""
    print("\n" + "="*60)
    print("Decoder-only 因果注意力")
    print("=" * 35)
    
    tokens = ["<s>", "今天", "天气", "很", "好"]
    
    print("规则: 每个token只能关注它自己和前面的所有token")
    print()
    
    for i in range(len(tokens)):
        current = tokens[i]
        previous = tokens[:i+1]  # 从开始到当前的所有token
        
        arrow = "→"
        if i == 0:
            arrow = "→ (只能关注自己)"
        elif i == len(tokens) - 1:
            arrow = "→ (可以关注前面所有token)"
        
        print(f"{current:4} {arrow:20} {', '.join(previous)}")

# 运行清晰版本
clear_causal_demo()

def minimal_demo():
    print("\n" + "="*60)
    """最简化的核心逻辑演示"""
    tokens = ["起始", "今天", "天气", "很", "好"]
    
    print("因果注意力核心规则:")
    print("每个位置只能看到它和它之前的所有位置")
    print()
    
    for i in range(len(tokens)):
        can_see = tokens[:i+1]
        print(f"生成 '{tokens[i]}' 时,模型能看到: {can_see}")

# 运行最简版本
minimal_demo()

输出结果:

输入提示: 人工智能的未来发展

生成的文本: 人工智能的未来发展方向-高端人才培养。 春节期间,百度联合国内一些知名人士、研发机构、科砙企业(包

因果注意力模式 (Decoder-only)

============================================================

输入序列: <s> 今天 天气 很

注意力模式:


<s> → <s>

今天 → <s>, 今天

天气 → <s>, 今天, 天气

很 → <s>, 今天, 天气, 很

Decoder-only 因果注意力

============================================================

规则: 每个token只能关注它自己和前面的所有token

<s> → (只能关注自己) <s>

今天 → <s>, 今天

天气 → <s>, 今天, 天气

很 → <s>, 今天, 天气, 很

好 → (可以关注前面所有token) <s>, 今天, 天气, 很, 好

因果注意力核心规则:

============================================================

每个位置只能看到它和它之前的所有位置

生成 '起始' 时,模型能看到: ['起始']

生成 '今天' 时,模型能看到: ['起始', '今天']

生成 '天气' 时,模型能看到: ['起始', '今天', '天气']

生成 '很' 时,模型能看到: ['起始', '今天', '天气', '很']

生成 '好' 时,模型能看到: ['起始', '今天', '天气', '很', '好']

6.2 因果注意力模式分析

python 复制代码
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import numpy as np
from modelscope import snapshot_download

class CausalAttentionVisualizer:
    def __init__(self, model_name="gpt2-chinese-cluecorpussmall"):
        cache_dir = "D:\\modelscope\\hub"
        self.model_name = "Fengshenbang/Wenzhong-GPT2-110M-chinese-v2"
        local_model_path = snapshot_download(self.model_name, cache_dir=cache_dir)
        self.tokenizer = AutoTokenizer.from_pretrained(local_model_path)
        self.model = AutoModelForCausalLM.from_pretrained(local_model_path, output_attentions=True)
        self.model.eval()
        
        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token
    
    def get_causal_attention_mask(self, seq_len):
        """生成因果注意力掩码"""
        # 创建下三角矩阵,1表示允许关注,0表示不允许
        mask = torch.tril(torch.ones(seq_len, seq_len))
        return mask
    
    def visualize_causal_pattern(self, text):
        """可视化因果注意力模式"""
        # 编码文本
        inputs = self.tokenizer(text, return_tensors='pt')
        seq_len = inputs['input_ids'].shape[1]
        
        print("因果注意力模式分析")
        print("=" * 40)
        print(f"序列长度: {seq_len}")
        print(f"输入序列: {text}")
        print("\n注意力模式:")
        print("-" * 30)
        
        # 生成因果注意力掩码
        causal_mask = self.get_causal_attention_mask(seq_len)
        
        # 输出注意力模式
        for i in range(seq_len):
            # 获取当前位置可以关注的token索引
            allowed_indices = torch.where(causal_mask[i] == 1)[0].tolist()
            
            if i == 0:
                # 第一个token只能关注自己
                print(f"位置 {i} → [位置 {i}]")
            else:
                # 其他位置可以关注从0到当前位置的所有token
                positions = [f"位置 {j}" for j in allowed_indices]
                print(f"位置 {i} → {positions}")
        
        return causal_mask

# 使用示例
visualizer = CausalAttentionVisualizer()
text = "今天天气很"
causal_mask = visualizer.visualize_causal_pattern(text)

def analyze_causal_attention_detailed():
    """详细分析因果注意力模式"""
    visualizer = CausalAttentionVisualizer()
    text = "今天天气很好适合出门"
    
    inputs = visualizer.tokenizer(text, return_tensors='pt')
    seq_len = inputs['input_ids'].shape[1]
    
    print("Decoder-only 因果注意力机制")
    print("=" * 50)
    print(f"输入序列长度: {seq_len}")
    print("\n完整的注意力模式矩阵:")
    print("-" * 40)
    
    # 生成因果注意力掩码
    causal_mask = visualizer.get_causal_attention_mask(seq_len)
    
    # 输出矩阵形式的注意力模式
    print("   ", end="")
    for j in range(seq_len):
        print(f"{j:2} ", end="")
    print()
    
    for i in range(seq_len):
        print(f"{i:2} ", end="")
        for j in range(seq_len):
            if causal_mask[i, j] == 1:
                print(" ● ", end="")
            else:
                print(" · ", end="")
        print(f"  位置 {i} 可以关注: {[k for k in range(seq_len) if causal_mask[i, k] == 1]}")
    
    print("\n其中: ● = 允许关注, · = 禁止关注")

# 运行详细分析
analyze_causal_attention_detailed()

def verify_model_attention():
    """验证实际模型的因果注意力"""
    visualizer = CausalAttentionVisualizer()
    text = "今天天气很"
    
    inputs = visualizer.tokenizer(text, return_tensors='pt')
    
    with torch.no_grad():
        outputs = visualizer.model(**inputs, output_attentions=True)
    
    attentions = outputs.attentions  # 所有层的注意力
    seq_len = inputs['input_ids'].shape[1]
    
    print("实际模型因果注意力验证")
    print("=" * 40)
    print(f"模型层数: {len(attentions)}")
    print(f"每层头数: {attentions[0].shape[1]}")
    print(f"序列长度: {seq_len}")
    
    # 检查第一层第一个头的注意力
    first_head_attention = attentions[0][0, 0]  # [seq_len, seq_len]
    
    print("\n第一层第一个头注意力模式:")
    print("位置 → [可以关注的位置] (注意力权重>0.01)")
    print("-" * 50)
    
    for i in range(seq_len):
        # 找出有显著注意力权重的位置
        significant_positions = []
        for j in range(seq_len):
            if first_head_attention[i, j] > 0.01:
                significant_positions.append(j)
        
        print(f"位置 {i} → 位置 {significant_positions}")

# 验证模型注意力
verify_model_attention()

import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

def plot_causal_attention_matrix(seq_len=6):
    """绘制因果注意力矩阵图"""
    # 创建因果注意力掩码
    causal_mask = torch.tril(torch.ones(seq_len, seq_len))
    
    plt.figure(figsize=(10, 8))
    plt.imshow(causal_mask, cmap='Blues', aspect='auto')
    
    # 添加网格和标签
    for i in range(seq_len):
        for j in range(seq_len):
            color = 'white' if causal_mask[i, j] > 0.5 else 'black'
            plt.text(j, i, '●' if causal_mask[i, j] > 0.5 else '·', 
                    ha='center', va='center', color=color, fontsize=20)
    
    plt.xlabel('Key Position (被关注的位置)')
    plt.ylabel('Query Position (关注的位置)')
    plt.title('Decoder-only 因果注意力矩阵\n(下三角模式)')
    plt.xticks(range(seq_len), [f'Pos {i}' for i in range(seq_len)])
    plt.yticks(range(seq_len), [f'Pos {i}' for i in range(seq_len)])
    plt.grid(False)
    
    # 添加解释
    plt.figtext(0.5, 0.01, 
               '● = 允许关注  |  · = 禁止关注\n每个位置只能关注当前位置及之前的位置', 
               ha='center', fontsize=12, style='italic')
    
    plt.tight_layout()
    plt.show()

# 绘制注意力矩阵
plot_causal_attention_matrix(4)

因果注意力模式分析

========================================

序列长度: 8

输入序列: 今天天气很

注意力模式:


位置 0 → [位置 0]

位置 1 → ['位置 0', '位置 1']

位置 2 → ['位置 0', '位置 1', '位置 2']

位置 3 → ['位置 0', '位置 1', '位置 2', '位置 3']

位置 4 → ['位置 0', '位置 1', '位置 2', '位置 3', '位置 4']

位置 5 → ['位置 0', '位置 1', '位置 2', '位置 3', '位置 4', '位置 5']

位置 6 → ['位置 0', '位置 1', '位置 2', '位置 3', '位置 4', '位置 5', '位置 6']

位置 7 → ['位置 0', '位置 1', '位置 2', '位置 3', '位置 4', '位置 5', '位置 6', '位置 7']

Decoder-only 因果注意力机制

==================================================

输入序列长度: 18

完整的注意力模式矩阵:


0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

0 ● · · · · · · · · · · · · · · · · · 位置 0 可以关注: [0]

1 ● ● · · · · · · · · · · · · · · · · 位置 1 可以关注: [0, 1]

2 ● ● ● · · · · · · · · · · · · · · · 位置 2 可以关注: [0, 1, 2]

3 ● ● ● ● · · · · · · · · · · · · · · 位置 3 可以关注: [0, 1, 2, 3]

4 ● ● ● ● ● · · · · · · · · · · · · · 位置 4 可以关注: [0, 1, 2, 3, 4]

5 ● ● ● ● ● ● · · · · · · · · · · · · 位置 5 可以关注: [0, 1, 2, 3, 4, 5]

6 ● ● ● ● ● ● ● · · · · · · · · · · · 位置 6 可以关注: [0, 1, 2, 3, 4, 5, 6]

7 ● ● ● ● ● ● ● ● · · · · · · · · · · 位置 7 可以关注: [0, 1, 2, 3, 4, 5, 6, 7]

8 ● ● ● ● ● ● ● ● ● · · · · · · · · · 位置 8 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8]

9 ● ● ● ● ● ● ● ● ● ● · · · · · · · · 位置 9 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

10 ● ● ● ● ● ● ● ● ● ● ● · · · · · · · 位置 10 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10]

11 ● ● ● ● ● ● ● ● ● ● ● ● · · · · · · 位置 11 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11]

12 ● ● ● ● ● ● ● ● ● ● ● ● ● · · · · · 位置 12 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12]

13 ● ● ● ● ● ● ● ● ● ● ● ● ● ● · · · · 位置 13 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12, 13]

14 ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● · · · 位置 14 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12, 13, 14]

15 ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● · · 位置 15 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12, 13, 14, 15]

16 ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● · 位置 16 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12, 13, 14, 15, 16]

17 ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● 位置 17 可以关注: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

10, 11, 12, 13, 14, 15, 16, 17]

其中: ● = 允许关注, · = 禁止关注

7. 图解说明

因果注意力模式:


📍 位置 0 (起始位置)

└── 只能关注: [位置 0] ← 自身

└── 意义: 序列开始,无历史信息

📍 位置 1

└── 可以关注: 位置 [0, 1]

└── 新增关注: 位置 1 ← 当前生成的位置

└── 历史信息: 1 个之前的位置

📍 位置 2

└── 可以关注: 位置 [0, 1, 2]

└── 新增关注: 位置 2 ← 当前生成的位置

└── 历史信息: 2 个之前的位置

📍 位置 3

└── 可以关注: 位置 [0, 1, 2, 3]

└── 新增关注: 位置 3 ← 当前生成的位置

└── 历史信息: 3 个之前的位置

📍 位置 4

└── 可以关注: 位置 [0, 1, 2, 3, 4]

└── 新增关注: 位置 4 ← 当前生成的位置

└── 历史信息: 4 个之前的位置

📍 位置 5

└── 可以关注: 位置 [0, 1, 2, 3, 4, 5]

└── 新增关注: 位置 5 ← 当前生成的位置

└── 历史信息: 5 个之前的位置

📍 位置 6

└── 可以关注: 位置 [0, 1, 2, 3, 4, 5, 6]

└── 新增关注: 位置 6 ← 当前生成的位置

└── 历史信息: 6 个之前的位置

📍 位置 7

└── 可以关注: 位置 [0, 1, 2, 3, 4, 5, 6, 7]

└── 新增关注: 位置 7 ← 当前生成的位置

└── 历史信息: 7 个之前的位置

四、总结对比

特性 Decoder-only (如 GPT) Encoder-only (如 BERT)
核心能力 连续生成 深度理解
工作方式 单向自回归,逐个生成 双向,同时处理全部输入
注意力机制 因果注意力(掩码) 全连接注意力
训练目标 预测下一个词 完形填空、句间关系
典型任务 写作、对话、翻译、代码生成 分类、标注、问答、情感分析
比喻 故事创作家、脱口秀演员 阅读理解大师、分析师

选择的决策:

  • 当我们需要对一段文本进行深度剖析、分类、提取信息或比较时,Encoder-only (BERT) 是更专业、更高效的工具。
  • 当我们需要模型创作内容、进行对话、根据指令完成任务或续写故事时,Decoder-only (GPT) 是唯一的选择。

综合来说,Encoder-only与Decoder-only架构代表了两种截然不同的设计哲学,Encoder-only如BERT,凭借双向注意力机制成为"理解大师",擅长文本分类、情感分析等深度语义理解任务,为企业提供精准的文本分析能力。而Decoder-only如GPT系列,通过因果注意力掩码实现自回归生成,化身为创作高手,在文本生成、对话系统和代码创作等场景表现卓越。

相关推荐
rit84324992 小时前
基于MATLAB的PCA+SVM人脸识别系统实现
人工智能·算法
一 铭2 小时前
Claude Agent Skills:一种基于 Prompt 扩展的元工具架构
人工智能·大模型·llm·prompt
连线Insight2 小时前
小马智行港股上市:自动驾驶从“技术追跑”到“商业领跑”的里程碑
人工智能
xier_ran2 小时前
深度学习:为什么不能将多层神经网络参数全部初始化为零以及如何进行随机初始化
人工智能·深度学习
扫地僧9852 小时前
[特殊字符]用于糖尿病视网膜病变图像生成的生成对抗网络(GAN)
人工智能·神经网络·生成对抗网络
文心快码BaiduComate2 小时前
疯了!双11,百度文心快码帮我省钱又赚钱?
人工智能
大刘讲IT2 小时前
赋能中小企业:基于五大开源模块的AI智能体构建方案与细化拆解
人工智能·经验分享·ai·开源·制造
来让爷抱一个2 小时前
企业级AI知识库新纪元:如何用开源力量重塑知识管理?
人工智能·开源
巴塞罗那的风2 小时前
Eino框架快速搭建出行agent(二)引入12306 mcp
人工智能·golang·mcp