SAM3 Agent 上下文窗口管理策略:多模态对话中的精细裁剪术
本文基于 SAM3 Agent 系统源码,深入分析其上下文窗口管理机制。在多模态 Agent 系统中,图像数据会导致上下文窗口快速膨胀,如何在有限的窗口内保留关键信息、丢弃冗余历史,是系统稳定运行的核心工程挑战。SAM3 Agent 通过消息裁剪、图片数量硬约束、重复检测与警告注入等机制,实现了对上下文窗口的精细控制。
1. 问题背景:多模态上下文的膨胀危机
1.1 文本 vs 图像的 token 消耗差异
在纯文本对话中,一轮对话通常消耗数百到数千个 token。而在多模态对话中,一张图像经过编码后可能消耗数千甚至上万个 token。以典型的多模态大模型为例:
| 内容类型 | 估算 token 消耗 |
|---|---|
| 一段文本消息(200 字) | ~300 tokens |
| 一张 1008×1008 图像(high detail) | ~1,500-5,000 tokens |
| SAM3 系统提示词(66K 字符) | ~16,000 tokens |
1.2 SAM3 Agent 的上下文膨胀场景
在 SAM3 Agent 的典型推理流程中,每一轮对话可能包含:
- 系统提示词(固定,~16K tokens)
- 用户原始输入(原始图像 + 查询文本)
- MLLM 的推理输出(思维链 + 工具调用)
- 工具执行结果(掩码渲染图 + 结果描述文本)
假设一次完整推理需要 3-5 轮对话,每轮新增 1-2 张图像,上下文将迅速膨胀至模型的窗口上限。如果不加控制,系统将面临以下问题:
- 超出模型上下文窗口限制,导致推理失败
- 早期信息被截断,MLLM 丢失关键上下文
- 过多的历史图像干扰 MLLM 对当前状态的判断
- 推理延迟随上下文长度线性增长
2. 核心机制:三段式消息裁剪
SAM3 Agent 通过 _prune_messages_for_next_round 函数实现了核心的消息裁剪策略。该函数在每轮工具调用完成后、下一轮 MLLM 生成前执行,确保输入给 MLLM 的消息列表始终保持精简。
2.1 裁剪算法
def _prune_messages_for_next_round(
messages_list,
used_text_prompts,
latest_sam3_text_prompt,
img_path,
initial_text_prompt,
):
该函数将消息历史划分为三个部分:
┌─────────────────────────────────────────────────────┐
│ Part 1:始终保留的头部消息(前 2 条) │
│ ├── messages[0]: 系统提示词 │
│ └── messages[1]: 用户原始输入(原始图像 + 查询文本) │
├─────────────────────────────────────────────────────┤
│ Part 2(丢弃):中间的历史消息 │
│ 所有不属于 Part 1 和 Part 3 的消息全部丢弃 │
├─────────────────────────────────────────────────────┤
│ Part 3:最近一次 segment_phrase 调用及其后续消息 │
│ 从后向前搜索,找到最近一次包含 segment_phrase │
│ 工具调用的助手消息,保留该消息及其后的所有消息 │
└─────────────────────────────────────────────────────┘
2.2 裁剪逻辑的代码实现
# Part 1: 始终保留前两条消息
part1 = copy.deepcopy(messages_list[:2])
# Part 3: 从后向前搜索最近一次 segment_phrase 调用
part2_start_idx = None
for idx in range(len(messages_list) - 1, 1, -1):
msg = messages_list[idx]
if msg.get("role") != "assistant":
continue
for content in msg["content"]:
if ("segment_phrase" in content.get("text", "")):
part2_start_idx = idx
break
if part2_start_idx is not None:
break
part2 = messages_list[part2_start_idx:] if part2_start_idx else []
# 最终消息列表 = Part 1 + Part 3
new_messages = list(part1) + list(part2)
2.3 设计原理
该裁剪策略基于以下核心假设:
-
系统提示词和原始输入是不可丢弃的基础上下文——MLLM 需要始终知道自己的角色定义和用户的原始需求。
-
每次调用
segment_phrase都会清除所有先前掩码——这意味着历史的分割结果已经失效,保留它们不仅无用,还会误导 MLLM 引用已不存在的掩码。 -
最近一次
segment_phrase调用及其后续消息构成了当前的"工作状态"——MLLM 需要知道当前有哪些掩码可用、之前对这些掩码做了什么判断。
2.4 裁剪前后的消息结构对比
假设经过 3 轮推理后的完整消息历史:
裁剪前(8 条消息):
[0] system: 系统提示词
[1] user: 原始图像 + "the leftmost child wearing blue vest"
[2] assistant: <think>...</think><tool>segment_phrase("person")</tool> ← 第一次分割
[3] user: "生成了 5 个掩码" + 掩码渲染图
[4] assistant: <think>...</think><tool>segment_phrase("child")</tool> ← 第二次分割
[5] user: "生成了 3 个掩码" + 掩码渲染图
[6] assistant: <think>...</think><tool>examine_each_mask</tool>
[7] user: "审查后保留 1 个掩码" + 新渲染图
裁剪后(6 条消息):
[0] system: 系统提示词
[1] user: 原始图像 + "the leftmost child wearing blue vest" + 警告文本
[4] assistant: <think>...</think><tool>segment_phrase("child")</tool> ← 最近一次分割
[5] user: "生成了 3 个掩码" + 掩码渲染图
[6] assistant: <think>...</think><tool>examine_each_mask</tool>
[7] user: "审查后保留 1 个掩码" + 新渲染图
消息 [2] 和 [3](第一次分割的调用和结果)被完全丢弃,因为那次分割的掩码已经被第二次分割覆盖。
3. 图片数量硬约束
3.1 断言机制
在每轮裁剪完成后,系统执行一个硬性断言:
assert count_images(messages) <= 2
该断言确保发送给 MLLM 的消息中永远不超过 2 张图片:
- 图片 1:原始输入图像(在 Part 1 的用户消息中)
- 图片 2:最新的掩码渲染图(在 Part 3 的工具结果消息中)
3.2 图片计数实现
def count_images(messages):
total = 0
for message in messages:
if "content" in message and isinstance(message["content"], list):
for content_item in message["content"]:
if isinstance(content_item, dict) and content_item.get("type") == "image":
total += 1
return total
3.3 为什么是 2 张?
这个数字的选择基于以下考量:
- MLLM 必须看到原始图像——用于判断颜色、位置等不受掩码渲染干扰的属性
- MLLM 必须看到最新的掩码渲染图——用于识别当前可用的掩码编号和位置
- 更多的图片会显著增加 token 消耗,且历史掩码图已经失效,保留无意义
2 张图片是信息完整性和上下文效率之间的最优平衡点。
3.4 examine_each_mask 的特殊处理
examine_each_mask 工具在执行时会为每个掩码生成 2 张新图片(全图视图 + 放大视图),但这些图片不会进入主对话的消息历史。它们被封装在独立的 iterative_checking_messages 中,通过独立的 MLLM 调用处理:
# 独立的消息列表,不影响主对话
iterative_checking_messages = [
{"role": "system", "content": iterative_checking_system_prompt},
{"role": "user", "content": [原图, 查询文本, 全图掩码图, 放大图]},
]
# 独立调用,结果不追加到主消息历史
checking_generated_text = send_generate_request(iterative_checking_messages)
审查完成后,只有最终的汇总结果(保留了哪些掩码 + 一张新的总览图)被追加到主对话中。这种设计避免了审查过程中的大量图片污染主对话的上下文。
4. 重复检测与警告注入
4.1 重复 text_prompt 的运行时检测
系统维护一个集合 USED_TEXT_PROMPTS,记录所有已使用过的 text_prompt:
USED_TEXT_PROMPTS = set()
# 每次调用 segment_phrase 时检查
current_text_prompt = tool_call["parameters"]["text_prompt"]
if current_text_prompt in USED_TEXT_PROMPTS:
# 拒绝执行,要求使用不同的 prompt
duplicate_prompt_message = f"You have previously used '{current_text_prompt}'..."
messages.append({"role": "user", "content": [{"type": "text", "text": duplicate_prompt_message}]})
else:
USED_TEXT_PROMPTS.add(current_text_prompt)
# 正常执行分割
4.2 警告文本注入
在消息裁剪过程中,如果存在已使用过但效果不佳的 text_prompt,系统会将警告信息注入到 Part 1 的用户消息中:
previously_used = [p for p in used_text_prompts if p != latest_sam3_text_prompt]
if part2 and len(previously_used) > 0:
warning_text = (
f'Note that we have previously called the segment_phrase tool with each '
f'"text_prompt" in this list: {list(previously_used)}, but none of the '
f'generated results were satisfactory. So make sure that you do not use '
f'any of these phrases as the "text_prompt" to call the segment_phrase tool again.'
)
# 将警告注入到第二条消息中
part1[1] = {
"role": "user",
"content": [
{"type": "image", "image": img_path},
{"type": "text", "text": f"...'{initial_text_prompt}'. " + warning_text},
],
}
4.3 双重防御的必要性
重复检测存在于两个层面:
| 层面 | 机制 | 作用 |
|---|---|---|
| 提示词层面 | 规则 8:“不得重复使用相同的 text_prompt” | 软约束,依赖模型遵守 |
| 代码层面 | USED_TEXT_PROMPTS 集合 + 拒绝执行 | 硬约束,模型无法绕过 |
| 上下文层面 | 警告文本注入 | 信息补充,帮助模型在裁剪后仍知道哪些 prompt 已失败 |
第三层(警告注入)的存在是因为消息裁剪会丢弃历史对话——如果 MLLM 在第一轮用了 “person” 但效果不好,第二轮用了 “child”,裁剪后第一轮的消息被丢弃,MLLM 在第三轮可能不知道 “person” 已经试过了。警告注入解决了这个信息丢失问题。
5. 消息总量硬约束
除了图片数量约束外,系统还对消息总数设置了上限:
# 消息历史不应超过 10 条
assert len(messages_list) < 10
该断言在裁剪函数入口处执行,确保即使裁剪逻辑出现异常,消息列表也不会无限增长。结合 max_generations 参数(默认 100)对总轮次的限制,系统在多个维度上防止了资源耗尽。
6. examine_each_mask 的上下文隔离策略
examine_each_mask 的实现展示了一种重要的上下文管理模式:通过独立调用实现上下文隔离。
6.1 问题:审查 N 个掩码需要 2N 张图片
如果将所有审查图片都放入主对话,审查 5 个掩码将新增 10 张图片(每个掩码 2 张),远超 2 张的硬约束。
6.2 解决方案:独立调用 + 结果汇总
# 主对话中:移除包含总览图的消息,替换为纯文本
messages.pop()
messages.append({"role": "user", "content": [{"type": "text", "text": "..."}]})
# 对每个掩码进行独立审查(不影响主对话)
masks_to_keep = []
for i in range(num_masks):
# 构建独立的消息列表
iterative_checking_messages = [...] # 包含 3 张图片
# 独立调用 MLLM
verdict = send_generate_request(iterative_checking_messages)
if "Accept" in verdict:
masks_to_keep.append(i)
# 审查完成后,只将汇总结果追加到主对话
# 新的总览图(只包含保留的掩码)+ 结果描述文本
messages.append({"role": "user", "content": [文本, 新总览图]})
6.3 设计优势
| 维度 | 不隔离(假设方案) | 隔离(实际方案) |
|---|---|---|
| 主对话图片数 | 2 + 2N(可能 12 张) | 始终 ≤ 2 张 |
| 主对话 token 消耗 | 爆炸式增长 | 可控 |
| 审查质量 | MLLM 被大量图片干扰 | 每次只关注一个掩码 |
| 系统提示词 | 需要在主提示词中加入审查规则 | 使用专用的审查提示词 |
7. 完整的上下文生命周期
将上述所有机制串联起来,一次完整推理过程中的上下文管理如下:
初始状态:
messages = [系统提示词, 用户输入(原图+文本)]
图片数: 1 | 消息数: 2
第 1 轮(segment_phrase):
→ MLLM 生成工具调用
→ 执行分割,追加结果(掩码图+文本)
messages = [系统提示词, 用户输入, 助手调用, 工具结果(掩码图)]
图片数: 2 | 消息数: 4
→ 裁剪:无需裁剪(Part 3 = 消息[2:4],与原始一致)
第 2 轮(segment_phrase,换了 text_prompt):
→ MLLM 生成新的工具调用
→ 执行分割,追加结果
messages = [系统提示词, 用户输入, 旧助手, 旧结果, 新助手, 新结果(新掩码图)]
图片数: 3 ← 超标!
→ 裁剪:丢弃旧助手+旧结果,注入警告
messages = [系统提示词, 用户输入(+警告), 新助手, 新结果(新掩码图)]
图片数: 2 ✓ | 消息数: 4
第 3 轮(examine_each_mask):
→ 主对话中移除掩码图消息,替换为纯文本
→ 独立调用 N 次 MLLM 进行审查(不影响主对话)
→ 审查完成,追加汇总结果(新总览图)
messages = [系统提示词, 用户输入(+警告), 旧助手, 纯文本, 新助手, 汇总结果(新总览图)]
→ 裁剪:保留最近的 segment_phrase 调用及后续
messages = [系统提示词, 用户输入(+警告), 旧助手(segment_phrase), 纯文本, 新助手, 汇总结果]
图片数: 2 ✓ | 消息数: 6
第 4 轮(select_masks_and_return):
→ MLLM 确认结果,调用 select_masks_and_return
→ 函数返回最终结果,推理结束
8. 与其他多模态 Agent 系统的对比
SAM3 的上下文管理策略在多模态 Agent 领域具有一定的代表性。以下将其与其他可能的策略进行对比:
8.1 策略对比
| 策略 | 优点 | 缺点 | SAM3 是否采用 |
|---|---|---|---|
| 滑动窗口(保留最近 N 条) | 实现简单 | 可能丢失关键的早期上下文 | ✗ |
| 全量保留 + 截断 | 保留完整历史 | 上下文溢出时信息丢失不可控 | ✗ |
| 语义摘要(用 LLM 总结历史) | 信息压缩率高 | 摘要可能丢失关键细节,增加延迟 | ✗ |
| 三段式裁剪(SAM3 方案) | 精确保留必要信息 | 需要领域知识设计裁剪规则 | ✓ |
| 上下文隔离(独立调用) | 避免污染主对话 | 隔离的调用无法利用主对话上下文 | ✓ |
8.2 SAM3 方案的适用条件
SAM3 的三段式裁剪策略之所以有效,依赖于以下领域特性:
segment_phrase的"清除语义":每次调用都会使历史掩码失效,因此历史分割结果天然可丢弃- 任务的"无状态性":最终决策只依赖当前可用的掩码,不依赖历史掩码的演变过程
- 原始输入的"锚定作用":用户查询和原始图像在整个推理过程中不变,始终是决策的基准
对于不具备这些特性的多模态 Agent 系统(如需要跨轮次积累信息的对话系统),该策略可能不直接适用。
9. 工程实践建议
基于 SAM3 的上下文管理实践,可以提炼出以下适用于一般多模态 Agent 系统的工程建议:
-
明确定义"必须保留"和"可以丢弃"的信息边界。SAM3 的做法是:系统提示词和原始输入永远保留,已失效的历史结果可以丢弃。
-
对图片数量设置硬约束。图片是上下文膨胀的主要来源,必须通过断言或限制机制确保图片数量可控。
-
通过独立调用实现上下文隔离。当某个子任务需要大量临时上下文(如审查多个掩码)时,将其封装为独立的 MLLM 调用,避免污染主对话。
-
信息丢失时主动补偿。裁剪会导致信息丢失,需要通过警告注入等机制将关键信息"浓缩"后重新注入。
-
多层防御优于单层防御。提示词规则、代码断言、运行时检测三者互补,任何单一层面都不足以保证系统稳定。
10. 总结
SAM3 Agent 的上下文窗口管理并非简单的"截断"或"滑动窗口",而是一套基于领域知识精心设计的多层机制:
- 三段式裁剪确保了信息的精确保留与丢弃
- 图片数量硬约束防止了多模态上下文的爆炸式膨胀
- 上下文隔离策略将高消耗的子任务封装在独立调用中
- 警告注入机制补偿了裁剪导致的信息丢失
- 多重断言构成了系统稳定性的最后防线
这些机制共同确保了 SAM3 Agent 能够在有限的上下文窗口内完成复杂的多轮多模态推理,为多模态 Agent 系统的工程实践提供了有价值的参考。
本文基于 SAM3 开源代码中的 agent_core.py 分析撰写,代码版本为 SAM 3.1(2026 年 3 月发布)。
参考资料
- SAM 3 GitHub 仓库:https://github.com/facebookresearch/sam3
- Efficient Transformers: A Survey:https://arxiv.org/abs/2009.06732
- LLM Context Window Management Best Practices:https://docs.anthropic.com/claude/docs/long-context-window-tips
更多推荐




所有评论(0)