这个系列写到第六篇,移动端自动化终于要聊到架构层面了。上一篇我们让大模型在模拟器和真机上跑通了用例生成、元素定位和截图断言,效果看起来不错,可一旦把业务流加长,问题立刻暴露——一个“全能Agent”要同时干规划、定位、点击、断言、总结五件事,上下文滚到第五步就开始上头,把上一个页面的截图当成当前操作的依据,甚至自己编一个断言结果。我在这套LLM驱动的自动化测试框架里做过最大的改动,就是把DroidRun/Mobilerun里的单体Agent拆成了八个各司其职的角色,让每个模型只干一件事。今天这篇就专门拆解这套角色级模型拆分方案,从动机、角色清单、Prompt设计、通信协议到踩坑实录,一次性讲透。
1. 单体Agent在移动端测试里的滑铁卢:为什么要对模型做角色拆分
1.1 一次真实任务的“翻车”现场
先说一个最常见的场景:测试某购物App的完整下单链路,从“首页搜索商品、加购、改数量、进结算页、预填地址、提交订单”这样一条业务流,听起来不难,但单体Agent跑起来就是一场灾难。
我印象最深的一次,Agent在执行到第三步时突然“失忆”——任务已经推进到结算页,它却还在按首页的UI树寻找搜索框,直接把结算页的商品列表当成搜索结果,对着“提交订单”按钮旁边的区域模拟点击。更要命的是,它在断言阶段输出了一条“页面找到商品标题,与预期一致”的日志,实际上页面上根本没有搜索框,整条结果全靠幻觉补全。这种问题不是偶发,只要业务流超过五步,成功率就开始断崖式下跌。
后来我复盘过这类失败,发现单体Agent的崩溃点几乎都集中在同一个环节:它需要同时维护“任务目标、当前页面状态、已经做过什么、接下来做什么、怎么验证”五类信息,而移动端UI每走一步都会产生新的截图和UI树,这些高密度信息不断塞进同一个上下文窗口,模型很快就分不清主次了。
1.2 单体Agent的三个结构性缺陷
把翻车现场抽象一下,单体Agent在移动端自动化里存在三个绕不开的结构性缺陷。
第一是上下文污染。规划指令、执行日志、UI截图、断言结果全挤在同一个对话窗口里,早期的高价值信息会被后期的噪声稀释。比如任务最开始定义的关键约束——某些按钮不能点击、某些弹窗需要绕过——等执行到后半程,模型早就把这些约束忘光了。
第二是指令竞争。大模型在同一轮推理里既要做“规划者”,判断下一步该去哪一页,又要做“执行者”,决定该不该点击某个坐标,还要做“验证者”,评估页面状态是否符合预期。这相当于让同一个人同时当项目经理、施工员和质检员,角色诉求相互打架。最典型的副作用是模型倾向于使用最省事的断言方式:“看起来一样”,而不是真正验证业务状态。
第三是失败不可解释。单体Agent失败之后,日志往往只有一句“操作未完成”或“目标元素未找到”,但你根本不知道问题出在规划错了、定位错了、执行超时了,还是断言标准不对。这种黑盒失败在测试框架里是致命的,因为自动化测试的核心价值之一就是快速定位缺陷归属,如今连缺陷归属都说不清。
1.3 DroidRun/Mobilerun 在这个问题上的定位差异
在介绍角色拆分之前,先明确DroidRun和Mobilerun这两个组件的分工,因为角色级拆分实际上是这两个组件共同遵守的设计原则。
Mobilerun在我这边的定位是“平台侧任务大脑”,它跟具体设备无关,负责任务解析、流程规划、结果断言和报告生成。任何不需要接触屏幕的智力活动,都放到Mobilerun里。DroidRun则是“端侧执行手脚”,面向Android设备,负责UI树采集、屏幕截图、动作注入和设备信号追踪,凡是必须贴到设备上做的事,都交给DroidRun。
为什么要分两边?因为不同角色对延迟、上下文和模型能力的要求是完全不同的。规划类角色可以接受几百毫秒延迟,它要把全局看得很清楚;执行类角色对延迟极其敏感,但只需要看“眼前这一点”。如果把两边混成一个Service,要么端侧被重的规划任务拖慢,要么平台侧被端侧UI数据灌爆。所以我的做法是:Mobilerun管理“想”的角色,DroidRun管理“做”的角色,两边通过结构化消息通信,谁也不直接访问对方的上下文。
2. 角色清单与职责边界:八个角色是怎么划分的
2.1 每个角色的职责、输入输出与推荐模型
角色级拆分的第一步是列角色清单。我最终在Mobilerun和DroidRun两侧划分出八个角色,用一个表格记录它们的职责边界,这样实现时就不会出现“两个角色抢同一个活儿”的情况。
| 角色 | 所属组件 | 核心职责 | 关键输入 | 关键输出 | 建议模型档位 |
|---|---|---|---|---|---|
| Orchestrator 编排者 | Mobilerun | 解析任务需求,维护任务状态机,调度其他角色 | 原始需求文本、历史任务摘要 | 状态转移指令、角色调用请求 | 强推理大模型 |
| Planner 规划者 | Mobilerun | 把需求拆成平台无关的步骤序列 | 需求文本、设备首屏快照摘要 | StepList 步骤清单 | 强推理大模型 |
| Observer 观察者 | DroidRun | 采集UI树和截图,剪枝去噪,输出坐标锚点 | 设备帧、UI层级 | 剪枝元素树、候选元素列表 | 多模态中模型 |
| Executor 执行者 | DroidRun | 把步骤转换为具体设备操作并注入 | 目标元素、动作类型 | 动作日志、操作结果 | 低延迟小模型 |
| Validator 验证者 | Mobilerun | 断言页面状态是否达到业务预期 | 观察快照、断言要求 | 断言结果、证据截图 | 多模态中模型 |
| Recovery 恢复者 | DroidRun + Mobilerun | 诊断失败原因,决定重试或重规划 | 失败日志、最新页面状态 | 重试动作或计划修订请求 | 强推理大模型 |
| Reporter 报告者 | Mobilerun | 汇总任务过程与断言结果,生成结构化报告 | 任务事件流、断言结果 | HTML/Markdown报告 | 长文本摘要模型 |
| Tracer 追踪者 | DroidRun | 记录设备信号、崩溃日志、性能数据 | 设备端信号流 | 结构化事件流 | 规则引擎即可 |
这八个角色里,Tracer比较特殊,它不走大模型,纯粹用规则引擎记录设备信号,但这恰恰是角色级拆分里最容易忽略的一环。没有Tracer,后面Recovery做根因诊断就没有数据可用,只能靠猜。
2.2 DroidRun侧与Mobilerun侧的边界划分原则
边界划分只有一个核心原则:一切跟设备状态强相关的能力放DroidRun,一切跟平台无关的智力活动放Mobilerun。
举个例子,Observer必须放DroidRun,因为UI树只能从设备上拿,截图也必须在设备侧完成。Planner必须放Mobilerun,因为规划“搜索商品、加购、结算”这个步骤序列根本不需要知道手机型号,它只需要一份经过剪枝的页面摘要就够了。Validator比较特殊,它的输入来自DroidRun的观察快照,但断言逻辑本身可以平台无关,所以我把它放在Mobilerun。这样做的直接好处是:以后要支持iOS,只需要重新实现DroidRun这一侧的Observer和Executor,Mobilerun这一侧的四个角色一行都不用改。
2.3 判断一个角色是否需要独立的四个标准
很多朋友问过我:为什么是八个角色,不是四个或者十二个?拆太细会让通信开销超过收益,拆太粗又会回到单体Agent的老路。我总结的判断标准是四问。
一问上下文是否冲突:两个职责需要的信息是否差异很大?比如规划需要全局信息,执行只需要局部信息,它们混在一起就会互相稀释。
二问失败责任是否可独立追踪:如果这个环节出错了,你是否希望日志能直接指向唯一负责人?只有拆分后才能做到。
三问是否会被复用:比如Observer的UI树结果,不但Executor要用,Validator和Planner也要用,这种注定被多角色消费的能力必须独立。
四问是否需要不同的模型能力:规划要强推理,执行要低延迟,断言要多模态,这些能力需求完全不同,塞进同一个模型实例里必然顾此失彼。
满足其中任意一条,就该拆;四条都不满足的,合并。
3. 角色的Prompt模板与上下文管理:把“会上头”的大模型关进笼子里
3.1 Orchestrator与Planner:只做计划,不碰操作
角色拆分之后,Prompt设计要跟角色职责严格对齐。最容易犯的错是给Planner塞一堆页面截图和UI树,让它“看着页面规划”,这又回到了单体Agent的老路。
我的Orchestrator Prompt只做三件事:解析任务目标、维护状态机、分派角色。它的输入模板大致是这样:
你是任务编排者。当前任务ID:{task_id} 任务目标:{user_requirement} 当前状态:{current_state} 已完成步骤:{completed_steps} 请输出下一步要调用的角色和传入参数。 约束:不要自己执行操作,不要自己断言,只调度。Planner的输入我做了一层信息隔离:它只接收需求文本和一份“设备快照摘要”,这份摘要是由Observer蒸馏过的,包含了当前页面有哪些关键入口,但不包含原始UI树和截图。Planner的Prompt模板:
你是测试规划者。请把下面的业务需求拆解为平台无关的步骤序列。 需求:{task_requirement} 当前页面摘要:{page_summary} 输出格式:JSON数组,每个元素包含step_id、action、target_text、expected_state。 要求:步骤数不超过15步,每一步必须可以独立验证。 温度:0.2,max_tokens:1500。我特意把温度压到0.2,因为规划阶段的输出要稳定可复现,不需要创意。创意在测试场景里往往等于随机性。
3.2 Observer:UI树剪枝与坐标锚定
Observer是整个角色级拆分里技术含量最高的一个,因为UI树是全链路信息密度最高的数据来源,也是最容易撑爆上下文的元凶。
Android的UI层级一棵树动辄几千个节点,如果把完整JSON喂给模型,一次采集就吃掉三四十K token,根本跑不了几步。我的做法是三级剪枝。第一级,只保留可视区域内的节点,不可见的、被遮挡的全部丢弃。第二级,只保留可交互或携带关键文本的节点:clickable、long-clickable、scrollable、editable,以及文本非空且作为标题类控件的节点。第三级,对RecyclerView这类长列表只保留当前屏能看到的条目。剪完之后,UI树从几十KB压到三四KB,信息量却几乎没有损失。
Observer的输出格式我设计成候选元素列表,让Executor可以直接用,Prompt模板如下:
你是界面观察者。请从给定的UI树中提取当前页面可操作元素。 要求:返回JSON数组,每个元素包含resource_id、text、bounds、clickable、editable。 只返回与业务操作可能相关的元素,过滤纯装饰节点。 如果按钮存在不可点击状态,请标记enabled=false。这里有个很关键的经验:Observer不要做语义总结,它只需要做“提取和标注”。一旦让Observer用自然语言描述页面,它就会开始脑补页面的业务含义,而这些脑补往往不准。
3.3 Executor:低温、低上下文化的“手”
Executor不应该拥有宏大的上下文,它只需要知道“点哪里”和“怎么点”。我把Executor的上下文窗口压到最小,只包含目标元素和动作类型。
你是操作执行者。请根据下面的操作指令执行动作。 动作类型:{action_type} 目标元素:{target_element_json} 前一操作结果:{last_action_result} 输出:执行成功或失败,失败时附上失败码和原因。 温度:0.1,max_tokens:300。温度0.1接近贪心解码,因为执行动作必须稳定,一点随机性都不能有。max_tokens压到300也是有意为之,执行层的输出根本不需要长篇大论,一个结构化结果就够了。
这里还要注意:Executor只回答“怎么做”,不回答“要不要做”。要不要做是Planner和Orchestrator的事。一旦Executor开始自作主张跳过某些步骤,测试链路的可追溯性就毁了。
3.4 Validator:断言模板与证据链
Validator是最容易“睁一只眼闭一只眼”的角色。如果你直接问大模型“这个页面是否符合预期”,它倾向于给一个模糊的“是”。所以我给Validator设计了一套半结构化的断言模板,从自然语言预期里解析出可计算的条件。
你是页面验证者。请根据以下断言要求验证当前页面。 预期描述:{expected_text} 判定类型:{assert_type} // EXIST / NOT_EXIST / TEXT_EQUALS / NUMERIC_GREATER / LAYOUT_CHANGED 页面快照:{snapshot_brief} 输出:pass/fail + confidence(0-1) + evidence。 如果confidence在0.6到0.8之间,必须标注REVIEW_REQUIRED。REVIEW_REQUIRED这个标记非常重要。它等于让模型承认“我不确定”,而不是强行用一个低置信度的判断糊弄过去。测试框架需要的是在不确定时停下来等人工确认,而不是假装确定继续跑。
3.5 上下文腐烂问题与蒸馏窗口策略
角色拆完了,Prompt设计好了,还有一个绕不开的工程问题:长时间任务里,每个角色自己的上下文也会越滚越长,产生session drift。所谓上下文腐烂,就是模型越到后面越依赖最近几轮的局部信息,把早期的全局约束忘了。
我的解法是三点。第一,每个任务节点完成后,把该节点的输出蒸馏成不超过50字的状态摘要,比如“第3步:已在结算页,商品数量已改为2,准备预填地址”。下次给Planner或Validator的输入里,用摘要替换原始操作日志。第二,角色之间传递的是结构化消息,不是自由文本对话,从根上避免两个模型“聊起来”。第三,每隔五个步骤做一次全局上下文压缩,把“任务蓝图 + 最近状态摘要 + 待办清单”三样东西打包,喂给Orchestrator做整体校验。
下表是我在实践里总结的上下文分配策略:
| 角色 | 上下文窗口 | 输入策略 | 输出长度限制 |
|---|---|---|---|
| Orchestrator | 全局窗口 | 任务蓝图 + 状态摘要 | 短指令 |
| Planner | 中等窗口 | 需求 + 页面摘要 | 步骤清单 |
| Observer | 短窗口 | 单帧UI树 | 候选元素列表 |
| Executor | 极短窗口 | 目标元素 + 上一次结果 | 结构化结果 |
| Validator | 中等窗口 | 断言要求 + 快照摘要 | pass/fail + evidence |
| Recovery | 按需拉取 | 失败日志 + 当前状态 | 诊断结论 |
4. 角色间通信协议与一次任务调度的完整链路
4.1 动作事件与消息的JSON Schema
角色拆完之后,通信协议设计得不好,模型们就会各自为政。我采用事件溯源的方式,所有角色之间的交互统一封装成action_event消息,落到消息队列里。这个消息格式长这样:
{ "task_id": "task_20250115_003", "event_id": "evt_000023", "source_role": "planner", "target_role": "executor", "timestamp": "2025-01-15T10:23:45.123Z", "action_type": "execute_step", "payload": { "step_id": 4, "action": "CLICK", "target_text": "提交订单" }, "status": "PENDING", "confidence": 0.97 }之所以不用两个角色直接调LLM,而要中间加一层事件流,原因有三:一是可回放,任务失败后可以把整个事件流倒回去看是哪一步出的问题。二是可审计,每个角色做了什么事、基于什么输入做的决策,全部有据可查。三是可分离,角色之间不感知对方的模型类型和部署位置,只要消息格式不变,随时可以替换某个角色背后的模型。
4.2 完整调度时序:从需求到报告
一次完整的测试任务,从用户输入需求到最终生成报告,时序是这样的:
- Mobilerun的Orchestrator接收需求文本,创建task_id,初始化任务状态机。
- Orchestrator调用Planner,Planner生成平台无关的StepList。
- DroidRun启动设备会话,Observer首屏采集UI树并剪枝,生成页面摘要。
- Planner拿到页面摘要后,把StepList中的抽象步骤映射为带坐标的具体操作。
- Executor执行第一个动作,ADB注入点击或输入事件,记录动作日志。
- Observer再次采集页面快照,交给Validator。
- Validator对照断言要求给出pass/fail和confidence。
- 如果失败,Recovery介入,先读Tracer的设备信号和Observer的页面快照,再决定重试、重规划或跳过。
- 全部步骤通过后,Reporter汇总事件流和断言结果,生成结构化报告,Tracer把整个链路归档。
对应到调度伪代码,大概是这样的逻辑:
task_id = orchestrator.create_task(requirement) steps = planner.plan(requirement, page_summary) for step in steps: element = observer.locate(step.target_text) if element is None: recovery.diagnose(step, tracer.events, observer.snapshot) if recovery.should_replan(): steps = planner.replan(task_id, recovery.reason) continue result = executor.execute(step.action, element) snapshot = observer.capture() verdict = validator.validate(step.expected_state, snapshot) if verdict == FAIL and recovery.retry_count < 2: recovery.retry(step, changed_strategy=True) elif verdict == FAIL: reporter.record_failure(step, verdict, snapshot) reporter.generate(task_id)4.3 幂等、重试与“模型间不互相调用”原则
这里要特别强调一条纪律:模型之间绝不直接调用模型。也就是说,Planner不要把自己的输出直接丢给另一个LLM去“商量”,所有跨角色交互必须经过Orchestrator和事件总线。否则两个大模型一来一回,token消耗成倍上涨,而且它们会互相强化幻觉,形成一种看似合理实际完全跑偏的共识。
幂等设计也不能少。同一个动作被执行两次,可能导致测试状态错乱,比如“增加数量”按钮被点了两次。解决办法是给每个动作分配幂等键,执行前先检查当前页面状态是否已经是动作完成后的预期状态,如果是,就跳过。重试则交给Recovery统一管,执行层自己不重试,重试前必须读取最新快照,确认重试条件成立。
5. 落地踩坑实录:状态丢失、UI树爆炸、误报与重试死循环
5.1 状态丢失:拆分角色后谁来记住“我们到哪一步了”
第一个坑来自我最初的粗心设计。当时我把角色拆开了,但没有单独设计任务状态存储,结果所有角色都只维护自己的窗口,任务推进到第8步时,Executor还傻傻认为自己在第3步的页面。
现象很直观:角色拆完之后,长流程的成功率反而没提升,甚至略有下降。原因就是任务的“全局进度”没有独立存储,进度信息散落在各个角色的上下文里,谁都不掌握全貌。
解决办法是引入了Tracer和一个轻量状态机。Orchestrator每次收到角色的事件消息后,更新当前状态字段,比如STEP_5_EXECUTED、STEP_6_VALIDATED,然后把状态摘要广播给后续会用到的角色。这等于给整个任务加了一条“全局记忆总线”,任何角色需要知道“我们现在到哪了”,直接从状态机读,而不是翻自己的上下文。
5.2 UI树爆炸:JSON化元素树吃掉了上下文
第二个坑是Observer最初直接输出完整UI树JSON。有一次测试一个带长列表的商品页,一轮采集就拿出六千多个节点,序列化之后接近80KB,Planner的上下文窗口瞬间被灌满,后续步骤的规划质量肉眼可见地下降。
我一开始尝试通过增加上下文窗口来解决,但这只是花钱买罪受,成本翻倍,问题还在。真正管用的是前面说的三级剪枝,再配合一个硬指标:单次UI树输入不超过5KB,超过就重新剪。经过剪枝后,同样的页面从80KB压到约4KB,Planner的规划速度明显变快,上下文压力彻底缓解。
这里有个容易被忽视的细节:剪枝规则必须写在Observer的Prompt里,要求它“只返回可能相关的候选元素”,并且给出明确的过滤信号。如果你只是笼统说“精简一点”,模型会按自己的理解乱剪,把真正需要的提交按钮也给剪掉。
5.3 截图断言误报:像素级比对为何不可靠
第三个坑来自Validator。最初我让多模态模型直接对比两张截图,“判断页面是否发生变化”,结果误报率高得离谱。字体加载延迟、图片懒加载、甚至系统状态栏时间变化,都会被模型当成“页面改变”的证据。
后来我把断言从“整图对比”改成“语义区域断言”。先把页面划分为几个业务区域,然后对每个区域单独提取关键信息,比如按钮文本、输入框内容、商品数量数值,再跟预期做语义匹配。配合一个专门设计的区域断言配置:
{ "regions": [ { "name": "checkout_button", "ocr": "提交订单", "must_exist": true }, { "name": "quantity_field", "ocr": "2", "must_equal": "2" } ] }这个改动把误报率从接近18%降到了4%左右。我得到的教训是:视觉模型适合做“语义理解”,不适合做“像素比对”。你要让它判断“结算按钮是否存在且文字正确”,而不是让它判断“这两张图一样吗”。
5.4 模型实例数量失控:成本与延迟的平衡
第四个坑属于贪心带来的。角色拆完之后,我最初让每个角色都用一个中大型模型实例,一个任务同时跑八个大模型,成本翻了好几倍不说,延迟也吃不消,单个动作周期从6秒拖到15秒以上。
后来我意识到:角色拆分不等于物理隔离,更不等于“每个角色都必须上大模型”。正确的做法是给角色分档位,让模型能力跟任务复杂度匹配。Executor这类确定性操作,用一个小模型甚至规则映射就够了;Observer需要一定的视觉能力,但用中档多模态模型也能干得很好;真正需要顶级推理的,只有Planner、Orchestrator和Recovery。
优化后的路由策略是:Planner和Recovery用强推理大模型,Observer和Validator用中档多模态模型,Executor和Reporter用小模型或规则引擎,Tracer纯规则。单任务成本降了约40%,平均耗时降了约30%,成功率反而因为延迟降低带来的超时减少而微涨。
5.5 Recovery无限重试:重试必须改变策略
最后一个坑最隐蔽,也最影响稳定性。Recovery角色上线后,我发现它经常陷入“点了一下没生效,再点一下还是没生效”的死循环。有一次测试登录流程,Recovery连续执行五次点击,每次都失败,最后把任务拖到超时,日志里全是同一个动作的重复记录。
根因是重试逻辑没有绑定策略变更条件。Recovery的Prompt写得太简单,看到失败就重放原动作,但原动作失败可能意味着元素根本没出现、坐标已经偏移、页面发生了结构性变化,这时重放一百遍也没用。
修复方案是给Recovery定三条硬规则:第一,重试上限2次,超过就不准再执行原动作。第二,每次重试前必须读取Observer的最新快照和Tracer的设备信号,确认页面状态变了没有。第三,连续两次失败后,Recovery必须调用Planner重规划,而不是继续重试。把“重试”和“重规划”分开,等于给失败的螺旋装了一个强制刹车。
6. 拆分的量化收益与后续扩展方向
6.1 收益量化:成功率、Token、排障时间
整个角色级模型拆分方案在我这边的实际场景里跑了将近两个月,我记录了一组内部压测数据,不同业务和机型会有浮动,但趋势是一致的:
| 指标 | 拆分前(单体Agent) | 拆分后(角色级) |
|---|---|---|
| 5步以内短流程成功率 | 86% | 94% |
| 15步以上长流程成功率 | 72% | 91% |
| 单任务平均token消耗 | 约28k | 约15k |
| 失败定位平均耗时 | 约20分钟 | 约7分钟 |
| 重试死循环发生次数 | 常见 | 基本消失 |
token消耗降下来主要是两个原因:一是UI树剪枝把喂给模型的输入大幅压缩了;二是角色之间不再互相传原始上下文,只传结构化消息和摘要。失败定位变快则完全归功于角色拆分后的责任追踪,日志可以直接定位到“这一步是Observer没找到元素,还是Validator误判”,不用再从一团乱麻里抽丝剥茧。
6.2 端侧小模型的蒸馏与Mobilerun的跨端复用
拆分的架构稳定之后,我现在在推两个扩展方向。
第一个方向是把Planner的“局部步骤修正”能力蒸馏到端侧小模型上。具体做法是:用大模型Planner在大量页面上生成“页面摘要到具体操作”的映射数据,然后拿这些数据微调一个参数量小得多的模型,部署到DroidRun旁边,让它在断网或者低延迟场景下也能完成“根据页面摘要把步骤映射到具体元素”的工作。大模型仍然保留在Mobilerun做全局规划,端侧小模型只负责最后一公里的精调。
第二个方向是借Mobilerun的平台无关设计做跨端复用。因为Planner、Orchestrator、Validator、Reporter四个角色只消费平台无关的StepList和页面摘要,只要iOS端实现了Observer和Executor协议,接入Mobilerun就能跑同一套规划逻辑和断言逻辑。DroidRun则继续演化为纯Android执行器,专门优化Android特有的UI层级适配和注入稳定性。
6.3 我个人的几个实操体会
最后聊几句纯个人折腾下来的体会,算不上结论,但对你直接开干应该有用。
角色拆分不是越细越好。我试过把Observer再拆成“截图采集者”和“UI树解析者”,结果两个角色之间的消息交互开销比它俩各自省下来的上下文还多,最后又合并了。拆分的下限是“职责不可再混合”,上限是“通信成本不超过独立收益”,在这之间找一个平衡点。
第二件是:先做状态机,再拆角色。如果你现在就要动手改造,我建议不要先急着设计角色的Prompt,先搭一个简单的任务状态管理器,能记录“当前步骤、已完成步骤、当前页面状态”就行。没有这个底座,拆分角色一定会出现第5节说的“谁都不知道我们到哪一步了”的问题。
第三件是:模型不是越强越好。Observer这个角色我用中档模型就够用了,换成最强的模型反而更容易画蛇添足,把一些无关的视觉细节写进输出。关键在于Prompt和剪枝做得好不好,而不是模型参数大不大。给Executor用小模型的时候我一度很担心,实测发现只要上下文足够干净,小模型的执行准确率跟大模型几乎没有区别,延迟还低了一大截。
这个系列后面我还会写DroidRun在真机集群上的调度策略,以及Observer在混合开发框架下的UI树适配问题。如果你正在搭类似的东西,希望这篇角色级拆分的记录能帮你少踩几个坑。