最近在整理一个软件项目的需求文档时遇到一个很现实的痛点团队成员用纸笔画了一堆UML用例图画得倒是挺欢回头要录入系统、跟踪变更时全傻眼了——重新描一遍费时费力手动去数角色、用例、关系简直是眼睛和耐心的双重折磨。后来我索性写了个小工具直接用LLM来做UML Use Case图的识别把图片扔进去自动输出结构化的用例列表和可编辑的UML源码整个流程清爽了很多。这次就把整个思路和踩坑过程整理出来给同样被用例图折磨的各位一个参考。这个项目适合谁一类是做需求分析、系统设计的软件工程师尤其是需要批量复用历史用例图的时候另一类是想尝试用LLM解决具体工程问题、但不想一上来就搞复杂Agent的同学。核心就一件事把“看图”这件事交给多模态LLM让它识别UML用例图中的Actor、Use Case、关联关系、泛化/包含/扩展关系最终输出成机器可读的JSON和可再绘制的PlantUML代码。1. 整体设计与思路拆解为什么要用LLM来识别用例图1.1 手动整理UML用例图为什么这么烦UML用例图是需求分析中最常用的图之一但它的“可维护性”极差。很多时候图画完就放在文档里或者贴在白板上拍了张照片之后想修改、复用、统计都得重新用工具画一遍。尤其当用例图数量多、关系复杂时手工整理几乎是一场灾难。传统识别方案也不是没有但都有各自的毛病。如果你面对的是图片格式的用例图常规套路是先OCR把文字提出来然后分析位置关系。但OCR只能拿到文字拿不到“哪个椭圆是用例”“哪个小人图是角色”“箭头连线是什么关系”——这些空间语义信息OCR完全无能为力。基于规则的方法呢你得写死一堆形状检测和箭头识别逻辑一旦图风格变化比如不同人画的图、不同UML工具导出的图规则就得重写鲁棒性极差。而用传统深度学习做目标检测又需要大量标注数据为用例图专门标一套数据集性价比太低。1.2 LLM解决的是“语义结构”双重理解问题LLM大语言模型尤其是多模态大模型天然具备两个能力一是视觉理解能看懂图片里的元素和布局二是推理能力能结合UML知识理解角色、用例、关系的语义。它不需要你专门针对用例图训练模型只要在提示词中告诉它UML用例图的规范它就能把图片里的内容“翻译”成结构化数据。举个例子你给它一张用例图它能认出“用户”在一个方框外面、代表Actor“登录”被一个椭圆包住代表Use Case“用户”和“登录”之间有一条实线连线代表关联关系。它还能进一步推理如果一个用例A指向用例B且带箭头可能“A include B”或“A extend B”具体是哪个看箭头方向和文本标注。这种能力是传统视觉识别很难做到的。1.3 方案选型多模态LLM直接识别 vs OCRLLM后处理在实践中有两种主流路线。路线A直接使用多模态LLM比如GPT-4o、Qwen-VL、Claude的视觉版读图一次输出结构化内容。优点是简单粗暴理解能力强能处理模糊的布局缺点是对于超大图或者高分辨率图容易漏掉细节而且输出不稳定。路线B先用OCR提取文字和坐标再结合图像分析把文字块组织成候选元素最后把候选元素交给LLM做关系推理。优点是可以结合传统方法补充细节但写起来复杂要自己做很多图像处理。我的建议是如果只是几十张图直接走路线A把图预处理一下再配合Few-shot提示词效果就很好了如果图特别多、要求高精度再考虑路线B用OCR兜底LLM做增强。这次分享的项目以路线A为主后续再讨论怎么优化。2. 核心细节解析与实操要点让LLM看懂用例图的提示词设计2.1 UML用例图有哪些元素必须识别要写提示词得先明确UML用例图里有哪几类关键信息。标准用例图包含角色Actor通常画成小人图标或矩形条位于系统边界方框外部代表外部用户或系统。用例Use Case通常用椭圆或圆角矩形表示位于系统边界内部代表一个功能单元。系统边界System Boundary一个矩形框内部是所有用例框外是角色。关联关系Association角色与用例之间的实线不带箭头。泛化关系Generalization角色或用例之间的空心三角箭头实线。包含关系Include用例之间带箭头的虚线箭头指向被包含的用例标注include。扩展关系Extend用例之间带箭头的虚线箭头指向被扩展的用例标注extend。LLM需要能识别这些符号以及它们之间的连线。在提示词里要把这些规范说清楚最好配一个示例。2.2 提示词的结构任务描述、输入说明、输出格式、示例把提示词拆成四块效果最稳定。第一块是任务描述明确告诉模型“你是一个UML专家现在要从一张用例图中提取结构化信息”。第二块是输入说明描述图片内容格式比如“图中Actor通常为小人图标或方框外文本Use Case为椭圆内文本”。第三块是输出格式强制模型返回JSON并定义JSON的字段结构。第四块是示例给一个很小的示意图和对应的JSON让模型模仿。输出JSON我一般这样设计{ actors: [用户, 管理员], use_cases: [登录, 找回密码], system_boundary: 在线商城系统, relationships: [ {from: 用户, to: 登录, type: association}, {from: 登录, to: 找回密码, type: include}, {from: 找回密码, to: 登录, type: extend} ], generalizations: [ {from: 管理员, to: 用户, type: generalization} ] }这里有个细节relationships中的type需要限定为枚举值比如association、include、extend。这样后面再转成PlantUML就非常方便。2.3 图片预处理放大、裁剪、去背景LLM识别图片的准确率受图片质量影响很大。实际项目中从PPT或PDF里截出来的用例图经常分辨率不够尤其是微小文字模型看不清就会产生幻觉。我实验下来最好的方式是先用OpenCV或PIL把图片放大两倍再用converter转成RGB模式最后等比压缩到模型支持的尺寸比如1024x1024。有些图有杂乱的背景先用简单的边缘检测加形态学处理把背景抹掉也能提升准确率。这一步虽然简单但作用巨大很多“识别错了”的问题其实是图片太糊。2.4 Few-shot示例比重复描述更有效大模型直接理解抽象描述的能力有限你告诉它“椭圆里的是用例小人图是角色”它依然可能把图上所有文字都当成用例。更好的做法是在提示词里附加一个示例一张简化用例图的ASCII表示和对应的JSON。比如我常用一个文本示例示例图 [用户] —— (登录) |-- (注册) // 关联注册包含登录|--表示泛化..表示依赖等但提示词里直接用纯文本描述即可。模型看了示例之后输出格式和语义理解会稳定很多。我在项目中试过不加示例和加示例加示例的准确率大约能提升20%左右。3. 实操过程与核心环节实现从图片到结构化JSON再到PlantUML3.1 环境准备和依赖安装我用的是Python主要依赖openai或其他兼容SDK、PIL、pydantic和plantuml命令行工具。代码结构很简单不涉及复杂框架。安装命令pip install openai pillow pydantic requests如果是本地调用通义千问或其他模型可以改用对应的SDK核心流程不变。下面代码我以OpenAI格式为例但您实际项目可以替换base_url和api_key来适配其他厂商。3.2 核心函数调用多模态LLM一个标准的图片识别函数长这样import base64 import json from openai import OpenAI client OpenAI(api_keyyour-api-key) def encode_image_to_base64(image_path): with open(image_path, rb) as f: return base64.b64encode(f.read()).decode(utf-8) def recognize_uml_use_case(image_path): img_b64 encode_image_to_base64(image_path) system_prompt 你是一名UML专家。你的任务是从用例图中提取结构化信息。 请识别Actor角色、Use Case用例、系统边界以及它们之间的关联、泛化、包含、扩展关系。 输出JSON格式必须包含以下字段 { actors: [角色名称列表], use_cases: [用例名称列表], system_boundary: 系统边界名称如果没有边界则为空字符串, relationships: [ {from: 源元素, to: 目标元素, type: association|include|extend} ], generalizations: [ {from: 子元素, to: 父元素, type: generalization} ] } 注意只有带箭头虚线和include标注的才是include关系只有带箭头虚线和extend标注的才是extend关系普通实线关联是association。 示例 图中“用户”链接到“登录”登录有一个箭头虚线指向“注册”并标注include。 输出{actors:[用户],use_cases:[登录,注册],relationships:[{from:登录,to:注册,type:include}]} response client.chat.completions.create( modelgpt-4o, # 替换为你的多模态模型 messages[ {role: system, content: system_prompt}, { role: user, content: [ {type: text, text: 请识别下图中UML用例图的所有元素和关系}, {type: image_url, image_url: {url: fdata:image/png;base64,{img_b64}}} ], } ], temperature0, max_tokens2000, response_format{type: json_object}, ) content response.choices[0].message.content return json.loads(content)有几个关键参数值得说一下。temperature我固定设为0确保输出稳定的JSON结构不产生随机变化。max_tokens设到2000如果图比较大、用例列表长2000不够用就需要加大。response_format强制返回JSON对象避免模型在JSON前后加说明文字。如果你的模型不支持response_format就需要在提示词里强调“只返回JSON不要其他文字”然后在代码里做后处理。3.3 图片放大预处理识别前我用PIL做放大和优化from PIL import Image def preprocess_image(input_path, output_path, target_width1600): img Image.open(input_path) img img.convert(RGB) ratio target_width / img.width if ratio 1: img img.resize((target_width, int(img.height * ratio)), Image.LANCZOS) img.save(output_path, PNG) return output_path放大要注意不是越大越好模型一般限制输入图片的像素数控制在1500px宽左右既能保证文字清晰又不会超出模型输入限制。对于有严重噪点的截图还可以加一步中值滤波这里先不展开。3.4 把JSON转成PlantUML源码得到结构化JSON之后下一步是把它转成可编辑的UML图。PlantUML是纯文本的UML生成工具维护起来非常方便。转换函数如下def json_to_plantuml(data): lines [startuml, skinparam backgroundColor #FFFFFF] # 定义角色 for actor in data[actors]: lines.append(factor {actor} as {actor}) # 定义用例 for uc in data[use_cases]: lines.append(fusecase {uc} as {uc}) # 系统边界 if data[system_boundary]: lines.append(frectangle {data[system_boundary]} {{) # 把用例放入边界内部的方法在边界块内引用用例但需要提前定义use case一般写法为在块内部定义 # 简化处理这里先不嵌套只展示边界名称真正嵌套需要顺序调整实际实现中我更推荐用PlantUML的rectangle包裹用例但要注意先定义用例还是先定义边界。一个常见做法是先定义所有actor和usecase再在矩形内通过usecase 登录 as 登录这种引用方式他们。更标准的写法是在矩形块内定义用例def json_to_plantuml(data): lines [startuml] for actor in data[actors]: lines.append(factor {actor} as {actor}) # 系统边界 boundary data.get(system_boundary, ) if boundary: lines.append(frectangle {boundary} {{) for uc in data[use_cases]: lines.append(f usecase {uc} as {uc}) if boundary: lines.append(}) # 关联关系 for rel in data.get(relationships, []): # 根据type输出不同箭头association为实线无箭头include为虚线虚线箭头extend为虚线箭头方向相反 if rel[type] association: lines.append(f{rel[from]} -- {rel[to]}) elif rel[type] include: lines.append(f{rel[from]} .. {rel[to]} : include) elif rel[type] extend: lines.append(f{rel[to]} .. {rel[from]} : extend) # 泛化关系 for gen in data.get(generalizations, []): lines.append(f{gen[from]} --| {gen[to]}) lines.append(enduml) return \n.join(lines)关于include和extend的方向要注意UML规范中A include B表示A包含B箭头从A指向B而A extend B表示A扩展B箭头从A指向B扩展用例指向被扩展用例。但图中经常画反需要模型理解文本标注。我在提示词中强调过但实际转换时还是常需要人工核对。3.5 一个完整的运行示例假设我有一张用例图名字叫login.png内容大致是一个角色User连接LoginLogin包含ValidateAdmin泛化User系统边界叫OnlineSystem。运行代码processed_image preprocess_image(login.png, login_preprocessed.png) result recognize_uml_use_case(processed_image) print(识别结果, json.dumps(result, ensure_asciiFalse, indent2)) uml_code json_to_plantuml(result) with open(output.puml, w, encodingutf-8) as f: f.write(uml_code) print(PlantUML 代码已保存到 output.puml)如果模型识别正确输出的result大约是这样{ actors: [User, Admin], use_cases: [Login, Validate], system_boundary: OnlineSystem, relationships: [ {from: User, to: Login, type: association}, {from: Login, to: Validate, type: include} ], generalizations: [ {from: Admin, to: User, type: generalization} ] }生成的output.puml内容大致如下startuml actor User as User actor Admin as Admin rectangle OnlineSystem { usecase Login as Login usecase Validate as Validate } User -- Login Login .. Validate : include Admin --| User enduml把这段代码贴到PlantUML官网或本地渲染就能得到一张干净、可编辑的用例图。这一步就把识别结果和现有工具链打通了。4. 常见问题与排查技巧实录识别不准、输出乱、转译出错怎么办4.1 文字太密或太小时LLM出现幻觉这是最高频的问题。模型会把图例里的文字、图名、装饰性文字也当成用例或角色。排查办法是先看输入的预处理图——如果人眼都看不清小字那LLM也会看不清。解决办法有两个一是提升分辨率并局部放大二是把大图切成若干小块分别识别再合并结果。切图识别我试过很有效的做法先用PIL把整张图按1.5倍重叠切成左右两半分别送入LLM识别最后在代码里合并两个JSON。这样每一块的文字更清晰准确率明显上升。代价是调用次数翻倍但值得。4.2 输出JSON格式不稳定或者多余解释文字即使设了response_format有些模型版本还是会在JSON外加代码块标记。我在代码里会做一层兜底解析import re def parse_json_response(content): match re.search(r\{.*\}, content, re.DOTALL) if match: try: return json.loads(match.group(0)) except: pass # 去掉代码块标记 cleaned content.strip().strip().strip() return json.loads(cleaned)这样即便模型输出前加了“json”之类的标记也能正确提取。4.3 include和extend方向总相反怎么办UML规范中包含关系A include B表示用例A在执行时会调用用例B箭头从A指向B标注include。扩展关系A extend B表示用例A是B的可选扩展箭头从A指向B但图中可能画成从B指向A。模型经常搞混方向。我的解决办法是在提示词里加入一句“请通过箭头的方向和标注文本判断如果标注是 则箭头起始是主用例箭头指向被包含用例如果标注是 则箭头起始是扩展用例箭头指向被扩展用例”。并且在Few-shot示例中专门展示这两种情况的区别。如果依然出错就在转PlantUML的脚本里增加一个--no-verify开关输出后再人工看一遍。4.4 系统边界内的用例识别为角色有些图把Actor也画在系统边界内部这不符合UML规则但实际图中经常出现。模型会被混淆把边界内的Actor也当作用例。我在提示词里补充了判定规则“Actor通常是小人图标或位于矩形外部如果某个名字在矩形内部且不是椭圆形也可能是角色但需根据上下文判断”。然后在后处理中如果某个名字既出现在actors又出现在use_cases优先去重并询问用户或者写成告警日志。目前我的做法是先去重再打印一条警告。4.5 调用LLM超时或请求报错图片过大会导致请求超时。有一次我传了一张4MB的PNG直接超时。解决办法是先压缩到合理尺寸并转成JPEG减少体积如果模型支持image_url本地文件可以先转成base64但要注意base64会膨胀三分之一体积。更稳妥的姿势是把图传到对象存储或者图床返回URL再传参。不过对本地工具来说base64最方便只要控制好图片尺寸和请求token即可。另外多模态模型对单图输入有大小限制如果提示词说“Input image is too large”就一定要缩小图片。4.6 判别效率一次调用 vs 多次调用如果你的图里有几十个用例一次调用很容易遗漏。我的建议是把图按系统边界切割成多个子图每个子图单独识别最后用代码合并。这种方式虽然调用次数多但每个子图的精度都非常高而且有利于后续你按模块维护用例。项目中我最终做成了“分块识别全局合并”的流水线准确率从一次调用的70%左右提升到95%以上。5. 扩展思路从文本需求直接生成用例图识别已有用例图只是第一步实际项目中我们很多时候手里只有自然语言的需求文档连图都没有。这时候可以用纯文本LLM不需要多模态从需求描述中提取用例和角色再生成UML。做法其实类似把需求文本发给模型让它按同样的JSON格式输出再用json_to_plantuml函数转成图。这种用法和图像识别并不冲突可以做成同一个工具的两个模式--input image识别图片--input text生成用例图。比如给模型一段需求“用户可以使用账号密码登录系统登录成功后可以修改个人信息管理员可以查看所有用户信息。”模型可能会输出{ actors: [用户, 管理员], use_cases: [登录, 修改个人信息, 查看用户信息], relationships: [ {from: 用户, to: 登录, type: association}, {from: 用户, to: 修改个人信息, type: association}, {from: 管理员, to: 查看用户信息, type: association} ] }这种“文本直接开户”的方式在前期需求探索阶段非常好用。但是要注意LLM从文本提取用例时容易过度拆分功能把不该独立的步骤也当成用例需要人工把关。我的习惯是让模型先列出所有候选用例再自己删减一轮。6. 实操心得与后续建议在整个项目过程中最大的体会是LLM真正解决的是“规则难以覆盖的变体理解”而不是替代你掌握UML知识。不要指望模型一次输出就是标准答案尤其是关系方向的判定最终还是要靠人和脚本双重校验。我建议所有做这个方向的朋友至少准备两套验证方式一是把PlantUML渲染成图片肉眼对比原始图二是写一个断言脚本检查输出的actors和use_cases数量是否一致、关系是否有断点。这样能把漏识别和误识别都暴露出来。以后如果图库继续增加还可以考虑把所有识别结果存成SQLite或JSON文件用向量数据库做用例检索这样需求变更时你可以快速找到所有关联用例评估影响范围。这也算是LLM识别UML用例图在需求管理上的一个延伸价值。最后再分享一个小技巧你用LLM识别完一张图后把那次的识别结果和图片原图一起存下来形成一个小的Few-shot样例库。下一次遇到类似风格的图可以在提示词里带上这个样例模型的识别准确率会高出一大截。我自己的项目就是用这个方式从上手时每张图都要手动修错到现在大部分图一次过省下了巨量时间。