
1. 从标题拆解 GenUI 到底在解决什么问题第一次看到“ZorvAI · GenUI 技术构架与功能介绍”这个标题很多人第一反应是又是一个 AI 生成 UI 的框架市面上类似概念确实不少但真正落到工程层面能同时把AgentLoop、WebView、Composable这三个词串起来的方案并不多。我拿到这个标题之后没有急着去堆概念而是先问自己三个问题它面向谁、它替代了什么、它的技术底座为什么是这三块。先说结论性的判断GenUI 本质上是一套由 Agent 驱动、以 WebView 为渲染容器、用 Composable 方式组织界面单元的生成式 UI 架构。它要解决的核心痛点是传统 App 里“界面写死、改一次发一次版”的低效问题以及纯前端动态化方案“能力受限、性能拉胯”的老毛病。适合阅读这篇内容的人包括正在做动态化/插件化架构的移动端工程师、想了解 Agent 如何落地到 UI 层的 AI 应用开发者、以及需要快速验证业务界面的产品技术负责人。为什么我要强调“工程层面”因为 GenUI 这个词很容易被讲成 PPT 概念。但只要你真正动手接过一个动态化项目就会知道难点从来不是“能不能生成一个按钮”而是生成出来的东西怎么渲染、怎么和原生通信、怎么保证返回栈正确、怎么在低端机上不卡。标题里出现的 WebView 和 Composable恰恰就是这些脏活累活的答案。我个人的经验是判断一个 GenUI 方案是否靠谱看三点就够了AgentLoop 是否闭环、WebView 是否被当作一等公民对待、Composable 的粒度是否合理。下面我就按这个思路把整个架构拆开讲中间会穿插大量实操细节和踩坑记录尽量做到你看完能直接对照自己的项目复现。2. GenUI 整体架构与核心思路拆解2.1 为什么是 AgentLoop 而不是一次性生成传统 AI 生成 UI 的做法基本是“用户一句话 → 模型吐一段 JSON → 前端渲染”一次性、无反馈。这种模式在 Demo 里很惊艳一上生产就露馅模型理解偏了没人纠正、渲染失败了没法回退、用户想微调只能重新描述一遍。GenUI 选择 AgentLoop 作为核心本质上就是把“生成”变成一个可迭代、可观测、可中断的循环过程。AgentLoop 的典型结构是感知Perception→ 规划Planning→ 执行Action→ 观察Observation→ 再规划。放到 GenUI 场景里感知是拿到用户的自然语言意图和当前页面上下文规划是决定这次要生成/修改哪些 Composable 单元执行是产出具体的 UI 描述并交给 WebView 渲染观察是收集渲染结果、用户点击、报错信息然后再进入下一轮。这个闭环的价值在于UI 不再是终点而是下一轮对话的输入。我实测下来AgentLoop 相比一次性生成最大的收益不是“更准”而是“可救”。模型第一次生成错了Loop 能根据渲染报错自动重试用户说“把刚才那个按钮改成红色”Loop 能基于上一轮的状态做增量修改而不是从零再来。这一点在真实业务里太重要了因为用户永远不会一次把需求说清楚。2.2 WebView 在 GenUI 里扮演什么角色很多人一听 WebView 就皱眉觉得它慢、它重、它是上个时代的产物。但在 GenUI 这套架构里WebView 的定位非常清晰它是动态 UI 的渲染容器和沙箱边界。为什么不用原生动态化方案因为原生动态化的表达能力受限于你预先埋好的组件而 GenUI 要的是“模型想生成什么就能渲染什么”这只有 Web 技术栈能做到。WebView 在这里承担了三件事。第一是渲染把 Agent 产出的 UI 描述通常是类 HTML/CSS 或组件树 JSON变成真实像素。第二是隔离动态生成的代码跑在 WebView 沙箱里即使出错也不会拖垮宿主 App。第三是通信通过 JSBridge 把用户的交互事件回传给 AgentLoop形成闭环。这三件事缺一不可尤其是隔离这一条是很多自研动态化方案翻车的地方。这里必须提一个热词里反复出现的坑WebView 的页面返回方式和常规页面不一样。在原生页面里返回就是 pop 一个栈但在 WebView 里返回可能意味着“网页内部 history 回退”“关闭当前 WebView”“通知宿主切换页面”三种完全不同的语义。GenUI 如果不处理好这个用户按返回键就会出现“页面没反应”或者“直接退出 App”的灾难性体验。后面我会专门用一节讲这个。2.3 Composable 粒度决定了架构的上限Composable 这个词在不同技术栈里含义不同在 GenUI 语境下我理解它是可组合、可复用、可独立描述的最小 UI 单元。一个按钮是 Composable一个卡片是 Composable一个表单区块也是 Composable。Agent 生成 UI 的过程本质上就是“选择合适的 Composable 并组装它们”。粒度设计是这里最考验功力的地方。粒度太粗比如把整个页面当成一个 Composable那 Agent 的灵活性就没了改一个标题都要重新生成整页粒度太细比如每个文字都当成独立 Composable那组合复杂度爆炸Agent 的规划负担极重渲染性能也会因为节点过多而下降。我的经验是以“业务语义块”为粒度最合适一个商品卡片、一个筛选栏、一个底部操作区这种用户能感知到的功能单元就是好的 Composable 边界。Composable 还有一个隐藏价值它是 Agent 和渲染层之间的契约。Agent 不需要知道 WebView 里具体怎么实现它只需要输出“我要一个商品卡片参数是这些”WebView 侧只要实现了这个 Composable 的渲染逻辑就能接住。这种解耦让两端可以独立演进Agent 换模型不影响渲染渲染换技术不影响 Agent。3. 核心细节解析与实操要点3.1 AgentLoop 的状态管理怎么做才不崩AgentLoop 听起来优雅但真正写起来状态管理是第一个大坑。因为 Loop 是多轮的每一轮都可能修改 UI你必须有一个地方存“当前 UI 长什么样”“上一轮用户说了什么”“哪些 Composable 被改过”。我见过不少实现是把状态塞在 Agent 的 prompt 里靠模型自己记结果轮次一多就失忆或者上下文超长直接报错。靠谱的做法是外置状态机 精简上下文。具体来说维护一份 UI State Tree记录当前所有 Composable 的 ID、类型、参数、层级关系每次进入新一轮只把“和本轮意图相关的子树”喂给模型而不是整棵树。这样既控制了 token 消耗又避免了模型被无关信息干扰。状态机的变更要走明确的 action比如ADD_COMPOSABLE、UPDATE_PROPS、REMOVE_COMPOSABLE而不是让模型直接吐一整棵新树。提示状态树一定要有版本号或快照机制。Agent 生成失败要能回滚到上一轮否则用户会看到界面“改一半卡住”的诡异状态。我踩过的一个坑是早期为了省事让模型每次输出完整 UI 描述然后整体替换。结果用户只是想把按钮文字从“提交”改成“确认”模型却把整个页面重新生成了一遍布局都变了。后来改成增量 patch 模式体验立刻稳定。这个教训就是AgentLoop 的“Loop”要体现在增量上而不是重复生成。3.2 WebView 渲染层的性能与兼容性处理WebView 渲染动态 UI性能是绕不开的。动态生成的 DOM 往往层级深、样式杂在低端 Android 机上很容易掉帧。我的优化顺序是这样的先做Composable 级别的缓存同一个 Composable 参数没变就复用已有 DOM不重新创建再做批量 DOM 操作把多次修改合并到一次 reflow最后才是考虑虚拟列表这类重型优化。兼容性方面热词里提到的“webview 历史版本合集”和“webview 测试网址”其实点出了一个大问题不同 Android 版本、不同厂商 ROM 自带的 WebView 内核差异巨大。你在一台新手机上跑得好好的 CSS 特性在老设备上可能直接不生效。GenUI 这种动态生成场景没法像传统 H5 那样针对固定浏览器做适配所以必须在渲染层做能力探测和降级。比如用CSS.supports检测某个属性不支持就换一种实现。还有一个实操细节android:visibility这个属性在 WebView 场景下经常被误用。原生开发里我们习惯用 visibility 控制显隐但在 WebView 里隐藏的 DOM 如果只是visibility: hidden它依然占位、依然参与布局计算如果 Agent 频繁切换显隐性能损耗会累积。更稳的做法是用display: none彻底移出布局流或者干脆在 Composable 层面做条件渲染不生成这个节点。3.3 Composable 的注册与发现机制Composable 要能被 Agent 使用前提是 Agent 知道“有哪些 Composable 可用”。这就需要一个注册与发现机制。最简单的做法是维护一个 Composable Registry每个 Composable 登记自己的名称、参数 schema、适用场景描述。Agent 在规划阶段先查这个 Registry再决定用哪些。这里的关键是参数 schema 要足够清晰。比如一个“商品卡片”Composable参数包括图片 URL、标题、价格、标签数组。如果 schema 写得含糊模型就会瞎填参数渲染出来就是空白或者错位。我的做法是给每个参数加上类型、是否必填、示例值甚至用自然语言描述它的用途。实测下来schema 写得越细Agent 一次生成的成功率越高。注意Registry 不要一次性全量塞给模型。Composable 数量一多prompt 会爆炸。正确做法是按当前页面场景做筛选只把相关的 Composable 暴露给 Agent。另外Composable 的版本管理也容易被忽略。当你更新了一个 Composable 的参数结构老的 Agent 生成结果可能就不兼容了。所以 Registry 里最好带上版本号渲染层做兼容处理避免“Agent 还在用旧 schema渲染层已经升级”的错位。4. 实操过程与核心环节实现4.1 从零搭一个最小可用的 GenUI 链路我建议任何想落地 GenUI 的团队都先做一个最小闭环不要一上来就搞全量。最小闭环包含四部分一个能接收自然语言的输入框、一个简化版 AgentLoop、一个只支持三五个 Composable 的 WebView 渲染页、一条 JSBridge 通信通道。跑通这个再往上加复杂度。具体步骤是这样的。第一步定义 Composable 协议用 JSON 描述 UI 树比如{type: card, props: {...}, children: [...]}。第二步在 WebView 里写一个渲染器递归解析这个 JSON映射到真实的 DOM 或前端组件。第三步写 AgentLoop 的骨架先用规则或简单模型做意图识别产出 JSON。第四步打通 JSBridge让 WebView 里的点击事件能回传到宿主触发下一轮 Loop。这个最小链路我大概花了两天搭起来跑通那一刻你会对整套架构有完全不同的理解。因为它逼着你面对所有真实问题JSON 怎么设计、渲染怎么容错、通信怎么保证顺序。这些在纯看架构图时是感受不到的。4.2 WebView 返回栈的正确处理姿势前面提到的返回问题这里展开讲。GenUI 场景下WebView 内部的页面跳转和宿主原生的页面栈是两套体系必须做映射。我的方案是WebView 内部维护自己的 history同时通过 JSBridge 把“当前能否回退”的状态同步给宿主。宿主在收到返回事件时先问 WebView“你还能回退吗”能就让 WebView 回退不能才执行原生 pop。具体实现上WebView 侧监听popstate或自己维护一个路由栈每次路由变化就调用bridge.notify(canGoBack, bool)。宿主侧重写返回逻辑先查这个状态。这样用户按返回键的体验就和原生一致了在 WebView 内部多级页面时逐级回退回到根页面再按才退出。提示Android 物理返回键和 iOS 侧滑返回都要处理而且要考虑 WebView 加载失败、白屏等异常情况下的兜底否则容易出现“返回键失灵用户只能杀进程”的糟糕体验。我踩过的坑是早期没做这个映射WebView 里跳了三层用户按返回直接退出了整个页面数据全丢。后来加上状态同步问题解决。这个点看起来小但它是 GenUI 能不能上生产的关键细节之一。4.3 一次完整的生成-渲染-反馈实录拿一个真实场景举例用户说“帮我做一个商品列表页要有搜索框和筛选”。AgentLoop 第一轮规划识别出需要search_bar、filter_bar、product_list三个 Composable。执行阶段产出 JSON 树WebView 渲染出来。观察阶段发现product_list没有数据因为用户没提供商品信息。于是 Loop 进入第二轮Agent 主动追问“商品数据从哪来”或者根据上下文调用一个数据接口。拿到数据后增量更新product_list的 propsWebView 只重渲染这个子树。用户看到列表出现点击某个商品事件通过 JSBridge 回传Loop 进入第三轮规划出“商品详情页”的 Composable 组合。整个过程用户只说了两句话界面却迭代了三轮。这个实录说明 GenUI 的价值不在“一次生成完美界面”而在“用对话把界面逐步逼近需求”。这也是为什么 AgentLoop 是核心而不是可选项。5. 常见问题与排查技巧实录5.1 渲染白屏与样式错乱的排查顺序白屏是 GenUI 最高频的问题。我的排查顺序固定为四步先看 Agent 产出的 JSON 是否合法很多时候是模型吐了非法 JSON 导致解析失败再看 Composable 类型是否在 Registry 里注册未注册的类型渲染器直接跳过然后看 props 是否符合 schema缺必填参数会导致组件渲染异常最后才查 WebView 本身的加载问题。样式错乱则多半是 CSS 作用域问题。动态生成的样式如果没做隔离很容易污染宿主或其他 Composable。我的做法是给每个 Composable 的样式加命名空间前缀或者直接用 Shadow DOM 做隔离。实测 Shadow DOM 在较新内核上表现很好老内核需要降级方案。5.2 AgentLoop 死循环与超时的处理AgentLoop 最怕死循环模型一直觉得没做好反复重试同一个操作。必须设置最大轮次和单轮超时。我的配置是单轮最多 3 次重试整个 Loop 最多 10 轮超过就中断并给用户一个兜底界面。同时要记录每轮的 action如果发现连续两轮 action 完全相同直接判定为死循环并跳出。注意超时时间不要设太短模型推理本身有延迟也不要太长用户等不起。我的经验值是单轮 15 到 30 秒根据模型响应速度调整。5.3 常见问题速查表问题现象可能原因排查方向解决建议界面白屏JSON 非法 / 类型未注册查 Agent 输出、查 Registry加 JSON 校验、补注册返回键失灵返回栈未同步查 JSBridge 状态通知实现 canGoBack 同步样式污染CSS 未隔离查样式作用域加前缀或 Shadow DOM低端机卡顿DOM 节点过多查 Composable 粒度合并粒度、加缓存Agent 死循环无轮次限制查 Loop 日志设最大轮次和超时参数错乱schema 不清晰查 Registry 定义补类型和示例这张表是我从多个项目里攒出来的基本覆盖了 80% 的线上问题。遇到新问题先往这几类里套能省很多时间。6. 我对 GenUI 落地的一些个人体会最后说点掏心窝的话。GenUI 这套架构技术上的难点其实都能靠工程手段解决真正难的是心态。很多团队做动态化做着做着就回到“预置组件 配置化”的老路因为那样可控。但 GenUI 的价值恰恰在于“不可控中的可控”——让模型去组合让 Loop 去纠偏让 WebView 去兜底。我在实际项目里最大的体会是不要追求 Agent 一次生成就对要追求错了能快速恢复。把精力花在状态管理、错误兜底、增量更新上比花在调 prompt 上回报高得多。另外Composable 的设计要克制不要为了炫技搞几百个先把十几个高频的做扎实覆盖大部分场景再慢慢扩。这个方向后续还能怎么扩展我个人的想法是往“跨端复用”走同一套 Composable 协议WebView 能渲染原生能渲染甚至小程序也能渲染Agent 产出的 UI 描述成为真正的中间层。到那时候GenUI 就不只是一个生成工具而是一套界面描述标准了。