
1. 从连线连到半夜说起ThunderAgent要解决什么问题1.1 一个真实的Dynamo加班场景先讲一个我自己的经历。去年做一个医疗综合体项目幕墙表皮有将近四千块渐变嵌板每块嵌板的尺寸、角度、连接件位置都不一样。按传统做法我得在Dynamo里拉一堆节点读取Excel参数、按编号匹配幕墙族、驱动自适应族点、再处理翻转逻辑。光是理顺那几十个节点的连线和数据流就花了我两个通宵。第二天早上看着满屏橙色警告节点我都不知道是从哪个端口开始断的。这种痛苦熟悉吗我相信每个深度用过Dynamo的人都经历过类似的阶段不是方案逻辑想不清楚而是把逻辑翻译成节点连线这个过程实在太琐碎、太容易错。尤其是当你需要临时调整某一个判断条件比如嵌板宽度大于1200时用A方案否则用B方案你得沿着线缆找到对应的Code Block改完再回头检查下游还有没有其他分支依赖这个值。所以我一直在想一个问题Dynamo的节点图本质上是一棵逻辑树这棵树由数据和操作组成。那能不能让一个智能代理Agent直接理解我的需求自动把这棵树构建出来我把这个想法落地成了一个内部工具代号就叫ThunderAgent。它不是一个全新的软件而是一套跑在Dynamo环境外面的自动化代理层核心工作是把自然语言描述的建模需求转换成可执行的Dynamo节点图脚本再回读结果做自校验。这篇内容想聊的就是这个东西的设计思路、实现管线、以及我在真实项目里跑它时踩过的一堆坑。如果你也在搞Dynamo批量处理、参数化建模或者对用自然语言生成程序这种工作流感兴趣这篇文章应该对你有用。1.2 传统Dynamo工作流的三个死结先拆解一下传统Dynamo工作流里最让人头疼的三件事是什么。第一是连线即文档带来的沟通成本。Dynamo的图就是程序但是图和图之间没法像代码那样做diff没法快速看出来这版和上一版到底哪里改了。团队协作时别人丢给你一个.dyn文件你光看懂它就要花不少时间更不用说去改它。第二是节点选择焦虑。Dynamo节点库里相关节点往往有七八个近似选项List.Map和List.MapAtLevel有什么区别Element.GetParameterValue和Element.GetParameterValueByName哪个性能更好这些细节理论上翻文档能查到但实际做项目时没有人会停下来逐个查。第三是改一处牵全局的脆弱性。Dynamo图的连线越复杂局部修改越容易引发连锁错误。你只是把某个输入从整数改成浮点数结果发现下游所有的List.FilterByBoolMask全部变成null。传统做法是慢慢排查但ThunderAgent的思路完全不同既然图是生成的那改需求时直接重新生成一版图把旧图整体替换掉从源头避免连线历史包袱。这三个死结指向同一个解让生成图的过程自动化、可重复、可校验。这也是我做ThunderAgent的出发点。1.3 ThunderAgent解决了什么、不解决什么先说解决了什么。ThunderAgent最擅长的场景是需求明确但表达繁琐的批量任务。比如把当前视图中所有类型名包含某关键词的族实例收集起来提取某个共享参数按楼层分组输出到CSV。这种逻辑本身不复杂但你手动拉节点可能要十五分钟而且每次换项目换参数名称都要重新拉一遍。用ThunderAgent你只需要说清楚做什么、对谁做、结果放哪它就能生成对应的Dynamo脚本你保存成自定义节点或者直接运行。但它不是什么都能干。ThunderAgent不擅长处理极度模糊的设计意图比如把立面做得更有韵律感这种话它没法直接转成算法。它也不负责判断你的BIM标准符不符合规范——它只能在给定图元集合和参数规则的前提下尽可能精确地执行你的指令。数据源如果有脏数据生成出的图一样会跑出错误结果。它的价值是把你从写程序里解放出来但没法替你做需求定义。2. ThunderAgent四条管线意图解析、节点映射、拓扑生成、回读校验2.1 意图解析把人话拆成可执行的动词和对象整个系统的第一步是把自然语言指令转成一个中间表示。这个中间表示不是Dynamo的节点JSON而是一种更接近操作语义的结构化数据。我把每条指令拆成三个核心要素对象选择对哪些图元操作、行为动作做什么处理、输出目标结果送到哪里。举个例子用户输入把当前视图所有类型名含幕墙嵌板的族实例的宽度参数导出成Excel表格。意图解析层会先做一个命名实体识别把族实例识别为对象类型类型名含幕墙嵌板识别为过滤条件宽度参数识别为要读取的属性导出成Excel识别为输出动作。这步听着简单但实际处理中有个容易翻车的点用户用的词经常和Dynamo节点名不完全一致。比如Excel表格实际可能是Data.ExportCSV或者Excel.WriteToFile这需要结合上下文做个归一化。此外意图解析层还要识别出指令里的隐含逻辑比如按楼层分组这种状语不是简单的过滤条件而是一个GroupBy操作它会影响下游的数据结构。我在实现时把状语分成三类前置过滤条件、中间变换逻辑、后置聚合逻辑。分词之后先挂到对应的槽位上再生成中间表示。2.2 节点映射Dynamo节点库的模糊匹配与版本约束中间表示生成之后下一步是把它映射到具体的Dynamo节点。这一步是整个ThunderAgent里最容易被低估的部分。Dynamo的节点库非常庞大而且不同版本之间节点的名称、端口数量、输入类型都有变化。比如旧版本里有个节点叫Element.GetParameterValueByName新版本里它被合并到Element.GetParameterValue端口从两个变成三个第三个端口多了个参数类型选项。如果在映射时没做版本校验生成的图一打开就是满屏的Missing Node。我采用的方案是本地维护一份节点Schema索引记录每个节点的名称、所属包、输入输出端口数量和类型、最低支持版本。模糊匹配时优先做精确匹配匹配不到再做语义相似度匹配把用户的自然语言描述和节点描述做向量比对。这个方案有一个明显的好处当多个节点都能实现同一功能时映射层可以按版本兼容性性能开销可读性三个维度打分排序。比如Element.GetParameterValue和Element.GetParameterValueByName都能取参数值但前者要求输入已解析的参数名称字符串后者可以从元素自动查找在性能上相差不大我一般会建议优先选后者因为少一个前置节点图更简洁。2.3 拓扑生成从指令序列到Graph结构有了节点映射下一步是把节点串成一张有向无环图。这里最关键的问题是数据流的方向。Dynamo的连线和普通程序的控制流不一样节点图是数据驱动的上游节点的输出端口接到下游节点的输入端口。拓扑生成器要解决的是哪个节点的输出应该连接到哪个节点的输入这个问题。我最初想直接用一个全局规划算法把所有可能的连接关系列出来再找最优路径。后来发现这个思路根本不实际真实Dynamo图动辄上百个节点节点间连接关系组合数爆炸穷举不现实。换成了一个务实路线分块生成逐层拼接。具体做法是把中间表示按操作顺序切成若干段每一段对应一个子图模板。比如选择所有门对应一个SelectByCategory子图过滤宽度大于900的门对应一个FilterByType子图输出到Excel对应一个Export子图。每个子图模板已经预先定义好了输入槽和输出槽拼接时只需要把上一个子图的输出槽连接到下一个子图的输入槽。这么做还有个额外好处每个子图模板是可以被单独测试和缓存的。如果某个子图模板生成的逻辑有问题只需要修那个模板不需要重新生成整个图。2.4 回读校验让Agent看自己的作业打分生成图之后最重要的一个环节是回读校验。这一步很多人会忽略但它恰恰是ThunderAgent能稳定跑起来的核心原因。简单说图生成之后系统会用一个独立于生成器之外的校验器把生成的图反编译回中间表示再和原始意图做对比。怎么反编译Dynamo图本质上是JSON结构每个节点有它的节点类型、输入端口连接、输出端口连接。校验器会遍历这个JSON把每个节点对应回节点库里的语义描述然后重新组装成一段伪自然语言。比如读到一个节点是Element.GetParameterValueByName输入是幕墙嵌板族实例和宽度参数那它就会重新拼出这样一句话读取幕墙嵌板族实例的宽度参数。再把这句话和用户原始的输入做语义相似度比对。如果相似度低于某个阈值就判定这次生成有问题触发重新生成或者向用户报告。这个机制解决了一个非常实际的痛点LLM生成东西经常看起来合理但实际错了错误可能藏在某个连线上人工检查会很费劲。回读校验相当于让Agent自己检查一遍作业虽然不能保证百分之百正确但能过滤掉大量低级错误。3. 跑通一次真实任务让ThunderAgent自动生成批量标记门窗的Dynamo脚本3.1 任务描述与模型选择说完了架构我们来看一个实际跑通的案例。这个案例是真实项目里用到过的把所有门和窗的族实例按房间号自动写入标记同时把标记位置定位到图元中心点的偏移位置。任务描述喂给ThunderAgent时是这样一句话 获取项目中所有门和窗的族实例读取它们所在的房间名称将房间名称写入该图元的标记参数标记位置设置在几何中心点上方向偏移300毫米。在实际模型选择上我建议优先选一个推理能力强一点的开源模型因为这类任务涉及多步推理和空间逻辑。如果只是简单的单步指令用小体积模型就够了。ThunderAgent在设计上和模型解耦你换一个底层模型不需要改动上层代码只影响生成质量。目前我本地跑的是Qwen系列的中等尺寸版本显存占用在可控范围内一次生成响应速度大约在十几秒。3.2 分步操作与生成的中间产物我们来看看ThunderAgent内部是怎么处理这条指令的。首先是意图解析分词后识别出以下关键槽位槽位识别结果说明对象门、窗族实例映射为 FamilyInstance 集合过滤条件类别为门或窗使用 BuiltInCategory 过滤属性所在房间名称对应 Room 的 Name 参数行为写入标记参数需要找到标记族并设置参数位置几何中心点上方向偏移300mm需要先拿 BoundingBox 中心点再向量偏移输出在视图中更新标记对应 OverrideElementGraphics 或直接移动标记这组结构化数据生成之后节点映射模块开始了它的工作。它的决策过程我大致描述一下选择所有门和窗All Elements of Category两个节点分别取门和窗的类别然后用 List.Combine 合并两个列表。这个地方很多人会直接用字符串拼接但Dynamo里的类别节点返回的是ElementId集合List.Combine才是正确的。读取房间名称这一步稍微复杂。需要先用 Element.GetParameterValueByName 读取图元的房间参数再通过该值查找到对应的Room对象。我当时的处理是生成一个 Python Script 子图因为这个交叉查找用节点串联会很绕。写入标记这里有个关键判断——标记是一个独立族实例不是门的子图元。所以要先通过 FamilyInstance.GetAssociatedElement 或选择全部标记图元再通过 Element.SetParameterByName 写入。ThunderAgent在意图解析时会自动补全这一步因为它知道写入标记意味着要操作标记族实例。位置计算从几何中心点出发用 Vector.ZAxis 乘300毫米得到偏移向量再通过 ElementTransform.SetTranslation 移动标记。最终生成的Dynamo图结构大致是两组类别选择节点 → 合并列表 → Python Script节点负责房间查找和标记写入→ 位置计算节点 → 转换节点。整个脚本的节点数量在45个左右比我手工拉图少了约三分之一主要省在那些中转节点上。3.3 校验闭环与人工介入点图生成之后回读校验器做了两件事。第一件事是语义回读把这段图反编译成文本检查是否有明显的信息丢失。第二件事是结构性检查遍历所有连线的端口类型确保每个输入端口接收的数据类型是正确的。比如Python Script节点的输出端口是Object类型接入数学运算节点时就会报类型错误这个结构性检查能直接抓到。我在实际运行中遇到过这类情况生成器把偏移300毫米映射成了一个300的数值节点但输入端口期望的是Vector类型校验器直接报错退出没有进入执行阶段。这就是回读校验的价值——它拦截了低级错误让我不用在Dynamo里对着满屏红点排查。当然校验器不是万能的。它只能验证内部一致性没法验证这个脚本是否真的符合项目标准。比如门窗标记应该放在左上角还是中心点上方这类主观标准它判断不了。所以实际使用中我会把人工介入点放在两个位置第一步意图确认第二步生成结果预览。ThunderAgent会先把结构化意图显示出来让用户确认你确实是想做这件事然后再生成图。这个机制在需求描述模糊时特别有用。4. 踩过的坑Agent生成Dynamo脚本的典型翻车现场4.1 节点版本不一致导致的哑弹先说一个最隐蔽的坑节点版本不匹配。ThunderAgent生成脚本的时候底层模型对节点库的记忆往往停留在某个它见过最多的版本。比如它可能习惯用旧版的节点名称但你在Revit 2024里跑的是最新的Dynamo很多旧节点已经被改名或废弃。最典型的例子是Select Model Elements旧版节点在新版里被拆成Select Model Elements By Category和Select Model Elements By Parameter两个节点。这类问题最坑人之处在于Dynamo不会在你打开图的时候立刻报错而是等你运行到那个节点时才弹黄色警告。你拿到一个看似完整的脚本运行结果却是大片null值然后你开始怀疑自己是不是操作顺序错了实际上只是节点版本不对。我的解决办法是在生成器内部强制要求节点映射层带上版本号。每个映射结果都必须记录对应的Dynamo版本和Revit版本生成完成后再做一次全图扫描检查是否有节点引用了当前环境不支持的版本。没有这一步你等于把一颗哑弹埋进了自己的脚本里。4.2 输入端口类型推断错误第二个高频坑是端口类型推断错误。Dynamo的节点端口是有类型约束的但LLM生成时经常忽略这个约束。比如 List.FilterByBoolMask 的两个输入一个要求是列表另一个要求是布尔列表。ThunderAgent早期版本在生成时偶尔会把布尔列表生成成整数列表导致过滤逻辑完全失效。这个问题根子不在模型身上而在中间表示的槽位定义不够严格。如果中间表示里规定了这个输入槽位接收List[bool]那么生成器在填充这个槽位时就必须找到一个输出类型匹配的节点。我现在的做法是给每个节点模板添加一层端口类型契约在任何连线操作前先做静态类型检查。一旦检查发现类型不匹配就自动插入一个类型转换节点比如 Number.ToInteger、Object.ToString 等等。这个机制听起来微不足道但它极大减少了生成脚本的运行期报错。以前十个生成脚本里大概有两三个会跑出类型错误加了这个契约检查之后几乎很少看到了。4.3 自定义节点的解析黑盒第三个坑比较冷门但也很多人会碰到自定义节点Custom Node的解析。ThunderAgent初始版本只认识Dynamo官方节点和常用第三方包节点一旦遇到项目里自己封装的自定义节点它就完全不认识。而自定义节点在真实项目里恰恰是主力工具——每个BIM团队都会封装自己的一套门窗标记、管线翻弯、管综调整节点。不认识就乱来。早期我遇到过一种情况ThunderAgent拿到一个项目后把自定义节点识别成一个GenericObject类型然后在下游强行插入了一个类型转换节点结果运行直接报错。更麻烦的是自定义节点内部可能是另一个完整的Dynamo图它的输入输出端口语义比较复杂很难从外部推断。解决思路是给ThunderAgent增加一个自定义节点学习机制。在项目初始化时用户可以把本地自定义节点目录导入进索引ThunderAgent会读取这些节点的名称、端口定义、描述信息把它们注册进节点映射表。如果是Dynamo的Python节点还能进一步读取源码注释提取更丰富的语义信息。这样生成时遇到自定义节点它至少知道该传什么类型的数据进去而不是瞎猜。4.4 上下文超长与中间遗忘第四个坑来自LLM本身的局限性上下文超长导致的中间遗忘。当我们需要让ThunderAgent生成一个上百个节点的复杂脚本时对话里的指令多轮叠加上下文很容易超过模型的上下文窗口。一旦超长模型就会忘记更早之前用户提过的一些关键约束生成出的图后面是对的前面是错的。我遇到过一个最典型的例子用户先说了门窗分开导出两个表格后面又追加了每个表格里再加项目编号列。结果生成的图里项目编号只加到了第二个表格第一个表格还是旧结构。原因就是模型在生成第一个表格的逻辑时已经忘记了后面追加的那句话。这个问题的解法不是把上下文窗口无限拉大而是改变工作方式把长任务拆分成多个短任务每个短任务只生成一个子图最后用固定的拼接规则把子图组合起来。每个短任务的上下文都很短模型不会遗忘。代价是子图之间需要预先定义好接口协议接错就会出问题。但这个代价是值得的因为稳定性的优先级远高于生成的自由度。5. 让ThunderAgent跑得更稳的几条经验5.1 模板注入与Schema约束经过几轮项目测试我总结出几条让ThunderAgent输出更稳定的经验。第一条是模板注入。每次发起生成前先向模型注入一段固定的、描述Dynamo节点结构和数据类型体系的系统提示词。这段提示词不需要很长但一定要包含核心约束所有节点名称必须来自提供的节点列表、所有连接必须遵循端口类型匹配原则、所有数值类输入必须标注单位。这段提示词的作用相当于给模型一道思想的防火墙。没有它模型会自由发挥生成一些结构上根本不可能直接运行的图。有了它模型的输出会被限制在一个合理的范围内。我在实际测试中对比过没有模板注入时生成质量方差很大有时很好有时完全不能用加上之后虽然偶尔还会有小问题但整体可用性大幅提升。5.2 语义缓存与相似意图复用第二条经验是缓存相似意图的生成结果。实际工作中很多人在Dynamo里反复做的是同一类任务比如导出当前视图所有图元参数到Excel按房间拆分并统计面积。这些任务只是参数名称不同结构完全一样。ThunderAgent里我加了一层语义缓存当新指令和某条历史指令的语义相似度超过阈值时优先复用历史生成过的图结构只替换其中的参数名称和过滤条件。这样既节省了生成时间又避免了同一个逻辑每次生成的图都不一样的维护难题。第一次生成可能是50个节点的图第二次生成因为复用了模板变成了42个节点第三次可能是55个节点——如果每次都重新生成团队成员根本没法维护。简单算笔账会更直观一条生成指令在本地模型上跑大概十几秒如果有缓存零秒就完成。从项目整体效率看这省下来的时间是很可观的。5.3 分步执行与图合并的取舍第三条经验和前面提到的拆分子图相关但这里我想多讲一下图合并的取舍。实际项目中你往往不会只让ThunderAgent生成一个图而是连续生成多个图最后想把这几个图合并成一个完整流程。这时就面临一个选择是直接把两个图首尾相接还是把每个图单独保存成子图然后调用我这两个方案都用过心得是图的规模决定选择。如果两个图加起来节点数不超过80个直接首尾相接运行效率高排查也方便。如果超过80个甚至上百个首尾相接会变成一个庞然大物任何一处出错都很难定位。此时应该把每个图先封装成自定义节点然后在主图里用两个自定义节点调用。自定义节点内部可以单独调试主图只保留两条清晰的调用线可读性和可维护性都会更好。5.4 给团队落地时的人员分工建议最后聊一点团队落地的经验。很多人拿到ThunderAgent这样的工具第一反应是让所有人都去用自然语言生成脚本。这个想法不太现实。实际落地时我建议把团队分成两层脚本维护者和脚本使用者。脚本维护者是少数懂Dynamo、懂数据逻辑的人他们负责审核ThunderAgent生成的图结构修正问题把常用的生成结果固化成模板或自定义节点。脚本使用者是大多数设计人员他们不直接面对ThunderAgent的生成细节而是通过一个预设好的指令模板菜单选择自己需要的功能。这样设计的好处是维护者负责质量使用者负责效率各司其职不会出现所有人都在和Agent对话、各自的脚本风格千奇百怪的混乱局面。我自己在这个项目里最深的体会是ThunderAgent这类工具真正的价值不是让不会写Dynamo的人突然会写了而是让会写的人从重复劳动里解放出来把精力花在真正需要判断力和设计经验的地方。工具越来越聪明是好事但定义需求、把控质量的那双手永远得是人自己。