Midscene 说"不绑定模型",到底是怎么做到的?
经常有人问我:Midscene 说自己不绑定模型,是不是就是把 base_url 和 model_name 换一下那么简单?
如果模型只是文本进、文本出,那确实差不多,换个 endpoint 的事。
但视觉 UI Agent 要模型做的事不太一样:它要模型看着一张截图,告诉我们某个元素在画面的什么位置。麻烦就出在这里——每个模型家族报告”位置”的方式都不一样。所以”不绑定模型”对我们来说,从来不是换个 API 那么轻松,而是要把这些不一样的地方,在框架内部悄悄吃掉,让你写的那句 ai('点击登录') 一个字都不用改。
要吃掉的差异,我分成几块来说。
第一块:每家模型的坐标系都不一样
让模型返回元素的 bounding box,你会发现各家给的根本不是一个东西。
Gemini 给的是 [ymin, xmin, ymax, xmax],归一化到 0 到 1000——注意它连 x 和 y 的顺序都是反的,先 y 后 x。Qwen2.5-VL 和 GPT-5 给的是 [xmin, ymin, xmax, ymax],而且是实打实的像素坐标。Doubao 和 UI-TARS 又回到 [xmin, ymin, xmax, ymax],但归一化到 0 到 1000。
如果框架对这些差异不管不问,同一句话在三个模型上会点到三个完全不同的地方。
所以我们在两头都做了适配。发请求的时候,prompt 会根据当前模型家族,告诉它该用哪种坐标格式(这部分在 bboxDescription 里);收到回复的时候,再按家族把坐标统一换算回屏幕像素(adaptBbox,内部对 Gemini、Qwen、GPT-5、Doubao 各有一套换算)。
绕完这一圈,你的脚本拿到的永远是同一种坐标,后面到底跑的是哪个模型,它不需要知道。接一个新模型家族,本质上是加一段坐标换算,而不是让你回头改用例。
第二块:模型有强有弱,循环该给的空间也不同
一个 UI Agent 跑起来是个循环:看截图、规划、动手、再看截图。这个循环允许模型试多少轮,我一开始也想过用一个固定值,后来发现不合适。
强的模型该给它更多探索空间,弱的得早点拦住,不然它会在错误的路上一直走下去,把幻觉越滚越大。
所以这个上限是跟着模型家族走的:普通 VLM 给 20 轮,UI-TARS 这类专门为界面操作训练过的给 40 轮,AutoGLM 给到 100 轮。
1 | const defaultReplanningCycleLimit = 20; // 标准 VLM |
换模型的时候这个数自动跟着变,你不用记、也不用手动调。
第三块:同一个脚本,规划和定位可以用不同的模型
Midscene 的模型配置是按”意图”分的,有三档:default、planning、insight。
不配的话,所有任务都走 default 那一个模型,省心。但你也可以单独给”规划”配一个推理强的模型,给”定位”配一个专门擅长在画面里找东西的模型,各用所长。
这件事还会往下游传。上一篇讲 [Locate](/2026/05/26/Locate 与 Action 为什么要拆成两步/) 的时候提过一个细节:只有”规划和定位用的是同一个默认模型”时,才会启用那条直接信任规划 bbox 的快路。背后的判断就是看这个配置——一旦你给定位单独配了模型,两个模型的坐标系可能对不上,框架就自动绕开那条快路。你在配置上做的选择,会一路影响到定位策略,但这一切对脚本是透明的。
那到底支持哪些模型
写到这你可能想问:说了半天,到底支持哪些?
目前做了专门适配的视觉模型家族有十几个:Qwen 系(2.5-VL、3-VL 以及更新的)、Doubao 的 vision 和 seed、Gemini、UI-TARS(含 Doubao 的几个变体)、GLM-V、AutoGLM、GPT-5。
“做了专门适配”的意思就是前面那些——每一家的坐标系和输出格式,框架里都有对应的处理。再多接一家,是给框架加一层,不是让你重写脚本。
这对一个团队意味着什么
把这些差异都吃进框架之后,”不绑定模型”才算落到实处。
它的好处其实挺朴素。哪天某家供应商降价了,你连夜切过去,脚本不动;出了个更强的新模型,你插上去,拿手里现成的回归用例直接对比;某家把你在用的模型下线了、涨价了、限流了,你早就不只依赖它一家。
抽象的价值,从来不在抽象本身,而在它底下的东西真的变了的那天,你不至于被拽着走。
Midscene 说"不绑定模型",到底是怎么做到的?

