ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI Agent技能开发:用Python实现hand-placed SVG-diagram

AI Agent技能开发:用Python实现hand-placed SVG-diagram 在 AI Agent 应用里让模型画一张架构图、流程图或依赖图一直是既实用又容易失控的需求。SVG-diagram 是一类新出现的 agent skill它把“用 SVG 画图”封装成一个可复用的技能并且要求模型以手工放置坐标hand-placed SVG的方式生成图表。所谓 hand-placed是指每个节点、每段文字、每条连线都由模型显式给出画布坐标而不是依赖图形库自动布局。这样做的核心价值是输出可预测、可继续编辑也方便人工审查。这篇文章会从 agent skill 的概念讲起拆解 SVG-diagram 的技能设计思路然后给出一个最小可运行的 Python 实现包含 skill 声明、JSON 布局格式、SVG 渲染代码、验证方式和常见问题排查。1. 先搞清楚 agent skill 是什么以及它和 agent 本身的分工很多人在检索 SVG-diagram 时会一并搜索agent skill、skill和agent的区别、ai agent skill这说明真正卡住的地方往往不是 SVG 语法而是 Agent 工程里的能力边界。如果没把 skill 和 agent 的关系想清楚后面写出来的技能大概率会变成一段堆满提示词的脚本。1.1 一个可复用的能力包而不是一个会思考的进程通俗地说agent 是“做决策的人”skill 是“某个岗位的标准作业手法”。agent 负责理解用户目标、拆解任务、选择工具、判断结果skill 负责回答一个问题当 agent 决定要画图时应该用什么样的步骤、输入、输出和边界条件把图正确画出来。SVG-diagram 作为一个 skill并不是一个独立运行的进程也不负责“理解用户想要什么图”。它更像是一份带执行逻辑的说明书告诉大模型 SVG 画布的坐标系是什么、节点和边怎么描述、连线端点如何计算、输出必须是什么结构。agent 只需要说“当前任务需要画架构图调用 SVG-diagram”剩下的绘图细节由 skill 定义的规则和代码落地。1.2 agent 负责决策skill 负责把某一类任务做对在 AI Agent 应用里agent 和 skill 的分工可以这样理解agent 负责编排分析用户问题决定调用哪个 skill把多个 skill 的结果串联起来判断是否继续重试。skill 负责执行针对某一类具体任务提供固定的输入结构、处理逻辑、输出格式和质量约束。workflow 负责流程把多个 skill、模型调用、人工确认、外部 API 按固定顺序串起来。plugin 负责接入外部系统比如读取数据库、发送邮件、调用支付接口。SVG-diagram 属于 skill因为它不依赖某个外部系统也不强制规定调用流程。它解决的是“如何让大模型稳定产出可渲染的 SVG”这一类横向能力。1.3 为什么 SVG-diagram 适合被设计成一个 skill画图能力如果直接写在 agent 的主提示词里会出现三个问题提示词越写越长agent 每次对话都要携带大量绘图规则浪费上下文窗口也容易让模型忽略其他任务。绘图规则没有代码兜底模型可能输出残缺 SVG或者编造不存在的元素属性。不同项目的画图需求不同把规则封装成独立 skill 后可以按需加载也可以在不同项目之间复用。SVG-diagram 作为 skill 的定位很清晰接收一段自然语言或结构化描述输出一个坐标清晰的 SVG。关键是它的输出不是“让模型自由发挥”而是“让模型按坐标系统逐元素摆放”。这本质上是在大模型的不确定性外面加了一层强约束。2. SVG-diagram 的思路让模型手放坐标而不是依赖自动布局SVG-diagram 最核心的设计选择是“hand-placed SVG”。这一点需要展开讲清楚因为它决定了技能的输入输出格式也决定了后续代码怎么写。2.1 自动布局看起来很省事但在 agent 输出场景并不可控常见的图表生成方式有两种。第一种是把描述交给图形库自动布局例如 Mermaid、Graphviz、D3 的 force layout。模型只需要写“A 依赖 BB 依赖 C”库会自动计算节点位置和连线路径。自动布局的优势是无脑、方便适合给人类阅读。但在 agent 输出场景里它有几个隐藏问题自动布局的结果不稳定。相同输入在不同版本库、不同渲染器里可能产生不同位置agent 无法精确控制。模型无法基于坐标继续编辑。比如用户说“把第三个节点往下移一点”模型生成的描述里根本没有坐标概念自然无法做局部调整。自动布局生成的 SVG 往往带有大量私有属性和嵌套结构既难审查也难做自定义标注。第二种是手放坐标。模型在生成 SVG 时必须为每个节点显式写出 x、y、width、height每条边都必须引用具体节点 id。这种方式要求模型多算几步但换来的结果是位置完全确定、输出结构清晰、后续可以按坐标做增量修改。2.2 手放坐标的坐标系统和元素模型在实际使用中SVG-diagram 会把画布抽象成三层元素第一层是画布声明宽度和高度。常见坐标系统以左上角为原点x 轴向右y 轴向下。第二层是节点每个节点有一个唯一 id、一组坐标、宽高和标签。第三层是边只引用源节点 id 和目标节点 id渲染时根据节点边界计算连线端点。这种元素模型非常接近 SVG 本身的构造方式又比直接写 SVG 更安全。模型不直接拼 SVG 标签而是输出 JSON 布局数据再由代码负责渲染。这样即使模型输出有误也容易做校验和修复。2.3 为什么不直接用 Mermaid、Graphviz 或 HTML CanvasMermaid 和 Graphviz 适合快速生成“能看的图”但它们很难满足精细控制。HTML Canvas 又需要维护画布状态而且输出是位图级别的操作不利于嵌入网页后继续选中、放大、标注。SVG 的优势在于它是文本描述、可嵌入 HTML、支持 CSS 样式和事件绑定并且每个元素都有独立坐标。SVG-diagram 选择手放坐标本质上是把“布局算法应该怎么排”这个问题从图形库手里拿回来交给模型在受限坐标系里完成。对于结构清晰的中小规模图表例如系统架构图、模块依赖图、项目路线图这种方式的可控性明显更好。当然手放坐标不适合超大规模图。如果要画几百个节点的知识图谱应该用自动布局或图数据库可视化工具。SVG-diagram 的适用场景是 20 到 50 个元素以内的结构图这个范围内模型算得过来人也容易审查。3. 最小可运行实现从 JSON 布局到 SVG 渲染现在进入实操。下面实现一个最小版本的 SVG-diagram skill。先不接大模型先完成从 JSON 布局数据到 SVG 渲染的完整链路。跑通之后再接入模型调用层。3.1 项目结构与环境准备建议使用 Python 3.10 以上版本。最小实现只依赖 Python 标准库不引入第三方依赖方便在任意开发环境快速复现。项目结构如下svg-diagram-skill/ ├── skill.yaml ├── agent_skill.py ├── examples/ │ └── sample_layout.json └── output/ └── diagram.svg如果后续要接入大模型再额外安装openai或其他模型 SDK。当前阶段先关注渲染链路。3.2 用 skill.yaml 声明技能skill.yaml 用于描述技能的名称、用途、参数和输入输出结构。不同 agent 框架对 skill 的承载格式不同这里给出一个通用示例name: svg-diagram description: - Create architecture diagrams, flowcharts, and dependency diagrams as hand-placed SVG. Always return structured JSON coordinates in the declared canvas coordinate system. version: 0.1.0 parameters: canvas_width: 1200 canvas_height: 800 default_node_width: 160 default_node_height: 80 input_schema: type: object properties: query: type: string description: natural language description of the diagram output_schema: type: string description: SVG content这段声明里有几个关键点description要让模型明白自己的任务边界是“生成 hand-placed SVG”不是直接写一段 SVG 字符串。parameters给模型提供画布默认值避免每次都在提示词里重复。input_schema说明这个 skill 接收自然语言描述。output_schema说明最终输出是 SVG 内容。实际项目里skill.yaml 会被 agent 框架读取并注入到系统提示词中。因此描述写得好不好直接决定模型输出质量。3.3 布局数据格式为了让模型输出可解析建议统一使用 JSON 描述布局。下面是一个示例{ canvas: { width: 1200, height: 800 }, nodes: [ { id: client, label: 客户端, x: 50, y: 300, w: 160, h: 80, fill: #eef4ff, stroke: #4f7cac }, { id: api, label: API 网关, x: 330, y: 300, w: 160, h: 80, fill: #eef4ff, stroke: #4f7cac }, { id: service, label: 业务服务, x: 610, y: 300, w: 160, h: 80, fill: #eef4ff, stroke: #4f7cac } ], edges: [ { from: client, to: api, label: HTTPS }, { from: api, to: service, label: RPC } ] }这个格式本身就是在给模型“圈定能力范围”模型不需要懂 SVG 标签只需要会计算坐标、起 id、描述边关系。剩下的渲染工作由代码完成。3.4 核心渲染代码创建agent_skill.py核心逻辑分四步创建 SVG 根节点、添加箭头定义、渲染边、渲染节点。import json import sys import xml.etree.ElementTree as ET from pathlib import Path ET.register_namespace(, http://www.w3.org/2000/svg) def route_edge(src: dict, dst: dict): 根据源节点和目标节点的边界计算折线路径。 sx src[x] src[w] sy src[y] src[h] / 2 tx dst[x] ty dst[y] dst[h] / 2 if tx sx: mx (sx tx) / 2 return [(sx, sy), (mx, sy), (mx, ty), (tx, ty)] return [(src[x], sy), (dst[x] dst[w], ty)] def add_arrow_defs(svg: ET.Element): defs ET.SubElement(svg, defs) marker ET.SubElement(defs, marker, { id: arrowhead, markerWidth: 10, markerHeight: 7, refX: 10, refY: 3.5, orient: auto }) ET.SubElement(marker, polygon, { points: 0 0, 10 3.5, 0 7, fill: #666666 }) def render_edge(svg: ET.Element, nodes: dict, edge: dict): src nodes[edge[from]] dst nodes[edge[to]] points route_edge(src, dst) points_text .join(f{x:.1f},{y:.1f} for x, y in points) ET.SubElement(svg, polyline, { points: points_text, fill: none, stroke: edge.get(stroke, #666666), stroke-width: 1.5, marker-end: url(#arrowhead) }) if edge.get(label): mx 0.0 my 0.0 for x, y in points: mx x my y mx mx / len(points) my my / len(points) label ET.SubElement(svg, text, { x: f{mx:.1f}, y: f{my - 6:.1f}, text-anchor: middle, font-size: 12, font-family: sans-serif, fill: #555555 }) label.text edge[label] def render_node(svg: ET.Element, node: dict): x node[x] y node[y] w node[w] h node[h] ET.SubElement(svg, rect, { x: str(x), y: str(y), width: str(w), height: str(h), rx: 6, ry: 6, fill: node.get(fill, #eef4ff), stroke: node.get(stroke, #4f7cac), stroke-width: 1.5 }) text ET.SubElement(svg, text, { x: str(x w / 2), y: str(y h / 2 5), text-anchor: middle, font-size: node.get(fontSize, 14), font-family: sans-serif, fill: #222222 }) text.text node.get(label, node[id]) def create_svg(data: dict) - str: canvas data.get(canvas, {width: 1200, height: 800}) width canvas.get(width, 1200) height canvas.get(height, 800) svg ET.Element(svg, { width: str(width), height: str(height), viewBox: f0 0 {width} {height} }) add_arrow_defs(svg) nodes {} for node in data.get(nodes, []): nodes[node[id]] node for edge in data.get(edges, []): if edge[from] in nodes and edge[to] in nodes: render_edge(svg, nodes, edge) for node in data.get(nodes, []): render_node(svg, node) xml_bytes ET.tostring(svg, encodingutf-8) xml_text ?xml version1.0 encodingUTF-8?\n xml_bytes.decode(utf-8) return xml_text def main(): input_path sys.argv[1] if len(sys.argv) 1 else examples/sample_layout.json output_path sys.argv[2] if len(sys.argv) 2 else output/diagram.svg input_file Path(input_path) if not input_file.exists(): print(flayout file not found: {input_file}) sys.exit(1) data json.loads(input_file.read_text(encodingutf-8)) svg create_svg(data) out_file Path(output_path) out_file.parent.mkdir(parentsTrue, exist_okTrue) out_file.write_text(svg, encodingutf-8) print(fSVG written to {out_file}) if __name__ __main__: main()代码里有几个需要留意的点route_edge只做基础折线路由。当目标节点在源节点右侧时生成一个带水平拐角的折线否则直接拉一条斜线。生产环境可以根据节点相对位置选择不同出口方向。边的中点标签取了所有折线点的平均值严格来说不是几何中点但足够满足大多数场景。要更精确可以按线段长度做插值。使用xml.etree.ElementTree生成 SVG文本内容会被自动转义避免常见的、等字符破坏 XML 结构。3.5 接入语言模型的骨架渲染链路跑通后再接入大模型就只需要做一件事把模型返回的自然语言描述转换成上面的 JSON 布局结构。def generate_layout_from_model(client, query: str, skill_context: str) - dict: messages [ { role: system, content: ( You are an SVG diagram planner. Return a JSON object with canvas, nodes, and edges. Use the coordinate rules from the skill context. Do not return SVG code directly.\n\n skill_context ) }, { role: user, content: query } ] response client.chat.completions.create( modelyour-model-name, messagesmessages, response_format{type: json_object} ) content response.choices[0].message.content return json.loads(content)这段代码是骨架不绑定具体模型厂商。client可以是任何兼容 OpenAI 风格的客户端。如果模型服务不支持response_format可以通过提示词强制要求输出 JSON再用下面的方法做校验。4. 关键参数和输出规范决定图表质量手放坐标这种方式最大的风险是模型算错坐标。为了把坐标错误率降下来必须把参数含义和约束条件说清楚。4.1 画布、节点和边的参数说明在 skill 的提示词或 schema 中建议明确以下参数参数类型含义常见值调大/调小影响canvas.widthint画布宽度1200越大可放置的节点越多但单屏观感变小canvas.heightint画布高度800同上node.xint节点左上角横坐标0 - width决定水平位置调大向右移动node.yint节点左上角纵坐标0 - height决定垂直位置调大向下移动node.wint节点宽度160调大容纳更长文字但减少可用空间node.hint节点高度80调大容纳多行文本但增加纵向占用edge.labelstring连线上的文字说明空字符串问题排查时可快速了解连线含义edge.pathstring连线类型line后续可扩展 curve、orthogonal 等坐标参数是最容易出错的地方。模型在计算坐标时必须同时考虑节点自身宽高和其他节点的位置。因此 skill 的提示词里要给出一段类似这样的说明Canvas is 1200x800. Node coordinates are the top-left corner. Node width default is 160, height default is 80. Keep at least 30px horizontal gap and 20px vertical gap between nodes. Keep all nodes inside the canvas.这段规则会直接影响模型输出的可用率。宁可把规则写死一点也不要让模型自由发挥。4.2 坐标规则与常见约束实际项目里可以约定这些约束所有节点 id 必须唯一且只包含字母、数字、下划线。边的 from、to 必须引用已经存在的节点 id。所有节点必须在画布范围内即x 0、y 0、x w canvas.width、y h canvas.height。同一层级的关键节点建议水平对齐避免连线交叉。有向依赖图尽量让边从左向右流动减少折线回溯。这些约束看起来琐碎但它们是后续自动校验的基础。模型输出 JSON 后代码可以逐项检查发现越界或缺失直接重试。4.3 输出校验规则接大模型后输出不能直接渲染要先做一次结构化校验。核心校验点如下def validate_layout(data: dict) - list[str]: errors [] if canvas not in data: errors.append(missing canvas) canvas data.get(canvas, {}) cw canvas.get(width, 0) ch canvas.get(height, 0) nodes data.get(nodes, []) node_ids set() seen_ids set() for node in nodes: nid node.get(id) if not nid: errors.append(node has no id) continue if nid in seen_ids: errors.append(fduplicate node id: {nid}) seen_ids.add(nid) x node.get(x, -1) y node.get(y, -1) w node.get(w, 0) h node.get(h, 0) if x 0 or y 0 or x w cw or y h ch: errors.append(fnode {nid} out of canvas) node_ids.add(nid) for edge in data.get(edges, []): if edge.get(from) not in node_ids: errors.append(fedge from {edge.get(from)} not found) if edge.get(to) not in node_ids: errors.append(fedge to {edge.get(to)} not found) return errors这段校验函数是 skill 在生产环境必须有的兜底。模型返回的数据一旦校验失败agent 可以把错误信息重新喂给模型让模型自己修正坐标。5. 运行验证文件、浏览器、命令行和渲染检查代码写完后不能只看“程序能跑”还要验证生成的 SVG 是否符合预期。5.1 命令行生成 SVG先用示例布局文件生成 SVGpython agent_skill.py examples/sample_layout.json output/diagram.svg正常输出SVG written to output/diagram.svg打开生成的文件应该能看到一个包含三个节点的横向链路每条边带箭头和文字标签。如果文件为空或提示找不到路径需要优先检查 examples 目录下的 JSON 是否存在以及svg根节点是否有xmlns。5.2 浏览器打开与 XML 校验用浏览器打开diagram.svg这是最直观的验证方式。另外可以用命令行工具检查 XML 是否合法xmllint --noout output/diagram.svg如果没有报错说明 XML 结构正常。如果系统没有xmllint可以安装后再试。OpenSSH 等工具不会包含这个命令通常需要单独安装。5.3 将 SVG 转成 PNG 的常用方式有些场景需要把生成的图表嵌入到文档或聊天消息中这时候要把 SVG 转成 PNG。常用的命令如下# 使用 librsvg rsvg-convert -w 1200 output/diagram.svg -o output/diagram.png # 使用 Inkscape inkscape output/diagram.svg --export-typepng --export-filenameoutput/diagram.png # 使用 Node.js 的 sharp 库 npx sharp-cli -i output/diagram.svg -o output/diagram.png转换完成后再确认图片尺寸和清晰度。SVG 是矢量图PNG 是位图转换时指定宽度可以避免默认尺寸过小。6. 常见问题与排查路径手放坐标方案的常见问题基本集中在模型输出不规矩、坐标计算错误、文本渲染异常这几类。下面按现象、原因、检查方式、解决方案来排查。6.1 模型返回结果不是合法 JSON现象模型输出了 SVG 标签、解释文字或者 JSON 后面跟着额外内容代码json.loads直接报错。原因提示词没有声明“只输出 JSON”或者模型返回时夹杂了思考过程。检查方式打印模型原始输出看第一个字符是否为{最后是否有多余文本。解决方案在系统提示词中增加“只输出 JSON不要输出代码块标记不要输出额外解释”。代理服务如果支持response_format优先开启 JSON 模式。失败时把解析错误回传给模型让它重新生成。预防建议把 skill 的 output schema 写清楚并在解析前先提取 JSON 片段例如只取第一个{到最后一个}之间的内容。6.2 节点重叠或坐标越界现象生成的 SVG 中两个节点叠在一起或者部分节点跑到画布外。原因模型没有正确累加节点宽度和间距可能只关注了起点坐标忽略了节点自身宽高。检查方式用校验函数检查节点是否越界可视化观察是否有重叠。解决方案在 skill 提示词里强制规定节点之间的最小间距并在代码校验时返回重叠或越界错误。对于横向布局可以要求模型按“固定列步长”对齐例如每列 x 坐标从 50 开始列间距为 200这样能显著减少重叠。预防建议提示词中明确写出“在计算坐标时请先为每个节点分配列号再根据列号计算 x”。有时候给一个列号方案比让模型直接算像素更稳定。6.3 连线没有对齐节点或指向错误现象边线一端在节点内部偏移箭头没有指向目标节点边界或者线穿过无关节点。原因边路由只做了简单折线当节点关系不是严格从左到右时端口计算就容易出错。检查方式检查route_edge的输入输出确认源节点和目标节点坐标是否正确。解决方案如果目标在源节点右侧使用带拐角的折线如果目标在左侧可以换用源节点左侧出口。复杂场景可以使用 Manhattan 路由绕开节点区域。预防建议在提示词中引导模型把节点按依赖顺序从左到右排列减少边回溯。对于反向边建议单独加文字标签说明而不是强行连折线。6.4 中文标签乱码或字体异常现象浏览器中中文显示为方块或者文字没有居中。原因SVG 中没有指定中文字体或者系统缺少对应字体。解决方案在text元素上设置font-familysans-serif, Microsoft YaHei, PingFang SC确保主流系统都能选到合适字体。保存文件时使用 UTF-8 编码。预防建议在 skill 输出的 JSON 规范中节点文本默认按 UTF-8 处理不要在代码里手动转码。6.5 SVG 文件无法打开或渲染为空现象双击 SVG浏览器显示空白控制台报 XML 解析错误。原因缺少?xml declaration或者根元素缺少xmlnshttp://www.w3.org/2000/svg。检查方式用文本编辑器打开 SVG查看第一行是否为 XML 声明根节点是否带命名空间。解决方案使用ET.register_namespace(, http://www.w3.org/2000/svg)并在写入文件时手动加 UTF-8 声明。预防建议把“生成合法 XML”作为 skill 输出校验的第一条规则而不是等渲染失败再排查。7. 生产环境使用 SVG-diagram 类技能的注意事项学习环境里能跑通最小案例和生产环境稳定输出高质量图表之间还隔着一段工程化距离。下面这些点是实际项目里最容易忽略的地方。7.1 skill 描述里要写清楚坐标规则和少量示例skill 描述是模型的“操作手册”。不要只写“生成 SVG”要写清楚坐标系统、默认画布、默认节点尺寸、间距要求、边方向和输出 JSON 结构。建议在描述末尾附一个三节点的极简示例。这个示例对模型理解布局格式的作用远大于长篇说明。7.2 对模型输出做结构化校验模型输出天然不稳定因此生成流程必须包含“校验-修复”循环。第一次生成后跑validate_layout如果出现越界、重复 id、边引用缺失等问题把错误列表反馈给模型让模型生成修正版本。最多重试两到三次超过后回到人工修复。7.3 SVG 安全与转义SVG 本质上是一种 XML可以被注入脚本。生产环境必须做安全处理所有文本内容通过 XML 库自动转义不直接拼接字符串。不允许模型输出script标签、onload、onclick等事件属性。如果使用第三方 SVGO 等工具预处理要开启脚本移除插件。需要嵌入网页时优先渲染到img标签或用 sanitize 库清洗后再插入。安全边界要放在 skill 描述里但更重要的是在代码层面拦截。模型提示词可以被绕过不能只依赖提示词保证安全。7.4 性能、缓存和失败重试图表生成涉及模型调用耗时通常比普通文本任务长。生产环境建议对相同输入做缓存短时间重复请求直接返回历史 SVG。设置模型调用的超时时间避免长时间无响应。并发请求要做好限流防止模型接口被打满。生成失败时记录原始 query 和错误类型方便回溯。7.5 可复用的落地检查清单以下清单可以直接用于项目上线前检查检查项验证方式通过标准skill 描述是否包含坐标规则阅读 skill.yaml有画布范围、节点尺寸、间距、边方向说明模型是否只输出 JSON运行一次 sample 请求原始返回可被 json.loads 解析节点是否都在画布内运行 validate_layout无越界节点边引用是否有效运行 validate_layout所有 from/to 都存在文本是否转义构造含的标签生成的 SVG 中显示正常SVG 是否可打开浏览器打开图表完整显示是否过滤脚本检查输出字符串不含 script、onerror、onclick是否设置字体检查 text 节点含中文字体回退列表是否限制重试次数查看调用代码最多重试 3 次后停止是否记录日志查看输出日志有 request id、错误类型、耗时7.6 后续可以扩展的方向SVG-diagram 的最小实现已经证明了“模型给坐标、代码渲染”这条路的可行性。后续可以按需扩展支持分组区域用 g 元素包裹一组节点实现模块边界。支持多行文本根据文本长度自动换行。增加 curve 边类型让连线更圆滑。加入局部自动布局例如让模型只规划分组坐标组内元素交给布局函数计算。与前端交互式编辑器结合让用户拖拽节点后回写 JSON形成“模型生成-人工微调”闭环。但每增加一个特性都要考虑是否破坏“手放坐标、输出可预测”的初衷。SVG-diagram 的真正价值不是追求把所有图表都画出来而是在需要精确控制的 agent 输出场景里给模型一套足够稳定、足够好审查的坐标生成规则。越往后扩展越要保持这层约束清晰可见。
返回列表