摘要
Long Context Model(长上下文模型)是近年来AI领域的热门技术,能处理超长文本(100K tokens甚至更多)。这篇为零基础读者详细讲解:什么是长上下文模型、为什么需要它、核心技术原理(稀疏注意力、旋转位置编码、Ring Attention)、主流模型对比、以及如何选择和使用长上下文模型。看完全篇,你就能搞清楚各种”Long-xxx”模型的区别了。
先理解一个痛点:上下文窗口的”断崖”
短上下文模型的尴尬
想象你在读一本小说,但每次只能看10页:
第一章(1-10页)✓
第二章(11-20页)✓
第三章(21-30页)✓
...第100章... ❌ 看不到!
问题:前面的剧情早就忘了!
短上下文模型的困境:
传统GPT-4:8K tokens
用户:给我总结这篇100页的论文
AI:???论文太长,我看不了
GPT-4-32K:32K tokens
用户:分析这50份用户反馈
AI:???还是装不下
长上下文模型的出现
Long Context Model:128K tokens
论文分析:✓(直接读完全文)
50份反馈:✓(一次性分析)
代码库理解:✓(跨文件分析)
一、什么是长上下文模型
定义
**长上下文模型(Long Context Model)**是指那些上下文窗口远超传统模型的LLM,能够一次性处理超长文本:
| 模型 | 上下文窗口 | 约等于 |
|---|---|---|
| GPT-4 原始版 | 8K | 6000个汉字 |
| Claude 2 | 200K | 15万字(一本小说) |
| Claude 3 | 200K | 同上 |
| Gemini 1.5 | 1M | 75万字(一部长篇小说) |
| Kimi | 128K/256K | 10万-20万字 |
| Yi-200K | 200K | 15万字 |
为什么需要长上下文
┌─────────────────────────────────────────────────────────────┐
│ 长上下文的典型应用场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📄 文档分析 │
│ ├─ 整本《哈利波特》一次性分析 │
│ ├─ 100份合同批量审查 │
│ └─ 法律条文全文引用 │
│ │
│ 💻 代码理解 │
│ ├─ 整个代码仓库理解(10000行+) │
│ ├─ 跨文件依赖分析 │
│ └─ Bug定位与修复 │
│ │
│ 🎬 多模态处理 │
│ ├─ 2小时视频内容理解 │
│ ├─ 100张图片序列分析 │
│ └─ 长时间音频转录 │
│ │
│ 📚 知识库问答 │
│ ├─ 整年邮件分析 │
│ ├─ 几千条聊天记录挖掘 │
│ └─ 数据库Schema全貌理解 │
│ │
└─────────────────────────────────────────────────────────────┘
二、长上下文的三大技术挑战
挑战1:注意力爆炸
标准Transformer的注意力是 O(n²) 复杂度:
n = 序列长度
8K tokens:6400万次计算
128K tokens:163.84亿次计算(256倍!)
挑战2:位置编码失效
传统位置编码(如Sinusoidal)在长序列上会”失效”:
问题:模型只训练过8192位置,突然遇到16384位置
→ 位置信息混乱
→ 注意力不知道谁在前谁在后
挑战3:内存不足
注意力计算需要存储所有Key-Value对:
每个token的KV缓存:
- 假设每个KV向量:128维
- 128K tokens
- 总内存:128K × 128 × 2 (K+V) × 4字节 ≈ 128MB
这只是单层!如果是40层的模型:5GB+!
三、核心技术原理
1. 稀疏注意力(Sparse Attention)
核心思想:不是所有token都需要互相注意
class SparseAttention:
"""
稀疏注意力实现
只关注局部窗口 + 全局token
"""
def __init__(
self,
local_window_size: int = 512,
global_tokens: list = None
):
"""
Args:
local_window_size: 局部窗口大小
global_tokens: 需要全局注意的token索引
"""
self.local_window = local_window_size
self.global_tokens = global_tokens or []
def create_attention_mask(
self,
seq_length: int,
query_pos: int
) -> torch.Tensor:
"""
创建稀疏注意力掩码
"""
mask = torch.zeros(seq_length, dtype=torch.bool)
# 1. 局部窗口:只看附近token
start = max(0, query_pos - self.local_window // 2)
end = min(seq_length, query_pos + self.local_window // 2)
mask[start:end] = True
# 2. 全局token:永远注意
for pos in self.global_tokens:
if 0 <= pos < seq_length:
mask[pos] = True
return mask
def compute_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor
) -> torch.Tensor:
"""
计算稀疏注意力
"""
seq_len = query.shape[1]
outputs = []
for pos in range(seq_len):
# 获取当前token的注意力掩码
mask = self.create_attention_mask(seq_len, pos)
# 计算注意力
q = query[:, pos:pos+1] # [batch, 1, dim]
k = key[:, mask] # [batch, local, dim]
v = value[:, mask] # [batch, local, dim]
# 注意力分数
scores = torch.matmul(q, k.transpose(-2, -1)) / (q.shape[-1] ** 0.5)
weights = torch.softmax(scores, dim=-1)
# 加权求和
output = torch.matmul(weights, v)
outputs.append(output)
return torch.cat(outputs, dim=1)2. 滑动窗口注意力(Sliding Window Attention)
Longformer的核心技术:
class SlidingWindowAttention:
"""
滑动窗口注意力
每层有不同大小的窗口,逐层扩大感受野
"""
def __init__(
self,
num_layers: int,
base_window: int = 64,
max_window: int = 4096
):
self.num_layers = num_layers
self.base_window = base_window
self.max_window = max_window
# 计算每层的窗口大小(指数增长)
self.layer_windows = self._compute_layer_windows()
def _compute_layer_windows(self) -> list:
"""计算每层的窗口大小"""
windows = []
for layer in range(self.num_layers):
# 底层小窗口,顶层大窗口
# 公式:min(base_window * 2^layer, max_window)
window = min(
self.base_window * (2 ** layer),
self.max_window
)
windows.append(window)
return windows
def get_window_size(self, layer: int) -> int:
"""获取指定层的窗口大小"""
return self.layer_windows[layer]
# Longformer风格的注意力模式
class LongformerAttentionPattern:
"""
Longformer的注意力模式
"""
# 注意力类型
ATTENTION_GLOBAL = "global" # 关注所有token
ATTENTION_LOCAL = "local" # 局部窗口
ATTENTION_WINDOW = "window" # 滑动窗口
ATTENTION_DILATED = "dilated" # 膨胀窗口
def __init__(self, seq_length: int):
self.seq_length = seq_length
self.attention_types = {}
def set_global_attention(self, token_indices: list):
"""设置全局注意力的token"""
for idx in token_indices:
self.attention_types[idx] = self.ATTENTION_GLOBAL
def set_local_attention(self, start: int, end: int):
"""设置局部注意力的范围"""
for idx in range(start, end):
if idx not in self.attention_types:
self.attention_types[idx] = self.ATTENTION_LOCAL
def get_attention_type(self, idx: int) -> str:
"""获取token的注意力类型"""
return self.attention_types.get(idx, self.ATTENTION_WINDOW)
def compute_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
token_idx: int
) -> torch.Tensor:
"""根据注意力类型计算"""
attn_type = self.get_attention_type(token_idx)
if attn_type == self.ATTENTION_GLOBAL:
# 全局:注意所有token
return self._global_attention(query, key, value)
elif attn_type == self.ATTENTION_LOCAL:
# 局部:注意窗口内token
return self._local_attention(query, key, value)
else:
# 窗口:滑动窗口注意力
return self._window_attention(query, key, value)3. 旋转位置编码(RoPE)—— 长上下文的关键
RoPE(Rotary Position Embedding)是让模型支持超长上下文的核心技术:
import numpy as np
class RotaryPositionEmbedding:
"""
旋转位置编码
核心思想:用旋转矩阵编码位置信息,可以外推到训练长度之外
"""
def __init__(self, dim: int, max_seq_len: int = 32768):
self.dim = dim
self.max_seq_len = max_seq_len
# 预计算旋转角度
self.inv_freq = self._compute_inv_freq(dim)
def _compute_inv_freq(self, dim: int) -> np.ndarray:
"""计算频率的倒数"""
# RoPE使用的频率范围
base = 10000.0
# 频率递减
inv_freq = 1.0 / (base ** (np.arange(0, dim, 2) / dim))
return inv_freq
def get_rotations(self, positions: np.ndarray) -> tuple:
"""获取旋转矩阵"""
# 计算角度
angles = positions[:, np.newaxis] * self.inv_freq[np.newaxis, :]
# 复数形式
cos = np.cos(angles)
sin = np.sin(angles)
return cos, sin
def rotate_query_key(self, q: np.ndarray, k: np.ndarray, positions: np.ndarray) -> tuple:
"""
对Query和Key应用旋转
"""
cos, sin = self.get_rotations(positions)
# 旋转公式:RoPE(q) = q * cos(θ) + rotate(q) * sin(θ)
# 简化实现
q_rotated = q * cos + self._rotate_half(q) * sin
k_rotated = k * cos + self._rotate_half(k) * sin
return q_rotated, k_rotated
@staticmethod
def _rotate_half(x: np.ndarray) -> np.ndarray:
"""旋转半个维度"""
x1 = x[..., :x.shape[-1]//2]
x2 = x[..., x.shape[-1]//2:]
return np.concatenate([-x2, x1], axis=-1)
# 演示RoPE如何处理长序列
def demo_rope_long_context():
"""
演示RoPE如何处理超长序列
"""
rope = RotaryPositionEmbedding(dim=128, max_seq_len=200000)
# 短序列(训练范围内)
short_pos = np.arange(100)
cos_s, sin_s = rope.get_rotations(short_pos)
print(f"短序列(100) - cos范围: [{cos_s.min():.4f}, {cos_s.max():.4f}]")
# 长序列(训练范围外)
long_pos = np.arange(100, 200000)
cos_l, sin_l = rope.get_rotations(long_pos)
print(f"长序列(200K) - cos范围: [{cos_l.min():.4f}, {cos_l.max():.4f}]")
# 关键:RoPE的cos/sin在长序列上仍然有合理值
# 这使得模型可以"外推"到训练长度之外
print("\nRoPE的优势:")
print("1. 不需要位置编码的线性增长")
print("2. 可以处理任意长度的序列")
print("3. 位置信息通过旋转自然编码")4. Ring Attention(环形注意力)
处理超长序列的分布式方案:
class RingAttention:
"""
环形注意力 - 分布式处理超长序列
多个设备形成一个环,每个设备处理一部分
"""
def __init__(self, num_devices: int, device_rank: int):
self.num_devices = num_devices
self.rank = device_rank
def local_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor
) -> torch.Tensor:
"""
本地注意力计算
"""
# 只计算本地的注意力
return self._scaled_dot_product(q, k, v)
def _scaled_dot_product(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor
) -> torch.Tensor:
"""缩放点积注意力"""
d_k = q.shape[-1]
scores = torch.matmul(q, k.transpose(-2, -1)) / (d_k ** 0.5)
attn_weights = torch.softmax(scores, dim=-1)
return torch.matmul(attn_weights, v)
def ring_forward(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor
) -> torch.Tensor:
"""
环形前向传播
"""
# 1. 计算本地注意力
output = self.local_attention(q, k, v)
# 2. 与下一个设备交换KV
for step in range(self.num_devices - 1):
# 发送KV到下一个设备
next_rank = (self.rank + 1) % self.num_devices
prev_rank = (self.rank - 1) % self.num_devices
# 接收来自上一个设备的KV
remote_k, remote_v = self._recv_from(prev_rank)
# 计算额外注意力
extra_attn = self._scaled_dot_product(q, remote_k, remote_v)
# 聚合
output = output + extra_attn / (step + 2)
return output
def _recv_from(self, src_rank: int) -> tuple:
"""从其他设备接收数据(伪代码)"""
# 实际实现需要分布式通信
pass
def _send_to(self, dst_rank: int, k: torch.Tensor, v: torch.Tensor):
"""发送数据到其他设备(伪代码)"""
pass
class FlashAttention:
"""
Flash Attention - 加速注意力计算
核心思想:分块计算,避免HBM访问
"""
def __init__(self, block_size: int = 128):
self.block_size = block_size
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor
) -> torch.Tensor:
"""
Flash Attention前向传播
"""
# 获取维度
batch_size, seq_len, num_heads, head_dim = q.shape
# 初始化输出和最大值
output = torch.zeros_like(q)
l = torch.zeros((batch_size, num_heads, seq_len, 1))
m = torch.full((batch_size, num_heads, seq_len, 1), -float('inf'))
# 分块遍历
for j in range(0, seq_len, self.block_size):
# 当前块
kj = k[:, :, j:j+self.block_size, :]
vj = v[:, :, j:j+self.block_size, :]
# 计算块内的attention
for i in range(0, seq_len, self.block_size):
qi = q[:, :, i:i+self.block_size, :]
# 恢复softmax计算
mi_new = torch.maximum(m[:, :, i:i+self.block_size], torch.max(qi @ kj.transpose(-2,-1) / (head_dim ** 0.5), dim=-1, keepdim=True)[0])
# ... 完整的Flash Attention实现省略
return output四、主流长上下文模型对比
模型横评
┌─────────────────────────────────────────────────────────────────────────┐
│ 长上下文模型对比表 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 模型 上下文窗口 关键技术 特点 │
│ ──────────────────────────────────────────────────────────────────── │
│ Claude 3 (Sonnet) 200K - 性能均衡 │
│ Gemini 1.5 Pro 1M - 超长上下文 │
│ Kimi (月之暗面) 128K/256K RoPE 中文优化 │
│ Yi-200K 200K RoPE 开源 │
│ Longformer 16K 滑动窗口 最早的长上下文 │
│ BigBird 16K 稀疏注意力 理论证明 │
│ Mamba ∞ SSM 无注意力 │
│ RWKV ∞ RNN+Attention 高效推理 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
各模型详细分析
Claude 3 (200K上下文)
优点:
- 性能稳定,长上下文表现好
- 支持文件上传
- 上下文理解能力强
缺点:
- 价格较贵
- 不能超过200K tokens
适用场景:
- 法律/金融文档分析
- 代码库理解
- 长篇小说创作
Gemini 1.5 (1M上下文)
优点:
- 超长上下文(100万tokens)
- 多模态能力强
- 价格相对便宜
缺点:
- 长上下文时质量可能下降
- 某些任务不如专用模型
适用场景:
- 音视频分析
- 超长文档处理
- 大规模代码分析
Kimi / Yi (开源长上下文)
优点:
- 开源可商用
- 中文优化好
- 可以本地部署
缺点:
- 需要自己优化
- 硬件要求高
适用场景:
- 企业内部部署
- 需要数据隐私的场景
- 定制化需求
五、如何选择和使用长上下文模型
选择指南
┌─────────────────────────────────────────────────────────────┐
│ 长上下文模型选择决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 开始 │
│ │ │
│ ├─ 场景:短文档(<10K tokens) │
│ │ └─ 推荐:普通GPT-4 / Claude 3足够了 │
│ │ │
│ ├─ 场景:中长文档(10K-200K tokens) │
│ │ ├─ 预算充足 → Claude 3 │
│ │ └─ 预算有限 → Kimi / Yi-200K │
│ │ │
│ └─ 场景:超长文档(>200K tokens) │
│ ├─ 需要多模态 → Gemini 1.5 │
│ └─ 本地部署 → Mamba / RWKV │
│ │
└─────────────────────────────────────────────────────────────┘
使用技巧
class LongContextUsageHelper:
"""长上下文模型使用助手"""
@staticmethod
def estimate_tokens(text: str) -> int:
"""估算token数"""
chinese = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
english = len(text.split()) - chinese
return int(chinese * 0.5 + english * 0.25)
@staticmethod
def check_window_size(
texts: List[str],
model_limit: int
) -> dict:
"""检查是否超过窗口限制"""
total = sum(LongContextUsageHelper.estimate_tokens(t) for t in texts)
return {
'total_tokens': total,
'model_limit': model_limit,
'within_limit': total <= model_limit,
'utilization': total / model_limit * 100,
'recommendation': 'OK' if total <= model_limit * 0.8
else '接近上限,建议优化'
if total <= model_limit
else '超出限制,需要截断'
}
@staticmethod
def smart_chunk(
text: str,
max_tokens: int,
overlap_tokens: int = 200
) -> List[str]:
"""
智能分块 - 按语义边界切分
"""
# 按段落分割
paragraphs = text.split('\n\n')
chunks = []
current_chunk = []
current_tokens = 0
for para in paragraphs:
para_tokens = LongContextUsageHelper.estimate_tokens(para)
if current_tokens + para_tokens > max_tokens:
if current_chunk:
chunks.append('\n\n'.join(current_chunk))
# 保留重叠
if overlap_tokens > 0 and current_chunk:
# 找到能放进overlap的内容
overlap_text = []
overlap_size = 0
for p in reversed(current_chunk):
p_tokens = LongContextUsageHelper.estimate_tokens(p)
if overlap_size + p_tokens <= overlap_tokens:
overlap_text.insert(0, p)
overlap_size += p_tokens
else:
break
current_chunk = overlap_text
current_tokens = overlap_size
else:
current_chunk = []
current_tokens = 0
current_chunk.append(para)
current_tokens += para_tokens
if current_chunk:
chunks.append('\n\n'.join(current_chunk))
return chunks
# 使用示例
def demo_long_context_usage():
"""演示长上下文模型使用"""
helper = LongContextUsageHelper()
# 假设有一篇论文
paper = "..." * 1000 # 模拟长文本
# 检查是否需要分块
result = helper.check_window_size([paper], model_limit=200000)
print(f"文档分析:{result}")
# 如果需要分块
if result['total_tokens'] > result['model_limit'] * 0.8:
chunks = helper.smart_chunk(
paper,
max_tokens=160000, # 留20%余量
overlap_tokens=1000
)
print(f"分块数量:{len(chunks)}")常见陷阱与避免
class LongContextPitfalls:
"""长上下文常见陷阱"""
@staticmethod
def pitfall_1_lost_in_middle():
"""
陷阱1:Lost in the Middle
模型对中间部分的信息记忆最差
"""
# 错误做法:把重要信息放在中间
prompt = f"""
开头:{context[:10000]}
中间(重要信息在这里):{important_info}
结尾:{context[10000:]}
问题:{question_about_important_info}
"""
# 正确做法1:把重要信息放开头或结尾
prompt_v1 = f"""
【重要信息】:{important_info}
上下文:{context}
问题:{question_about_important_info}
"""
# 正确做法2:使用标记强调
prompt_v2 = f"""
上下文:{context}
【!!! 重要 !!!】:{important_info}
问题:{question_about_important_info}
"""
return prompt_v1, prompt_v2
@staticmethod
def pitfall_2_redundancy():
"""
陷阱2:上下文冗余
长上下文包含太多无关信息
"""
# 错误做法:直接塞入全部内容
# prompt = full_document
# 正确做法:先摘要,再提问
def summarize_then_ask(doc: str, question: str) -> str:
# 1. 摘要关键部分
summary_prompt = f"""从以下文档中提取与回答问题相关的内容:
问题:{question}
文档:{doc}
只提取直接相关的部分,不要添加解释。"""
relevant = llm.generate(summary_prompt)
# 2. 用相关部分回答
return f"""相关文档内容:
{relevant}
问题:{question}
请基于以上内容回答。"""
return summarize_then_ask
@staticmethod
def pitfall_3_ignoring_structure():
"""
陷阱3:忽略文档结构
"""
# 错误做法:把Markdown当纯文本
# prompt = raw_markdown_text
# 正确做法:利用结构
def use_structure(markdown_doc: str, question: str) -> str:
return f"""# 文档结构
{md_to_outline(markdown_doc)}
---
# 问题
{question}
请基于上述结构回答,重点关注相关章节。"""
return use_structure六、生产环境实战
构建长上下文RAG
class LongContextRAG:
"""长上下文RAG系统"""
def __init__(
self,
llm_client,
embed_model,
vector_store,
max_context_tokens: int = 160000
):
self.llm = llm_client
self.embed = embed_model
self.store = vector_store
self.max_tokens = max_context_tokens
def query(
self,
question: str,
filter_metadata: dict = None
) -> str:
"""
查询 - 自动决定使用普通RAG还是长上下文
"""
# 1. 检索相关文档
results = self.store.search(
query=question,
top_k=10,
filter=filter_metadata
)
# 2. 计算总token
total_tokens = sum(
self._estimate_tokens(r['content'])
for r in results
)
# 3. 决定策略
if total_tokens <= self.max_tokens:
return self._direct_context_query(question, results)
else:
return self._hierarchical_query(question, results)
def _direct_context_query(
self,
question: str,
results: list
) -> str:
"""直接上下文查询"""
context = '\n\n'.join([r['content'] for r in results])
prompt = f"""基于以下上下文回答问题。如果上下文中没有答案,说明不知道。
上下文:
{context}
问题:{question}
答案:"""
return self.llm.generate(prompt)
def _hierarchical_query(
self,
question: str,
results: list
) -> str:
"""分层查询 - 先摘要再回答"""
# 1. 将结果分成多个批次
batches = self._batch_results(results, batch_size=3)
# 2. 每批生成摘要/答案
batch_summaries = []
for batch in batches:
context = '\n\n'.join([r['content'] for r in batch])
summary_prompt = f"""阅读以下内容,用2-3句话总结其核心观点:
{context}
核心观点:"""
summary = self.llm.generate(summary_prompt)
batch_summaries.append(summary)
# 3. 基于所有摘要回答
combined_summary = '\n'.join(
f"- {s}" for s in batch_summaries
)
final_prompt = f"""基于以下各部分的摘要回答问题:
{batch_summaries}
问题:{question}
请综合以上内容给出完整答案。"""
return self.llm.generate(final_prompt)
def _batch_results(
self,
results: list,
batch_size: int
) -> list:
"""分批处理结果"""
batches = []
for i in range(0, len(results), batch_size):
batches.append(results[i:i+batch_size])
return batches
@staticmethod
def _estimate_tokens(text: str) -> int:
chinese = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
english = len(text.split()) - chinese
return int(chinese * 0.5 + english * 0.25)七、总结与展望
┌─────────────────────────────────────────────────────────────────────┐
│ Long Context Model 速查 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 🎯 核心价值 │
│ └─ 突破传统8K/32K限制,支持100K+ tokens处理 │
│ │
│ 🔧 关键技术 │
│ ├─ 稀疏注意力:只关注关键token │
│ ├─ 滑动窗口:不同层不同感受野 │
│ ├─ RoPE:支持超长序列的位置编码 │
│ ├─ Ring Attention:分布式处理超长序列 │
│ └─ Flash Attention:高效计算注意力 │
│ │
│ 📊 主流模型 │
│ ├─ Claude 3:200K,最均衡 │
│ ├─ Gemini 1.5:1M,最长 │
│ └─ Kimi/Yi:开源可商用 │
│ │
│ 💡 使用技巧 │
│ ├─ 重要信息放开头或结尾 │
│ ├─ 避免上下文冗余 │
│ ├─ 利用文档结构 │
│ └─ 留10-20%余量 │
│ │
│ 🚀 未来趋势 │
│ ├─ 更长:向1M+甚至无限上下文发展 │
│ ├─ 更快:推理效率持续优化 │
│ └─ 更便宜:成本进一步降低 │
│ │
└─────────────────────────────────────────────────────────────────────┘
相关主题
参考文献
- Beltagy, I., et al. (2020). Longformer: The Long-Document Transformer. arXiv:2004.05150.
- Zaheer, M., et al. (2020). Big Bird: Transformers for Longer Sequences. NeurIPS 2020.
- Su, J., et al. (2022). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864.
- Liu, H., et al. (2023). Ring Attention. arXiv:2308.10860.
- Dao, T., et al. (2022). FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. NeurIPS 2022.
- Gemini Team (2024). Gemini 1.5: Unlocking Multimodal Understanding Across Millions of Tokens of Context.