news 2026/10/1 14:38:56

多模态技术概论

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多模态技术概论

一、多模态技术概论

1.1 什么是多模态

模态(Modality):信息的载体形式。文本、图像、音频、视频、点云、表格……

多模态(Multimodal):模型能同时理解和处理多种模态,并建立它们之间的关联。

text

单模态:文本 → 文本(GPT) 图像 → 标签(ResNet) 多模态:图像 + 文本 → 文本(GPT-4V) 文本 → 图像(Stable Diffusion) 图像 + 文本 → 图像(风格迁移) 视频 + 音频 + 文本 → 描述(视频理解)

1.2 为什么需要多模态

场景单模态的局限多模态的价值
医疗只看病历,看不到影像影像 + 病历联合诊断
电商只看标题,抓不到视觉卖点图 + 文 + 评论联合推荐
自动驾驶只靠雷达摄像头 + 雷达 + 地图融合
教育只有题目文本题目 + 手写步骤 + 语音讲解
搜索只能文字搜以图搜图、以图搜文

1.3 多模态的核心挑战

挑战 1:表示鸿沟(Representation Gap)

图像是像素矩阵(连续、空间结构),文本是离散 token。怎么把它们放到同一个语义空间?

解法:对比学习(Contrastive Learning)

text

图像 → 图像编码器 → 向量 v_img 文本 → 文本编码器 → 向量 v_txt ↓ 拉近匹配的 (图,文),推远不匹配的 ↓ 同一个语义空间

代表:CLIP(OpenAI,2021)—— 用 4 亿图文对训练,让"猫的图片"和"cat 的文字"向量接近。

挑战 2:对齐(Alignment)

图像的"红色圆形"和文本的"red circle"怎么对应到同一概念?

  • 粗粒度对齐:整图 ↔ 整句(CLIP)

  • 细粒度对齐:区域 ↔ 词(GLIP、Grounding DINO)

挑战 3:融合(Fusion)

拿到图像特征和文本特征后,怎么合并?

融合方式说明代表
早期融合原始输入层就拼早期多模态模型
中期融合中间层交叉注意力ViLBERT、LXMERT
晚期融合各自出结果再合并简单分类
统一融合全塞进一个 TransformerGPT-4V、LLaVA

挑战 4:模态缺失

现实中经常缺模态(只有图没文,只有文没图)。要能鲁棒处理。

1.4 多模态技术演进

text

2015-2018 早期融合(VQA、图像描述) ↓ 2019-2020 双塔 + 交叉注意力(ViLBERT、UNITER) ↓ 2021 CLIP 用对比学习统一表示 ↓ 2022 Flamingo 把视觉接进 LLM ↓ 2023 LLaVA / MiniGPT-4 开源 VLM 爆发 ↓ 2024 GPT-4o / Gemini 原生多模态 ↓ 2025 Agent + 多模态 + 具身智能

1.5 多模态的主要任务

任务输入输出
图像分类图类别
图像描述图文本
VQA图 + 问答
图文检索图或文匹配项
OCR图文字
图像生成文图
视频理解视频描述/问答
视觉定位图 + 文本框

二、Vision Transformer(ViT)原理与从零实现

2.1 为什么需要 ViT

CNN 的局限:

  • 局部感受野,需堆很多层才能看全局

  • 卷积核固定,缺少动态性

  • 长距离依赖建模弱

Transformer 的优势:

  • 自注意力天然全局

  • 可扩展性强(堆数据、堆参数就涨点)

  • 统一架构(和 NLP 一致)

2020 年 Google 的论文:《An Image is Worth 16x16 Words》
核心思想:把图像切成 patch,当成"单词"喂给 Transformer。

2.2 ViT 的核心原理

Step 1:图像切 patch

text

输入图像:224 × 224 × 3 切 patch:16 × 16 patch 数量:(224/16) × (224/16) = 14 × 14 = 196 个 每个 patch:16 × 16 × 3 = 768 维

Step 2:Patch Embedding

每个 patch 展平成 768 维向量,通过线性层映射到模型维度 D(如 768)。

python

patch (16×16×3=768) → Linear(768, D) → token (D维)

等价于:用 stride=16、kernel=16 的卷积。

Step 3:加 [CLS] token 和位置编码

text

[CLS] + 196 个 patch token = 197 个 token ↓ + 位置编码(可学习) ↓ 输入 Transformer
  • [CLS]:类似 BERT,用它的输出做分类

  • 位置编码:因为自注意力无序,要显式告诉模型"谁在谁旁边"

Step 4:Transformer Encoder

标准 Transformer Encoder 堆 L 层,每层:

text

x = x + MultiHeadSelfAttention(LayerNorm(x)) x = x + MLP(LayerNorm(x))

Step 5:分类头

取[CLS]token 的输出,接一个 MLP:

text

[CLS] output → LayerNorm → Linear(D, num_classes) → softmax

2.3 从零实现 ViT

下面给一个完整可跑的 ViT,用 CIFAR-10 做图像分类。

python

# -*- coding: utf-8 -*- """ 从零实现 Vision Transformer 依赖:pip install torch torchvision """ import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms import math # ============================================================ # 1. Patch Embedding:把图像切成 patch 并映射到 D 维 # ============================================================ class PatchEmbedding(nn.Module): def __init__(self, img_size=32, patch_size=4, in_channels=3, embed_dim=192): """ CIFAR-10 是 32x32,patch_size=4 → 8x8=64 个 patch """ super().__init__() self.img_size = img_size self.patch_size = patch_size self.num_patches = (img_size // patch_size) ** 2 # 用卷积实现 patch 切分 + 线性映射 self.proj = nn.Conv2d( in_channels, embed_dim, kernel_size=patch_size, stride=patch_size ) def forward(self, x): # x: [B, 3, 32, 32] x = self.proj(x) # [B, D, 8, 8] x = x.flatten(2) # [B, D, 64] x = x.transpose(1, 2) # [B, 64, D] return x # ============================================================ # 2. 多头自注意力 # ============================================================ class MultiHeadAttention(nn.Module): def __init__(self, embed_dim, num_heads, dropout=0.0): super().__init__() assert embed_dim % num_heads == 0 self.embed_dim = embed_dim self.num_heads = num_heads self.head_dim = embed_dim // num_heads self.scale = self.head_dim ** -0.5 # Q, K, V 投影 self.qkv = nn.Linear(embed_dim, embed_dim * 3) self.proj = nn.Linear(embed_dim, embed_dim) self.dropout = nn.Dropout(dropout) def forward(self, x): B, N, D = x.shape # 计算 QKV: [B, N, 3D] → 拆成 3 个 [B, N, D] qkv = self.qkv(x).chunk(3, dim=-1) # 每个 reshape 成 [B, num_heads, N, head_dim] q, k, v = [ t.reshape(B, N, self.num_heads, self.head_dim).transpose(1, 2) for t in qkv ] # 注意力分数: [B, H, N, N] attn = (q @ k.transpose(-2, -1)) * self.scale attn = attn.softmax(dim=-1) attn = self.dropout(attn) # 加权求和: [B, H, N, head_dim] out = attn @ v # 拼回: [B, N, D] out = out.transpose(1, 2).reshape(B, N, D) out = self.proj(out) return out # ============================================================ # 3. MLP(FFN) # ============================================================ class MLP(nn.Module): def __init__(self, embed_dim, mlp_ratio=4.0, dropout=0.0): super().__init__() hidden = int(embed_dim * mlp_ratio) self.fc1 = nn.Linear(embed_dim, hidden) self.fc2 = nn.Linear(hidden, embed_dim) self.dropout = nn.Dropout(dropout) self.act = nn.GELU() def forward(self, x): x = self.fc1(x) x = self.act(x) x = self.dropout(x) x = self.fc2(x) x = self.dropout(x) return x # ============================================================ # 4. Transformer Encoder Block # ============================================================ class TransformerBlock(nn.Module): def __init__(self, embed_dim, num_heads, mlp_ratio=4.0, dropout=0.0): super().__init__() self.norm1 = nn.LayerNorm(embed_dim) self.attn = MultiHeadAttention(embed_dim, num_heads, dropout) self.norm2 = nn.LayerNorm(embed_dim) self.mlp = MLP(embed_dim, mlp_ratio, dropout) def forward(self, x): # Pre-Norm 结构(更稳定) x = x + self.attn(self.norm1(x)) x = x + self.mlp(self.norm2(x)) return x # ============================================================ # 5. ViT 主模型 # ============================================================ class ViT(nn.Module): def __init__( self, img_size=32, patch_size=4, in_channels=3, num_classes=10, embed_dim=192, depth=6, num_heads=6, mlp_ratio=4.0, dropout=0.1, ): super().__init__() self.patch_embed = PatchEmbedding(img_size, patch_size, in_channels, embed_dim) num_patches = self.patch_embed.num_patches # [CLS] token(可学习) self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) # 位置编码(可学习),长度 = 1 + num_patches self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim)) self.pos_drop = nn.Dropout(dropout) # Transformer Encoder 堆叠 self.blocks = nn.ModuleList([ TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout) for _ in range(depth) ]) self.norm = nn.LayerNorm(embed_dim) self.head = nn.Linear(embed_dim, num_classes) self._init_weights() def _init_weights(self): nn.init.trunc_normal_(self.pos_embed, std=0.02) nn.init.trunc_normal_(self.cls_token, std=0.02) for m in self.modules(): if isinstance(m, nn.Linear): nn.init.trunc_normal_(m.weight, std=0.02) if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.LayerNorm): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) def forward(self, x): B = x.shape[0] # 1. Patch embedding: [B, 3, 32, 32] → [B, N, D] x = self.patch_embed(x) # 2. 加 [CLS] cls = self.cls_token.expand(B, -1, -1) # [B, 1, D] x = torch.cat([cls, x], dim=1) # [B, N+1, D] # 3. 加位置编码 x = x + self.pos_embed x = self.pos_drop(x) # 4. 过 Transformer for block in self.blocks: x = block(x) # 5. 取 [CLS] 输出分类 x = self.norm(x) cls_out = x[:, 0] return self.head(cls_out) # ============================================================ # 6. 训练脚本 # ============================================================ def train(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"[INFO] 使用设备: {device}") # 数据增强 transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) train_set = datasets.CIFAR10("./data", train=True, download=True, transform=transform_train) test_set = datasets.CIFAR10("./data", train=False, download=True, transform=transform_test) train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=0) test_loader = DataLoader(test_set, batch_size=256, shuffle=False, num_workers=0) # 模型 model = ViT( img_size=32, patch_size=4, num_classes=10, embed_dim=192, depth=6, num_heads=6, mlp_ratio=4.0, dropout=0.1, ).to(device) n_params = sum(p.numel() for p in model.parameters()) print(f"[INFO] 参数量: {n_params/1e6:.2f}M") # 优化器 + 调度 optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.05) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 训练 epochs = 30 for epoch in range(epochs): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() logits = model(imgs) loss = criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (logits.argmax(1) == labels).sum().item() total += imgs.size(0) scheduler.step() train_acc = correct / total avg_loss = total_loss / total # 测试 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in test_loader: imgs, labels = imgs.to(device), labels.to(device) logits = model(imgs) correct += (logits.argmax(1) == labels).sum().item() total += imgs.size(0) test_acc = correct / total print(f"Epoch {epoch+1:2d}/{epochs} | " f"Loss {avg_loss:.4f} | " f"Train {train_acc:.4f} | Test {test_acc:.4f}") torch.save(model.state_dict(), "vit_cifar10.pth") print("[INFO] 模型已保存 vit_cifar10.pth") if __name__ == "__main__": train()

跑起来的结果(参考):

  • 30 epoch 后 CIFAR-10 测试准确率 ~80%

  • 参数量约 2.8M

  • 单卡 3090 约 10 分钟

2.4 ViT 的关键设计点

为什么用 [CLS] token

  • 类似 BERT,让一个专门的 token 汇聚全局信息

  • 也可以用全局平均池化(GAP)替代,效果差不多

位置编码为什么可学习

  • 原论文对比过:可学习 vs 正弦,效果差不多

  • 但可学习的在小数据集上略好

  • 大分辨率时需要插值

Pre-Norm vs Post-Norm

  • Post-Norm(原 Transformer):x = Norm(x + Sublayer(x))

  • Pre-Norm(ViT 用):x = x + Sublayer(Norm(x))

  • Pre-Norm训练更稳定,适合深层网络

ViT 为什么需要大数据

  • 没有 CNN 的归纳偏置(局部性、平移不变性)

  • 小数据上不如 ResNet

  • JFT-300M 上训练后超越 CNN

  • 用DeiT的蒸馏方法可以在小数据上训好

2.5 ViT 的变体

模型改进
DeiT知识蒸馏,小数据也能训
Swin层级结构 + 滑动窗口,效率高
BEiT掩码图像建模,自监督
MAE掩码自编码器,重建像素
DINOv2自监督,特征质量极高
EVA从 CLIP 蒸馏,强大

三、VLM:视觉大语言模型

3.1 什么是 VLM

VLM(Vision-Language Model):能同时理解图像和文本,并生成文本的大模型。

代表:

  • 闭源:GPT-4V、Claude 3.5 Sonnet、Gemini 1.5

  • 开源:LLaVA、Qwen-VL、InternVL、CogVLM、MiniCPM-V

3.2 VLM 的核心架构

通用范式:视觉编码器 + 投影层 + LLM

text

图像 → [Vision Encoder] → 视觉特征 ↓ [Projector] ↓ 文本 → [Tokenizer] → token ──→ 拼接 → [LLM] → 输出文本

三个核心组件

1. Vision Encoder(视觉编码器)

通常用CLIP ViT或SigLIP,冻结或微调。

python

# 输入: [B, 3, 336, 336] # 输出: [B, N, D_v] N=576, D_v=1024 (CLIP-L/14)
2. Projector(投影层)

把视觉特征映射到 LLM 的词嵌入空间。

常见方案:

方案说明代表
线性层一个 LinearLLaVA-1.0
MLP2 层 MLPLLaVA-1.5
Q-Former用可学习 query 压缩BLIP-2
Perceiver Resampler类似 Q-FormerFlamingo
C-Abstractor卷积 + 池化Honeybee

python

# LLaVA 的 projector class Projector(nn.Module): def __init__(self, vision_dim=1024, llm_dim=4096): super().__init__() self.mlp = nn.Sequential( nn.Linear(vision_dim, llm_dim), nn.GELU(), nn.Linear(llm_dim, llm_dim), ) def forward(self, x): return self.mlp(x)
3. LLM(语言模型)

接收"视觉 token + 文本 token"的拼接序列,输出文本。

3.3 VLM 的训练流程(以 LLaVA 为例)

Stage 1:预训练对齐(Feature Alignment)

  • 冻结:Vision Encoder + LLM

  • 训练:只训 Projector

  • 数据:图文对(CC3M、LAION),任务=图像描述

  • 目的:让视觉特征对齐到 LLM 词空间

  • 成本:便宜(只训一个 MLP)

Stage 2:指令微调(Instruction Tuning)

  • 冻结:Vision Encoder

  • 训练:Projector + LLM(或 LoRA)

  • 数据:多模态指令数据(LLaVA-Instruct-150K)

  • 目的:让模型能对话、问答、推理

  • 成本:贵(要训 LLM)

3.4 从零实现一个 Mini VLM

下面用预训练的 CLIP + 一个简单 LLM 的思路演示。为了能跑,我用一个小模型(GPT-2)代替。

python

# -*- coding: utf-8 -*- """ Mini VLM: CLIP Vision Encoder + Projector + GPT-2 依赖:pip install torch transformers pillow """ import torch import torch.nn as nn from transformers import CLIPVisionModel, GPT2LMHeadModel, CLIPProcessor from PIL import Image # ============================================================ # 1. Projector # ============================================================ class Projector(nn.Module): def __init__(self, vision_dim=768, llm_dim=768): super().__init__() self.mlp = nn.Sequential( nn.Linear(vision_dim, llm_dim), nn.GELU(), nn.Linear(llm_dim, llm_dim), ) def forward(self, x): return self.mlp(x) # ============================================================ # 2. Mini VLM # ============================================================ class MiniVLM(nn.Module): def __init__(self, vision_model_name="openai/clip-vit-base-patch32", llm_name="gpt2"): super().__init__() # 视觉编码器 self.vision_encoder = CLIPVisionModel.from_pretrained(vision_model_name) vision_dim = self.vision_encoder.config.hidden_size # 768 # 语言模型 self.llm = GPT2LMHeadModel.from_pretrained(llm_name) llm_dim = self.llm.config.n_embd # 768 # 投影层 self.projector = Projector(vision_dim, llm_dim) # 冻结视觉编码器 for p in self.vision_encoder.parameters(): p.requires_grad = False def forward(self, pixel_values, input_ids, attention_mask=None, labels=None): B = pixel_values.shape[0] # 1. 视觉编码 with torch.no_grad(): vision_out = self.vision_encoder(pixel_values=pixel_values) vision_features = vision_out.last_hidden_state # [B, N, D_v] # 2. 投影到 LLM 空间 vision_embeds = self.projector(vision_features) # [B, N, D_llm] # 3. 文本 embedding text_embeds = self.llm.transformer.wte(input_ids) # [B, L, D_llm] # 4. 拼接 [视觉 token][文本 token] inputs_embeds = torch.cat([vision_embeds, text_embeds], dim=1) # 5. attention mask 补齐 if attention_mask is not None: vision_mask = torch.ones(B, vision_embeds.size(1), device=attention_mask.device) attention_mask = torch.cat([vision_mask, attention_mask], dim=1) # 6. labels 补齐(视觉部分不参与 loss) if labels is not None: vision_labels = torch.full( (B, vision_embeds.size(1)), -100, dtype=labels.dtype, device=labels.device ) labels = torch.cat([vision_labels, labels], dim=1) # 7. 过 LLM outputs = self.llm( inputs_embeds=inputs_embeds, attention_mask=attention_mask, labels=labels, ) return outputs # ============================================================ # 3. 推理示例 # ============================================================ def inference_demo(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = MiniVLM().to(device) model.eval() processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") # 造一张假图 img = Image.new("RGB", (224, 224), color="red") pixel_values = processor(images=img, return_tensors="pt")["pixel_values"] pixel_values = pixel_values.to(device) # 输入文本 prompt = "This image shows a" from transformers import GPT2Tokenizer tokenizer = GPT2Tokenizer.from_pretrained("gpt2") tokenizer.pad_token = tokenizer.eos_token inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = model( pixel_values=pixel_values, input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], ) # 简单取 logits(未训练,仅演示结构) print("[INFO] 模型结构 OK") print(f"[INFO] logits shape: {outputs.logits.shape}") if __name__ == "__main__": inference_demo()

这只是一个结构演示,真实训练需要:

  1. 大规模图文对

  2. 分阶段训练(对齐 → 指令微调)

  3. 多卡分布式

3.5 VLM 的关键技术点

1. 视觉 token 数量问题

CLIP ViT-L/14 在 336×336 分辨率下产生576 个 token。如果图像再大,token 更多,吃掉 LLM 上下文。

解法:

  • 池化:直接平均池化

  • Q-Former:用固定数量 query 压缩(BLIP-2 用 32 个)

  • Perceiver Resampler:类似

  • Token Merging:合并相似 token

2. 高分辨率处理

方案 A:直接放大图像 → token 爆炸
方案 B:切图(LLaVA-1.6 / Qwen-VL)

  • 整图缩到固定大小

  • 切成多个子图分别编码

  • 拼一起

方案 C:动态分辨率(Qwen2-VL)

  • 按原图比例切 patch

3. 多图 / 视频

  • 多图:每张图编码后拼接

  • 视频:抽帧 + 时序建模

4. 训练数据

数据类型用途规模
图文对(CC3M、LAION)对齐预训练百万-十亿
VQA(VQAv2、OKVQA)问答十万
指令数据(LLaVA-Instruct)对话15 万
OCR 数据文字识别百万
Grounding 数据定位十万

5. 训练技巧

  • LoRA / QLoRA:省显存

  • 梯度检查点:省显存

  • 混合精度:bf16

  • DeepSpeed ZeRO:多卡

3.6 VLM 评估

Benchmark测什么
MMBench综合能力
MMMU大学水平多学科
MathVista数学推理
MME感知 + 认知
POPE幻觉
TextVQA文字识别
SEED-Bench综合

3.7 VLM 的挑战

  1. 幻觉:描述图中不存在的东西

  2. 细粒度理解:小物体、密集文字

  3. 空间推理:左/右/上/下

  4. 长视频:时序理解

  5. 效率:视觉 token 太多

  6. 安全:越狱、有害内容


四、ViT 与 VLM 的关系

text

ViT(视觉编码器) ↓ CLIP(图文对齐,ViT + 文本编码器) ↓ VLM(CLIP ViT + Projector + LLM) ↓ GPT-4V / LLaVA / Qwen-VL

一句话:ViT 是 VLM 的"眼睛",LLM 是"大脑",Projector 是"神经连接"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 14:36:54

DINOv2纯视觉大模型实战:自监督特征提取与检索分类分割应用

我去年在做一个细粒度商品检索项目时,遇到了一个特别典型的困境:用CLIP提取的特征做相似度召回,粗看没问题,但客户要的是“花纹完全一致”的那种匹配,CLIP的语义特征根本分不清近似纹理的差异。后来我把特征提取器换成…

作者头像 李华
网站建设 2026/10/1 14:35:10

Codex不是安装问题,而是开发者认知重构

1. 这不是技术门槛问题,而是认知偏差的典型症状“用不上最先进的 Codex?先别急着说自己不行”——这句话乍看像一句鸡汤,但在我过去三年深度参与数十个AI开发工具链落地项目的过程中,它几乎成了我每次技术分享开场必说的一句话。C…

作者头像 李华
网站建设 2026/10/1 14:34:55

Media Encoder ME2026安装教程附下载地址

前言 Media Encoder ME2026 是 Adobe 旗下的一款专业视频渲染与媒体处理工具,支持各类音视频格式的转码输出。不管你是在做视频剪辑、后期包装还是批量媒体处理,ME2026 都能帮你把渲染效率提上来。这篇 Media Encoder ME2026安装教程 会把从下载到安装的…

作者头像 李华
网站建设 2026/10/1 14:34:27

LLM Agent记忆系统实战:hindsight回溯机制与MCP可插拔架构

1. 从“hindsight”这个词说起:为什么它值得单独拿出来聊 “hindsight”直译过来是“后见之明”,但在 LLM Agent 这个圈子里,它指向的东西要具体得多—— Agent 的记忆系统 。你如果最近在折腾 Agent 相关的东西,大概率已经发现…

作者头像 李华