☰
别再让AI硬写:基于IR与DSL的Agent可视化生成方案
2026/10/15 12:38:18 网站建设 项目流程

1. 为什么“让 AI 硬写”这条路越来越走不通

过去一年我接触了不少 Agent 项目,从内部工具到对外产品都有。一个很普遍的现象是:大家一开始都倾向于让大模型直接输出最终产物——直接写代码、直接生成页面、直接拼配置。刚开始 demo 阶段确实惊艳,但只要进入真实业务,问题就集中爆发了。最典型的就是生成结果不可控:同样的输入,今天生成一个能跑的版本,明天生成一个字段名对不上的版本,后天干脆把整个结构改得面目全非。你没法 review,也没法 diff,更没法做版本回滚。

这就是标题里说的“别再让 AI 硬写”的核心痛点。所谓“硬写”,指的是把大模型当成一个黑盒文本生成器,让它一次性吐出最终可执行的东西。这种做法在Agent 可视化生成场景下尤其危险,因为可视化产物往往涉及结构、样式、交互、数据绑定多个维度,任何一个维度漂移都会导致整体不可用。

那替代方案是什么?就是这篇文章要展开的可视化生成方案:让 Agent 不直接产出最终产物,而是产出一个中间表示(IR),再由一个稳定的渲染层把 IR 转成最终结果。这个思路借鉴了编译器领域的经典做法——前端负责把源码转成 IR,后端负责把 IR 转成目标平台代码。Agent 在这里扮演的是“前端”,它只需要保证 IR 的正确性,而不需要关心最终渲染成什么样。

这套方案涉及几个关键词:Agent、可视化生成、IR、SDK、DSL。简单说,DSL 是面向人的描述语言,IR 是面向机器的中间结构,SDK 是给业务方接入的能力包,Agent 负责把自然语言意图翻译成 DSL 或 IR。四者配合,才能让可视化生成从“抽卡”变成“工程”。

适合谁来读这篇?如果你正在做 Agent 相关的产品、正在被生成结果不稳定折磨、或者想给自己的工具链加一层可视化生成能力,那这篇内容应该能帮你少走一些弯路。下面我会从整体设计、核心细节、实操落地、问题排查几个层面,把这套方案拆开讲清楚。

2. 整体设计思路:Agent 只负责“翻译”,不负责“施工”

2.1 从“端到端生成”到“分层生成”的转变

传统做法是端到端:用户说一句话,Agent 直接输出 HTML 或某个平台的配置文件。这种模式的问题在于,Agent 的每一次输出都是一次“全量创作”,它没有稳定的中间锚点。你今天让它生成一个表单,它可能用 div 拼,明天可能用 table 拼,后天可能用某个组件库的语法。你没法约束它,因为约束的粒度太粗了。

分层生成的核心思想是把“意图理解”和“产物构造”解耦。Agent 只做意图理解,输出一个结构化的 IR;渲染层只做产物构造,把 IR 转成目标格式。这样一来,Agent 的输出空间被大幅压缩,它不需要考虑“这个按钮该用什么标签”,只需要考虑“这里有一个按钮,文案是什么,点击后触发什么动作”。输出空间越小,稳定性越高。

我实测下来,同样一个“生成一个带搜索的列表页”的需求,端到端模式下 Agent 的输出有十几种写法,而 IR 模式下基本收敛到两三种结构。这个收敛带来的直接好处就是可测试——你可以对 IR 写断言,可以对比两次生成的差异,可以做回归测试。端到端模式下这些几乎做不了。

2.2 IR 到底长什么样:一个可落地的结构设计

IR 的设计是整套方案的地基。设计得太粗,Agent 还是要做很多决策,稳定性上不去;设计得太细,Agent 的学习成本高,容易输出非法结构。我的经验是,IR 应该覆盖结构、属性、事件、数据四个维度,但每个维度只保留必要的字段。

结构维度用树形节点表示,每个节点有 type、id、children。type 是节点类型,比如 container、text、input、button。id 用于后续引用和事件绑定。属性维度用键值对表示,比如 text 节点的 content、input 节点的 placeholder。事件维度用 action 列表表示,每个 action 有 trigger 和 handler。数据维度用 binding 表示,说明这个节点的数据来自哪里。

这里有个关键决策:IR 要不要包含样式信息。我的建议是只包含语义化的样式标记,比如 size: large、variant: primary,而不是具体的 px 值或颜色码。原因很简单,具体样式应该由渲染层根据设计系统决定,Agent 不应该关心“主按钮是 #1890ff 还是 #1677ff”。这样既保证了视觉一致性,又降低了 Agent 的输出复杂度。

2.3 DSL 与 IR 的分工:一个给人看,一个给机器看

很多人会把 DSL 和 IR 混为一谈,其实它们服务对象不同。DSL 是面向人的描述语言,它的价值在于可读、可写、可版本管理。比如你可以用 DSL 描述一个页面,然后把这个 DSL 文件提交到 Git,做 code review。IR 是面向机器的中间结构,它的价值在于结构稳定、易于程序化处理。

在实际方案里,DSL 和 IR 可以是同一套东西的不同表示。比如 DSL 用 YAML 写,IR 用 JSON 表示,两者之间做一层转换。也可以 DSL 就是 IR 的文本化形式,Agent 直接输出 DSL,渲染层解析 DSL 得到 IR。我倾向于后者,因为少一层转换就少一个出错点。

但要注意,DSL 的设计要克制。我见过一些方案把 DSL 设计得极其灵活,支持条件、循环、函数调用,结果 Agent 根本学不会,输出全是语法错误。DSL 应该只覆盖 80% 的常见场景,剩下 20% 用扩展字段或自定义节点兜底。宁可 DSL 简单一点,也不要让它变成一门编程语言。

2.4 SDK 的定位:让业务方不用关心 IR 细节

SDK 是这套方案对外的接口。业务方不需要知道 IR 长什么样,只需要调用 SDK 提供的方法,比如 render(dsl)、validate(dsl)、export(dsl, target)。SDK 内部负责 DSL 解析、IR 构建、渲染执行、错误处理。

SDK 的另一个重要作用是能力封装。比如你的渲染层支持 Web、小程序、桌面端多个目标,SDK 可以根据 target 参数自动选择对应的渲染器。业务方只需要传一个 target,不需要关心底层差异。这种设计在跨端场景下特别有用,也是这套方案相比端到端生成的一大优势——端到端模式下,你让 Agent 生成 Web 和生成小程序,得写两套 prompt,维护成本翻倍。

3. 核心细节解析:Agent 如何稳定产出 IR

3.1 用 Schema 约束 Agent 的输出空间

Agent 输出不稳定的根本原因是输出空间太大。解决办法是用 Schema 把输出空间框死。具体做法是:为 IR 定义一个 JSON Schema,然后在 Agent 的调用里强制它按 Schema 输出。现在主流的大模型 API 都支持结构化输出,你可以直接把 Schema 传进去,模型会保证输出符合 Schema。

Schema 的设计有几个要点。第一,必填字段要少,只保留 type 和 id 这种核心字段,其他都设为可选,避免 Agent 因为缺字段而报错。第二,枚举值要明确,比如 type 只能是预定义的几种,不能让 Agent 自由发挥。第三,嵌套层级要浅,最好不超过三层,太深了 Agent 容易搞错层级关系。

我踩过的一个坑是:一开始 Schema 设计得太严格,要求每个节点都必须有 style 字段,结果 Agent 为了满足 Schema,给所有节点都塞了一个空的 style,反而增加了噪音。后来改成 style 可选,Agent 只在需要的时候才输出,IR 干净了很多。

3.2 Few-shot 示例比长篇 prompt 更有效

约束 Agent 输出的另一个手段是给示例。我的经验是,三个精心设计的 few-shot 示例,效果胜过一千字的 prompt 说明。示例要覆盖典型场景:一个简单节点、一个带嵌套的结构、一个带事件绑定的交互。每个示例都要完整展示输入和输出,让 Agent 模仿。

示例的选择有讲究。不要选太复杂的例子,否则 Agent 会过度模仿复杂度;也不要选太简单的,否则 Agent 学不到结构。最好是选那种“稍微想一想就能理解”的例子。另外,示例的顺序也有影响,把最典型的例子放在最后,因为模型对靠近输出的内容记忆更深。

这里有个实操技巧:把示例做成可配置的。不同业务场景可能需要不同的示例,比如表单场景和图表场景的 IR 结构差异很大。你可以把示例存在配置里,根据场景动态注入。这样一套 Agent 逻辑可以复用到多个业务线,不用为每个场景重写 prompt。

3.3 校验与修复:给 Agent 的输出加一道保险

即使有 Schema 约束,Agent 偶尔还是会输出非法结构。这时候需要一层校验和修复。校验分两步:先做 Schema 校验,检查结构是否合法;再做语义校验,检查引用是否有效、事件是否绑定到了存在的节点、数据绑定是否指向了合法的数据源。

校验不通过怎么办?有两种策略。一种是直接报错,让上层决定是否重试。另一种是自动修复,比如把非法的 type 替换成默认值,把悬空的引用删掉。我的建议是两者结合:结构性错误直接报错,因为这说明 Agent 理解出了大问题;细节性错误自动修复,比如缺失的可选字段补默认值。

自动修复要谨慎,不能修得太多,否则会把 Agent 的错误掩盖掉,导致问题积累到渲染阶段才爆发。我一般会记录所有修复动作,在日志里输出,方便排查。如果某个修复动作频繁出现,说明 prompt 或 Schema 需要调整。

3.4 版本管理:DSL 文件也要进 Git

DSL 作为文本格式,天然适合版本管理。我强烈建议把 DSL 文件纳入 Git 管理,每次 Agent 生成后自动提交,附带生成时的输入和上下文。这样做有几个好处:可以追溯每次生成的变化,可以对比不同 prompt 版本的效果,可以在出问题时快速回滚。

版本管理还有一个隐藏价值:积累训练数据。你每次生成的 DSL 和对应的自然语言输入,都是一条训练样本。积累到一定量之后,你可以用这些数据做微调,进一步提升 Agent 的稳定性。这比单纯调 prompt 的天花板高得多。

不过要注意,DSL 文件不要存得太碎。我见过有的方案每次生成都存一个新文件,结果仓库里几千个文件,根本没法管理。建议按业务实体聚合,比如一个页面一个文件,每次生成覆盖更新,Git 历史里保留变化记录。

4. 实操落地:从零搭一套可视化生成链路

4.1 环境准备与依赖选型

先说一下我这套方案的技术选型,供参考。Agent 侧用主流的大模型 API,支持结构化输出的那种。DSL 解析用现成的 YAML 或 JSON 解析库,不建议自己写 parser。IR 校验用 JSON Schema 校验库,比如 ajv。渲染层根据你的目标平台选,Web 端可以用 React 或 Vue,小程序端用原生或跨端框架。

依赖选型的原则是能用现成的就不自己造。DSL 解析、Schema 校验、模板渲染这些都有成熟库,自己写不仅费时,还容易出 bug。把精力放在 IR 设计和 Agent 调优上,这才是这套方案的核心竞争力。

环境准备阶段还要考虑一件事:渲染层的设计系统。如果你的产品有统一的设计规范,渲染层应该直接对接这套规范,IR 里的语义化样式标记映射到设计系统的 token。这样生成出来的产物天然符合视觉规范,不需要额外调整。

4.2 定义你的第一版 DSL 与 IR

假设我们要做一个简单的表单生成场景。先定义 DSL,用 YAML 写:

type: form id: user-form children: - type: input id: username props: label: 用户名 placeholder: 请输入用户名 binding: user.name - type: input id: password props: label: 密码 inputType: password binding: user.password - type: button id: submit props: text: 提交 variant: primary events: - trigger: click handler: submitForm

对应的 IR 就是这份 YAML 解析后的对象。可以看到,DSL 里没有具体的样式值,只有 variant: primary 这种语义标记。渲染层拿到 IR 后,根据 variant 查设计系统的 token,决定实际的颜色和尺寸。

定义 DSL 的时候要注意命名的稳定性。type 的命名一旦确定就不要改,因为 Agent 的 prompt 和示例都依赖这些命名。如果确实要改,做好版本兼容,老版本 DSL 要能继续解析。

4.3 编写 Agent 的 System Prompt 与示例

System Prompt 的核心是告诉 Agent 它的角色和输出格式。我的写法大概是这样:

你是一个界面描述生成器。用户会用自然语言描述他们想要的界面,你需要输出符合以下 Schema 的 DSL。 Schema: [这里放 JSON Schema] 规则: 1. 只输出 DSL,不要输出任何解释 2. 每个节点必须有 type 和 id 3. id 必须唯一,用英文小写加连字符 4. 样式用语义化标记,不要写具体数值 5. 事件绑定只引用已定义的 handler 名称 示例: [这里放 2-3 个 few-shot 示例]

注意规则要短,不要写成长篇大论。Agent 对短规则的遵循度明显高于长规则。另外,规则之间不要有冲突,否则 Agent 会随机选一条遵循。

4.4 渲染层的实现要点

渲染层的核心是一个递归函数:拿到 IR 节点,根据 type 找到对应的渲染器,渲染出目标平台的组件,然后递归渲染 children。事件绑定在渲染时挂载,数据绑定在渲染时求值。

渲染层要处理几个边界情况。第一,未知 type:如果 IR 里出现了渲染层不认识的 type,要有兜底策略,比如渲染成一个占位符并打日志,而不是直接崩溃。第二,循环引用:虽然 Schema 里可以禁止,但渲染层还是要做防护,避免死循环。第三,数据绑定失败:绑定的数据源不存在时,要有默认值,不能报错。

渲染层的性能也值得关注。如果 IR 很大,递归渲染可能成为瓶颈。可以考虑做一层缓存,把渲染结果缓存起来,IR 没变就不重新渲染。另外,渲染层最好支持增量更新,IR 局部变化时只重渲染变化的部分。

4.5 端到端跑通一个完整案例

我们来跑一个完整案例。用户输入:“帮我生成一个登录表单,包含用户名、密码和提交按钮,提交后调用登录接口。”

Agent 收到输入后,结合 System Prompt 和示例,输出 DSL。假设输出如下:

type: form id: login-form children: - type: input id: username props: label: 用户名 placeholder: 请输入用户名 binding: login.username - type: input id: password props: label: 密码 inputType: password binding: login.password - type: button id: submit-btn props: text: 登录 variant: primary events: - trigger: click handler: handleLogin

SDK 拿到 DSL 后,先解析成 IR,然后校验。校验通过后,渲染层根据 target 参数选择渲染器,生成最终产物。如果是 Web 端,就渲染成 React 组件;如果是小程序端,就渲染成对应的组件。

整个过程里,Agent 只做了一件事:把自然语言翻译成 DSL。它不需要知道 React 怎么写,也不需要知道小程序的组件规范。这就是分层生成的价值。

5. 常见问题与排查技巧实录

5.1 Agent 输出格式错误的排查思路

格式错误是最常见的问题,表现是 Agent 输出的 DSL 解析失败。排查分三步。第一步,看原始输出,确认是 JSON 语法错误还是 Schema 不匹配。JSON 语法错误通常是 Agent 多输出了 markdown 代码块标记,或者少写了括号。Schema 不匹配通常是字段名写错或类型不对。

第二步,检查 prompt。如果格式错误频繁出现,说明 prompt 里的 Schema 说明不够清晰,或者示例不够典型。可以尝试把 Schema 简化,或者增加一个“错误示例”告诉 Agent 什么是不允许的。

第三步,检查模型。不同模型对结构化输出的支持程度不同。如果某个模型频繁出错,换一个支持结构化输出更好的模型试试。我实测下来,支持原生结构化输出的模型,格式错误率能降低一个数量级。

5.2 生成结果不符合预期的定位方法

有时候 DSL 格式没问题,但生成的内容不符合预期。比如用户要一个搜索框,Agent 生成了一个普通输入框。这种问题的定位方法是:把 Agent 的输出和用户的输入对齐看。如果 Agent 的输出里完全没有搜索相关的语义,说明是意图理解出了问题,需要调整 prompt 或增加示例。如果 Agent 的输出里有搜索语义但渲染出来不对,说明是渲染层的问题。

意图理解问题还有一个常见原因是用户输入太模糊。比如用户说“做个列表”,Agent 不知道是普通列表还是带搜索的列表。这时候可以在 Agent 前面加一层澄清逻辑,让 Agent 先反问用户,确认需求后再生成。这比让 Agent 猜要靠谱得多。

5.3 渲染层报错的常见原因

渲染层报错通常有几类。第一类是未知 type,前面说过,要有兜底。第二类是属性缺失,比如 button 节点没有 text 属性,渲染出来是空按钮。这类问题可以在校验阶段拦截,要求必填属性必须存在。第三类是事件 handler 未定义,渲染时找不到对应的处理函数。这类问题需要渲染层和业务层约定好 handler 的注册机制。

我踩过的一个坑是:渲染层对属性类型做了强校验,比如 size 必须是枚举值,结果 Agent 输出了一个不在枚举里的值,直接报错。后来改成宽松校验,非法值降级到默认值并打警告日志,稳定性好了很多。渲染层要健壮,不能因为一个字段的问题导致整个页面渲染失败。

5.4 性能问题的优化方向

性能问题主要出现在两个环节:Agent 调用和渲染。Agent 调用慢通常是模型本身的问题,可以通过缓存、批处理、异步化来优化。比如相同的输入直接返回缓存结果,多个生成请求合并成一批。

渲染慢通常是 IR 太大或渲染逻辑太重。优化方向包括:IR 分片渲染,先渲染首屏再渲染剩余部分;渲染结果缓存,IR 不变就不重新渲染;渲染逻辑简化,避免在渲染函数里做复杂计算。

还有一个容易被忽略的点是DSL 文件的大小。如果 DSL 文件很大,解析和传输都会慢。可以通过拆分 DSL,把大页面拆成多个小模块,按需加载。这也能提升可维护性。

5.5 常见问题速查表

问题现象可能原因排查方向解决建议
DSL 解析失败输出含 markdown 标记检查原始输出在 prompt 里明确禁止代码块
Schema 校验不通过字段名或类型错误对比 Schema 定义简化 Schema,增加示例
生成内容不符合预期意图理解偏差对齐输入输出增加澄清逻辑或补充示例
渲染报未知 typeIR 含未定义节点检查 type 枚举渲染层加兜底,prompt 里限定 type
事件不触发handler 未注册检查注册机制统一 handler 注册规范
渲染性能差IR 过大或逻辑重分析渲染耗时分片渲染,加缓存
Agent 调用慢模型响应慢检查调用链路加缓存,批处理,异步化

6. 一些实操心得与后续扩展方向

6.1 我踩过的几个坑

第一个坑是过早追求 DSL 的完备性。一开始我想把 DSL 设计得支持所有场景,结果 DSL 越来越复杂,Agent 越来越学不会。后来砍掉了一半的特性,只保留核心场景,稳定性反而上来了。DSL 要克制,先覆盖 80% 场景,剩下的用扩展机制兜底。

第二个坑是忽视渲染层的健壮性。早期渲染层遇到非法 IR 直接抛异常,导致整个页面白屏。后来改成宽松模式,非法节点降级渲染,页面至少能用。渲染层是最后一道防线,一定要健壮。

第三个坑是没有做版本管理。早期 DSL 文件直接覆盖,出了问题没法回滚。后来接入 Git,每次生成自动提交,问题排查效率提升了很多。版本管理不是可选项,是必选项。

6.2 后续可以扩展的方向

这套方案跑通之后,有几个方向可以继续扩展。一是多目标渲染,同一份 IR 渲染到 Web、小程序、桌面端多个平台,这是分层生成相比端到端生成的最大优势。二是可视化编辑,把 IR 暴露给用户,让用户可以在生成结果上做微调,微调后的 IR 再回写。三是模板沉淀,把高频生成的 IR 沉淀成模板,下次直接复用,减少 Agent 调用。

还有一个方向是Agent 的自我修正。当校验失败时,把错误信息回传给 Agent,让它重新生成。这相当于给 Agent 加了一个反馈回路,能显著提升一次通过率。不过要注意控制重试次数,避免无限循环。

6.3 给正在做类似方案的同行几句实在话

如果你正在做 Agent 可视化生成,我的建议是:先把 IR 设计好,再考虑 Agent。很多人一上来就调 prompt,结果 IR 设计得不合理,怎么调都不稳定。IR 是地基,地基不稳,上面怎么盖都晃。

另外,不要追求一步到位。先跑通一个最小场景,比如只支持表单生成,验证整套链路。跑通之后再逐步扩展节点类型和场景。我见过太多方案一开始就铺得很大,结果每个场景都不稳定,最后不了了之。

最后,把校验和修复做扎实。Agent 不可能 100% 正确,校验和修复是保证系统可用的关键。宁可多花时间在这上面,也不要指望 Agent 永远不出错。这套方案的价值不在于 Agent 多聪明,而在于整个系统多稳定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询