十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Text-to-BIM/CAD实战:LLM+MCP+几何引擎生成可编辑模型

Text-to-BIM/CAD实战:LLM+MCP+几何引擎生成可编辑模型 1. 从一句话到一栋楼Text-to-BIM/CAD 到底在解决什么问题第一次听到“Text-to-BIM”这个概念是在一个做装配式建筑的朋友那里。他当时吐槽说甲方发来一段文字描述——“三层框架结构层高3.6米柱距6米外墙用200厚加气混凝土砌块”——然后要求当天出一版可编辑的BIM模型。放在以前这活儿得一个熟手在Revit里点上一整天。而现在他想试试能不能让大模型直接把这段话“翻译”成几何体。这就是Text-to-BIM/CAD要干的事把自然语言描述的建筑构件、尺寸约束、空间关系自动转换成可编辑的BIM几何体或CAD图纸。注意关键词是“可编辑”——不是生成一张图片不是渲染一个效果图而是生成带参数、带构件属性、能在Revit或AutoCAD里继续修改的模型文件。为什么这件事突然变得可行了三个条件同时成熟了。第一LLM对自然语言中空间关系和尺寸参数的理解能力大幅提升像“柱距6米”这种带单位的约束现在的模型基本不会理解错。第二MCP协议的出现让LLM能够直接调用CAD/BIM软件的原生API不再需要中间层做格式转换。第三IFC和DXF这些开放格式的解析库越来越成熟Python生态里有ifcopenshell、ezdxf这样的工具让“生成几何体”这件事的门槛降到了写几十行代码的水平。这篇文章适合谁看如果你是在设计院做二次开发的技术人员或者是想切入AEC领域的AI工程师又或者你只是好奇“大模型到底能不能画图”的建筑从业者下面的内容应该都能给你一些可以直接上手的东西。我会从整体架构讲到具体实现包括参数怎么算、MCP怎么接、踩过哪些坑尽量把每个环节都拆开说清楚。2. 整体架构设计为什么是“LLM MCP 几何引擎”三层结构2.1 核心思路让语言模型做它擅长的事很多人第一反应是让LLM直接输出DXF或IFC文件的内容。我试过走不通。原因很简单LLM擅长的是语义理解和逻辑推理不擅长精确的数值计算和格式编码。你让它生成一个DXF的实体段它可能会把组码写错或者把坐标算偏。更致命的是一旦模型规模稍微大一点token数量就爆炸了生成速度慢到无法接受。所以正确的分工是这样的LLM负责把自然语言解析成结构化的中间表示我习惯叫它“构件描述JSON”几何引擎负责把JSON转成实际的BIM/CAD几何体MCP负责在两者之间传递数据和调用工具。这个分工的核心逻辑是——让每个组件做它最擅长的事。中间表示的设计是整个架构的关键。我用的格式大概长这样{ project: { name: 示例项目, units: mm }, elements: [ { type: column, id: C1, position: {x: 0, y: 0, z: 0}, section: {shape: rect, width: 400, height: 400}, height: 3600, material: C30混凝土 } ] }这个JSON就是LLM的输出目标。它不需要知道IFC的实体定义不需要知道DXF的组码只需要把用户说的话翻译成这种结构化的描述。几何引擎拿到这个JSON之后再调用ifcopenshell或ezdxf的API去生成实际文件。2.2 为什么选MCP而不是直接调APIMCPModel Context Protocol在这里的角色是“工具调用层”。你可能会问我直接在Python里写个函数让LLM通过function calling调用不就行了吗为什么要多一层MCP区别在于复用性和标准化。Function calling是绑定在特定LLM平台上的换一个模型就要重写一遍。而MCP是一个协议标准你写一个MCP Server任何支持MCP的客户端都能调用。更重要的是MCP Server可以独立部署CAD/BIM软件那边只需要暴露一个MCP接口LLM这边换什么模型都不影响。我目前的架构是这样的本地跑一个MCP Server里面注册了三个工具——parse_description把自然语言转成构件JSON、generate_ifc把JSON转成IFC文件、generate_dxf把JSON转成DXF文件。LLM通过MCP协议调用这些工具整个流程串起来就是用户输入文字 → LLM解析并调用parse_description → 得到构件JSON → LLM调用generate_ifc → 得到IFC文件 → 用户在Revit里打开继续编辑。2.3 几何引擎的选型IFC还是DXF这取决于你的目标用户。如果用户要在Revit、ArchiCAD这类BIM软件里继续工作那必须走IFC路线因为IFC是BIM领域的通用交换格式保留了构件类型、材质、属性这些语义信息。如果用户只是要在AutoCAD里看图、改图那DXF就够了生成速度快兼容性好。我两个都做了。IFC用ifcopenshellDXF用ezdxf。ifcopenshell的API比较绕但功能全ezdxf的API更直观上手快。下面分别说。3. 核心细节解析从自然语言到构件JSON的映射规则3.1 尺寸和单位的处理自然语言里的尺寸表达非常灵活。“柱距6米”“柱子400乘400”“层高3米6”这些说法LLM都能理解但理解之后必须统一转成毫米。我的做法是在system prompt里明确要求所有尺寸输出前必须转成毫米并且在JSON里标注单位。这里有个坑中文里“米”和“毫米”经常混用而且“3米6”这种口语化表达LLM有时候会理解成3.6米有时候会理解成3米6毫米。我的解决办法是在prompt里加一条规则——“如果数字后面跟‘米’且数字小于100按米处理如果数字大于100且没有明确单位按毫米处理”。这个规则覆盖了绝大多数情况。另一个坑是坐标原点。用户说“柱子放在角落”这个“角落”是哪个角落我的处理方式是默认以建筑左下角为原点X轴向右Y轴向上Z轴向上。如果用户没有明确说明就按这个默认规则来。如果用户说了“以中心为原点”那就在JSON里加一个origin字段来标记。3.2 构件类型的识别与映射建筑构件的类型很多柱、梁、板、墙、门、窗、基础、楼梯等等。LLM需要根据上下文判断用户说的是哪种构件。比如“400乘400的竖向构件”大概率是柱“200厚的竖向分隔构件”大概率是墙。这个判断逻辑我写在prompt里给了一些关键词映射规则。但更可靠的做法是让用户明确说构件类型。我在交互设计上做了一个折中如果用户描述里没有明确构件类型LLM会先输出一个“待确认”的JSON然后在对话里问用户“您说的是柱还是墙”用户确认后再生成最终文件。这样虽然多一轮交互但准确率大幅提升。构件类型的映射表大概是这样的自然语言关键词映射构件类型IFC实体DXF图层柱、柱子、竖向承重ColumnIfcColumnCOLUMN梁、横梁、水平承重BeamIfcBeamBEAM板、楼板、地板SlabIfcSlabSLAB墙、墙体、隔墙WallIfcWallWALL门、门洞DoorIfcDoorDOOR窗、窗洞WindowIfcWindowWINDOW这个表可以根据项目需要扩展。关键是LLM在解析时要有明确的映射依据不能靠猜。3.3 空间关系的表达“柱子沿X轴方向每6米一根共5根”——这种描述涉及阵列和重复。我的处理方式是在JSON里支持array类型的元素定义{ type: column, array: { direction: x, count: 5, spacing: 6000 }, start_position: {x: 0, y: 0, z: 0} }几何引擎拿到这个定义后会自动生成5根柱子的实例。这样比让LLM输出5个独立的柱子对象要高效得多token消耗也少。对于更复杂的空间关系比如“柱子围绕中心点环形布置”我目前还没有做到完全自动化。LLM能理解这个描述但输出成JSON时容易出错。我的临时方案是让LLM输出一个pattern字段标记为“circular”然后几何引擎里写一个专门的环形阵列函数来处理。这个函数需要用户补充半径和数量LLM会在对话里追问。4. 实操过程从零搭建一个Text-to-BIM的最小可行系统4.1 环境准备与依赖安装先列一下我用的工具栈Python 3.103.11更稳3.12有些库还没适配ifcopenshellIFC文件生成ezdxfDXF文件生成mcpMCP协议实现openai或anthropic的SDKLLM调用安装命令pip install ifcopenshell ezdxf mcp openaiifcopenshell在Windows上安装有时候会报错主要是C编译依赖的问题。我的经验是直接用conda装比pip稳conda install -c conda-forge ifcopenshellezdxf是纯Python的pip装就行没遇到过问题。4.2 MCP Server的搭建MCP Server的核心是注册工具函数。我用的是官方Python SDK代码结构大概是这样的from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types server Server(text-to-bim) server.list_tools() async def handle_list_tools(): return [ types.Tool( nameparse_description, description将自然语言建筑描述解析为构件JSON, inputSchema{ type: object, properties: { description: {type: string} }, required: [description] } ), types.Tool( namegenerate_ifc, description根据构件JSON生成IFC文件, inputSchema{ type: object, properties: { elements_json: {type: string}, output_path: {type: string} }, required: [elements_json, output_path] } ) ] server.call_tool() async def handle_call_tool(name, arguments): if name parse_description: result parse_description(arguments[description]) return [types.TextContent(typetext, textresult)] elif name generate_ifc: result generate_ifc(arguments[elements_json], arguments[output_path]) return [types.TextContent(typetext, textresult)]parse_description这个函数本身不调用LLM它只是把用户输入包装一下返回给LLM。真正的解析逻辑在LLM那边——LLM收到工具返回的原始描述后自己推理出构件JSON然后再调用generate_ifc。等等这里有个逻辑问题如果parse_description只是原样返回那为什么要多这一步直接让LLM解析不就行了吗对你说得对。我后来把parse_description去掉了改成LLM直接输出构件JSON然后调用generate_ifc。MCP Server只保留生成文件的工具。这样少一轮交互速度更快。4.3 IFC文件生成的核心代码generate_ifc函数的实现大概是这样的import ifcopenshell import ifcopenshell.api import json def generate_ifc(elements_json, output_path): data json.loads(elements_json) # 创建IFC项目 model ifcopenshell.file(schemaIFC4) project ifcopenshell.api.run(root.create_entity, model, ifc_classIfcProject, namedata[project][name]) # 设置单位 ifcopenshell.api.run(unit.assign_unit, model, length{is_metric: True, raw: MILLIMETERS}) # 创建场地和建筑 site ifcopenshell.api.run(root.create_entity, model, ifc_classIfcSite, nameSite) building ifcopenshell.api.run(root.create_entity, model, ifc_classIfcBuilding, nameBuilding) storey ifcopenshell.api.run(root.create_entity, model, ifc_classIfcBuildingStorey, nameLevel 1) # 聚合关系 ifcopenshell.api.run(aggregate.assign_object, model, products[site], relating_objectproject) ifcopenshell.api.run(aggregate.assign_object, model, products[building], relating_objectsite) ifcopenshell.api.run(aggregate.assign_object, model, products[storey], relating_objectbuilding) # 遍历构件并生成几何体 for elem in data[elements]: if elem[type] column: column ifcopenshell.api.run(root.create_entity, model, ifc_classIfcColumn, nameelem[id]) # 创建矩形截面 profile model.create_entity(IfcRectangleProfileDef, ProfileTypeAREA, XDimelem[section][width], YDimelem[section][height] ) # 拉伸成体 extrusion model.create_entity(IfcExtrudedAreaSolid, SweptAreaprofile, Depthelem[height], ExtrudedDirectionmodel.create_entity(IfcDirection, DirectionRatios(0, 0, 1)), Positionmodel.create_entity(IfcAxis2Placement3D, Locationmodel.create_entity(IfcCartesianPoint, Coordinates(elem[position][x], elem[position][y], elem[position][z])) ) ) # 关联几何表示 shape_rep model.create_entity(IfcShapeRepresentation, ContextOfItemsmodel.by_type(IfcGeometricRepresentationContext)[0], RepresentationIdentifierBody, RepresentationTypeSweptSolid, Items[extrusion] ) product_def model.create_entity(IfcProductDefinitionShape, Representations[shape_rep]) column.Representation product_def # 放入楼层 ifcopenshell.api.run(spatial.assign_container, model, products[column], relating_structurestorey) model.write(output_path) return fIFC文件已生成{output_path}这段代码的关键点是用ifcopenshell.api.run来创建实体而不是直接model.create_entity。前者会自动处理很多关系绑定后者需要手动处理容易漏。4.4 DXF文件生成的简化方案DXF这边简单很多因为不需要处理构件语义只需要画线画矩形import ezdxf def generate_dxf(elements_json, output_path): data json.loads(elements_json) doc ezdxf.new(R2010) msp doc.modelspace() for elem in data[elements]: if elem[type] column: x elem[position][x] y elem[position][y] w elem[section][width] h elem[section][height] # 画矩形 points [(x, y), (xw, y), (xw, yh), (x, yh), (x, y)] msp.add_lwpolyline(points, dxfattribs{layer: COLUMN}) doc.saveas(output_path) return fDXF文件已生成{output_path}DXF的优点是生成快、兼容性好缺点是丢失了构件语义。用户拿到DXF之后只能看到一堆线不知道哪根线是柱、哪根是梁。所以如果用户需要后续编辑还是推荐IFC。5. 常见问题与排查技巧实录5.1 LLM解析尺寸时出错怎么办这是最常见的问题。用户说“层高3米6”LLM有时候输出3600有时候输出3600000。我的解决办法是在prompt里加一个“单位归一化”步骤要求LLM在输出JSON之前先把所有尺寸转成毫米并且在JSON里加一个_debug字段记录原始值和转换过程。比如{ height: 3600, _debug: { original: 3米6, conversion: 3.6m * 1000 3600mm } }这个_debug字段在生成文件时会被忽略但排查问题时非常有用。你可以直接看到LLM是怎么理解用户输入的。5.2 IFC文件在Revit里打开后构件位置不对这个问题我踩过好几次。原因通常是坐标系没对齐。IFC的坐标系和Revit的坐标系有时候会有偏移特别是当项目基点不是原点的时候。我的解决办法是在生成IFC时显式设置IfcSite的RefLatitude和RefLongitude为0并且确保所有构件的坐标都是相对于项目原点的。另外在Revit里打开IFC时选择“原始坐标”而不是“项目内部坐标”这样位置就对了。还有一个坑是单位。IFC默认单位是米但我的JSON里是毫米。如果忘记在IFC里设置单位Revit会按米来解析结果就是所有构件都小了1000倍。所以unit.assign_unit这一步绝对不能省。5.3 MCP连接超时或工具调用失败MCP Server如果是本地跑的一般不会有网络问题。但如果你把MCP Server部署在远程就要注意超时设置。我遇到过LLM调用generate_ifc时超时原因是IFC生成比较慢特别是构件数量多的时候。解决办法有两个一是把MCP Server的超时时间调大默认是30秒我调到了120秒二是把IFC生成改成异步的LLM调用后立即返回一个任务ID然后轮询查询生成状态。第二种方案更复杂但体验更好。5.4 常见问题速查表问题现象可能原因排查方法解决方案构件尺寸偏大/偏小1000倍单位未统一检查JSON里的单位标注在prompt里强制要求转毫米IFC在Revit里位置偏移坐标系未对齐检查IfcSite的坐标设置设置RefLatitude/Longitude为0MCP工具调用超时生成耗时过长查看MCP Server日志调大超时时间或改异步LLM输出的JSON格式错误prompt不够明确检查LLM原始输出加JSON schema约束DXF打开后图层混乱图层未正确设置检查dxfattribs统一图层命名规范5.5 几个实操心得第一个心得不要试图让LLM一次生成完整的建筑模型。我试过让LLM直接输出一栋三层楼的完整IFC结果token消耗巨大而且中间任何一步出错都要重来。正确的做法是分步生成——先生成一层确认无误后再生成下一层最后合并。第二个心得给LLM的prompt里一定要加few-shot示例。我放了三个示例一个简单的单根柱子、一个带阵列的柱网、一个带墙和板的简单房间。有了这三个示例LLM的输出准确率从大概60%提升到了90%以上。第三个心得IFC文件的验证很重要。ifcopenshell自带验证功能生成后跑一下ifcopenshell.validate能发现很多潜在问题。我一般在生成后自动跑一遍验证如果有错误就返回给LLM让它修正。6. 工具选型与扩展方向6.1 LLM的选择为什么我用Claude而不是GPT这不是广告纯粹是实测结果。在解析建筑描述这个任务上Claude对空间关系的理解更准确特别是涉及“沿X轴”“围绕中心”“对称布置”这类描述时Claude的输出更符合预期。GPT-4在这方面偶尔会犯迷糊比如把“沿X轴每6米一根”理解成“沿Y轴”。当然这跟prompt也有关系。如果你用GPT-4可能需要在prompt里把空间关系的规则写得更细。我的建议是先用几个典型描述测试一下看哪个模型在你的场景下表现更好。6.2 几何引擎的扩展从IFC4到IFC4.3IFC4.3是最新版本增加了不少基础设施领域的实体类型比如道路、桥梁、隧道。如果你做的是市政项目建议直接用IFC4.3。ifcopenshell对IFC4.3的支持已经比较完善了我实测下来没遇到大问题。6.3 后续可以加的功能目前这个系统只支持基本的柱、梁、板、墙。后续可以扩展的方向很多一是支持门窗洞口这个需要在墙上做布尔运算ifcopenshell有现成的API二是支持钢筋这个比较复杂需要定义钢筋的走向和间距三是支持多楼层目前只生成单层多楼层需要处理楼层之间的复制和偏移。还有一个有意思的方向是反向操作——从BIM模型生成自然语言描述。这个在审图场景下很有用比如自动生成“本层共有12根柱截面均为400x400混凝土强度等级C30”这样的描述。技术上就是把IFC解析成构件JSON然后让LLM生成文字。这个我还没做但思路是通的。6.4 关于MCP生态的观察MCP协议出来之后AEC领域的工具接入速度比我想象的快。除了CAD/BIM还有一些做结构计算的工具也在接入MCP。这意味着未来LLM可以直接调用结构计算引擎根据自然语言描述生成计算模型、跑完计算、再生成BIM模型。整个链条打通之后从“一句话描述”到“带计算书的三维模型”可能只需要几分钟。不过目前MCP在AEC领域的落地还处于早期很多工具只是做了个demo稳定性和功能完整度都还不够。我的建议是先用MCP做原型验证等生态成熟了再上生产。7. 一个完整的实操案例从描述到IFC文件7.1 输入描述假设用户输入“生成一个单层厂房层高6米柱距6米共4跨每跨2根柱柱子截面400x400外墙200厚。”7.2 LLM解析结果LLM经过推理后输出{ project: {name: 单层厂房, units: mm}, elements: [ { type: column, id: C1-C8, array: {direction: x, count: 8, spacing: 6000}, start_position: {x: 0, y: 0, z: 0}, section: {shape: rect, width: 400, height: 400}, height: 6000, material: C30 }, { type: wall, id: W1, start: {x: 0, y: 0, z: 0}, end: {x: 48000, y: 0, z: 0}, thickness: 200, height: 6000 } ] }注意这里LLM自动算出了总长度4跨×6米/跨×2根/跨48米所以墙的终点X坐标是48000。这个计算LLM做对了但有时候会算错所以我在prompt里加了一条“所有坐标计算必须展示计算过程”。7.3 生成IFC并验证调用generate_ifc后得到IFC文件然后在Revit里打开。检查要点柱子位置是否正确、截面尺寸是否正确、墙的厚度和高度是否正确。如果都对了就可以继续在Revit里加门窗、加屋面。7.4 这个案例的耗时从输入描述到拿到IFC文件整个流程大概15秒。其中LLM解析占5秒IFC生成占10秒。如果构件数量更多IFC生成时间会线性增长。100根柱子的项目大概需要30秒。8. 踩过的坑与避坑指南第一个坑LLM输出的JSON里有时候会带注释。JSON标准不支持注释但LLM有时候会加//开头的行。我的解决办法是在解析JSON之前先做一次清洗把//后面的内容删掉。第二个坑ifcopenshell的API在不同版本之间变化很大。我一开始用的0.6版本后来升级到0.7发现create_entity的参数变了。建议锁定版本不要随便升级。第三个坑DXF的图层颜色。默认情况下所有图层都是白色在CAD里看起来一片糊。我后来加了一个图层配置给不同构件类型分配不同颜色可读性好了很多。第四个坑MCP Server的日志。MCP协议本身不输出日志排查问题很麻烦。我后来在Server里加了一个日志文件记录每次工具调用的输入和输出出问题时直接看日志。第五个坑LLM的token限制。如果用户描述很长加上few-shot示例很容易超过模型的上下文窗口。我的解决办法是把few-shot示例精简到最核心的三个并且把用户描述截断到500字以内。9. 性能优化与规模化考虑9.1 批量生成时的性能瓶颈当构件数量超过500个时IFC生成会成为瓶颈。ifcopenshell的create_entity每次都要做一次类型检查累积起来很慢。我的优化方案是批量创建——先用model.create_entity创建所有实体最后再统一做关系绑定。这样能减少大概40%的生成时间。9.2 缓存策略如果用户反复生成相似的模型可以考虑加缓存。我的做法是把构件JSON的哈希值作为keyIFC文件路径作为value存在本地。下次遇到相同的JSON就直接返回缓存文件。这个优化在调试阶段特别有用因为调试时经常重复生成同一个模型。9.3 并发处理MCP Server默认是单线程的如果多个用户同时调用会排队。我的解决方案是用asyncio做异步处理每个工具调用跑在独立的协程里。不过IFC生成是CPU密集型的异步帮助不大最终还是受限于CPU核心数。如果要支持高并发可能需要用多进程或者分布式部署。10. 我对这个方向的一些个人判断Text-to-BIM/CAD目前还处于“能用但不够好用”的阶段。对于简单的、规则化的建筑描述比如柱网、墙体、楼板准确率已经可以接受。但对于复杂的、带异形构件的描述比如“弧形屋面”“螺旋楼梯”LLM的解析能力还差得远。不过这个方向的价值是明确的。建筑设计行业有大量重复性的建模工作这些工作完全可以交给LLMMCP几何引擎的组合来完成。设计师只需要做创造性的部分重复性的部分让机器去做。我个人的判断是未来两年内Text-to-BIM会成为BIM软件的一个标准功能。就像现在CAD软件都有“导入图片”功能一样未来的Revit可能直接内置一个“文字生成模型”的按钮。到那时候今天写的这些代码可能就变成历史了。但在此之前自己动手搭一套系统理解其中的原理和坑还是很有价值的。最后分享一个我在调试时常用的小技巧如果LLM输出的JSON有问题不要直接改prompt先把LLM的原始输出打印出来看看它到底是怎么理解的。很多时候问题不在prompt而在LLM对某个词的理解和你不一样。找到那个词在prompt里给它一个明确的定义问题就解决了。
返回列表