Agent可视化生成方案:用IR中间表示替代AI硬写代码的工程实践 1. 当让AI直接写代码开始不灵了过去一年我经手过至少七八个 Agent 相关的项目从最简单的帮我生成一个表单页面到复杂的根据需求文档自动产出整套前端工程。一开始大家的思路都很朴素把需求丢给大模型让它直接吐代码能跑就行。但项目做到第二个月、第三个月问题就集中爆发了——生成的代码风格飘忽不定、组件复用率几乎为零、改一处崩三处最要命的是你根本没法保证同一个需求描述两次生成的结果是一致的。这就是让 AI 硬写的典型困境。所谓硬写就是跳过中间抽象层让模型从自然语言直接跳到最终产物HTML、Vue 组件、React 代码、甚至整棵 DOM 树。这种做法在 Demo 阶段看着很爽一旦进入真实工程就原形毕露。于是行业里开始出现一个明显的趋势Agent 可视化生成方案——不再让模型直接写代码而是让模型生成一种中间表示IR再由确定性的渲染引擎把 IR 转成最终产物。这个思路的核心价值在于把创意生成和结构落地这两件事拆开。模型擅长的是理解意图、做决策、填内容而布局计算、样式约束、组件映射这些确定性极强的工作交给代码去做。关键词里的Agent、可视化生成、AI、IR、SDK这五个词基本就勾勒出了这条技术路线的全貌。这篇文章我想把这条路线拆透讲清楚为什么 IR 是关键、可视化生成到底怎么落地、SDK 层该怎么设计以及我在实操中踩过的那些坑。不管你是刚接触 Agent 开发的新手还是已经在做生成式前端的老手应该都能从中找到能直接抄作业的部分。2. 硬写代码为什么必然翻车从三个真实故障说起2.1 故障一同一个需求两次生成两套结构我做过一个实验用同一段需求描述生成一个带搜索框、分页、批量删除的用户列表页连续调用同一个模型十次。结果十次里有七次结构完全不同有的用 table 直接铺有的用 div 模拟表格有的把分页放在顶部有的放在底部还有两次干脆把批量删除做成了单行操作。这不是模型不稳定这么简单而是自然语言到代码之间存在巨大的语义鸿沟模型每次都在用自己的方式填补这个鸿沟填补方式自然千差万别。对于个人玩具项目这种不确定性无所谓。但对于团队协作这意味着你没法做代码审查、没法做组件复用、没法写测试用例。你面对的是一个黑盒每次输出都是新的黑盒。2.2 故障二改一个字段整个布局塌了更隐蔽的问题出现在迭代阶段。假设第一版生成没问题产品说把状态那一列改成带颜色的标签。你把这句话丢给模型让它基于现有代码修改。结果模型很可能顺手把整个表格重写了一遍顺带改了列宽、改了间距、甚至换了组件库的写法。你只是想改一个单元格的渲染方式它却给你做了一次重构。根本原因还是硬写模型看到的是一大坨代码文本它没有结构的概念只有token 序列的概念。它不知道哪部分是布局、哪部分是数据、哪部分是样式所以任何修改都可能波及全局。2.3 故障三复杂交互直接超出模型能力边界当需求涉及拖拽排序、虚拟滚动、联动筛选、动态表单这类复杂交互时硬写方案基本就废了。模型能生成一个看起来像的版本但事件绑定、状态管理、边界条件处理全是漏洞。你花在修 bug 上的时间比自己从零写还多。这三个故障指向同一个结论让模型直接产出最终代码等于让它同时承担产品经理、架构师、UI 设计师、程序员四个角色而它最擅长的其实只有第一个和最后一个的一部分。正确的做法是把中间那几个角色交给确定性的系统模型只负责它真正擅长的事。3. IR 到底是什么把意图和实现隔开的那层膜3.1 用生活类比理解 IRIRIntermediate Representation中间表示这个词来自编译器领域。编译器不会把 C 语言直接翻译成机器码而是先转成一种中间形式再做优化最后才生成目标代码。这样做的好处是前端语言有几十种后端架构有几十种如果两两直接对接就是几百套翻译逻辑有了 IR只需要语言到 IR和IR 到架构两套逻辑组合起来就能覆盖所有情况。Agent 可视化生成里的 IR 是同一个道理。自然语言有无数种表达方式最终产物有无数种技术栈React、Vue、小程序、Flutter、甚至设计稿如果让模型直接对接每一种产物成本高到无法维护。中间加一层 IR模型只需要学会自然语言到 IR剩下的IR 到产物由渲染引擎确定性完成。3.2 一份典型的可视化 IR 长什么样IR 的本质是一棵结构化的树每个节点描述这是什么组件、它有哪些属性、它的子节点是什么。我用一个简化版的 JSON 结构来说明这是我在项目里实际用过的形态{ type: Page, meta: { title: 用户管理, route: /users }, children: [ { type: SearchBar, props: { placeholder: 搜索用户名, fields: [name, status] } }, { type: DataTable, props: { dataSource: api:/api/users, rowKey: id, selectable: true }, columns: [ { key: name, label: 用户名, render: text }, { key: status, label: 状态, render: tag, mapping: { 1: 启用, 0: 禁用 } }, { key: actions, label: 操作, render: actions, items: [edit, delete] } ] }, { type: Pagination, props: { pageSize: 20, position: bottom-right } } ] }这份 IR 里没有任何一行是最终代码。它描述的是语义这里有个搜索栏、有个数据表格、表格有三列、状态列要用标签渲染。至于这个表格在 React 里用 Ant Design 还是 Arco在 Vue 里用 Element Plus 还是 Naive UI那是渲染引擎的事跟模型无关。3.3 IR 带来的三个确定性收益第一是可校验。IR 是结构化数据你可以写 schema 去校验它type 是不是合法组件、props 有没有必填项、columns 的 render 类型是否在支持列表里。模型偶尔抽风生成了不存在的组件校验层直接拦下来让它重试而不是把错误代码吐给用户。第二是可复用。同一份 IR 可以渲染成 React 版本、Vue 版本、甚至导出成 Figma 设计稿。产品经理改需求时你改的是 IR所有产物同步更新不用维护多套代码。第三是可迭代。用户说把状态列改成标签你只需要在 IR 里把那一列的 render 从 text 改成 tag其他节点纹丝不动。修改范围被精确限定在一个节点内不会波及全局。提示IR 的粒度设计是个权衡。粒度太粗比如整个页面就一个节点模型还是要写大量细节粒度太细每个像素都描述IR 会膨胀到模型无法稳定生成。我的经验是以业务组件为粒度一个搜索栏、一个表格、一个表单算一个节点这个粒度下模型生成最稳。4. 可视化生成方案的完整链路拆解4.1 从需求到 IR模型该做什么、不该做什么在这条链路里模型的任务被大幅收窄了它只需要把用户的自然语言需求翻译成符合 schema 的 IR。它不需要知道 React 的 hooks 怎么写不需要知道 CSS 的 flex 怎么用只需要知道用户要一个列表页列表页通常包含搜索、表格、分页。为了让模型稳定输出 IR我在实操中总结了几个关键做法。首先是给足组件清单把系统支持的所有组件类型、每个组件的 props 定义、可选的 render 类型全部作为上下文喂给模型。模型看到清单就不会瞎编组件名。其次是给 few-shot 示例放三到五个需求描述 → IR的完整对照模型照着模仿格式稳定性会大幅提升。最后是强制 JSON 输出用结构化输出能力约束模型避免它夹带解释性文字。这里有个反直觉的经验不要试图让模型一次生成完整页面的 IR。页面越复杂模型越容易在中途丢失上下文导致后半部分结构错乱。我的做法是分层生成先让模型生成页面的骨架有哪些区块再针对每个区块单独生成细节 IR最后合并。这样每一步的上下文都很短稳定性明显更好。4.2 从 IR 到产物渲染引擎的确定性魔法渲染引擎是这条链路里最无聊但最可靠的部分。它接收 IR遍历节点树对每个节点查表找到对应的组件实现把 props 映射进去递归渲染子节点。整个过程没有任何 AI 参与纯确定性代码。渲染引擎的设计要点有三个。第一是组件注册表维护一个 type 到组件实现的映射新增组件只需要注册不用改引擎逻辑。第二是props 适配层IR 里的 props 是抽象的渲染时要转成具体组件库的 props比如 IR 里的selectable: true在 Ant Design 里要转成rowSelection配置。第三是样式隔离IR 不描述具体样式样式由主题系统统一注入保证同一份 IR 在不同项目里渲染出的视觉风格一致。// 渲染引擎的核心逻辑简化版 const componentRegistry { Page: PageRenderer, SearchBar: SearchBarRenderer, DataTable: DataTableRenderer, Pagination: PaginationRenderer, }; function renderNode(node, context) { const Renderer componentRegistry[node.type]; if (!Renderer) { throw new Error(Unknown component type: ${node.type}); } const props adaptProps(node.type, node.props, context); const children (node.children || []).map(child renderNode(child, context) ); return Renderer({ props, children, meta: node.meta }); }这段代码看着简单但它承担了整个方案的确定性职责。模型再怎么飘只要 IR 合法渲染结果就是可预期的。4.3 SDK 层把能力封装成别人能用的东西关键词里的 SDK 指的是把整套能力封装成开发者可以直接调用的接口。一个完整的 Agent 可视化生成 SDK 通常包含四层接入层接收需求描述、返回 IR 或产物、模型编排层管理 prompt、调用模型、处理重试、IR 处理层校验、合并、转换、渲染层IR 到产物的转换。SDK 设计最容易踩的坑是把模型调用暴露给使用者。有些 SDK 让使用者自己传 API key、自己选模型看似灵活实则把复杂度转嫁出去了。我的建议是 SDK 内部封装好模型编排对外只暴露输入需求、输出产物这样的高层接口需要定制时再通过配置项开放。// SDK 对外接口示例 import { VisualAgent } from visual-agent-sdk; const agent new VisualAgent({ schema: myComponentSchema, theme: antd, target: react, }); const result await agent.generate({ requirement: 生成一个带搜索和分页的用户列表页, context: { apiBase: /api }, }); // result.ir 是中间表示result.code 是最终代码 console.log(result.ir); console.log(result.code);这种设计下使用者不需要懂 prompt 工程不需要懂 IR schema只需要描述需求、拿到产物。这才是 SDK 该有的样子。5. 我在实操中踩过的五个坑5.1 坑一IR schema 设计得太理想化第一版 schema 我设计得非常完备每个组件都定义了二三十个 props想着覆盖所有场景。结果模型根本记不住这么多字段生成时要么漏字段要么填错类型。后来我把 schema 砍到每个组件只保留 5 到 8 个核心 props其余用默认值兜底生成成功率从六成提到了九成以上。教训是schema 是给模型看的不是给人看的。字段越少、语义越清晰模型越稳。那些边缘场景的配置用默认值加手动覆盖的方式处理不要塞进 schema。5.2 坑二忽略 IR 的版本兼容项目迭代到第三个月我给 DataTable 加了一个新 props结果历史生成的 IR 全部渲染失败——因为老 IR 里没有这个字段渲染引擎却按新逻辑去读。这个坑很典型IR 是持久化数据一旦存下来就要考虑向后兼容。解决方案是给 IR 加版本号渲染引擎按版本号走不同的适配逻辑。新字段一律给默认值老 IR 缺字段时用默认值补上。这个机制必须在项目早期就建立后期补代价很大。5.3 坑三把复杂交互硬塞进 IR我一度想用 IR 描述拖拽排序、虚拟滚动这类复杂交互结果 IR 结构爆炸模型完全生成不出来。后来想明白了IR 应该描述结构而不是行为。交互逻辑应该由组件自身实现IR 只负责声明这个表格支持拖拽排序这个事实具体怎么拖、怎么排序是组件内部的事。这个边界划清之后IR 一下子清爽了模型生成也稳定了。5.4 坑四渲染引擎和组件库耦合太深早期渲染引擎直接依赖 Ant Design导致想换组件库时几乎要重写。后来我把渲染引擎拆成通用逻辑 适配器两层通用逻辑处理节点遍历、props 映射适配器负责具体组件库的转换。换库时只写新适配器通用逻辑不动。5.5 坑五没有给模型留我不知道的出口模型有时候会遇到它不理解的组件类型如果强制它输出它就会瞎编。我在 schema 里加了一个Unknown类型允许模型在不确定时输出这个类型渲染时降级成一个占位符。这样虽然产物不完美但至少不会崩而且你能从日志里看到哪些需求是模型搞不定的针对性优化。6. 不同技术栈下的落地差异6.1 React 生态组件化天然契合React 的组件模型和 IR 的树结构几乎是同构的落地最顺。IR 节点直接映射成 React 组件props 直接透传children 递归渲染。需要注意的主要是状态管理——IR 描述的是静态结构动态状态比如表格的选中行、搜索框的输入值需要在渲染时注入通常用 Context 或状态库统一管理。6.2 Vue 生态模板编译的额外一层Vue 的模板语法和 IR 结构也很接近但 Vue 有编译期优化直接生成 render 函数比生成模板字符串更高效。我的做法是 IR 直接转成 Vue 的 h 函数调用跳过模板编译。这样产物更可控也避免了模板字符串拼接带来的转义问题。6.3 小程序与跨端IR 作为统一抽象层小程序、Flutter、React Native 这些跨端场景恰恰是 IR 方案价值最大的地方。因为各端组件差异大如果让模型直接生成各端代码成本极高。有了 IR模型只生成一份各端写各自的渲染适配器一份需求多端复用。我在一个多端项目里用这套方案把三端的开发工作量压缩到了原来的三分之一。技术栈IR 到产物的转换方式主要难点React节点映射为组件调用状态注入、事件绑定Vue节点映射为 h 函数响应式数据对接小程序节点映射为 wxml js组件能力受限、需降级Flutter节点映射为 Widget 树布局模型差异大7. 性能与并发Agent 生成绕不开的现实问题7.1 生成延迟的构成与优化一次完整的可视化生成延迟主要来自三块模型推理、IR 校验与合并、渲染。模型推理通常占大头尤其是复杂页面需要多次调用时。优化手段有几个分层生成并行化骨架生成后各区块的细节 IR 可以并发请求IR 缓存相同或相似需求直接命中缓存流式渲染IR 边生成边渲染用户能更快看到结果。我在项目里实测一个中等复杂度的列表页串行生成要 8 到 12 秒改成骨架加并行细节后压到了 4 秒左右加上流式渲染用户感知的等待时间进一步缩短。7.2 并发场景下的稳定性当多个用户同时使用生成能力时模型调用会排队延迟飙升。这时候 SDK 层需要做请求队列和限流避免把模型打爆。同时要做好失败重试模型偶尔返回非法 IR 是常态重试时把校验错误信息作为反馈喂回去第二次成功率会高很多。注意重试一定要设上限我见过因为重试逻辑写错导致无限循环、把额度烧光的案例。建议最多重试两次两次都失败就降级返回一个基础模板。8. 这套方案适合谁以及怎么开始如果你在做的是一次性 Demo需求简单、不要求复用那硬写代码确实更快没必要上 IR 这套。但如果你面对的是持续迭代的产品、多端复用的场景、或者需要团队协作的工程那 IR 方案带来的确定性收益会远超它的搭建成本。上手路径我建议这样走先用一个最小 schema 跑通需求到 IR 到单一产物的闭环验证模型生成 IR 的稳定性然后逐步扩充组件清单覆盖你项目里最常用的那批组件接着把渲染引擎的适配层抽出来为将来换技术栈留口子最后再考虑多端、并发、缓存这些进阶能力。不要一上来就追求大而全我见过太多项目死在想一次做完上。关于模型选择我的经验是不要迷信最大的模型。IR 生成这个任务对模型的结构化输出能力要求高于知识广度很多中等规模的模型在给定清晰 schema 和 few-shot 的情况下表现并不比大模型差而且延迟和成本都更友好。具体选哪个拿你自己的真实需求做一轮对比测试最靠谱。最后分享一个我一直在用的小技巧把每次生成的 IR 和对应的用户需求都存下来定期回看那些生成失败的案例。你会发现失败往往集中在某几类需求上针对性地补 few-shot 示例或者调整 schema比盲目换模型有效得多。这套方案的本质不是让 AI 更聪明而是把 AI 放在它该在的位置上剩下的交给确定性系统。想清楚这个边界很多设计决策就自然清晰了。