1. 为什么“硬写”这条路越来越走不通了过去一年我经手过六七个 Agent 相关的项目从内部工具到面向用户的产品都有。一个很明显的感受是让大模型直接输出最终产物在 Demo 阶段惊艳在工程阶段崩溃。你让它生成一段前端代码它给你一段看起来对、跑起来报错的字符串你让它画一张流程图它给你一段描述得天花乱坠但结构完全错乱的文本。问题不在于模型不够聪明而在于我们让它同时承担了“理解意图”和“精确构造”两件事而后者恰恰是语言模型最不擅长的。这就是Agent 可视化生成方案要解决的核心矛盾。所谓可视化生成不是简单地让 AI 画个图而是把 Agent 的输出从“自由文本”转变为“结构化中间表示IR”再由确定性的渲染层把 IR 转成最终的图形、界面或代码。AI 负责语义理解和决策IR 负责精确表达SDK 负责稳定渲染——三层各司其职这才是当前 Agent 可视化生成的主流趋势。这篇文章适合三类人看正在做 Agent 产品、被“AI 输出不稳定”折磨的开发者想了解可视化生成架构选型的技术负责人以及刚接触 Agent 开发、想知道行业里大家到底怎么落地的新手。我会把架构思路、IR 设计、SDK 选型、实操步骤和踩坑经验全部摊开讲尽量做到你看完就能对照自己的项目动手改。2. 可视化生成的整体架构与核心思路拆解2.1 从“端到端生成”到“三段式管线”的范式转变早期做 Agent 可视化大家的直觉做法是用户说一句话Agent 直接返回一段 SVG 或者一段 HTML前端拿到就渲染。这个方案在 2023 年很流行因为实现简单一个 prompt 就能跑通。但实际用下来问题一大堆生成的 SVG 经常有语法错误、坐标越界、层级混乱生成的 HTML 里塞满了内联样式改都没法改更致命的是同样的输入两次输出结构可能完全不同前端根本没法做稳定的交互绑定。于是行业里逐渐收敛到一个三段式管线第一段语义解析层。Agent 接收自然语言输出的是结构化的 IRIntermediate Representation中间表示通常是 JSON 或类 JSON 的树形结构。这一层只关心“要表达什么”不关心“怎么画”。第二段IR 校验与优化层。对 Agent 输出的 IR 做 schema 校验、补全默认值、做布局约束求解。这一层是确定性的代码不是模型。第三段渲染层SDK。把校验后的 IR 交给渲染 SDK输出最终的图形、界面或代码。这一层完全确定性同样的 IR 永远得到同样的结果。这个转变的本质是把“生成”这个动作拆成了“决策”和“构造”。模型只做它擅长的决策构造交给代码。我实测下来同样的可视化任务三段式管线的首次成功率能从端到端的 40% 左右提升到 85% 以上而且失败的时候错误是可定位的——你能明确知道是 IR 结构错了还是渲染参数错了。2.2 IR 设计整个方案的地基IR 设计得好不好直接决定这套方案能不能长期维护。我见过太多项目 IR 设计得太随意结果 Agent 输出的字段五花八门渲染层要写一堆兼容逻辑最后又退化成“硬写”。一个好的可视化 IR 应该满足几个条件。第一schema 要严格且可校验。用 JSON Schema 或者 Zod 这类工具定义清楚每个节点的类型、必填字段、取值范围。Agent 输出后先过一遍校验不合格的直接打回重试而不是让脏数据流到渲染层。第二IR 要面向语义而非面向像素。比如你要画一个流程图IR 里应该是{type: node, label: 开始, next: node_2}这种语义描述而不是{x: 100, y: 200, width: 80}这种像素坐标。布局交给渲染层的布局引擎去算Agent 不该操心坐标。第三IR 要可组合、可嵌套。复杂可视化往往是简单元素的组合IR 支持嵌套后Agent 可以用递归的方式生成复杂结构。我一般会建议 IR 分两层一层是逻辑层 IR描述元素之间的语义关系一层是布局层 IR描述最终的位置和样式。Agent 只生成逻辑层布局层由确定性的布局算法从逻辑层推导出来。这样 Agent 的负担最轻输出最稳定。2.3 为什么 SDK 选型比模型选型更关键很多人做 Agent 可视化把 90% 的精力花在调 prompt 和选模型上SDK 随便挑一个能用就行。这是个巨大的误区。模型决定的是“上限”SDK 决定的是“下限”。一个成熟的渲染 SDK 能帮你处理掉大量脏活布局计算、样式继承、事件绑定、动画过渡、导出格式转换。选 SDK 的时候我会重点看几个维度。渲染能力是只支持 2D 图形还是支持 3D、支持富文本、支持自定义组件布局引擎有没有内置的自动布局算法力导向、层次布局、网格布局扩展性能不能注册自定义节点类型和渲染器导出能力能不能导出 SVG、PNG、PDF甚至导出成可运行的代码生态与文档社区活跃度、示例丰富度、TypeScript 类型定义是否完整。举个具体的例子如果你做的是流程图、架构图这类场景像基于 Canvas 或 SVG 的图形库配合自动布局引擎就是很稳的组合如果你做的是数据看板那可能需要一个支持图表 DSL 的渲染层如果你做的是 UI 界面生成那 IR 到组件树的映射就是核心SDK 要能对接你现有的组件库。选错了 SDK后面 IR 设计得再好也白搭。3. 核心细节解析与实操要点3.1 Agent 输出 IR 的 Prompt 工程要点让 Agent 稳定输出符合 schema 的 IRprompt 设计有几个关键点。第一把 schema 直接写进 system prompt并且用 few-shot 示例展示“输入自然语言 → 输出 IR”的完整映射。示例要覆盖边界情况比如空输入、超复杂输入、含歧义的输入。第二明确告诉模型不要输出解释性文字只要 IR。很多模型习惯性地加一句“好的这是你要的结构”这行文字会直接破坏 JSON 解析。第三用结构化输出能力。现在主流模型 API 大多支持 JSON mode 或者 function calling强制模型按 schema 输出比纯 prompt 约束可靠得多。我自己的做法是system prompt 里放 schema 定义和 3 到 5 个 few-shot 示例user prompt 里放实际需求然后用 JSON mode 强制输出。如果模型返回的 IR 校验失败把校验错误信息作为新一轮输入喂回去让它修正一般重试一到两次就能过。这个重试机制很重要别指望一次就完美。3.2 IR 校验与容错别让脏数据流到渲染层IR 校验层是整个管线的守门员。我一般会做三层校验。第一层是结构校验字段类型对不对、必填项有没有、枚举值是否合法。这层用 JSON Schema 就能搞定。第二层是语义校验引用的节点 ID 是否存在、有没有循环引用、数值是否在合理范围。这层要写自定义校验逻辑。第三层是业务校验比如流程图里有没有孤立节点、看板里数据源字段是否匹配。这层跟具体业务强相关。校验失败后的处理策略也很关键。我的经验是能自动修复的就自动修复不能修复的才打回给 Agent。比如缺了可选字段直接补默认值节点 ID 重复自动重命名引用了不存在的节点删掉这条边。只有结构性错误才值得让 Agent 重试因为重试是有成本的——多一次模型调用多一份延迟。把容错逻辑写扎实能省下大量 token 和时间。3.3 布局引擎把坐标计算从 Agent 手里拿走这是可视化生成里最容易被低估的一环。很多人让 Agent 直接输出坐标结果就是元素重叠、越界、间距不均。正确的做法是Agent 只输出元素和关系布局引擎负责算坐标。常见的布局算法有几类。层次布局适合流程图、组织架构图按依赖关系分层同层元素水平排列。力导向布局适合关系网络图节点之间根据连接强度自动散开。网格布局适合看板、卡片列表规整排列。树形布局适合思维导图、目录结构。选哪种取决于你的可视化类型很多 SDK 内置了这些算法直接调用就行。如果 SDK 没内置也可以自己实现。层次布局的核心是拓扑排序加分层力导向布局的核心是模拟物理弹簧系统迭代收敛。听起来复杂但开源实现很多拿来改改就能用。关键是布局要和 IR 解耦IR 变了布局自动重算而不是把坐标写死在 IR 里。3.4 渲染 SDK 的接入与自定义扩展接入 SDK 的时候我建议先做一个最小闭环定义最简单的 IR比如只有矩形和连线跑通“Agent 生成 → 校验 → 渲染”全流程再逐步加复杂度。这样能快速验证架构是否可行也能尽早发现 SDK 的坑。自定义扩展是绕不开的。标准 SDK 提供的节点类型往往不够用你需要注册自己的节点渲染器。比如你要画一个带图标的卡片节点就要继承 SDK 的基础节点类重写渲染方法。这里有个经验自定义节点尽量保持无状态所有状态从 IR 里读不要在渲染器里存状态。这样 IR 一变重新渲染就是幂等的不会出现状态残留的 bug。另外事件绑定也要提前设计。可视化产物往往需要交互——点击节点弹详情、拖拽调整位置、连线编辑。这些交互产生的事件要能反向映射回 IR形成双向绑定。如果一开始没设计好后面加交互会非常痛苦。4. 完整实操流程与关键环节实现4.1 环境准备与依赖选型假设我们要做一个流程图生成 Agent技术栈我推荐这样组合后端用 Node.js 或 Python 都行模型调用走主流大模型 APIIR 校验用 ZodTypeScript或 PydanticPython渲染层用一个支持自动布局的图形库。前端如果是 WebCanvas 或 SVG 方案都可以如果要导出高质量矢量图SVG 更合适。依赖装好之后先定义 IR schema。我以一个简化的流程图 IR 为例// ir-schema.ts import { z } from zod; const NodeSchema z.object({ id: z.string(), type: z.enum([start, process, decision, end]), label: z.string(), next: z.array(z.string()).default([]), }); const EdgeSchema z.object({ from: z.string(), to: z.string(), label: z.string().optional(), }); export const FlowIRSchema z.object({ version: z.literal(1.0), nodes: z.array(NodeSchema), edges: z.array(EdgeSchema).default([]), meta: z.object({ title: z.string().optional(), direction: z.enum([TB, LR]).default(TB), }).default({}), }); export type FlowIR z.infertypeof FlowIRSchema;这个 schema 里direction控制布局方向next和edges描述连接关系节点只有语义类型没有坐标。Agent 生成的就是这个结构。4.2 Agent 调用与 IR 生成接下来写 Agent 调用逻辑。核心是把 schema 转成模型能理解的描述加上 few-shot 示例强制 JSON 输出。// agent.ts const SYSTEM_PROMPT 你是一个流程图生成助手。根据用户描述输出符合以下 schema 的 JSON { version: 1.0, nodes: [{id: n1, type: start|process|decision|end, label: 文本, next: [n2]}], edges: [{from: n1, to: n2, label: 可选}], meta: {title: 标题, direction: TB|LR} } 规则 1. 只输出 JSON不要任何解释文字。 2. 每个节点 id 唯一next 里引用的 id 必须存在。 3. 决策节点至少有两个分支。 4. 必须有且只有一个 start 节点。 示例输入用户登录流程 示例输出{version:1.0,nodes:[{id:n1,type:start,label:开始,next:[n2]},{id:n2,type:process,label:输入账号密码,next:[n3]},{id:n3,type:decision,label:验证通过?,next:[n4,n5]},{id:n4,type:process,label:登录成功,next:[n6]},{id:n5,type:process,label:提示错误,next:[n2]},{id:n6,type:end,label:结束,next:[]}],edges:[],meta:{title:用户登录,direction:TB}} ; async function generateIR(userInput: string): PromiseFlowIR { const response await callLLM({ system: SYSTEM_PROMPT, user: userInput, responseFormat: json, }); const raw JSON.parse(response); return FlowIRSchema.parse(raw); }这里有个细节edges我留了空数组因为连接关系已经通过next表达了渲染层可以从next自动推导出边。这样 Agent 的负担更轻少写一个字段就少一个出错点。当然如果你的场景需要边上有额外信息比如条件标签那就让 Agent 显式输出 edges。4.3 IR 校验与自动修复拿到 IR 后先校验校验失败尝试修复。修复逻辑我一般写成一个独立的函数// validate.ts function repairIR(ir: any): { ir: FlowIR; repaired: boolean; errors: string[] } { const errors: string[] []; let repaired false; // 收集所有节点 id const ids new Set((ir.nodes || []).map((n: any) n.id)); // 修复 next 中不存在的引用 for (const node of ir.nodes || []) { if (Array.isArray(node.next)) { const valid node.next.filter((id: string) ids.has(id)); if (valid.length ! node.next.length) { errors.push(节点 ${node.id} 的 next 包含无效引用已清理); node.next valid; repaired true; } } } // 确保有 start 节点 const hasStart (ir.nodes || []).some((n: any) n.type start); if (!hasStart ir.nodes?.length) { ir.nodes[0].type start; errors.push(缺少 start 节点已将第一个节点设为 start); repaired true; } const result FlowIRSchema.safeParse(ir); if (!result.success) { return { ir: ir as FlowIR, repaired, errors: [...errors, ...result.error.issues.map(i i.message)] }; } return { ir: result.data, repaired, errors }; }这个修复函数能处理掉大部分常见问题。实测下来Agent 首次输出的 IR 大概有 20% 到 30% 需要修复但其中 90% 以上都能自动修复真正需要打回重试的不到 5%。4.4 布局计算与渲染布局这一步我用一个层次布局算法把逻辑 IR 转成带坐标的布局 IR。核心思路是拓扑排序分层然后同层水平均分。// layout.ts interface LayoutNode extends FlowIR[nodes][number] { x: number; y: number; width: number; height: number; } function computeLayout(ir: FlowIR): LayoutNode[] { const nodeMap new Map(ir.nodes.map(n [n.id, n])); const inDegree new Mapstring, number(); ir.nodes.forEach(n inDegree.set(n.id, 0)); ir.nodes.forEach(n n.next.forEach(id { if (inDegree.has(id)) inDegree.set(id, inDegree.get(id)! 1); })); // 拓扑分层 const layers: string[][] []; const visited new Setstring(); let current ir.nodes.filter(n inDegree.get(n.id) 0).map(n n.id); while (current.length) { layers.push(current); current.forEach(id visited.add(id)); const next: string[] []; for (const id of current) { const node nodeMap.get(id)!; for (const nid of node.next) { if (!visited.has(nid) !next.includes(nid)) { const remaining nodeMap.get(nid)!.next.filter(x !visited.has(x)); if (remaining.length 0 || remaining.every(x visited.has(x) || next.includes(x))) { next.push(nid); } } } } current next; } // 计算坐标 const NODE_W 160, NODE_H 60, GAP_X 40, GAP_Y 80; const result: LayoutNode[] []; layers.forEach((layer, li) { const totalW layer.length * NODE_W (layer.length - 1) * GAP_X; const startX -totalW / 2; layer.forEach((id, ni) { const node nodeMap.get(id)!; result.push({ ...node, x: startX ni * (NODE_W GAP_X), y: li * (NODE_H GAP_Y), width: NODE_W, height: NODE_H, }); }); }); return result; }拿到带坐标的节点后渲染就简单了。用 SVG 或 Canvas 把节点和连线画出来即可。渲染层完全确定性同样的 IR 永远得到同样的图。4.5 导出与二次编辑可视化产物往往需要导出。SVG 导出最直接因为渲染层本来就是 SVG 的话序列化一下就行。PNG 导出需要先把 SVG 转成 Canvas 再转图片。如果要做二次编辑就要把渲染层的事件反向映射回 IR——拖拽节点更新坐标编辑标签更新 label删除节点更新 nodes 和 edges。这部分工作量不小建议一开始就把 IR 作为唯一数据源渲染层只是 IR 的投影这样双向绑定会自然很多。5. 常见问题与排查技巧实录5.1 Agent 输出格式不稳定的排查思路这是最高频的问题。表现是模型有时候输出纯 JSON有时候包在 markdown 代码块里有时候前面加一句“好的”。排查顺序是这样的先确认有没有开 JSON mode没开的话优先开开了还不行检查 system prompt 里有没有明确禁止解释文字再不行在解析前加一层清洗逻辑用正则把 JSON 部分抠出来。我常用的清洗函数function extractJSON(text: string): string { // 去掉 markdown 代码块包裹 const codeBlockMatch text.match(/(?:json)?\s*([\s\S]*?)/); if (codeBlockMatch) return codeBlockMatch[1].trim(); // 找到第一个 { 和最后一个 } const start text.indexOf({); const end text.lastIndexOf(}); if (start ! -1 end ! -1) return text.slice(start, end 1); return text; }这层清洗能救回大部分格式问题。但根治还是要靠 JSON mode 加严格 prompt。5.2 复杂图布局错乱的解决技巧节点一多布局就容易乱。常见表现是连线交叉严重、节点重叠、层级混乱。解决思路分几步。先检查拓扑排序有没有处理环——流程图里如果有循环比如“重试”回到上一步拓扑排序会失败需要先做环检测把环上的边标记出来特殊处理。再检查同层节点数是不是太多太多的话考虑拆成多列或者换布局方向。最后检查节点尺寸是不是差异太大尺寸差异大时均分间距会导致视觉不均需要按实际尺寸动态计算间距。如果层次布局实在搞不定可以退而求其次用力导向布局虽然不够规整但至少不会重叠。或者引入专业的图布局库它们对复杂场景的处理成熟得多。5.3 渲染性能问题的优化方向节点数超过几百个之后渲染性能会明显下降。优化方向有几个。虚拟化渲染只渲染视口内的节点视口外的用占位符。分层渲染静态元素和动态元素分开渲染静态的缓存成图片。减少重绘IR 变化时只重绘受影响的部分而不是全量重绘。Web Worker把布局计算放到 Worker 里避免阻塞主线程。我实测下来虚拟化渲染对性能提升最明显几百个节点的图从卡顿变成流畅。但实现复杂度也最高建议节点数真的上去了再做。5.4 常见问题速查表问题现象可能原因排查方向解决手段模型输出非 JSON未开 JSON mode / prompt 不严检查 API 参数和 system prompt开 JSON mode加清洗逻辑IR 校验失败schema 太严 / 模型理解偏差看具体校验错误信息放宽可选字段加 few-shot节点重叠布局算法未处理尺寸差异检查布局参数动态计算间距换布局算法连线交叉严重层次划分不合理检查拓扑排序结果调整分层策略换布局方向渲染卡顿节点数过多看渲染帧率虚拟化渲染Worker 计算交互无响应事件未绑定 / IR 未双向同步检查事件监听建立 IR 与视图的双向绑定导出图片模糊分辨率不足检查导出参数提高 DPI用矢量导出5.5 几个我踩过的坑第一个坑是过早优化 IR。一开始就把 IR 设计得很复杂支持各种花哨特性结果 Agent 根本生成不对。后来我改成从最小 IR 开始跑通再加字段反而顺利得多。第二个坑是把布局逻辑写进 Agent。让模型输出坐标结果每次都不一样前端没法做交互。后来把坐标计算完全交给布局引擎问题消失。第三个坑是忽略重试成本。一开始校验失败就无脑重试token 消耗飞快。后来加了自动修复层重试率降了一个数量级。第四个坑是SDK 选型只看功能不看类型定义。选了个没有 TypeScript 类型的库接入时各种 any维护起来痛苦不堪。后来选型必看类型定义完整度。6. 方案扩展与个人经验这套三段式管线跑通之后扩展性其实很好。IR 换一套 schema就能从流程图扩展到组织架构图、思维导图、数据看板。渲染 SDK 换一个就能从 Web 扩展到移动端甚至桌面端。Agent 那层甚至可以接多个模型做协作——一个负责理解需求一个负责生成 IR一个负责校验修正这就是所谓的多 AI 协作在可视化场景的落地方式。我在实际项目里还做过一个有意思的扩展把用户对生成结果的修改操作也记录成 IR 的 diff这样下次生成类似内容时可以把历史 diff 作为 few-shot 示例喂给 Agent让它学习用户的偏好。实测下来用了几轮之后Agent 生成的 IR 越来越贴近用户习惯返工率明显下降。最后分享一个小技巧如果你的可视化场景对实时性要求高可以把 IR 生成和渲染做成流式的——Agent 一边生成 IR 片段渲染层一边增量渲染用户能实时看到图在“长出来”。这个体验比等全部生成完再一次性渲染好得多实现上也不复杂把 IR 的 nodes 数组做成可追加的渲染层监听追加事件即可。这套方案不是什么高深技术核心就是一句话别让 AI 干它不擅长的精确构造把脏活累活交给确定性的代码。想清楚这个剩下的都是工程问题。