ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenTiny NEXT 实战:AI SDK 与 GenUI 如何让 Web 应用智能化

OpenTiny NEXT 实战:AI SDK 与 GenUI 如何让 Web 应用智能化 1. 从 HC 2026 现场聊起OpenTiny NEXT 到底在解决什么问题今年 HC 大会我全程跟了下来OpenTiny 展台前前后后去了三趟。倒不是说展台布置得多花哨而是他们这次抛出来的 OpenTiny NEXT 确实踩到了我最近半年做项目时反复纠结的一个点Web 应用怎么从“人操作界面”平滑过渡到“人指挥 AI、AI 操作界面”。先说清楚它是什么。OpenTiny NEXT 是 OpenTiny 团队在 HC 2026 上发布的一套面向 Web 应用的智能化能力集合核心由三块拼起来一套AI SDK、一套GenUI生成式 UI运行时以及把两者串起来的Agent 调度层。它能干的事用一句话概括就是——让原本写死的 Web 页面具备“根据用户意图动态生成界面、动态调用能力”的本事。解决的是传统 Web 应用在 AI 时代最尴尬的问题后端模型越来越聪明前端界面却还是那套固定表单和固定按钮用户想干点计划外的事只能干瞪眼。这套东西适合谁看如果你是中高级前端、全栈工程师或者正在负责把公司内部系统往智能化方向改造的技术负责人那这篇值得你花时间。如果你只是刚入门 Web建议先收藏等把组件化、状态管理这些基础打牢了再回来啃不然容易看得云里雾里。我下面会按“设计思路—核心细节—实操落地—踩坑排查”这条线把我在现场问到的、回来自己搭 Demo 验证过的内容尽量完整地摊开讲。2. 整体设计思路拆解为什么是 SDK GenUI Agent 这个组合2.1 传统 Web 智能化改造的三条路以及它们各自的坑在 OpenTiny NEXT 出来之前我接触过的 Web 智能化改造基本走三条路每条我都实际用过坑也踩得比较清楚。第一条是硬编码 AI 入口。就是在页面右上角挂个对话框用户输入问题前端把内容发给大模型拿到回复渲染成文本。这条路最简单一周就能上线但问题是 AI 和页面是两张皮——AI 只能“说”不能“做”。用户问“帮我把这个订单的收货地址改到杭州”AI 只能回一句“请点击右上角编辑按钮手动修改”体验直接断裂。第二条是函数调用Function Calling硬桥接。把页面上的关键操作封装成一个个函数注册给模型模型决定调哪个。这条路比第一条进了一步AI 能“做”事了但界面还是死的。模型调完函数页面状态变了可用户看到的还是原来那套 UI没有任何“AI 正在为你操作”的反馈而且每加一个功能就要手动注册一个函数维护成本随功能数量线性上涨。第三条是低代码平台 AI 生成配置。用低代码的 Schema 描述页面让 AI 生成 Schema 再渲染。这条路理论上最优雅但实际落地时低代码平台本身的 Schema 表达能力就是天花板稍微复杂一点的交互就描述不了而且生成出来的 Schema 经常有语法错误调试起来非常痛苦。OpenTiny NEXT 的思路本质上是把第二条和第三条的优点捏在一起同时绕开它们各自的硬伤。它不要求你把所有操作都封装成函数也不要求你用一套受限的 Schema 描述页面而是提供了一套运行时可组合的能力单元让 AI 在运行时按需拼装。2.2 SDK 负责“能力暴露”GenUI 负责“界面生成”Agent 负责“决策调度”这三层的分工我在现场跟他们的工程师聊了很久回来又自己画了几遍架构图理解下来是这样的。AI SDK这一层核心是把 Web 应用里已有的能力——不管是前端组件的方法、后端接口、还是第三方服务——用一种统一的方式“暴露”出去。注意不是重新实现一遍而是包装。比如你有一个tiny-grid表格组件它本身有setDataSource、refresh、exportExcel这些方法SDK 做的事情就是把这些方法注册成 AI 可识别的“工具”并附上自然语言描述和参数说明。这一步的关键在于描述的质量描述写得好模型选工具的准确率能差出三四十个百分点这个后面实操部分我会细说。GenUI这一层是我个人觉得最有意思的。它不是让 AI 生成 HTML 字符串——那样既危险又难维护——而是让 AI 生成一棵组件描述树这棵树由 OpenTiny 已有的组件库来渲染。打个比方传统方式是让 AI 直接画一幅画画得好不好全看运气GenUI 的方式是让 AI 从一盒标准积木里挑积木、搭结构搭出来的东西一定是能用的因为每块积木本身都是经过测试的组件。这个设计决策非常关键它把“生成的不确定性”限制在了结构层面而不是渲染层面。Agent 调度层则是那个“大脑”。它接收用户输入判断是需要直接回答、还是需要调用工具、还是需要生成新界面然后编排执行顺序。比如用户说“把上个月的销售数据按区域汇总然后给我一个能下钻的图表”Agent 会先调数据接口拿数据再让 GenUI 生成一个带下钻能力的图表组件树最后把数据灌进去。整个过程对用户来说就是一句话的事。2.3 为什么不做成纯 Prompt 方案而要引入 SDK 这层“中间层”现场有人问过这个问题我觉得问得很好。纯 Prompt 方案就是把页面能力用文字描述给模型让模型直接输出操作指令。听起来更简单为什么还要搞个 SDK他们工程师的回答我记下来了核心是可靠性和可观测性。纯 Prompt 方案里模型输出的指令是自由文本前端要解析这段文本才能执行解析失败就是线上事故。而 SDK 这层中间层把“模型输出”和“实际执行”之间的契约固定下来了——模型输出的必须是符合 SDK 定义的结构化调用参数类型、必填项、取值范围都在 SDK 里校验过不合法的调用直接在前端就被拦下来不会传到后端造成脏数据。另外SDK 天然带日志和埋点每次 AI 调用了什么工具、传了什么参数、返回了什么结果全都有记录出了问题能回溯。纯 Prompt 方案要做到同等可观测性得自己从头搭一套成本反而更高。这个取舍我认为是务实的。做企业级应用可靠性永远排在“炫酷”前面。3. 核心细节解析AI SDK 的工具注册与 GenUI 的组件描述树3.1 工具注册描述写得好模型选得准AI SDK 里最核心的 API 就是工具注册。我按现场演示和回来自己试的结果整理了一个最小可用的注册示例。假设你有一个订单查询接口想暴露给 AIimport { createTool, registerTools } from opentiny/next-sdk; const queryOrderTool createTool({ name: queryOrder, description: 根据订单号查询订单详情返回订单状态、金额、收货地址等信息。当用户询问某个具体订单的情况时使用此工具。, parameters: { type: object, properties: { orderId: { type: string, description: 订单号通常是 16 位数字字符串例如 2026052012345678 } }, required: [orderId] }, handler: async ({ orderId }) { const res await fetch(/api/order/${orderId}); return res.json(); } }); registerTools([queryOrderTool]);这段代码本身不复杂但有几个细节决定了实际效果。description 的写法是重中之重。我实测下来把“什么时候用这个工具”写进 description比只写“这个工具是干什么的”效果明显更好。上面那个例子里“当用户询问某个具体订单的情况时使用此工具”这句话能显著降低模型在用户问“最近有什么促销”时误调订单查询的概率。另外参数描述里给出具体格式示例比如“16 位数字字符串例如 2026052012345678”能让模型在用户说“查一下我昨天那个单子”时主动去上下文里找订单号而不是瞎编一个。handler 里不要做重业务逻辑。SDK 的 handler 应该只做“转发”把参数传给真正的业务接口拿到结果原样返回。我见过有人在 handler 里写了一大堆数据转换逻辑结果模型拿到的返回结构和它预期的不一样后续推理全乱套。正确的做法是让 handler 保持薄数据转换放在业务层做handler 只负责“取数”。工具数量要控制。我试过一个页面注册了 40 多个工具结果模型选工具的准确率掉到了六成左右经常在几个相似工具之间反复横跳。后来砍到 15 个以内准确率回到九成以上。所以我的经验是按场景分组注册用户进入订单模块就只注册订单相关工具进入报表模块再切换而不是一股脑全注册上去。3.2 GenUI 的组件描述树结构对了界面就对了GenUI 这块我花的时间最多因为它的思路和常见的“AI 生成代码”完全不同。它生成的是一棵 JSON 结构的组件描述树类似这样{ component: TinyLayout, props: { direction: vertical }, children: [ { component: TinySearch, props: { placeholder: 输入订单号搜索, onSearch: queryOrder } }, { component: TinyGrid, props: { columns: [ { field: orderId, title: 订单号 }, { field: status, title: 状态 }, { field: amount, title: 金额 } ], dataSource: {{orderList}} } } ] }这棵树交给 GenUI 运行时运行时会去组件注册表里找对应的组件把 props 传进去渲染出来。整个过程中AI 不需要知道 TinyGrid 内部是怎么实现的它只需要知道“有个叫 TinyGrid 的组件接受 columns 和 dataSource 这两个 props”。组件注册表是 GenUI 的地基。你得先把项目里可用的组件注册进去每个组件附上 props 的 schema 描述。这一步和工具注册的逻辑一样描述越精确生成结果越靠谱。我建议只注册那些展示型、容器型的组件比如布局、表格、表单、图表不要把带复杂副作用的组件比如直接触发支付的按钮注册进去避免 AI 生成出危险界面。数据绑定用占位符不要硬编码。上面例子里dataSource写的是{{orderList}}这是一个占位符运行时会被真实数据替换。如果让 AI 直接把数据写进描述树里一是数据量大导致生成慢二是数据一变就得重新生成完全没必要。占位符机制让“结构生成”和“数据填充”解耦结构可以缓存复用数据实时更新。生成结果要做白名单校验。GenUI 运行时在渲染前会检查描述树里用到的组件是否都在注册表里、props 是否符合 schema。不符合的直接拒绝渲染并返回错误信息给 Agent让 Agent 重新生成。这个校验环节是安全底线绝对不能省。我试过故意让模型生成一个未注册的组件名运行时确实拦住了没有出现白屏或者报错崩溃。3.3 Agent 调度什么时候该“说”什么时候该“做”Agent 调度层的决策逻辑我理解下来是三个判断的串联意图是否需要操作页面、操作是否已有现成工具、结果是否需要新界面呈现。如果用户只是问“这个订单什么时候发货”Agent 判断不需要操作页面直接调查询工具拿数据用自然语言回答即可。如果用户说“帮我把这个订单的地址改一下”Agent 判断需要操作但修改地址这个动作如果没有注册成工具它会回复“暂不支持直接修改请点击编辑按钮”。如果用户说“给我看看最近一周的订单趋势”Agent 判断需要新界面就会触发 GenUI 生成一个图表组件树。这里有个设计细节我觉得很聪明Agent 的决策过程对用户是可见的。页面上会有一个状态条显示“正在查询数据”“正在生成图表”“正在渲染”用户知道 AI 在干什么不会觉得卡住了。这个反馈机制看似简单但对体验的提升非常大尤其是生成复杂界面需要几秒钟的时候。4. 实操落地从零搭一个带 GenUI 的订单管理页面4.1 环境准备与依赖安装我回来之后用 Vue 3 Vite 搭了个 Demo把流程完整走了一遍。先把依赖装上npm create vitelatest tiny-next-demo -- --template vue cd tiny-next-demo npm install opentiny/next-sdk opentiny/vue opentiny/genui-runtime这里有个版本匹配的坑要提醒。opentiny/next-sdk和opentiny/genui-runtime的版本号必须对应我一开始装了 SDK 的最新版和 runtime 的次新版结果运行时一直报“组件注册表格式不匹配”。后来查了文档才知道这两个包是配套发布的版本号前两位要一致。建议直接看官方 release note 里标注的配套版本别自己乱配。4.2 注册组件与工具组件注册我挑了五个最常用的TinyLayout、TinySearch、TinyGrid、TinyChart、TinyButton。工具注册了三个queryOrder、queryOrderList、queryOrderTrend。注册代码我整理成了一个独立模块// registry.js import { registerComponents } from opentiny/genui-runtime; import { registerTools, createTool } from opentiny/next-sdk; registerComponents([ { name: TinyGrid, propsSchema: { columns: { type: array, required: true }, dataSource: { type: string, required: true }, pagination: { type: boolean, default: true } } }, { name: TinyChart, propsSchema: { type: { type: string, enum: [line, bar, pie], required: true }, dataSource: { type: string, required: true }, xField: { type: string, required: true }, yField: { type: string, required: true } } } // ... 其他组件 ]); registerTools([ createTool({ name: queryOrderList, description: 查询订单列表支持按状态和时间范围筛选。当用户想查看一批订单时使用。, parameters: { type: object, properties: { status: { type: string, enum: [pending, shipped, completed] }, dateRange: { type: string, description: 时间范围格式如 2026-05-01~2026-05-20 } } }, handler: async (params) { const res await fetch(/api/orders? new URLSearchParams(params)); return res.json(); } }) // ... 其他工具 ]);注意propsSchema里的enum和default这些约束会直接影响 GenUI 生成结果的合法性。比如TinyChart的type限定为 line/bar/pie 三个值模型就不会生成一个type: scatter的图表导致渲染失败。4.3 接入 Agent 并处理生成结果Agent 的接入比我想象的简单核心就是初始化一个实例把用户输入喂进去然后监听它输出的事件import { createAgent } from opentiny/next-sdk; import { renderGenUI } from opentiny/genui-runtime; const agent createAgent({ model: your-model-endpoint, tools: registeredTools, components: registeredComponents }); agent.on(genui, async (tree) { const container document.getElementById(genui-container); const result await renderGenUI(tree, container, { dataContext: currentDataContext }); if (!result.success) { console.error(GenUI 渲染失败:, result.errors); agent.feedback(渲染失败请重新生成, result.errors); } }); agent.on(message, (text) { appendChatMessage(assistant, text); }); async function handleUserInput(input) { appendChatMessage(user, input); await agent.run(input); }dataContext是数据占位符的解析上下文比如描述树里写了{{orderList}}运行时就会去dataContext.orderList取值。这个上下文要在每次数据更新后同步刷新否则生成的界面会显示旧数据。4.4 实测效果与性能数据我拿这个 Demo 跑了大概五十次不同类型的用户输入记录了一些数据供你参考。输入类型示例平均响应时间生成成功率纯问答“订单 2026052012345678 什么状态”1.2s100%工具调用“查一下待发货的订单”1.8s96%简单 GenUI“把订单列表展示出来”3.5s92%复杂 GenUI“按区域汇总销售额并画柱状图”6.8s84%复杂 GenUI 的失败主要集中在图表组件的字段映射上模型有时候会把xField和yField搞反或者生成一个数据里不存在的字段名。我的处理方式是让运行时在渲染失败时把错误信息回传给 AgentAgent 拿到错误后自动重试一次重试的成功率大概能到七成。这个“失败自动重试”的机制我强烈建议加上能省掉大量人工干预。5. 常见问题与排查技巧实录5.1 工具注册了但模型不调用怎么排查这是我最开始遇到的头号问题。注册了工具用户输入也明显该调工具但模型就是直接回答不调。排查下来通常是三个原因。description 写得太泛。比如写“查询订单”模型不知道什么时候该用。改成“根据订单号查询单个订单的详细信息当用户提到具体订单号或询问某个订单状态时使用”调用率立刻上来了。参数 schema 有歧义。比如一个参数叫id没写 description模型不知道这个 id 是订单 id 还是用户 id干脆不调。每个参数都要写清楚它是什么、什么格式。工具数量和上下文长度冲突。工具描述本身也占 token注册太多工具会把上下文挤爆模型反而看不清。控制在 15 个以内超过就分组。5.2 GenUI 生成的界面“能看不能用”问题出在哪生成的界面渲染出来了但按钮点了没反应或者表格数据是空的。这种情况九成是事件绑定和数据绑定没接上。事件绑定方面描述树里写的onSearch: queryOrder这个queryOrder必须是已经注册过的工具名运行时才能找到对应处理函数。如果写了一个没注册的名字按钮就是死的。数据绑定方面dataSource: {{orderList}}里的orderList必须在dataContext里存在否则表格就是空的。我建议在开发阶段打开运行时的 debug 模式它会把每个占位符的解析结果打出来一眼就能看出哪个没接上。5.3 生成结果不稳定同样的输入两次结果不一样这个是大模型的固有特性没法完全消除但可以缓解。我的做法是给 GenUI 生成加温度参数控制把 temperature 调到 0.2 以下生成结果会稳定很多。另外在系统提示里明确“优先使用已注册组件不要发明新组件名”也能减少随机性。如果某个界面需要高度稳定可以考虑把生成结果缓存起来相同意图直接复用不走模型。5.4 常见问题速查表现象可能原因排查动作模型不调工具description 太泛/参数无说明/工具过多补全描述精简工具数界面渲染失败组件未注册/props 不符合 schema查运行时错误日志核对注册表按钮无响应事件名未注册检查 onXxx 是否对应已注册工具数据为空占位符未解析检查 dataContext 是否包含对应 key生成结果飘忽temperature 过高调低温度加系统提示约束响应太慢工具或组件过多上下文过长分组注册按需加载5.5 几个我踩过的坑你大概率也会遇到坑一在 handler 里抛异常没捕获。SDK 的 handler 如果抛异常整个 Agent 流程会中断用户看到的就是“出错了”没有任何有用信息。正确做法是在 handler 内部 try-catch把错误包装成结构化返回让模型知道“这次调用失败了原因是 xxx”它才能决定是重试还是换方案。坑二GenUI 生成的组件树层级过深。模型有时候会生成七八层嵌套的布局组件渲染出来性能很差而且样式容易乱。我在系统提示里加了一句“布局层级不超过三层”情况改善很多。坑三忘了处理并发。用户快速连续发两条消息Agent 会并发处理两个 GenUI 生成结果可能同时往同一个容器里渲染导致界面错乱。加一个简单的请求队列或者取消机制就能解决我是在 Agent 层加了个isProcessing标志处理中就不接受新输入。坑四数据上下文没及时更新。用户先查了订单列表然后说“把刚才的结果画成图”如果dataContext没更新图表画出来是空的。我的做法是每次工具调用返回后自动把结果写进dataContext并给一个语义化的 key这样后续 GenUI 生成时能直接引用。6. 我对这套方案的实际体会与后续可扩展方向用下来这段时间我最大的感受是 OpenTiny NEXT 这套东西的边界感很清晰。它没有吹嘘“什么都能生成”而是老老实实把能力限制在“已注册组件 已注册工具”的范围内生成结果一定是可渲染、可执行的。这个克制在企业级场景里比什么都重要。我见过太多 AI 生成方案Demo 阶段惊艳一上生产就各种边界情况爆炸根本原因就是没有约束。另外一点它的渐进式接入设计很友好。你不需要一次性把整个应用都改造成 GenUI 模式可以先从一个小模块试起比如把报表页面的图表生成交给 GenUI其他页面保持原样。跑通了再逐步扩大范围。这种可以“小步快跑”的方案落地阻力比那种“推倒重来”的方案小得多。后续如果要继续扩展我觉得有两个方向值得试。一是把 GenUI 生成结果持久化用户觉得某个生成的界面好用可以保存下来下次直接加载不用重新生成既快又稳。二是多轮对话中的界面继承用户说“在这个图表基础上再加一个筛选器”Agent 能拿到上一轮的组件树在其基础上修改而不是从头生成。这两个方向 OpenTiny 的文档里提到了正在规划我挺期待的。最后分享一个我在实操中总结的小技巧给每个工具和组件都写一句“反例说明”。比如订单查询工具的 description 里加一句“不要用于查询用户信息”图表组件的 schema 里加一句“不要用于展示非数值型数据”。这些反例能帮模型划清边界减少误用。我加上之后工具误调率大概降了两成效果比想象中好。
返回列表