1. 为什么“让 AI 硬写”正在成为过去式
过去一年我接触了不少做 Agent 项目的团队,从两三个人的小作坊到几十人的中台组都有,一个特别普遍的现象是:大家一开始都倾向于让大模型直接生成最终产物。你要一个表单,就让模型吐一段 JSON;你要一个流程,就让模型写一段代码;你要一个页面,就让模型拼一段 HTML。刚开始 demo 跑得挺爽,一旦进入真实业务,问题就全冒出来了。
最典型的三个坑:第一,输出不稳定。同一个 prompt,今天生成的 JSON 字段叫userName,明天变成user_name,后天干脆少一个字段。第二,无法校验。模型生成的东西语法上可能没错,但语义上完全不符合业务约束,你只能靠人肉 review。第三,改不动。业务方说“把这个按钮挪到右边”,你没法在生成结果上做增量修改,只能重新生成一遍,前面调好的东西全丢了。
这就是“Agent 可视化生成方案”要解决的核心问题。它的思路其实很朴素:别让 AI 直接写最终产物,让 AI 写中间表示(IR),再由确定性的渲染器/编译器把 IR 变成最终产物。这个中间表示可以是 DSL,可以是结构化的 JSON Schema,也可以是一套 SDK 调用序列。AI 只负责它擅长的部分——理解意图、做决策、填参数;而结构、约束、渲染这些确定性的事情,交给代码去做。
我个人的判断是,这套思路在未来一两年会成为 Agent 生成类项目的主流架构。原因很简单:大模型的能力边界是概率性的,而工程系统需要的是确定性。可视化生成方案本质上是在概率和确定之间架了一座桥。这篇文章我会把这套方案的来龙去脉、核心设计、实操细节和踩坑经验完整拆一遍,适合正在做 Agent 项目、低代码平台、AI 生成工具的同学参考,也适合刚接触 Agent 开发想少走弯路的朋友。
2. 可视化生成方案的整体设计与思路拆解
2.1 从“端到端生成”到“IR 中转”的范式转变
先把这个范式转变讲清楚。传统的端到端生成,链路是用户意图 -> LLM -> 最终产物。可视化生成方案的链路是用户意图 -> LLM -> IR -> 渲染器/编译器 -> 最终产物。多了一层,但这一层是整个方案的灵魂。
为什么多这一层就稳了?打个比方。你让一个装修师傅直接给你砌一面墙,他可能砌歪,可能砖缝不齐。但如果你让他先画一张施工图(IR),图纸上有明确的尺寸、材料、位置,然后让施工队按图施工(渲染器),那结果的可控性就完全不一样了。LLM 就是那个设计师,它擅长的是“这面墙应该在哪、多高、什么风格”,而不是“每一块砖怎么摆”。把后者交给确定性的渲染器,才是正确的分工。
IR 这层带来的直接好处有三个。可校验:IR 是有 schema 的,字段类型、取值范围、必填项都能定义,生成完立刻能跑校验。可修改:用户说“把标题改大一点”,你只需要改 IR 里的一个字段,不用重新生成整个产物。可复用:同一份 IR 可以渲染成 Web、移动端、甚至导出成图片,一套逻辑多端复用。
2.2 IR、DSL、SDK 三者的定位与取舍
热词里同时出现了 IR、DSL、SDK,很多人会搞混。我用一句话区分:IR 是数据,DSL 是语法,SDK 是接口。
IR(Intermediate Representation)是中间表示,本质是一棵结构化的数据树,通常用 JSON 或类似结构描述。它回答的是“这个产物由哪些元素组成、元素之间什么关系”。DSL(Domain Specific Language)是领域特定语言,是 IR 的一种文本化表达形式,比如你设计一套button { text: "提交", action: submit }这样的语法。SDK 则是给开发者或 Agent 调用的编程接口,Agent 通过调用 SDK 的方法来构建 IR。
那到底该选哪个?我的经验是这样:
| 方案 | 适合场景 | 优势 | 劣势 |
|---|---|---|---|
| 纯 JSON IR | 结构简单、字段固定的场景 | 实现快、易校验 | 表达力有限、嵌套深了难读 |
| 自定义 DSL | 需要人写、需要版本管理的场景 | 可读性好、diff 友好 | 要写 parser、维护成本高 |
| SDK 调用 | Agent 生成、程序化构建 | 类型安全、IDE 友好 | 需要设计 API、跨语言麻烦 |
实际项目里我通常推荐以 JSON IR 为核心,DSL 作为可选的人机交互层,SDK 作为 Agent 的调用入口。三者不是互斥的,而是同一套 IR 的不同“视图”。Agent 通过 SDK 构建 IR,人可以通过 DSL 编辑 IR,最终都落到同一份 JSON 上,由同一个渲染器处理。这样既保证了 Agent 生成的稳定性,又给了人介入修改的空间。
2.3 为什么这套方案特别适合 Agent 场景
Agent 和普通程序最大的区别是:Agent 的决策是动态的、多步的、带不确定性的。如果让 Agent 直接生成最终产物,每一步的不确定性都会累积到结果上,误差会放大。而引入 IR 之后,Agent 的每一步只需要产出一个局部正确的 IR 片段,最后再组装。
举个例子。一个生成报表页面的 Agent,如果直接写 HTML,它要同时考虑布局、样式、数据绑定、交互逻辑,任何一处出错整个页面就废了。但如果它先产出 IR:{ type: "table", columns: [...], dataSource: "..." },那它只需要保证这个片段结构对,渲染器负责把它变成正确的 HTML。Agent 的认知负担大大降低,出错概率也随之下降。
这也是为什么我一直在强调:Agent 可视化生成的核心不是“让 AI 更聪明”,而是“让 AI 只做它该做的事”。把确定性的部分从 AI 手里拿走,交给代码,这才是工程化的思路。
3. 核心细节解析与实操要点
3.1 IR Schema 设计:约束要刚刚好
IR 的 schema 设计是整个方案的地基,设计得好后面一路顺,设计得差后面全是补丁。我踩过的最大一个坑是:schema 设计得太自由。一开始我让 IR 支持任意嵌套、任意字段,结果 Agent 生成出来的结构千奇百怪,渲染器根本处理不过来。
后来我总结出一个原则:schema 的约束要“刚刚好”——足够表达业务,又足够限制 Agent。具体怎么做?三个要点。
第一,用枚举代替自由字符串。比如按钮类型,不要用type: string,要用type: "primary" | "secondary" | "danger"。这样 Agent 只能在有限选项里选,不会瞎编。
第二,限制嵌套深度。我一般把 IR 的嵌套控制在 3 到 4 层以内。太深了 Agent 容易迷路,渲染器也难写。如果业务确实需要深层结构,用引用(ref)来扁平化。
第三,必填项要明确。每个节点哪些字段必须有,哪些可选,schema 里写死。Agent 生成后立刻用 JSON Schema 校验,不合格的直接打回重生成,不要让它带着错误往下走。
{ "type": "object", "required": ["type", "id"], "properties": { "type": { "enum": ["container", "text", "button", "table"] }, "id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, "props": { "type": "object" }, "children": { "type": "array", "maxItems": 20 } } }上面这段 schema 就是我在多个项目里验证过的“最小可用 IR”模板。type用枚举锁死,id用正则约束命名,children限制数量防止 Agent 无限展开。看着简单,但能挡掉 80% 的脏数据。
3.2 Agent 侧:如何让模型稳定产出合规 IR
Schema 定好了,接下来是让 Agent 稳定地产出符合 schema 的 IR。这里有几个实操要点,都是血泪教训。
第一,用 function calling 或 structured output,别用纯文本解析。早期我用 prompt 让模型输出 JSON,然后正则提取,结果模型经常在 JSON 前后加解释文字,或者用 markdown 代码块包起来,解析起来极其痛苦。现在主流模型都支持结构化输出,直接约束输出格式,省心太多。
第二,给 few-shot 示例,但别给太多。示例能显著提升生成质量,但给多了会占满上下文,还会让模型过度模仿示例。我的经验是给 2 到 3 个覆盖不同场景的示例就够了,重点是覆盖边界情况,比如空列表、可选字段缺失。
第三,把校验结果反馈给 Agent。Agent 生成 IR 后,先跑校验,如果失败,把具体的错误信息(哪个字段、什么错误)作为下一轮输入喂回去,让它修正。这个“生成-校验-修正”的循环通常 1 到 2 轮就能收敛。我实测下来,加了校验反馈之后,一次通过率从 60% 左右提升到了 90% 以上。
注意:校验反馈的 prompt 要具体,不要只说“格式错误”,要说“字段
children[2].type的值buton不在允许的枚举中,请从button、text、container中选择”。错误信息越具体,Agent 修正得越快。
3.3 渲染器:确定性是唯一要求
渲染器是 IR 到最终产物的翻译层,它的核心要求只有一个:确定性。同样的 IR 输入,必须产出同样的结果,不能有任何随机性。
渲染器的实现方式取决于你的目标产物。如果目标是 Web 页面,渲染器就是一个把 IR 转成 React/Vue 组件的函数;如果目标是图片,渲染器就是一套绘制逻辑;如果目标是配置文件,渲染器就是一个序列化器。
我建议渲染器用注册表模式实现:每种 IR 节点类型注册一个处理函数,渲染时根据type字段查表调用。这样新增节点类型只需要注册新函数,不用改核心逻辑。
const renderers = { container: (node, ctx) => `<div>${node.children.map(c => render(c, ctx)).join('')}</div>`, text: (node, ctx) => `<span>${escape(node.props.content)}</span>`, button: (node, ctx) => `<button>{ "type": "dashboard", "title": "销售数据看板", "children": [ { "type": "chart", "id": "sales_trend", "props": { "chartType": "line", "dataSource": "sales_daily", "xField": "date", "yField": "amount" } }, { "type": "table", "id": "top_products", "props": { "dataSource": "product_rank", "columns": ["name", "sales", "growth"] } } ] }第二步,写 Agent 的 system prompt。核心是告诉模型:你的任务是产出符合 schema 的 IR,不要输出任何解释文字,遇到不确定的字段用默认值。
第三步,接结构化输出。以 OpenAI 的接口为例,用response_format约束成 JSON,并把 schema 传进去。这样模型输出的就是纯 JSON,不用解析。
第四步,写校验函数。用 JSON Schema 校验库(比如 ajv)跑一遍,失败就把错误喂回给 Agent 重试。
第五步,写渲染器。把 IR 转成实际的 HTML 或组件。图表部分可以接 ECharts 或 Chart.js,表格部分自己写个简单的渲染函数。
第六步,加一层缓存。同样的用户意图,IR 大概率是一样的,缓存起来能省不少 token。我用的是意图文本的 hash 作为 key,命中率在重复场景下能到 40% 以上。
4.2 参数计算:IR 节点数量与 token 消耗的关系
这里分享一个我实测出来的经验公式,帮你预估成本。假设每个 IR 节点的平均 token 消耗是T_node(包含 type、id、props 等字段),节点数量是N,那么一次生成的输出 token 大约是N * T_node。我实测下来,一个结构简单的节点(比如 text)大约 30 到 50 token,一个复杂节点(比如带数据绑定的 chart)大约 100 到 150 token。
如果你的看板有 10 个节点,平均 80 token,那输出就是 800 token 左右。加上 system prompt 和 few-shot 示例(通常 500 到 1000 token),一次生成的总消耗在 1500 到 2000 token。这个量级用主流模型成本很低,但如果你的场景节点数上百,就要考虑分批生成或者用更小的模型了。
提示:节点数超过 50 的时候,建议把 IR 拆成多个子 IR 分别生成,最后合并。一次性生成太多节点,模型容易在后半段“偷懒”,字段质量明显下降。
4.3 实操现场:一次完整的生成与修正过程
我拿一个真实案例走一遍。用户输入:“给我做一个展示最近一周订单量的折线图,标题叫订单趋势”。
第一轮生成,Agent 输出:
{ "type": "dashboard", "title": "订单趋势", "children": [ { "type": "chart", "id": "order_trend", "props": { "chartType": "line", "dataSource": "orders_weekly", "xField": "date", "yField": "count" } } ] }校验:通过。schema 里chartType是枚举,line合法;dataSource是字符串,合法。
渲染:渲染器把chart节点转成 ECharts 配置,dataSource映射到实际的数据接口,页面正常显示。
用户反馈:“把折线改成柱状图”。这时候不需要重新生成整个 IR,只需要改一个字段:chartType从line改成bar。改完重新渲染,秒级响应。这就是 IR 方案相比端到端生成的最大优势——增量修改。
如果用户说“再加一个表格展示 Top 10 商品”,那就在children里追加一个table节点,同样不用动已有的部分。整个过程 Agent 只需要处理“追加”这个局部操作,认知负担极小。
5. 常见问题与排查技巧实录
5.1 Agent 生成的 IR 总是缺字段怎么办
这是最高频的问题。原因通常有两个:一是 schema 里没把字段标成 required,模型觉得可省就省了;二是 prompt 里没强调“所有必填字段必须出现”。
解决办法:在 schema 里明确 required,并且在 system prompt 里加一句“输出必须包含所有 required 字段,缺失字段会导致校验失败”。另外,可以在 few-shot 示例里故意展示一个“完整字段”的例子,让模型有样学样。我加了这两条之后,缺字段的问题基本消失了。
5.2 渲染出来的结果和预期不符怎么排查
渲染问题排查有个固定套路:先看 IR,再看渲染器,最后看数据。
第一步,把 Agent 生成的 IR 打印出来,人肉看一眼结构对不对。很多时候问题出在 IR 本身就错了,比如dataSource写错了名字。
第二步,如果 IR 没问题,用一份手写的、确定正确的 IR 喂给渲染器,看渲染结果对不对。如果手写 IR 渲染正常,说明渲染器没问题,问题在 Agent 生成的 IR 上。
第三步,如果 IR 和渲染器都没问题,那就是数据源的问题。检查数据接口返回的字段名和 IR 里声明的xField、yField是否一致。
这个三步排查法能覆盖 95% 的渲染问题,比盲目 debug 高效得多。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方式 |
|---|---|---|---|
| IR 校验失败 | 字段缺失或类型错误 | 看校验错误信息 | 反馈给 Agent 重生成 |
| 渲染报 unknown type | IR 里有未注册的节点类型 | 检查 type 字段 | 注册新渲染函数或修正 IR |
| 页面空白 | 数据源为空或字段不匹配 | 看网络请求和数据 | 修正 dataSource 或字段映射 |
| 生成结果不稳定 | prompt 约束不够 | 对比多次生成结果 | 加 few-shot、用结构化输出 |
| token 消耗过高 | 节点数太多或示例太长 | 统计输入输出 token | 分批生成、精简示例 |
5.4 几个我踩过的坑
坑一:schema 太宽松导致 Agent 乱发挥。早期我用了additionalProperties: true,结果 Agent 加了一堆自定义字段,渲染器全不认识。后来改成additionalProperties: false,世界清净了。
坑二:渲染器静默失败。有一次渲染器遇到未知节点类型直接返回空字符串,导致页面缺了一块但没报错,排查了半天。后来改成遇到未知类型直接抛错,问题立刻暴露。
坑三:IR 缓存没考虑用户权限。缓存 key 只用了意图文本,结果 A 用户生成的看板被 B 用户命中了,数据泄露。后来缓存 key 加上了用户 ID 和数据权限范围,才解决。
注意:如果你的 IR 里包含数据源引用,缓存一定要带上权限维度,否则很容易出安全问题。这个坑我在两个项目里都见过,代价都不小。
6. 方案扩展与个人经验
6.1 从单端渲染到多端复用
IR 方案最大的想象空间在于多端复用。同一份 IR,你可以写多个渲染器:一个渲染成 Web 页面,一个渲染成移动端原生组件,一个渲染成 PDF 报告,一个渲染成图片。Agent 只需要生成一次 IR,多端自动适配。
我在一个项目里就做过这事:同一份看板 IR,Web 端用 React 渲染,导出报告用服务端渲染成 PDF,分享到社交平台用 canvas 渲染成图片。三个渲染器加起来不到 800 行代码,但省掉了三套独立的生成逻辑。这个投入产出比非常高。
6.2 让 IR 支持组合与继承
当你的 IR 节点类型越来越多,会发现很多节点有共同的属性。这时候可以引入“组件”概念:把常用的节点组合封装成一个可复用的组件,IR 里用ref引用它。这样 Agent 生成时可以用更少的节点表达更复杂的结构,token 消耗也降下来了。
比如一个“带标题的图表卡片”,可以封装成一个组件,Agent 只需要写{ type: "ref", name: "titled_chart", props: {...} },渲染器展开成完整的节点树。这个思路和前端组件化是一模一样的,本质都是抽象和复用。
6.3 我个人在实际操作中的体会
做了这么多 Agent 生成项目,我最大的体会是:不要试图让 AI 做所有事,也不要试图让代码做所有事。AI 擅长理解模糊意图、做开放决策;代码擅长执行确定性逻辑、保证一致性。可视化生成方案的本质,就是在这两者之间划一条清晰的边界。
这条边界划在哪里,取决于你的业务。约束越强、结构越固定的场景,边界越往 AI 那边推,让 AI 多做决策;约束越弱、越需要灵活性的场景,边界越往代码那边推,让 AI 只做最核心的判断。找到这条边界,比堆砌任何 prompt 技巧都重要。
最后分享一个小技巧:在 IR 里加一个meta字段,记录生成来源、时间戳、Agent 版本。这个字段渲染器不处理,但排查问题时特别有用。你能一眼看出这个 IR 是哪次生成的、用的哪个版本的 prompt,回溯起来方便很多。这个习惯我坚持了两年,救过我好几次。