这是《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 包。关键加了四样:
- 分层颜色表 :前端蓝
#dae8fc、后端绿#d5e8d4、数据橙#ffe6cc、基础设施紫、外部红------模型不用「猜」配色,查表就行; - Swimlane 完整示例:游泳道是出错率最高的元素,给整段示例,避免「子元素 parent 指错容器」;
- 三种连线示例:基础连线、容器连线、带 waypoint 绕行,覆盖九成场景;
- 工具链衔接 :明确写「下一步 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.md、kubernetes.md、flowchart.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 冲突、「转义 < >」灭转义错误。
第二层工具校验 是拦截。注意 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 高效得多,也好维护得多。
这篇讲了什么?
- 让 AI 画架构图,靠的是工具链 + Prompt 工程,不是特殊模型能力。 核心哲学是「反向剔除」------代码用
ensureMxFile()补全<mxfile>/<root>/根节点这些机械模板,模型只生成核心的<mxCell>,错误面和 token 都砍掉约 40%。 - Prompt 迭代四轮,可用率 40%→55%→75%→90%。 每次提升都来自从失败案例里提炼具体规则(连线错开
exitY=0.3/0.7、waypoint 绕行),而非笼统喊「画好看点」。方法论是「告诉模型不要做什么」------因为它的默认行为往往就是你不想要的那种。 - 三个工具分工 + 三层校验兜底。
display_diagram(造)/append_diagram(续)/edit_diagram(改)形成完整工具链,get_shape_library按需加载 100+ 云图标语法;校验不走「解析→重生成」循环,而是 Prompt 预防 → 工具拦截 → 正则核实三层,每条错误消息都是一个微型修正 Prompt。
下一篇预告 :进阶应用到这儿就收尾了------harness 的核心器官(心脏、手脚、眼睛、记忆、韧性、进阶)我们全拆完了。从第 13 篇起,整个系列转入产品化 :怎么把这套 harness 变成桌面、Web、手机都能用的产品。先看最基础的一环------一条消息的旅程:同一条消息,在磁盘里、在网络上、在你屏幕上,其实是三张完全不同的面孔,为什么要这么设计?再聊流式渲染怎么做到丝滑不卡顿。