本文基于 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 张图像,上下文将迅速膨胀至模型的窗口上限。如果不加控制,系统将面临以下问题:

  1. 超出模型上下文窗口限制,导致推理失败
  2. 早期信息被截断,MLLM 丢失关键上下文
  3. 过多的历史图像干扰 MLLM 对当前状态的判断
  4. 推理延迟随上下文长度线性增长

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 设计原理

该裁剪策略基于以下核心假设:

  1. 系统提示词和原始输入是不可丢弃的基础上下文——MLLM 需要始终知道自己的角色定义和用户的原始需求。

  2. 每次调用 segment_phrase 都会清除所有先前掩码——这意味着历史的分割结果已经失效,保留它们不仅无用,还会误导 MLLM 引用已不存在的掩码。

  3. 最近一次 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 张?

这个数字的选择基于以下考量:

  1. MLLM 必须看到原始图像——用于判断颜色、位置等不受掩码渲染干扰的属性
  2. MLLM 必须看到最新的掩码渲染图——用于识别当前可用的掩码编号和位置
  3. 更多的图片会显著增加 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 的三段式裁剪策略之所以有效,依赖于以下领域特性:

  1. segment_phrase 的"清除语义":每次调用都会使历史掩码失效,因此历史分割结果天然可丢弃
  2. 任务的"无状态性":最终决策只依赖当前可用的掩码,不依赖历史掩码的演变过程
  3. 原始输入的"锚定作用":用户查询和原始图像在整个推理过程中不变,始终是决策的基准

对于不具备这些特性的多模态 Agent 系统(如需要跨轮次积累信息的对话系统),该策略可能不直接适用。

9. 工程实践建议

基于 SAM3 的上下文管理实践,可以提炼出以下适用于一般多模态 Agent 系统的工程建议:

  1. 明确定义"必须保留"和"可以丢弃"的信息边界。SAM3 的做法是:系统提示词和原始输入永远保留,已失效的历史结果可以丢弃。

  2. 对图片数量设置硬约束。图片是上下文膨胀的主要来源,必须通过断言或限制机制确保图片数量可控。

  3. 通过独立调用实现上下文隔离。当某个子任务需要大量临时上下文(如审查多个掩码)时,将其封装为独立的 MLLM 调用,避免污染主对话。

  4. 信息丢失时主动补偿。裁剪会导致信息丢失,需要通过警告注入等机制将关键信息"浓缩"后重新注入。

  5. 多层防御优于单层防御。提示词规则、代码断言、运行时检测三者互补,任何单一层面都不足以保证系统稳定。

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
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐