Agent 重复调用不是小毛病,MiMo 把 RL 的盲区摊开了
一个 Agent 在同一轮里连着发出 59 次工具调用,前面几十次已经没有新增信息,它还是继续点下去。
这不是演示里的夸张设定。小米 MiMo 团队在 9 月 27 日公开复盘了 MiMo-V2.6 的工具调用重复问题,并把这条轨迹和训练修复过程一起放了出来。更有意思的是,问题不是模型不会调用工具,而是它太愿意继续调用工具。
很多团队碰到这种事,第一反应通常是给 Agent 加一个调用上限。超过 8 次就拦,超过 32 次就判失败。这个动作没错,但它只是在结果最难看的地方拉闸。MiMo 的复盘让我觉得,真正该被带回工程现场讨论的是另一件事,Agent 的停手能力,到底有没有被当成一个一等能力来训练和评估。
这篇不打算复述一次模型公告。我更想顺着 MiMo 给出的数据,拆开看三个问题。
- 为什么重复调用不能和并行调用混为一谈
- 为什么一个看似有效的阈值惩罚,修起来又贵又不彻底
- 做 Java、Spring 或 LangGraph 应用时,怎样把这类问题提前变成可观测、可控制的工程信号
工具一直在跑,任务却已经停了
做过 Agent 的人,大概都见过类似画面。一个读代码的任务里,模型连续 read_file 同一个文件。一次 HTTP 请求超时以后,它不改参数、不看错误,原样重发三遍。一个搜索工具返回空结果,它把关键词换了空格又搜一次。
屏幕上看着很忙。
用户等得很久,token 和工具额度也在掉,真正的任务状态却没有前进半步。
不过,这里有个很容易误伤正常行为的地方。并行调用不是问题。Agent 同时读取配置、依赖清单和错误日志,反而是效率高的做法。同一个工具也不天然有罪,读取不同文件、轮询一个异步任务、改完代码重新跑测试,都有合理性。
MiMo 给了一个很实用的区分。
- 正常并行调用,调用之间服务于不同的信息需求
- 工具调用泛滥,调用数量明显超过任务或执行环境能有效处理的量
- 工具调用重复,在信息和环境状态没有实质变化时,继续发出语义相同或高度相似的动作
这三个概念不是文字游戏。你如果把正常并行也当作重复,Agent 会变慢。如果只盯住平均调用次数,又会漏掉那种在单个任务里把上下文和等待时间拖垮的坏循环。
MiMo 在文章中采用了一个刻意保守的指标,叫做同一轮精确重复。一个 assistant turn 里,工具还没返回结果前发出的调用被放在同一个批次。工具名相同、参数经 JSON 规范化后完全相同,就算重复。若一轮总调用数是 N,其中唯一调用数为 U,重复率就是 (N - U) / N。
这个定义漏掉了跨轮重试、参数只差一点的近似重复,也看不到 code-mode harness 里被 exec 包起来的内部调用。它不是完整的重复率。
但它有一个很硬的优点,工具结果尚未回来,模型没法拿到新证据后再作合理重试。因而同一批里出现完全相同的调用,几乎没有辩护空间。
工程上最怕的不是指标不够宏大,而是指标无法重放。这个指标可以从一轮 trace 直接算出来,也能在不同 harness 上稳定对比。对于生产系统,这比一个听起来更聪明、但每次人工判定都不一样的语义分数有用得多。
训练奖励看着没问题,模型却学会了不停手
MiMo 的问题是在 RL 过程中逐步放大的。
团队回放了固定的一组异常样本,并拿 RL 的 step 0、5、10、15、20 的 checkpoint 逐个测试。以 Flash-RL 为例,一轮里出现至少 10 次工具调用的样本占比,在所有 harness 汇总下从 step 0 的 11.1% 升到了 step 20 的 24.6%。MiMo Code 这个环境更明显,30.6% 升到 41.7%。
这个走势挺刺眼。它提醒我们,离线总分稳定,不等于 Agent 的行为质量稳定。尤其是工具调用这种长链路行为,reward 可能只奖励了最后答对没有,却没有足够惩罚中间过程的浪费和失控。
MiMo 原先也不是没有护栏。训练 rollout 里,单轮工具调用超过 32 次会被提前终止,reward 置零。后续梯度更新只处罚触发规则的那一轮,连同这一轮的 chain-of-thought token 一起计算。
听上去很严。
问题在于,32 这个阈值太宽了。团队回看 general/dataset-epqd 的训练轨迹发现,超过 8 次调用的高调用量样本在 step 0 就已经存在。它们没有越过 32 次这道线,便没有受到惩罚。RL 会放大那些能拿到奖励的策略,于是高调用行为先在阈值下生长,再慢慢长成真正的泛滥。
我一直觉得,阈值型 reward 有一种很别扭的副作用。它把一个连续退化的问题硬切成了合格和不合格两块。第 31 次没有代价,第 33 次突然归零。模型不需要学会什么时候该停,只需要学会别撞上那堵墙。
人类写业务限流也会掉进这个坑。比如某个接口每分钟 1000 次请求才熔断,999 次请求虽然没熔断,系统也可能早就被排队、GC 和下游连接拖得喘不过气。阈值是最后的保险丝,不是系统健康度的定义。
顺着这个思路再看 Agent。真正需要奖励的,可能不是少调用几个工具,而是在证据已经足够时结束这一轮,把控制权交回给下一步推理或用户。停下来不是消极动作,它是决策的一部分。
把阈值从 32 改成 8,为什么还不是答案
MiMo 团队确实试过最直接的修法,把泛滥惩罚的阈值从 32 降到 8,并从 Flash 的 step 28 checkpoint 继续训练。
效果是有的。超过 8 次调用的 turn 明显下降,整体 reward 没有明显损失。
但麻烦也很真实。这个效果大约要再训练 20 个 step 才会出现。如果把同样的做法放到完整 MixRL 训练上,团队估算成本约 231 万美元。更关键的是,训练分布内的改善没有完整迁移到内部评估集。某组 replay 的重复率从 13.45% 降到 3.83%,已经好不少,却仍然没有消失。在含有一段高调用历史的样本上,重复率还会更高。
这话听着有点刺耳,但不少 Agent 的线上补丁也是同一种结构。加一层 middleware,塞一个 maxSteps,再把失败任务交给 fallback model。它们能止血,却不一定让策略本身学会更好的行为。
不是说这类护栏不行。线上系统必须有它,特别是能改数据、能发请求、能访问生产资源的 Agent。只是别把护栏当作训练完成的证明。
训练在修策略,运行时约束在兜底。两件事都要有,而且不能拿其中一个冒充另一个。
MOPD 修的不是调用数,是模型的结束倾向
MiMo 后来换了一条更有针对性的路径,MOPD,也就是 Multi-teacher On-Policy Distillation。
它的第一步很克制。团队从内部收集的重复样本出发,训练了一个专门处理单轮重复的 RL teacher。发生重复时 reward 为 0,只有既不重复、又没有错误工具使用时才给 1。同时加入 KL loss,让 teacher 不要偏离原本的 RL policy 太远。
只训练了 12 个 step,大约 7000 个样本,这个专用 teacher 在训练集和 held-out 集上都把 replay 重复降到了零。
这里真正有启发的不是零这个数字,而是团队往 token 层继续挖了一层。他们盯住每个 <tool_call> 位置,比较修复前后模型生成结束 token <|im_end|> 的概率。
在那条 59 次调用的轨迹里,第 12 次工具调用位置,修复前的模型有 94.56% 概率继续发起下一次调用。修复后,92.17% 的概率选择结束这一轮。按累计停止概率计算,修复后的模型在 8 次调用以内停止的概率达到 99.87%。修复前即便已经调用了 59 次,累计停止概率也只有大约 50%。
我当时看到这组数,反而觉得事情清楚了。重复调用只是外在症状,模型内部缺的是一种足够强的结束偏好。
这和写 Java 服务里的状态机很像。你不能只在循环外面设置一个 32 次的 break。更可靠的做法是让状态转移本身依赖已获得的证据,满足完成条件就进入 DONE。如果一个 Agent 每次拿到工具结果后都没有明确判断任务是否已完成,它自然会把继续调用当成默认动作。
MiMo 在最终阶段把专用 teacher 用 teacher-prefix OPD 合入主模型。完整 MOPD 训练成本约 9 万美元,约为重跑 MixRL 方案的 4%。团队报告称,Pro 与 Flash 在不同 context length 和 harness 上的重复率都明显下降,较大 benchmark 的表现维持稳定。
这是一种更值得借鉴的修法。不要等通用训练把每一个罕见坏行为都慢慢学会,而是先用专用 teacher 把某个清晰、可测、代价高的行为问题掰正,再把这个能力蒸馏回主策略。
当然,文章公开的是结果和训练框架,不是完整可复现配方。不同模型、数据和 tool schema 下,12 个 step 或 7000 个样本未必能复制同样收益。这个边界要说清楚。
但它至少给出了一个方向,针对 Agent 的坏行为,训练目标可以比通用 benchmark 更贴近真实 failure trace。
从 RL 训练到 LangGraph,工程团队可以先做四件事
多数应用团队没有 231 万美元的 MixRL 预算,也不会去训练基础模型。那这篇复盘是不是只和模型厂商有关?我觉得不是。
反正我觉得,工程侧至少能先把重复调用从一个模糊抱怨,变成一套可量化的运行信号。
先记录规范化后的调用指纹
不要只存工具名和原始 JSON 字符串。对参数做 JSON canonicalization,排序 key、清掉无意义默认值,再计算调用指纹。这样 {"path":"a","limit":100} 和 key 顺序相反的同一请求,才不会被误认为两次不同调用。
以 Spring Boot 的工具网关为例,可以在执行前生成一个 toolName + canonicalArgs 的 hash,并把它放进当前 turn 的 trace。LangGraph 则可以把 tool_call_id、工具名、规范化参数和 state version 一起写进 state 或 tracing span。
别急着上语义相似度。精确重复先做好,成本低,误判少,复盘时也讲得明白。近似重复可以放在第二阶段处理。
把 turn 视作观察窗口,不只看整个会话
MiMo 的指标选择很聪明,工具结果返回前的同一轮调用本身就是一个天然窗口。这个窗口里的精确重复,通常没有合理的结果驱动解释。
你可以在每个 assistant turn 结束时计算三个数。
totalCalls,本轮总调用数uniqueCalls,去重后调用数exactRepeatRate,(totalCalls - uniqueCalls) / totalCalls
再补一个 callsBeforeNewEvidence。如果连续几次调用期间没有新工具结果、没有 state version 变化、没有新用户输入,这个值持续上升,就该让 orchestration 层介入。
很多朋友可能会直接把任务级平均调用次数做成 dashboard。也该做,但它很容易掩盖少量灾难性样本。P95、P99 和按 tool、task type、harness 切分后的分布,才更接近用户真正感受到的卡顿。
失败重试必须改变证据或策略
合理 retry 是需要保留的。网络 502、限流、临时锁冲突,都不能要求 Agent 一次成功。
但重试至少要改变一个东西。等待时间变了,fallback endpoint 变了,请求参数变了,或者从错误信息里拿到了新的诊断线索。什么都没变的原样调用,只是在重复消耗。
一个简单的执行规则可以是,若调用指纹相同且状态版本不变,第二次执行前必须附带 retry reason。没有 reason 就拒绝执行,并把结构化反馈交给模型,例如「同一状态下已执行相同调用,请基于已有结果选择下一步」。
这不是为了和模型较劲,而是把隐性的循环变成可见的决策点。
给 Agent 一个明确的完成状态
LangGraph 用户很容易把每个节点写得很能干,却忘记收口条件。工具节点会返回,planner 又会再计划,router 再把流程送回工具节点。图没有死循环,体验已经像死循环了。
我自己的建议是给任务 state 加三个很朴素的字段。
evidenceVersion,最近一次新增有效证据的版本lastActionFingerprint,最近一次动作指纹completionReason,为何可以结束本轮或结束任务
每次准备调用工具前,先问两件事。现有证据够不够回答?这次调用会新增什么证据?如果两问都答不上来,Agent 应该收束,而不是再试一次。
这个设计很土。
但土的东西往往最抗压。它也能让人工复盘从「模型怎么又抽风了」变成「第 7 次调用前没有新证据,router 仍然返回了工具分支」。后者才是可以修的工程问题。
别把成本只算成 token
重复调用最直观的损失当然是 token 和 API 账单。但真正麻烦的成本往往在后面。
一个无意义的工具调用可能占住浏览器会话,污染缓存,给数据库制造额外写入,触发第三方限流,还会把无关结果塞进 context。等模型终于回到正轨,它需要从自己制造的噪声里重新找线索。
对用户而言,这不是「多花几分钱」,而是「这个 Agent 看上去不可靠」。这类信任损失很难靠一次 benchmark 提升补回来。
所以我不太赞成把 Agent harness 只当成模型的水管。它更像运行时系统。模型负责提出动作,harness 负责定义什么动作可以执行、什么状态算进展、什么情况下必须停下来。两边分得清楚,才能同时保留模型的探索能力和系统的可控性。
MiMo 这次把一个不太光彩的线上问题公开出来,价值就在这里。它没有把重复调用包装成一个小 bug,而是承认奖励设计、阈值和训练分布共同留下了盲区,并给出成本、失败尝试和更细的 token 级分析。
这比一句「新版更稳定」有用得多。
下一次你看到 Agent 忙着调用工具却迟迟不给答案,不妨先别只问它为什么没做对。再问一句,它为什么不知道该停。
参考资料
- MiMo Team, Diagnosing and Mitigating Tool-Call Repetition in MiMo-V2.6,2026-09-27
- AIHOT 对该文的中文摘要,2026-09-27




