Switch-SWE拆解:同仓库任务切换一次,编码Agent解决率平均掉11.5pp,上下文管理怎么补回15.5pp
一个编码 Agent 在仓库级基准上能解一半的 issue,看起来已经能独立干活了。但真实开发从来不是单线程的:你让它修一个功能修到一半,转头塞给它另一个 bug,过一会儿再让它回来接着干——它还能接得住吗?
上海交大和同济大学的团队把这个日常场景做成了一个基准,叫 Switch-SWE,论文在 2026 年 11 月 1 日挂上 arXiv。答案相当扎心:7 个主流编码 Agent,切换后焦点任务解决率平均掉 11.5 个百分点,掉得最多的一个直接少了 15 个百分点,相对降幅 32.3%。
更难受的是账本的另一面:切换会话的 token 消耗不但没省,反而涨到不间断会话的 4.5 到 11.1 倍。花更多的钱,交更差的活——这就是当前编码 Agent 在多任务会话里的真实处境。
这篇文章把 Switch-SWE 的论文拆开:基准怎么搭、七个 Agent 的成绩单、成本账、两类翻车现场,以及论文给出的插件式补救方案 RCM 到底补回了多少。最后给一个可以自己跑的最小实现,把「任务状态记忆」这件事落到代码里。
一、基准怎么搭:200 个任务,400 场配对会话
Switch-SWE 的核心设计是「配对」。它从 19 个 Python 仓库里选出 200 个焦点任务,每个任务跑两遍:一遍不间断,一口气做完;一遍切换,中途插入同仓库的其他未完成任务。两遍加起来是 400 个会话规格。
任务本身不是拍脑袋造的,而是从真实的 issue、pull request 和 review 历史里重建的。一个任务被拆成一串有序的实质请求:第一段来自 issue 标题和正文,后续段落按 pre-merge 评审轮次的时间顺序整理,自动消息、纯进度的评论、没有新需求的 approval 全部被剔除。
每个任务还必须过执行资格关:至少有一个 fail-to-pass 测试,在基线版本上失败、打上参考修复后通过;同时指定的 pass-to-pass 测试在两个状态下都通过,证明没有回归。环境版本全部钉死,保证可复现。
切换会话里,焦点任务会被四个同仓库的未完成任务打断。论文给的示意调度是:焦点任务第一阶段,然后 B、C、D、E 四个任务各推进一段,焦点任务第二阶段,再四个任务各推进一段,焦点任务第三阶段,最后提交。调度在评测前固定,每个任务的请求顺序保持不变。
关键约束是一个投影等式:把切换会话里属于焦点任务的请求抽出来,必须和不间断会话完全一致。需求文本、执行验证器、基线快照、阶段预算,两种条件下全部相同,唯一变量是「中间插没插别的活」。只有焦点任务的最终补丁被评分,插入任务的实现不进入主分数。
还有一个细节常被忽略:每个任务有自己独立的持久 checkout,五个工作区路径角色中立。也就是说,磁盘上的代码从不打架——这个基准刻意排除了文件冲突,考的是纯粹的会话级任务恢复。每个请求有 900 秒的执行预算。
评测协议同样钉得很死:焦点补丁必须能成功应用到基线上,且 fail-to-pass 与 pass-to-pass 测试的并集全部通过才算 resolved。一旦确认是基础设施故障而非 Agent 的问题,评测方会用同一份存档的提交物重跑评测,不让环境抖动污染分数。四个中断任务各自贡献一段有序的请求前缀,焦点任务的收尾请求永远排在整个会话的最后,保证「回来完成它」这个动作必然发生。
二、七个 Agent 的成绩单
论文测了七个模型加脚手架的组合,全部使用中等推理档位。焦点任务解决率的完整对比如下:
| 模型 | 脚手架 | 不间断 | 切换后 | 掉点 |
|---|---|---|---|---|
| Claude Opus 5 | Claude Code | 53.5% | 41.5% | 12.0pp |
| GPT-5.6 Sol | Codex | 52.5% | 38.0% | 14.5pp |
| GLM-5.3 | OpenCode | 49.0% | 43.0% | 6.0pp |
| Kimi K3 | KimiCode | 48.0% | 34.5% | 13.5pp |
| DeepSeek-V4.1-Flash | OpenCode | 46.5% | 31.5% | 15.0pp |
| GPT-5.6 Terra | Codex | 41.0% | 28.0% | 13.0pp |
| Gemini 3.7 Flash | Gemini CLI | 29.5% | 23.0% | 6.5pp |
几个值得盯住看的点。不间断成绩最高的是 Claude Opus 5 的 53.5%,但切换后的第一名换成了 GLM-5.3 的 43.0%,它的掉点也是全场最小的 6.0 个百分点。DeepSeek-V4.1-Flash 掉得最狠:从 46.5% 跌到 31.5%,15 个百分点的绝对差,相当于每三个能解的题丢一个。
读这张表要避开两个直觉陷阱。掉点小不等于能力强——Gemini 3.7 Flash 的 6.5pp 建立在 29.5% 的低基数上,可掉的空间本来就小。掉点大也不等于能力弱——GPT-5.6 Sol 的基数是 52.5%,它切换后的 38.0% 仍然压过多数对手的不间断成绩。
真正被这个基准单独量出来的,是「恢复力」这个维度:它和不间断解题能力相关,但明显是另一回事。七个配置全部呈现正掉点,没有一个 Agent 在被打断后还能维持原有水平。
这个坑之所以以前没被量到,是因为相邻基准各管一段。SWE-bench 系列考的是单次 issue 到补丁的完整闭环;SWE-Touch 考用户中途改代码后的适应;SWE-ContextBench 考从已执行过的相关任务里复用经验;InterruptBench 考网页导航中的目标变更;ContinuityBench 考从保留的环境状态里恢复;Handoff Debt 考把未完成的任务交接给另一个 Agent 的代价。Switch-SWE 卡的位置是它们都没覆盖的组合:同一个 Agent、同一段连续会话、同一个仓库、多个都未完成的任务——这恰恰是日常结对开发里最普通的场面。
三、成本账:花得更多,交得更差
解决率下跌只是账本的一半。论文第二张表统计了 token 消耗、模型调用次数和工具调用次数,切换会话全线变贵。
完整的切换会话消耗不间断会话 4.5 到 11.1 倍的 token,而模型调用次数只涨 2.4 到 4.3 倍。token 的增速明显快过调用次数的增速,意味着每次调用平均要处理更厚的上下文——中断任务的历史全部留在会话里,被后续每一次调用反复吞咽。
以最极端的 DeepSeek-V4.1-Flash 配 OpenCode 为例:不间断会话平均 859 万 token,切换会话平均 1.02 亿 token。Claude Opus 5 配 Claude Code 从 314.5 万涨到 1823.7 万。即便是控制得最好的 GLM-5.3,也从 490 万涨到 1497 万。
有人会说:切换会话干了五个任务的活,贵是应该的。论文因此单独切了一刀——只看「恢复焦点任务的实质阶段」,即去掉首次请求、去掉中断任务的执行、去掉最终提交,仅计算 Agent 回来接着干焦点任务的那几段。
结果是:仅这几个恢复阶段,就消耗掉整个不间断会话 1.41 到 2.95 倍的 token,而模型调用次数并没有同比例增加。换句话说,Agent 回到老任务时,是背着全部中断历史在工作的。花十倍的钱办一件事已经够疼了,更疼的是其中相当一部分开销,只是在为「记得自己刚才在干嘛」买单。
把解决率和成本两组数字叠在一起,会得到一个反直觉的结论:多任务并行在编码 Agent 身上不但不省时间,反而同时在质量和成本两个方向上收税。对按 token 计费的 API 用户,这笔税直接进账单;对本地部署的用户,它体现为更长的排队和更高的显存占用。论文没有给出「所以别并行」的结论,但数据本身已经把话说了:在当前的原生上下文管理下,让一个 Agent 串行做完五件事,比让它在五件事之间来回跳要便宜得多,也可靠得多。
四、打断的位置很关键:同目录和异目录差一倍
Switch-SWE 还做了一个精细的切分:按中断任务和焦点任务的源码目录是否重叠,把 200 个任务分成 near 和 far 两组,每组各 100 个。near 组的中断任务和焦点任务共享直接源码目录,far 组则完全不在同一个目录。
结果非常整齐:七个配置在 far 组的掉点全部大于 near 组。near 组平均掉 8.1 个百分点,far 组平均掉 14.9 个百分点,差距接近一倍。配置内的 near-far 差从 3.0 到 10.0 个百分点不等。
最夸张的是 GLM-5.3 配 OpenCode:near 组只掉 1.0 个百分点,几乎免疫同目录打断;far 组直接掉到 11.0 个百分点。连差异最小的 Kimi K3 也是同方向,near 组 12.0、far 组 15.0。
这个 pattern 最容易被误读,论文自己也做了排除:会不会只是 far 组的题目更难?不会。七个配置里有五个,在不间断条件下反而是 far 组做得更好——也就是说没有打断时,far 组的题对它们来说更容易;一旦加入打断,它们在 far 组反而摔得更惨。排序反转说明,不间断成绩解释不了恢复失败,真正的变量是上下文:邻近的工作留下了可复用的痕迹,异目录的打断则把相关痕迹冲得一干二净。
论文措辞很谨慎,明确说这只是关联而非因果。但对工程实践来说,方向已经足够清楚:让 Agent 在同一个模块里连续打转,比让它在毫不相干的模块之间来回跳要安全得多。
五、翻车现场:两类病,三种死法
论文对失败轨迹做了人工检查,把恢复失败归成两类反复出现的行为模式。第一类叫跨任务需求混淆:Agent 把别的任务的需求、文件或结论混进了焦点任务。第二类叫任务状态丢失:Agent 忘了焦点任务早期阶段已经定下来的约束,在新需求上盖楼时把旧地基拆了。
失败的发生位置则分成三种:实现阶段出错、提交阶段出错、执行不完整。一个值得记住的结论是,实现恢复和提交恢复是可以分离的——代码改对了,不代表能交出对的补丁。
看三个具体案例。第一个来自 Click 库:焦点任务先要求字面值 True 的布尔标志走某条转换规则,后续阶段又引入可调用默认值。切换后的 Agent 成功实现了新的 callable 分支,却没有把它和早先的字面值规则调和起来——新逻辑盖在了不完整的旧状态表示上。它自己的本地检查全部通过,但留档验证器拒绝了未设防的字面值分支。这是典型的需求状态丢失:不是没能力写代码,是忘了「之前还答应过什么」。
第二个案例来自 Moto 库的 SNS 到 SQS 投递修复,跑的是 GPT-5.6 Sol 配 Codex。Agent 在四个其他工作区轮转一圈后,正确回到了焦点工作区,连消息属性缺失、raw 与非 raw 投递、负载精确保留这些细节都处理对了。但到了提交阶段,它把「该交什么」的范围搞混了,开始用循环扫描全部五个工作区的 git diff。不间断版本交出的补丁通过了全部 5 个 F2P 和 8 个 P2P 测试,切换版本则没能产出可用的最终补丁。实现对了,提交毁了——这正是跨任务干扰钻进提交流程的样子。
第三个是正面案例,来自 NetworkX 的平面对偶图任务,跑的是 DeepSeek-V4.1-Flash 配 OpenCode。切换后的 Agent 先实现了按组件遍历的表示,中断回来后在此基础上建 dual_graph(),并记录原始面与半边关联映射;再下一次回来加收缩操作时,继续把这些映射当作不可破坏的溯源信息来维护,25 个 F2P 和 41 个 P2P 测试全过。成功的恢复不是「记住最后一条指令」,而是扩展早期表示的同时,守住之前各阶段建立的不变量。
把三个案例并排看,两类病的分工很清楚:需求状态丢失伤在实现深处,本地测试查不出来,只有留档验证器能抓到;跨任务混淆伤在提交边界,代码可能全对,交付物却是错的。前者骗过自检,后者骗过流程,共同的根源都是恢复那一刻的上下文里少了点东西,或者多了点东西。
六、RCM:不动 Agent 本身,只在上下文边界动刀
针对这两类病,论文给出的药方叫 Resumption Context Manager,简称 RCM。它的定位很克制:一个插件,挂在 OpenCode 的上下文边界上,不改仓库状态,不改工具语义,不改原生的模型-工具循环,只调整每次调用喂给模型的消息。
RCM 有两个部件。第一个是任务历史路由器。每来一条用户请求,路由器根据语义目标、显式的任务引用、活动路径、仓库名和会话里出现过的代码符号,把请求分配给已有任务或新建任务条目。历史按「交互组」存储:一条用户请求、Agent 的响应动作、工具返回结果绑在一起,不再拆散。任务恢复时,只有属于该任务及显式相关任务的组被注入,共享指令和未分配交互保持可见。

路由器有一条保守红线:分配置信度低于 0.8 时,保留全部可用历史,宁可多给上下文,也不冒险过滤掉关键信息。这个设计直接回应了上下文管理的经典翻车点——激进裁剪省下的 token,往往不够赔裁错内容造成的损失。
第二个部件是任务状态记忆,为每个任务维护一份简洁记录,固定六个字段:当前目标、累积约束、已完成工作、待办工作、相关文件或符号、未解决的不确定点。更新规则也定了基调:用户请求定义需求,工具输出提供观测到的仓库状态,而 Assistant 自己的陈述只算「声称」,必须被后续工具证据支持才算数。当前用户指令永远压过存储状态——记忆是辅助,不是圣旨。
对原生压缩机制的处理也很讲究:OpenCode 自己的 compaction 保持开启,但压缩期间 RCM 会绕过任务级过滤,并附加指令要求保留所有主题的目标、约束、进度和未决工作,包括那些正被挂起的任务。挂起任务的状态恰恰是最容易被压缩冲掉的,而它又是恢复时最需要的东西。
效果上,消融实验分两步走。只加路由器:DeepSeek-V4.1-Flash 的切换解决率回升 11.0 个百分点,GLM-5.3 回升 3.5 个,而不间断成绩只变动负 1.0 到正 0.5,几乎不伤正常发挥。再加上状态记忆凑成完整 RCM:DeepSeek 再涨 4.5 个,GLM 再涨 2.0 个——合计最高补回 15.5 个百分点,把 DeepSeek 的切换掉点基本填平。far 组的收益大于 near 组,说明 RCM 补的正是上下文被冲淡的那种伤。
还有一个容易藏猫腻的地方论文主动堵上了:路由和记忆的辅助调用计入同一个 900 秒预算和同一笔 token 账,不靠表外算力换成绩。
和通用的上下文管理方案比,RCM 的边界感值得单独说一句。它不做语义检索之外的花活,全部信号都来自会话内已经可见的内容,不引入外部知识库。替换策略也定得很细:原始用户需求和最近的交互逐字保留,一字不动;较早的 Assistant 与工具往返,只有在注入的记忆已经覆盖其携带的信息时才被替换。这套规则的潜台词是,记忆是历史的索引,不是历史的替代品——宁可让旧消息和记忆短暂共存,也不让转述走样有机会污染原始需求。
当然,5.5 到 15.5 个百分点的回升是在两个模型上测出来的,幅度差异本身也值得注意:基线掉点越大的配置,RCM 补回得越多。GLM-5.3 本来就只掉 6.0 个,可补的空间自然小;DeepSeek 掉了 15.0 个,RCM 几乎把它填平。这个相关性暗示 RCM 补的是一种特定类型的伤,而不是普遍增强 Agent 的能力。
七、自己动手:一个能跑的任务状态记忆原型
RCM 的思想不依赖 OpenCode,任何会话式 Agent 都能借用。下面这个自包含的 Python 脚本把两个核心部件做成了一个缩水但真实可运行的版本:交互组按任务归档,路由器按路径与符号重叠估算置信度,低于阈值就退回全量历史;任务状态记忆固定六个字段,在每次交互后增量更新。脚本模拟一场五任务交织会话,对比「全量历史」和「路由加记忆」两种策略在恢复时刻的上下文体积与关键约束召回情况。
from dataclasses import dataclass, field
# ---------- 任务状态记忆:六个固定字段 ----------
@dataclass
class TaskState:
goal: str = ""
constraints: list = field(default_factory=list)
completed: list = field(default_factory=list)
pending: list = field(default_factory=list)
symbols: set = field(default_factory=set)
uncertainties: list = field(default_factory=list)
def render(self) -> str:
lines = [f"GOAL: {self.goal}"]
for name, items in [("CONSTRAINTS", self.constraints),
("COMPLETED", self.completed),
("PENDING", self.pending),
("SYMBOLS", sorted(self.symbols)),
("OPEN", self.uncertainties)]:
if items:
lines.append(f"{name}: " + "; ".join(sorted(items)))
return "\n".join(lines)
# ---------- 交互组:请求、动作、工具结果不拆散 ----------
@dataclass
class Group:
task: str
request: str
path: str
symbols: set
tokens: int # 该交互组占用的上下文估算
class Session:
def __init__(self, confidence_floor=0.8):
self.groups = []
self.states = {}
self.floor = confidence_floor
def _confidence(self, task: str, path: str, symbols: set) -> float:
hits, total = 0, 3
if any(g.task == task and g.path == path for g in self.groups):
hits += 1
if any(g.task == task and g.symbols & symbols for g in self.groups):
hits += 1
if task in self.states:
hits += 1
return hits / total
def add(self, task, request, path, symbols, tokens, constraint=None):
self.groups.append(Group(task, request, path, symbols, tokens))
st = self.states.setdefault(task, TaskState())
if not st.goal:
st.goal = request
if constraint:
st.constraints.append(constraint)
st.symbols |= symbols
st.pending = [p for p in st.pending if p not in request]
return self._confidence(task, path, symbols)
def resume_context(self, task: str, path: str, symbols: set):
"""返回 (注入的token量, 注入文本)。"""
conf = self._confidence(task, path, symbols)
if conf < self.floor: # 保守红线:宁可多给,不可错删
picked = self.groups
else:
picked = [g for g in self.groups if g.task == task]
state = self.states.get(task, TaskState())
body = state.render() + "\n" + "\n".join(g.request for g in picked)
return sum(g.tokens for g in picked) + len(state.render()) // 4, body
# ---------- 模拟一场 Switch-SWE 式交织会话 ----------
s = Session()
s.add("click", "修复布尔标志:字面值 True 必须按 flag_value 转换",
"src/click/core.py", {"flag_value", "is_bool_flag"}, 4200,
constraint="字面值 True 的转换规则不可回归")
s.add("moto", "SNS 投递 SQS 时保留原始负载", "moto/sns/models.py",
{"sns", "sqs", "raw"}, 3800)
s.add("networkx", "实现平面对偶图并记录半边溯源", "networkx/planar.py",
{"dual_graph", "half_edge"}, 5100)
s.add("flask", "蓝图注册时校验端点冲突", "flask/blueprints.py",
{"Blueprint", "endpoint"}, 3300)
s.add("django", "迁移自动检测器忽略代理模型", "django/db/migrations.py",
{"autodetector", "proxy"}, 4600)
# 中断若干轮之后,Agent 回到 click 焦点任务
conf = s.add("click", "现在给参数默认值支持可调用对象",
"src/click/core.py", {"default", "callable"}, 2000)
full_tokens = sum(g.tokens for g in s.groups)
routed_tokens, injected = s.resume_context(
"click", "src/click/core.py", {"default", "callable"})
print(f"路由置信度: {conf:.2f}")
print(f"全量历史上下文: {full_tokens} tokens")
print(f"路由+记忆上下文: {routed_tokens} tokens "
f"({routed_tokens / full_tokens:.0%})")
print("早期约束是否仍在上下文中:",
"字面值 True 的转换规则不可回归" in injected)
跑起来会看到三组数字:路由置信度、两种策略的上下文体积对比、以及最关键的一项——焦点任务第一阶段定下的「字面值转换规则不可回归」这条约束,在恢复时刻的注入文本里是否还在。全量历史策略把它埋在五倍体积的噪声里,路由加记忆策略则以约两成的体积把它放在最显眼的位置。体积缩小带来的不是能力损失,恰恰相反:噪声少了,旧约束反而更不容易被新需求覆盖。
这个原型只有几十行,缺了真实实现里的语义嵌入、缓存分配和压缩联动,但骨架是诚实的:交互不拆散、置信度有红线、状态分六字段、用户指令压过记忆。把它接到任何会话式 Agent 的消息组装层之前,都值得先用 Switch-SWE 的配对思想给自己跑一次对照实验——同一批任务,打断与不打断各跑一遍,看恢复掉点到底落在哪个区间。
八、边界与没测到的东西
这篇论文也有明确的边界。200 个任务全部来自 Python 仓库,其他语言生态的恢复表现没有数据。RCM 只在 OpenCode 这一个脚手架上、用两个模型做过消融,换到 Claude Code 或 Codex 上是否还有同样幅度的提升,需要重新验证。near-far 的局部性结论是关联而非因果,论文自己把话说得很死。七个配置统一用中等推理档位,拉满推理能不能扛住打断,同样没有答案。
即便不看插件,这个基准对日常使用也有两条直接的操作建议。第一,并行多任务时给 Agent 明确的任务边界:不同任务尽量落在不同工作区,恢复时把当前目标和早期约束用一句话重述一遍——这不需要任何插件,只是在模仿状态记忆该干的事。第二,尽量把同一个模块的修改连续安排,少做跨目录的大跳跃,near 组和 far 组接近一倍的掉点差就是证据。
再往深走一步,团队可以把「恢复质量」纳入自己的 Agent 监控。每次会话里发生任务切换时,记录恢复那一刻注入上下文里包含了几条早期约束、焦点工作区是否被正确选中、最终补丁扫了几个工作区。这三个信号恰好对应论文里的两类病和三种死法,不需要发论文级别的评测,就能在自己的流量里算出真实的恢复掉点。
编码 Agent 的评测正在从「能不能解一道题」走向「能不能在真实工作流里持续干活」。Switch-SWE 量出的 11.5 个百分点,量的是从演示视频到日常工位之间的距离。代码和 400 个会话规格都在 GitHub 上开源,地址是 github.com/Miraclecsc/Switch-SWE,值得拉下来对着自己的 Agent 跑一遍。
更多推荐


所有评论(0)