12|进阶②:让 AI 画架构图——Prompt 工程一线实践

这是《Agent全栈开发实战》的第 12 篇,也是「进阶应用」这一段的收尾。整个系列以 catbuddy(一个本地优先的 AI 编程助手,约 3.6 万行 TypeScript)为案例。

上一篇我们让主循环派生子代理并发干活、还让 Agent 知道「我是谁」;这一篇换个角度------挑一个看起来很「玄」的能力:你跟 catbuddy 描述一个系统,它直接给你画出一张能在 Draw.io 里打开的架构图。听起来像是模型独有的魔法?恰恰相反。这篇我想说服你:画图这件事,靠的不是模型多神,而是工具链 + Prompt 一轮一轮磨出来的工程------我会把可用率从 40% 打磨到 90% 的全过程摊开给你看。


一个反直觉的开场

先说个反直觉的结论:让 LLM 画出一张「能用」的架构图,最不重要的就是模型本身

我第一次做这功能的时候,思路特别朴素------Draw.io 的文件不就是个 XML 吗?我把一个完整的 .drawio 文件丢给模型当样例,说「照这个格式,把项目架构画出来」。结果你猜怎么着?十次里有六次是废的:要么 id="0" 这个根节点忘了写,要么连线穿过中间的方块、几条线挤在一起糊成一团,要么干脆 XML 都没闭合。

我一度以为是模型不行。直到我换了三家模型,发现失败模式一模一样------这才反应过来:不是模型笨,是我把活儿派错了。模型擅长「这块放哪、连到谁」这种创意决策,但你让它去抠 XML 嵌套层级、平衡画布坐标、给每条线算绕行路径,这是在拿它的短板硬刚。

后面这套从 40% 到 90% 的演化,核心就一句话:把机械的活儿交给代码,把创意的活儿留给模型,然后用 Prompt 把两者之间的接口对齐。下面一步步拆。


1. Draw.io XML:一个 650 行才出一张图的格式

先看清楚对手。Draw.io(现在叫 diagrams.net)底层是 mxGraph XML------一套描述图形节点、连线、布局的标记语言。一个完整文件长这样:

javascript 复制代码
<mxfile>
  <diagram name="架构图" id="diagram-1">
    <mxGraphModel dx="1422" dy="794" grid="1" page="1" ...>
      <root>
        <mxCell id="0"/>                     <!-- 根节点,必须有 -->
        <mxCell id="1" parent="0"/>          <!-- 默认图层,必须有 -->
        <mxCell id="2" value="前端" style="..." vertex="1" parent="1">  <!-- 你的第一个方块 -->
          <mxGeometry x="40" y="40" width="160" height="60" as="geometry"/>
        </mxCell>
        <mxCell id="3" style="..." edge="1" parent="1" source="2" target="4"/>  <!-- 一条连线 -->
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>

真正承载「信息」的就是那几个 <mxCell>,外面套了 <mxfile><diagram><mxGraphModel><root> 四层壳,外加两个雷打不动的根节点。一张基础三层架构图,完整 XML 能写到 650 行------其中六成是每次都长一个样的模板。

你让模型从头到尾生成这整坨,等于让它在四层嵌套里不出一个错。这不现实。


2. 「反向剔除」:让代码做 boilerplate,让模型做创意

这是整个设计里我最想安利的一个洞察。

大多数人的 Prompt 思路是正向的:把完整输出格式摆给模型,让它照抄。但面对 Draw.io 这种壳比肉多的格式,这条路是死胡同------你抄得越全,出错点越多。

catbuddy 的做法是反过来------从完整 XML 里,把模型不需要操心的部分全部剔掉

代码承担包装层。 display_diagram 工具收到模型吐出来的裸 mxCell 片段后,自动包成一个完整文件:

javascript 复制代码
function ensureMxFile(xml: string): string {
  const trimmed = xml.trim()
  if (trimmed.startsWith('<mxfile')) return trimmed
  if (trimmed.startsWith('<mxGraphModel')) {
    return `<mxfile><diagram name="架构图" id="diagram-1">${trimmed}</diagram></mxfile>`
  }
  // 最常见的分支:模型只给了裸 mxCell,代码把四层壳和根节点全补上
  return `<mxfile><diagram ...><mxGraphModel ...><root>` +
    `<mxCell id="0"/><mxCell id="1" parent="0"/>${trimmed}</root></mxGraphModel></diagram></mxfile>`
}

三个分支覆盖了模型可能的三种输出------有的模型「自作聪明」加了点壳,有的没加。不管哪种,代码都能兜住。

模型只负责裸 <mxCell> Skill 文件里这句话写得很白(关于 Skill 是什么、怎么挂进来,第 05 篇讲过,这里只用):

Generate ONLY bare <mxCell> elements --- 别带任何壳标签,系统会自动包成合法的 .drawio

配三条铁规则:ID 从 "2" 开始递增("0"/"1" 系统占了)、所有 mxCell 是平级兄弟不许嵌套、顶层元素 parent="1"你压根不用告诉模型什么是 <mxfile>,它只要关心「这块放哪、连到谁」。

这条路为什么走得通?三点:

  • 认知负担小 :模型只需理解 <mxCell> 一种元素,不用扛五层嵌套的模板;
  • 错误面小:每层壳都是潜在出错点,剔掉四层壳就消灭了四类错误;
  • token 省:生成的 XML 缩短约 40%,同样的上下文窗口能画更复杂的图。

一句话:别教模型生成完整 XML,让代码去补它不擅长的机械部分。


3. 四轮迭代:可用率 40% → 90% 是怎么磨出来的

「反向剔除」解决了「壳」的问题,但「肉」本身------布局、连线------还是模型生成。这部分纯靠 Prompt 打磨,我们迭代了四轮。先看全景:

每一档的提升,全都来自从失败案例里提炼具体规则,而不是把「请画得好看一点」这种废话说得更大声。

第 1 版:零约束,可用率 40%

最初的 Prompt 极简------「你是 Draw.io 图表专家,请分析架构生成 XML」,附一个三节点示例。结果约 40% 可用。主要翻车点:忘根节点、容器内子元素 parent 指错、连线漏了 source/target、XML 特殊字符没转义。

第 2 版:加布局约束,可用率 55%

加了空间规则:所有元素 x=0~800, y=0~600、起点 x=40,y=40、相邻间距 150~200px。结果 55%。 布局不再挤成一坨了,但连线还是灾难------多条线重叠、双向连线互相穿过。

第 3 版:连线五规则,突破到 75%

这是我们投入最多的一版。盯着上百个失败案例看,提炼出 5 条连线规则,写进 Skill:

ini 复制代码
## Edge Routing --- 5 Rules
1. 平行连线错开:同两节点间的线用 exitY=0.3 / exitY=0.7 岔开。
2. 双向连线:A→B 从右出(exitX=1),B→A 从左出(exitX=0)。
3. 每条边都显式设 exitX/exitY/entryX/entryY。
4. 用 waypoint 绕开障碍:在起点终点之间的图形旁边绕路。
5. 连在边的中点,别连角落(不要 entryX=1,entryY=1)。

第 4 条 waypoint 绕行专治「连线穿过中间节点」这个高频毛病------给模型一个带 <Array as="points"> 拐点的示例,它就知道怎么让线绕开挡路的方块。结果跳到 75% ,连线质量已经到了「基本不用手动调」的水平。

第 4 版(当前):完整 Skill + 工具链,90%

当前版把整套规范沉淀成一个可插拔的 Skill 包。关键加了四样:

  1. 分层颜色表 :前端蓝 #dae8fc、后端绿 #d5e8d4、数据橙 #ffe6cc、基础设施紫、外部红------模型不用「猜」配色,查表就行;
  2. Swimlane 完整示例:游泳道是出错率最高的元素,给整段示例,避免「子元素 parent 指错容器」;
  3. 三种连线示例:基础连线、容器连线、带 waypoint 绕行,覆盖九成场景;
  4. 工具链衔接 :明确写「下一步 Call display_diagram({ xml })」,告诉模型生成完该干嘛。

当前约 90%。 剩下 10% 的锅主要是超复杂图(20+ 节点)撞 ID 冲突或超出画布。

这里我想停下来强调一句话,它是这四轮迭代真正的方法论:告诉模型「不要做什么」,往往比「要做什么」更有效。 因为模型的默认行为,常常恰好就是你不想要的那种------它默认会把平行线画重叠、默认会让连线走直线穿过方块。你与其正向描述一个完美结果(它本来就想做但做不到),不如直接堵掉那条默认的歪路。「连在中点,别连角落」「双向线一个从左出一个从右出」------这些都是在做反向剔除:不是加规则,是在删模型的坏习惯。


4. 三个工具的「委托式」分工

Prompt 只是一半,另一半是工具链。catbuddy 给画图配了三个独立的 Agent Tool,分工干净得像流水线(工具怎么注册、怎么被调度,第 04 篇讲过,这里只看它们各自的职责):

display_diagram ------造。 适合中等复杂度(10~15 节点以内)一次成图。它入口处有个有意思的校验,不是用 XML parser,而是直接看开头字符:

php 复制代码
function looksLikeDrawioXml(xml: string): boolean {
  const trimmed = xml.trim()
  return trimmed.startsWith('<mxfile')
    || trimmed.startsWith('<mxGraphModel')
    || trimmed.startsWith('<mxCell')
}

比 XML 解析宽容,比不校验严格。 万一模型脑子一抽吐出 Mermaid 或 Markdown 表格,直接拒了并给明确指引,而不是把一坨非法 XML 写进文件。

append_diagram ------续。 大项目 30+ 节点的图,模型一次输出会被上下文窗口截断。这个工具允许分片追加,每片都先抽出完整 <mxCell> 再逐条验证:

javascript 复制代码
function validateCells(existingXml: string, cells: string[]): string | null {
  if (cells.length === 0) return 'No complete <mxCell> elements found in xml'
  const seen = new Set<string>()
  for (const cell of cells) {
    const id = cellId(cell)
    if (!id) return 'Every appended mxCell must have an id attribute'
    if (id === '0' || id === '1') return 'Do not append root cells id="0" or id="1"'
    if (seen.has(id)) return `Duplicate cell id in appended fragment: ${id}`
    if (hasCell(existingXml, id)) return `Cell already exists in diagram: ${id}`
    seen.add(id)
  }
  return null
}

ID 缺失、撞根节点、片内重复、跟已有图重复------四种情况各给一句人话错误,模型一看就知道哪错了。

edit_diagram ------改。 图出来后用户常要微调。它做的是基于 ID 的精确增删改,核心是个用 lookahead 断言的正则:

javascript 复制代码
function cellRegex(cellId: string): RegExp {
  const id = escapeRegExp(cellId)
  return new RegExp(
    `<mxCell\b(?=[^>]*\bid=["']${id}["'])[^>]*(?:\/>|>[\s\S]*?<\/mxCell>)`,
    'm',
  )
}

用 lookahead 而非捕获组,保证只命中目标 ID 那个 cell,绝不误伤别人。删除时还会级联清掉引用它的连线,不留悬空的线头。

三个工具各管一摊display 负责造、append 负责续、edit 负责改。每个工具的 description 里都带完整示例和错误指引,模型自己就能判断什么时候用哪个。


5. get_shape_library:按需加载的「图例手册」

画云架构图(AWS、K8s),你要的不只是方块和连线------你要 EC2 图标、S3 图标、Lambda 图标。Draw.io 内置 1000+ 专业图标,但每个的 style 语法都不一样。

如果把所有图标语法全塞进 System Prompt,不光烧 token,还会在模型根本用不到的时候硬塞给它,干扰判断。

catbuddy 的做法是按需加载 ------get_shape_library 工具让模型需要时主动查:

javascript 复制代码
const safe = sanitizeName(raw)
const filePath = path.join(libDir, `${safe}.md`)
const resolved = path.resolve(filePath)
// 路径遍历防护:防模型用 ../../ 读到系统文件
if (!resolved.startsWith(path.resolve(libDir))) {
  return 'Error: invalid library path.'
}
const content = fs.readFileSync(filePath, 'utf-8')
return content

图标库就是一堆纯 Markdown 文件(aws4.mdkubernetes.mdflowchart.md),每个给出服务的 shape 名和 style 语法,外加一份 100+ 条目的分类清单:Compute(ec2、lambda...)、Storage(s3、efs...)、Database(rds、dynamodb...)、Networking(vpc、api_gateway...)。设计上三个点:

  • 文件即图例 :不用改代码,加个 .md 就支持新图标库;
  • 路径遍历防护sanitizeName + startsWith 双保险,堵死 ../../
  • 友好降级:库不存在时返回的是可用库列表,而不是干巴巴一句「not found」。

6. 三层校验:不靠「解析→报错→重生成」循环

你可能以为,画错了就「生成 → 解析 XML → 失败 → 把错误喂回模型 → 重画」对吧?catbuddy 偏偏没有这个循环。取而代之的是三层渐进式校验:

第一层 Prompt 约束 是预防性的------在模型动笔前就消掉最常见的歪路:「只生成裸 mxCell」灭包装错误、「ID 从 2 递增」灭 ID 冲突、「转义 &lt; &gt;」灭转义错误。

第二层工具校验 是拦截。注意 display_diagram 那句错误消息的措辞------它不只说「格式错误」,而是把模型最容易犯的替代方案点名列出来:

javascript 复制代码
if (!looksLikeDrawioXml(rawXml)) {
  return 'Error: display_diagram.xml must be actual Draw.io XML. ' +
    'Do not pass Markdown, ASCII diagrams, Mermaid, or plain text. ' +
    'Generate valid Draw.io XML and retry.'
}

这给了模型足够上下文去「理解自己错在哪」,然后在下一次 tool call 里改对。

第三层正则校验 是核实。edit_diagram 不信任模型传来的 cell_id 一定存在,每次都做真实性检查------找不到就回「cell 不存在,建议先用 read_file.drawio 看实际有哪些 ID」。

为什么不用「解析→修正」循环?因为把解析器错误喂回模型,效率太低 。XML parser 报的是「line 47: unexpected token」,模型很难从这反推出「哦是我第 12 个 mxCell 的 parent 写错了」。三层校验的策略反过来------让每一层的错误消息本身就是一个微型修正 Prompt :发生了什么 + 可能原因 + 下一步建议,全用人话写,前缀统一 Error: 方便模型判断这是错误而非正常结果。


7. 顺手总结:可复用的四条 Prompt 工程原则

这套画图实践里,有四条原则其实跟「画图」无关,换个结构化生成场景照样能用:

原则 一句话 怎么落地
反向剔除 系统做 boilerplate,模型做 creative 生成 JSON 别让它写外层壳、只写 items;模板里 60% 是机械重复的,那部分就不该模型生成
示例密度 一个好示例 > 十条文字规则 Skill 里近一半篇幅是示例,每种元素都给样例;模型从示例提模式的能力远强于从规则推导
错误即微 Prompt 报错是给模型看的,不是给人看的 每条错误带「发生什么 + 可能原因 + 下一步」,统一 Error: 前缀
分层约束 别堆一个巨型 System Prompt Skill(全局知识)+ Tool description(局部)+ Shape Library(领域)+ 错误消息(反馈),每次 tool call 只看相关那部分

最后这条尤其想强调:catbuddy 的画图能力是四个信息来源拼起来的,不是一坨 2000 字的 System Prompt。模型决策时综合这四层,但每次只需要看到当下相关的那一层------这比塞一个巨无霸 Prompt 高效得多,也好维护得多。


这篇讲了什么?

  1. 让 AI 画架构图,靠的是工具链 + Prompt 工程,不是特殊模型能力。 核心哲学是「反向剔除」------代码用 ensureMxFile() 补全 <mxfile>/<root>/根节点这些机械模板,模型只生成核心的 <mxCell>,错误面和 token 都砍掉约 40%。
  2. Prompt 迭代四轮,可用率 40%→55%→75%→90%。 每次提升都来自从失败案例里提炼具体规则(连线错开 exitY=0.3/0.7、waypoint 绕行),而非笼统喊「画好看点」。方法论是「告诉模型不要做什么」------因为它的默认行为往往就是你不想要的那种。
  3. 三个工具分工 + 三层校验兜底。 display_diagram(造)/append_diagram(续)/edit_diagram(改)形成完整工具链,get_shape_library 按需加载 100+ 云图标语法;校验不走「解析→重生成」循环,而是 Prompt 预防 → 工具拦截 → 正则核实三层,每条错误消息都是一个微型修正 Prompt。

下一篇预告 :进阶应用到这儿就收尾了------harness 的核心器官(心脏、手脚、眼睛、记忆、韧性、进阶)我们全拆完了。从第 13 篇起,整个系列转入产品化 :怎么把这套 harness 变成桌面、Web、手机都能用的产品。先看最基础的一环------一条消息的旅程:同一条消息,在磁盘里、在网络上、在你屏幕上,其实是三张完全不同的面孔,为什么要这么设计?再聊流式渲染怎么做到丝滑不卡顿。

相关推荐
东小西1 小时前
番外篇一:《Spring AI Alibaba 到底是啥?一张图理清两者关系》
openai·ai编程
逻辑帧2 小时前
洞见AI本质系列-参数的演化
ai编程
9i编程2 小时前
工具是编程的铠甲(下篇):从文件对比、全文搜索到数据库设计
后端·openai·ai编程
用户45989204565162 小时前
我用 AI Agent 自动生成了整个 App 的业务地图(开源)
ai编程·客户端
ServBay2 小时前
MCP Server 是什么?为什么是2026年开发团队的必备?
aigc·ai编程·mcp
Cerrda2 小时前
把团队 Mock 工作流做成可安装 Skill:faker-mock-setup 上架 skills.sh 实践
ai编程·cursor
八号当铺3 小时前
使用 Figma Agent Kit:插件 + MCP + 还原 Skill,打通本地设计协作
前端·人工智能·ai编程
无责任此方_修行中3 小时前
搓了一个国产大模型与 AI Agent 比价工具
前端·后端·ai编程
奈斯先生vector4 小时前
遗留系统不是让 AI 重写一遍:代码智能体驱动的行为保护式现代化
ai编程
MomentYY4 小时前
RAG 图检索&多跳推理:有些答案需要“顺藤摸瓜”
人工智能·agent·ai编程