Revit-RAG:在 AI 和用户之间搭一座桥
引言
写在 Revit 2027 发布之后
Autodesk Revit 2027 AI Assistant 已于近日更新。新的 AI 能力已经可以更准确地拆解用户需求,引导用户选择所需数据,同时也开放了与 Claude、Cursor 等工具连通的权限。
也正因为如此,我更希望这个项目能够给一些非程序员背景的工程师一点启发:即使不直接写传统代码,也可以尝试制作属于自己的 Tools,并把它们真正应用到 Revit 工作流中。
当然,这篇文章也多少有些”拖延症”——文章一直拖到 Revit 2027 发布之后才写完,期间一些原本想讨论的技术方向已经被官方能力覆盖了。:)
框架介绍
如果你最近用过 Claude、GPT-5、Gemini 这些主流大模型写代码,你大概会发现一件事:模型本身的代码生成能力已经不差了。 在大多数语言、大多数框架下,写出语法正确、结构合理、考虑了边界情况的代码——这些模型的准确率早就到了 90–95%,但只要你让它写 Revit 代码,会出现一些错误的情况。
不是因为模型不会写 C#,而是因为它够不到项目里的数据和准确的API。由于没法真正打开你的 .rvt 文件,看不到你项目里实际加载了哪些族类型,不知道你当前选中的元素是哪个,更不知道你公司的命名规范长什么样。
而模型在面对”我不知道”这件事时,有一种顽固的默认行为——它会猜,而且会猜得很自信。
举一个我观察过太多次的例子。你让它”创建一根 300x300 的混凝土柱”,模型大概率会写出这样的代码:
1 | var symbol = new FilteredElementCollector(doc) |
这段代码看起来非常合理。300x300 在 Revit 项目里是一个极其常见的混凝土柱规格,大量训练数据里都出现过这个尺寸。模型不是在编造一个完全离谱的值,它是在用训练数据里最常见的值做默认假设。
但你的项目里可能根本没有叫 “300x300” 的族类型。可能你公司的命名是 “KZ-300×300”,可能用的是 “矩形柱_300_300”,也可能是 “Rect-300x300mm”。模型不知道,于是它选了一个”看起来最像”的——结果运行时拿到 null,或者更糟,拿到了一个名字里碰巧包含 “300x300”、但并不是你想要的族。
这就是 AI 写 Revit 代码最核心的失败模式:不是代码错,是默认值错;不是不会写,是在缺数据的位置选择了”猜”。这种错误隐蔽、概率高、调试成本大——代码能编译、能运行、有时候甚至能产生看起来对的结果。
revit-api-rag 解决的就是这件事。它不是想让 AI 变得更会写代码,AI 已经够会了。它要做的是另一件事:
作为 AI 和用户之间的一座桥,把 Revit 内部那 5–10% 模型本来够不到的真实数据,结构化地、可信地、低延迟地送到生成过程里——让 AI 不再需要”猜”。
具体来说,这个框架做四件事。
第一,作为 AI 和用户之间的桥梁。 用户的请求往往是模糊的(”创建一根柱子”),AI 输出需要的却是精确的(哪个族、哪个标高、哪个坐标)。这中间的信息差,要么由用户反复澄清填补,要么由 AI 凭训练数据猜。两种方式都不好。框架的桥梁作用是把项目里的真实数据自动注入进来,让用户不用反复澄清,让 AI 不用乱猜。
第二,为最终生成提供更准确的上下文。 通过 RAG 检索真实的 API 签名、SDK 示例,再加上对活 Revit 文档的查询,注入到 prompt 里的不再是”模型记忆里的 Revit”,而是”你这个项目里真实存在的 Revit”。
第三,不同模型在不同环节各司其职。 Embedding 用 OpenAI 的 text-embedding-3-large,rerank 用 Cohere rerank-v3.5,主生成用 Gemini,意图判断和轻量任务用更小的模型。每一步用最适合的模型,不强求一个模型解决所有问题。这种组合方式比”全部用 GPT-4”或”全部用 Claude”既便宜又准。
第四,框架本身也是一个学习样本。 如果你也在做”领域知识 + AI 自动编程”这类系统,这套实现里包含的——原始资料的剪枝清洗、embedding 流程、Agent 调用、Workflow 编排、Skill 封装、Tool 生成与复用——每一块都尽量保持了独立性,可以单独参考、抽出来用。

一、让”有逻辑、没代码”的人参与编程
当 AI 写代码的准确率被推到 95% 以上之后,那些不会编程但有深厚领域经验的人,是不是终于可以参与到自动化工作里了?
设计行业的真实情况
我所在的 BIM / 结构工程行业里,绝大多数从业者每天都在做”参数化”的工作——只是他们不写代码:
- 结构工程师计算配筋时的逻辑:根据跨度、荷载、抗震烈度查规范、套公式、出结果
- 建筑师做立面方案时的逻辑:模数、对位、阴影分析、规范限高
- 室内设计师做柜体时的逻辑:人体工学尺寸、五金件位置、板材排布
- 机电工程师布管时的逻辑:管径计算、避让规则、坡度要求
这些人都有非常清晰的”算法思维”。 他们每天都在做”输入参数 → 应用规则 → 得到输出”的事——这本质上就是程序员的工作模式。
但他们卡在一个完全不属于自己专业领域的地方:程序语言的语法。要把”梁配筋逻辑”写成 Revit 的 C# 自动化脚本,需要学 OOP、Revit API、LINQ、Transaction 模式——这些跟”懂结构”完全是两个学科。
过去三十年解决这个问题的方式只有两种:
- 学编程:少数有兴趣、有时间的工程师转型,但成本极高,且把 80% 的时间花在”语法”而不是”领域”上
- 找开发者:公司养二开团队,工程师把需求口述出来,开发者写代码——回到了引言里画的那个低效闭环
第三种路径——“AI 帮我把领域语言翻译成代码”——以前一直存在,但准确率太低。60% 在专业场景里几乎不可用,因为剩下 40% 的失败需要懂代码的人来调,需求方还是得找开发者。
AICoding改变了什么
当AI Coding能稳定到 95%+ 时,发生了一个质变:
失败的那 5% 不再需要”懂代码的人”,而是”懂领域的人”。
这 5% 的失败大多不是 C# 语法错,是领域意图描述不清——比如用户说”梁不能太密”,但没说”密度不超过多少”。这种问题不需要程序员调代码,只需要 BIM 工程师把需求再说清楚一点,AI 重新生成一遍就好了。
这一刻,“会不会写代码”不再是参与编程工作的门槛。门槛变成了:
- 你能不能把你的领域逻辑说清楚?
- 你能不能在出问题时判断是逻辑错了还是表达错了?
- 你能不能验证 AI 给出的结果是否符合你的领域规范?
这些恰恰是资深领域专家最强的能力——他们做了二十年这件事,逻辑、判断、验证早就内化了。
这个框架解决的是”跨行业编程”问题
我现在愿意用一个更准确的说法来形容这个框架:
它不是让 AI 替代程序员,而是让一位结构工程师可以直接调用程序员才能写出来的代码——而不需要先变成程序员。
每个行业都有自己的”代码”——结构工程师用规范公式,会计用 Excel 函数,律师用法条引用,医生用诊断流程图。这些都是不同形式的”程序”。但真正的代码(C#、Python、JavaScript)只是其中一种——它是给计算机看的代码。
设计师、工程师、医生这些人,已经掌握了自己行业的”代码”。他们缺的不是逻辑能力,是把自己行业的代码翻译成计算机代码的能力。
AI 在 95%+ 准确率上做的事,本质就是这个翻译。把翻译质量推到这个程度需要做哪些事——RAG、Dynamic Choices、Tool Solidification、Skills、抵抗训练等等——是这篇文章后面要讲的内容。但翻译做到位之后,那些”有逻辑但不会写代码”的人,第一次真的能参与到自动化工作里——不是作为需求方,是作为作者。
愿景
它意味着每个行业的资深从业者,在不学编程的前提下,可以把自己几十年积累的领域知识、规范判断、专业直觉,直接转化为可执行的工具——给自己用,给同行用,给后来人用。
一个结构工程师可以把自己的”配筋判断逻辑”做成一个工具,团队里的新人调用一次就能复刻他二十年的经验。
一个建筑师可以把自己的”立面美学规则”做成一个工具,给项目其他部门用。
一个室内设计师可以把自己的”柜体设计 know-how”做成一个工具,自动适配不同的户型。
这个框架里有两层专门为这件事准备:
Tool Solidification让设计师”做一遍”成功的,AI 把它固化为可复用模板;
Skills让设计师”写一段规则”,AI 在做相关任务时自动遵循。两条路径互补——前者沉淀执行步骤,后者沉淀判断原则——通过工具库与规则库在团队、行业里流传下去。
它放大的是人的领域专业度,而不是替代它。
二、AI 写 Revit 代码的失败的原因
引言里我提到 AI 在缺数据的位置会”猜”。这一节我把这个根因拆开讲。它在实际代码里有三种典型表现形式,看起来不一样,本质是同一件事。
一:用训练数据里的”常见值”做默认假设
这就是引言里那个 300x300 柱子的例子的延伸。它不只发生在族类型上,还会发生在很多其他地方。
模型常常会写出这样的代码:
1 | // 默认填了一个"看起来像样"的标高 |
这些值都不是凭空捏造的。它们都是真实 Revit 项目里高频出现的值——所以模型从训练数据里学到了”创建柱子时,柱距大概是 6000,层高大概是 3000,标高大概叫 Level 1 或 1F”。
问题是:用户的项目不一定符合这些”统计常识”。 用户公司的标高命名规范可能是 “B1F / 1F / 2F”,可能是 “L01 / L02”,也可能是中文 “首层 / 二层”。模型按”训练数据最常见”猜的那个默认值,命中率比想象中低很多。
而且这种错误最难调试——代码能跑、能产出结果,只是结果不对。
二:.First() 假装做选择
当模型被迫在一个集合里选一个,但又不知道该选哪个时,它会写:
1 | var symbol = collector.OfClass(typeof(FamilySymbol)) |
.First() 在这里起的作用,是”我必须给出一个值,所以我从集合里拿第一个”。但用户真正想要的是某个具体的族类型,不是”集合里的第一个”。
这种代码不是错在语法,是错在它假装在做选择,实际上是在掩盖一个”模型本来不知道的事实”。语义上它和上一种”猜常见值”是一样的——都是模型用一种”看起来合理”的方式回避了”我不知道”这件事。
三:编造看起来合理的 API 签名
1 | // 模型生成 |
这种错误也是同一类——模型记得 Wall.Create 这个名字,但具体参数顺序记不清,于是它按”通常这种 API 大概长什么样”补齐。在 Revit 这种 API 表面积巨大、版本之间会变签名的库里,这种”按经验推测”的概率不低。
还有一类:忽略执行环境
最后还有一类失败和上面三种略有不同——它不是”猜错了”,是”完全没考虑到”:
- 模型自己
using (Transaction t = new Transaction(...)),但插件端已经统一包了一层事务 → 嵌套报错 - 把 6000 当作 feet 直接传给 API,最后建了一面 1800 米高的墙
FamilySymbol没Activate()就直接用,调用时抛异常
这一类问题的根因不是”缺数据”,是”模型不知道这个执行环境有什么不成文的约束”。但解决思路类似:该由系统约定的事,不要交给模型临场判断。
共同的根因
把这四种表现拼在一起看,根因就一句话:
模型被要求做它做不了的决定时,它会用训练数据里的”常见模式”来填空。
填出来的东西看起来合理,因为它确实在统计上很常见。但”统计上常见”和”你这个项目里真实存在”完全是两件事。
这就是为什么单纯改 prompt 没用——你可以告诉模型”不要猜”,但你没办法告诉它”你这个项目里实际有什么”。这必须由系统层面去补。

三、把 RAG 当作信任基础设施,而不是”加点资料”
很多人对 RAG 的理解停留在”给模型加点背景知识”。但在这类自动编程系统里,RAG 真正的作用是另外一件事:
它为代码生成划定了可信边界。
模型不是在自由发挥,而是在一组已经验证过、结构化清洗过的 API 候选里做组合。它能写什么,取决于检索阶段把什么放进了上下文。
这个视角的转变会改变后续所有设计:
- 如果 RAG 是”加资料”,那把文档塞进 prompt 就够了;
- 如果 RAG 是”划边界”,就必须思考:怎么保证检索出来的东西是 API 真实存在的?怎么保证版本是对的?怎么保证关联对象(
Level、FamilySymbol、BuiltInCategory)也一起被带上?
这也是为什么我没有用最简单的”切片 + embedding”做法,而是做了一条四层流水线。

四、给模型补两类数据:API 知识 + 项目状态
到这里 RAG 的”划边界”思路有了,剩下就是工程问题:怎么把对的边界画出来。
具体做的是两件事——
- 用 RAG 流水线给模型注入 API 知识(这个方法存不存在、签名是什么)
- 用 Dynamic Choices 给模型注入 项目状态(你这个项目里实际有什么)
两件事缺一不可。前者补的是”模型记不准 API 细节”的问题;后者补的是”模型够不到运行时数据”的问题——也就是引言里那个 300x300 的根因。
RAG 流水线:四层做的事
整条流水线的核心思路是把”不靠谱的部分”前置到离线阶段做,运行时只做轻量、稳定的事:
1 | 自然语言 → Query Rewrite → Dual Search → Rerank → Hydrate → 注入生成 |
每一层解决一个具体问题:
- Query Rewrite:用户说”创建一面墙”,向量库里存的是
Wall.Create、WallType、Curve。先让 LLM 把用户语言改写成含 API 关键词的扩展查询,把”语言鸿沟”补上。 - Dual Search:API 文档(
revit_api)和 SDK 示例(revit_sdk)分两个独立索引并行召回。前者负责”这个方法存不存在”(约束),后者负责”通常这件事怎么写”(示范)。两者起的作用不同,不能混。 - Rerank:向量相似不等于语义相关。Cohere 的 rerank 模型把”差不多”过滤成”对”。经验上这一步对最终质量的提升比换更大的 embedding 模型明显。
- Hydrate:向量库只存 id 和 embedding,完整字段(方法签名、参数列表、remark、关联示例)从 SQLite 取回——向量库做召回入口,SQLite 做完整存储,职责分清楚。
设计目标不是”召回得多”,是”召回得准、且精确反映 API 真实存在的样子”。

Dynamic Choices
但 RAG 解决不了一类问题——模型不知道你当前打开的 .rvt 项目里有什么。
考虑这个请求:”在一层创建两根结构柱”。
模型要生成的代码会用到至少两个”项目级实体”:一个具体的 FamilySymbol(结构柱族类型)、一个具体的 Level(一层)。这两件东西不存在于 API 文档中,也不存在于 SDK 示例中——它们存在于用户当前打开的 Revit 文档里。
如果模型不知道这些,它就只能编造一个名字、用 .First() 凭空抓一个、或者把它当作必填参数让用户补——三种都不理想。这就是引言里 300x300 失败模式的根因。
Dynamic Choices 的做法是:在调用 LLM 生成代码之前,先通过插件端查询当前文档,把”项目里实际可用的候选”作为上下文一并交给模型:
1 | 用户: "在一层创建两根结构柱" |
把”猜测”换成”查询”。生成出来的代码不再依赖 .First(),而是直接引用真实存在的元素。
更重要的副作用:当上下文里写着”项目里有 W12X65 / HSS6X6X3/8 / 矩形柱-300x300”时,模型不会再去编造一个不存在的族类型名。它的输出空间被真实数据物理上收紧了——这一点在抵抗模型默认行为上比任何 prompt 提示词都管用(这个机制第八章会专门讲)。

五、Tool Solidification
到这里为止,整条链路已经能比较稳定地把一句自然语言变成可执行代码。但还剩最后一个问题:
每次都从零生成,重复劳动有没有办法消除?更进一步——能不能让系统从执行历史里”学会”什么是稳定模式?
LLM 在固化时做的事:识别”什么是参数,什么是结构”
让 LLM 阅读多次成功的执行记录,回答一个具体问题:
这些代码里,哪些值是 dynamic parameter(每次都变的),哪些是 static structure(每次都一样的)?
举例:今天用户说”从 A 到 B 创建 3 米高的墙”,明天说”从 C 到 D 创建 2.5 米高的墙”。LLM 以 API 为依据,在获得动态变化的参数后,会得出:
- Dynamic parameters:起点坐标、终点坐标、高度、墙类型、标高
- Static structure:API 调用顺序、单位换算(mm → feet)、Transaction 包装、错误处理
然后 LLM 把 dynamic parameters 抽出来作为接口参数,把 static structure 保留为 code template,输出一个结构化的工具定义:
1 | name: create_wall_by_two_points |
这个判断不是简单的”找字面量替换成变量”——而是基于多次执行对比哪些值真正在变。如果一个值始终是 304.8(mm 转 feet 的换算系数),LLM 不会把它参数化;如果一个值在不同请求里反复变化(如坐标、高度),它就会被识别为参数。
随着固化流程持续跑,新的执行还会继续观察已固化工具的 dynamic parameter 范围——比如某个工具最初只支持 single level,但实际执行中发现用户开始传入 level list,系统会触发参数定义的扩展。工具不是写一次就定型的,是会随着使用持续演化的。
复用:在系统内 + 通过 MCP 对外
工具固化之后,下次遇到结构类似的请求:
- 系统先匹配工具库
- 命中后直接填参数执行
- 整个过程零 LLM 调用,毫秒级返回
但更重要的是——这些 YAML 工具的结构本身就直接对齐 MCP 协议。name、description、params schema 几乎就是 MCP 工具定义需要的全部字段。这意味着工具库不只在 RAG 系统内部用:
1 | revit-api-rag 工具库(YAML) |
这条通路把第十二章讲的 RAG/MCP 融合具体落地了——程序员用 MCP 客户端探索新场景 → 在 RAG 系统里多次执行并验证 → 固化为工具 → 自动反向暴露给所有 MCP 客户端。一个工具沉淀下来,所有人都能用。
固化层不只是 RAG 系统的”缓存”——它同时是面向整个 AI Agent 生态的工具供应来源。系统使用得越久,对外提供的工具就越多。
工具库的增长在某种意义上就是系统的增长。每一次成功执行不再是一次性产物,而是给整个生态增加了一份资产。

工具不健康时怎么办
工具会有失效的时候——参数模型变了、用户的 Revit 版本不兼容、某个边界条件触发了 bug。所以工具库不只是写入,还要监控:
- 连续失败超过阈值 → 工具标记为”不健康”,下次回退到 RAG 重新生成
- 长期未使用 → 降权,避免误匹配
- 命中率低 → 重新检视参数定义是否过宽
- Dynamic parameter 范围突变 → 触发参数 schema 扩展或工具拆分
这部分属于工程细节,但它决定了工具库能不能长期运转。没有健康监控的工具库会随时间退化为一堆半失效的代码。
六、Skills:让用户把”领域规则”直接写给 AI
到这里,系统已经把”通用 API 知识”(RAG)、”项目状态”(Dynamic Choices)、”成功路径”(Tool Solidification)三件事都做了。但还有一类知识没覆盖——
每个团队、每个项目都有自己的规范、约定、最佳实践。这些东西写不进 RAG(因为不是 API 知识),也存不进 Tool 库(因为不是具体执行步骤)。它们是”在做这类事时应该遵循什么原则”的高层规则。
举几个具体例子:
- 一家结构事务所的内部规定:”柱距不应超过 9 米,超过的需要标注审核”
- 一个 BIM 团队的命名规范:”族类型用 KZ-300×300 这种格式,不允许其他写法”
- 一个项目的图纸约定:”立面图视图的尺寸标注必须显示到毫米”
这些都不是 Revit API 文档里能查到的事情,也不是某个 SDK 例子能演示的。它们是专业人员的”经验沉淀”——以前都活在 PDF 规范、Word 文档、群聊截图里,AI 接触不到。
Skills 是为这件事新加的一层:让用户用自然语言写规则,AI 在做相关任务时自动遵循。
Skills 是什么
每个 Skill 是一个 Markdown 文件,放在项目的 .ai-rules/skills/<skill-name>/SKILL.md 路径下。结构非常简单:
1 | # 柱距标注规范 |
就这么直接。没有特殊语法,没有 YAML 配置,就是 markdown。任何能写 Word 规范文档的人都能写。
Skills 怎么被系统使用
当用户说”在二层创建柱网”时,系统会:
- 像往常一样做意图识别 →
create_column_grid - 新增一步:在 Skills 库里做语义匹配,找出适用的 Skill
- 命中”柱距标注规范”这个 Skill
- 把整段规则塞进生成 prompt 里
- 代码生成时模型会自动遵循——比如生成的代码会真的检查柱距、对超过 9 米的标记审核标志
整个流程对用户是透明的。他们写一次规则,之后所有相关任务都会自动遵循。
为什么 Skills 是”原则层”而 Tool 是”执行层”
回到第一章关于”受益者”的论点——让设计师参与编程,最大的障碍是”如何把领域知识传给 AI”。
之前系统给的答案是 Tool Solidification:让设计师做一遍正确的,AI 把它固化为模板。 这条路有效,但有个限制——它只能固化”具体的执行步骤”,固化不了”高层规则”。
Tool 解决的是:”这件事按这几步做”
Skills 解决的是:”这类事应该遵循什么原则”
举个对比:
- Tool:”创建柱子的执行步骤” — 用 FilteredElementCollector 拿族、激活、放置、返回 ElementId
- Skill:”柱距规范” — 不管具体步骤是什么,柱距超 9 米就要审核
Tool 是细颗粒的”执行模板”,Skills 是粗颗粒的”判断原则”。两者结合,才是完整的领域知识沉淀。

Skills 才是设计师真正最低门槛的入口
Tool Solidification 需要设计师做一遍正确的(再让 AI 固化);Skills 只需要设计师写一段规则就行。
这两个门槛差别很大:
- “做一遍”意味着要走完整个交互流程、验证结果、确认无误——半小时起步
- “写一段规则”只需要打开一个 markdown 文件,把心里早已有的判断写下来——10 分钟以内
后者的门槛低到几乎和写 Word 规范文档一样。
我观察过几个使用过这个系统的设计师:他们最先用上的不是 Tool 固化,是 Skills。“写规则”是他们最熟悉的工作方式——他们整个职业生涯都在做这件事,只是过去这些规则只能写在 PDF 规范里给人看,现在写一遍就能让 AI 按规则做事。
这是 Skills 最有意思的地方——它不要求设计师改变工作习惯,反而把他们最熟悉的”写规范”动作直接接到了 AI 上。
七、系统里的几个 Agent:各司其职 + 抵抗 LLM 的”乐于助人”本能
到这里我讲了系统的几个知识层:数据层(RAG)、事实层(Dynamic Choices)、执行复用层(Tool Solidification)、原则层(Skills)。但还没讲清楚这些层之间是怎么串起来的——也就是 Agent 协同这一部分。
为什么不用一个大 LLM 调用做完所有事
一个直觉的做法是:把整个流程压缩成一次 LLM 调用——把用户输入、API 文档、Revit 项目数据一股脑塞进去,让它”端到端”输出可执行代码。
我试过,结果并不符合要求。原因不只是上下文太长(虽然这也是问题),更根本的原因是:不同任务对模型的要求差别很大。
- 意图分类要快、要便宜、要 deterministic(同样的输入要同样的输出)
- 数据审计要长上下文、能比对源文件
- 代码生成要稳、要准、要懂语义
- 参数化抽取需要语义理解,但不需要写代码能力
用一个模型做所有事既不经济也不准。拆成多个 Agent,每个 Agent 选最合适的模型、用最合适的 prompt——这才是这套系统能跑得动的根本原因。
下面讲几个核心 Agent,每个都说一下它解决的问题、用的模型、关键的 prompt 设计。
1. Orchestrator(总控状态机)
不是 LLM——是一个状态机。它根据当前对话状态决定下一个 Agent 是谁:意图还没识别就调意图 Agent,参数还没收齐就调槽位 Agent,所有信息齐全就调代码生成 Agent。
Orchestrator 自己不调用任何 LLM,它的存在是为了让其他 Agent 可以保持单一职责。这是一个很容易被忽略但非常重要的工程决策——LLM Agent 之间的协调逻辑不应该交给另一个 LLM,因为状态机的行为应该是确定的、可调试的。
2. Intent Classification Agent(意图识别)
这是第一个 LLM Agent,也是第一个决策点。它要回答的问题就一个:
用户这次想做的事属于哪种交互类型?
我把所有 Revit 操作分成三类:
- DIRECT:单步直接操作(删除、查询、修改属性)——不需要族类型选择
- SELECT_FAMILY:需要选族类型的创建(结构柱、家具、设备)
- SELECT_BOTH:需要宿主 + 族类型的创建(窗户在墙上、门在墙上)
这个分类有一条强约束:任何创建物理元素的操作绝不允许分类为 DIRECT。如果模型试图说”创建柱子是直接操作”,那一定是错的——创建必须先选类型。
用 Gemini Flash 做这件事,因为它快、便宜、足够准。LLM 不可用时(限流、超时)有正则关键词回退,简单分类够用。
3. Slot Engine Agent(槽位填充)—— 系统里最复杂的 Agent
这是整个系统里 Prompt 最长、规则最多的 Agent。它要做的事是:
通过对话,把生成代码所需要的全部参数都收齐——但绝不允许模型自己猜任何一个值。
一个反直觉的设计选择
最开始我做这件事时是用传统的 Slot Filling 思路:为每个意图维护一份 YAML 槽位定义,写明每个参数的名字、类型、验证规则。这套东西在我那个版本里大概有 974 行,后来全删了。
原因是:每加一个新操作都要手写一份槽位定义,维护成本随支持的操作数线性增长。而且对于”自定义操作”这种意图(比如用户说”创建一个坡道”,这是我没预先定义过的操作),根本写不了硬编码槽位。
新的做法是:让 LLM 阅读真实的 Revit API 文档(从 RAG 拿来的),然后自主决定该收集哪些参数。
1 | 用户输入"创建 3 根结构柱" |
之后用户每回答一个问题,不再调用 LLM——前端只是顺序填进 slots,等所有 slots 填满就进入下一阶段。这样首轮一次 LLM 调用,后续全是纯逻辑,延迟低、成本低。
这个 Agent 的具体 prompt 规则非常多——其中最关键的几条放到了下一节”抵抗训练”里专门讲。
4. Code Generator Agent(代码生成)
这是整个系统里输出质量最关键的 Agent,所以用的是最贵的模型(Claude)。
它的核心约束在第十章会详细讲——只输出方法体、不开 Transaction、必须用 document 变量、单位换算前置。这些约束加起来把模型的输出空间收得很紧,但仍然给它充分的代码逻辑发挥空间。
一个值得单独提的设计是 <thinking> 标签:让模型在写代码前先输出一段子任务分解:
1 | <thinking> |
这段思考链不只是为了 debug——它强迫模型在写代码之前先把整个流程在头脑里跑一遍,列出风险点。这种”先想再写”的结构对最终代码质量的提升非常明显。
5. Tool Solidification Agent(工具固化)
第五章已经详细讲过它做什么。这里补一点关于它内部逻辑的细节。
它的有趣之处在于不能只看一次执行就决定哪些值是参数。一次执行里,所有字面量看起来都”可能是参数”——但只有跨多次执行对比,才能真正分辨出 dynamic parameter(每次都变)和 static structure(每次都一样)。
这个判断不能用硬规则做,必须靠 LLM 的语义理解。Prompt 里我让它按下面这个步骤思考:
- 读取这一类请求的多次执行历史(每次的 thinking chain + 实际生成的代码 + user selections)
- 对比代码里每个字面量值在不同执行间的变化情况
- 对每个值问自己两个问题:“它在不同执行间是否真的在变?”(频率检测) + “如果换一个用户、换一个场景,它逻辑上应该变吗?”(语义判断)
- 两个答案都是 yes 才参数化;只有一个是 yes 需要标记为”待观察”,再积累几次执行再决定
第三步那个语义判断很重要——某些值可能在已有执行历史里恰好都一样(比如 5 次都用了同一个标高),但语义上它本应是参数。这种”统计上没变但语义上该变”的值如果被错过,工具会因为参数定义过窄而无法适配新场景。
这套逻辑也让工具能持续演化:当一个已固化工具被以”略微不同”的方式调用时(比如用户突然传入一组 levels 而不是单个 level),系统会重新触发参数 schema 评估,把这个工具升级为更通用的版本。工具不是一次性产物,是会跟着使用持续生长的。
6. 数据准备阶段的 Agent(离线)
最后简单提一下离线的几个 Agent,它们不参与运行时但决定了整套系统的下限:
- Stage-1 质量审计(Gemini Flash):扫 27,000 条 API 记录,按 8 条扣分规则给每条记录打分。重点是它衡量的是**”解析质量”**——HTML 里有的内容是否被解析进了 JSON——而不是”内容好不好”。这个区分很重要:删掉一条解析失败的记录会丢失 API 覆盖面,但解析对了但内容本身朴素的记录是合法的,应该保留。
- Stage-2 修复(Claude Sonnet):对低分记录读 HTML 源文件,按”只修不造”的原则修复——只补回 HTML 里有但 JSON 里没的字段,不凭空发挥。
- Golden Code 生成(Claude):从 SDK 的 200+ 项目里精炼出最有教学价值的代码片段,过滤掉 UI、日志、样板代码这些噪音。
这三个 Agent 加起来构成了”知识库的质量管控”,决定了 RAG 能不能拉到准的资料。它们的成本不算低,但只在数据更新时跑一次——典型的”用预处理换运行时质量”的取舍。
7. LLM Adapter:让多模型组合在工程上可行
最后一个不算 Agent 但很关键的模块:LLM Adapter。
整套系统用了多个模型——Gemini Flash 做轻量任务,Claude 做高质量生成,OpenAI embedding 做向量化,Cohere 做 rerank。如果每个 Agent 都直接调原生 SDK,代码会变成一团乱麻。
Adapter 做的事是把所有模型调用统一到一个接口下,并提供主备切换:
1 | primary: |
主模型 403(限额)、429(限流)、5xx、超时都会自动切到备用模型。这种容错在生产环境是必须的——任何一个供应商挂了都不应该让整个系统挂掉。
通过 OpenRouter 做统一网关,还顺带解决了一件事:支持中国网络环境。Gemini、Claude、OpenAI 这些 API 在国内访问稳定性参差不齐,统一通过 OpenRouter 走代理可以让部署变简单。
Agent 协同图景
把这些 Agent 串起来看,整体的数据流大致是:
1 | 用户输入 |
每一步都是松耦合的——任何一个 Agent 可以独立替换、独立测试、独立调优。这是多 Agent 架构最大的工程优势:复杂度被切成了可独立处理的小块,而不是堆在一个 prompt 里。

八、抵抗训练:怎么让模型拒绝它的”乐于助人”本能
这一节单独讲,因为它是这套系统能用的核心,也是引言里那个”AI 默认值/编造”问题的真正解法。
LLM 有一种你必须正视的行为模式:当它不知道某个值时,它倾向于”主动帮忙”——给一个看起来合理的默认值,而不是承认”我不知道”。这种行为是 RLHF 训练的产物。在通用对话场景里,”给答案”比”反复问问题”更受奖励,于是模型学到了”猜一个合理值比问用户更 helpful”。
但在 Revit 这种需要严格对齐项目数据的场景下,”主动帮忙”是灾难。引言里那个 300x300 柱子的例子就是典型:模型不知道项目里有什么族类型,于是它”贴心地”给一个训练数据里最常见的 300x300。代码能跑,结果错。
那么——怎么训练模型去违反这种本能?
这里说的”训练”不是 fine-tuning。这套系统里没有改一行模型权重,全部是 prompt 层面的行为约束。但效果上跟训练等价:通过 prompt 把模型从”主动猜”的默认行为,扳到”明确拒绝猜”的状态。
下面是几个真正起作用的技巧。
一:先把模型的”能力边界”明确告诉它
这是 Slot Engine 里最关键的一句 prompt:
You are NOT connected to a live Revit session. You CANNOT look up types, levels, or positions.
这句话表面上是事实声明,实际上是对模型角色的重新定义。
在没有这句话的情况下,模型默认假设它”无所不知”——它会说”使用第一个可用的柱类型”或”假设标高为 1F”。加了这句之后,模型从”全知顾问”变成”信息收集员”——它的工作不是给答案,是问问题。
这一句话的效果比在 prompt 后面写十条”不要猜”的提醒都强。因为它改的是模型的角色认知,而不是它的具体行为。当你定义了”我是谁”,”我该做什么”会自动跟进;反过来不行。
二:明确的 FORBIDDEN 列表 + “这是错误”标签
光说”不要猜”没用,模型会把它当作软建议。要把它写成硬约束:
FORBIDDEN behaviors (these are ERRORS):
- Picking a default type/family
- Inventing coordinates
- Assuming a level
- Picking StructuralType silently
(these are ERRORS) 这个标签很关键——它把”不应该做”升级成”做了就是 bug”。RLHF 训练过的模型对 “error” 这个词非常敏感,输出空间会显著收紧。
这种”显式禁令”比”隐式引导”有效得多。一个常见的错误是写”请尽量使用真实数据”——这种软引导模型基本会忽略。换成 “FORBIDDEN: 编造默认值 (this is an ERROR)”,命中率会肉眼可见地下降。
三:给模型一个”诚实”的出口
光禁止还不够,还要给它一条合规的备选行为:
For EVERY parameter, you MUST either:
a) Extract its EXACT value from the user’s input text, OR
b) Create a question for the user
这一步把”承认不知道”变成了一个正向行为。模型不再纠结”如果我不答,是不是不够 helpful”——它现在有明确的合规出路:问。
这条原则可以推广:禁止某种行为时,必须同时提供合法替代。否则模型会感受到”两难”,最后还是会选它训练时被奖励过的行为。
四:让 RAG 上下文出现在 prompt 里,物理上挤掉模型的”记忆”
这是一个不太被讲但非常重要的点。
模型的”幻觉”很多时候来自训练数据里的记忆——它”记得”Wall.Create 大概有几个参数,”记得”标高通常叫 Level 1。当 prompt 上下文里没有更具体的资料时,它就用这些记忆。
但当 prompt 里实际放进了 RAG 拉来的真实方法签名:
1 | NewFamilyInstance(XYZ location, FamilySymbol symbol, Level level, |
模型的”记忆”会被这段更具体、更近的上下文压制——它会优先使用眼前的资料而不是训练记忆里的版本。这就是 RAG 真正起的作用:它不只是给信息,是把模型的注意力从训练记忆拉到当前上下文。
五:Dynamic Choices
前面四个技巧是 prompt 层面的,最后这个是架构层面的。
无论 prompt 写得多好,如果模型真的”想”猜,它还是有空间的。但如果你在 prompt 里把项目里真实存在的族类型列表直接放进去:
1 | ## Available Family Types in current project |
模型就没办法猜 “300x300” 了——因为上下文里写得很清楚,能选的就这三个。这时候它的输出空间已经被物理收紧到合法选项之内。
这就是为什么 Dynamic Choices 不只是”加点信息”——它是抵抗训练的最后一道护栏。Prompt 约束是”软”的,可以被模型”创造性发挥”绕开;放进真实数据是”硬”的,模型没有发挥空间。
一个实测的对比
用同样的请求”创建一根 300x300 的混凝土柱”,分别在三种配置下测试:
| 配置 | 输出行为 |
|---|---|
| 纯 LLM(无任何约束) | 直接 .FirstOrDefault(s => s.Name.Contains("300x300")) —— 编造 |
| LLM + FORBIDDEN prompt | 仍有约 30% 概率会”创造性”绕过禁令 |
| LLM + FORBIDDEN prompt + Dynamic Choices | 100% 列出真实候选,让用户选 |
这组数据的结论很直接:单靠 prompt 约束不够,必须搭配真实数据注入。两者一起用才是抵抗训练的完整解法。

九、为什么不是 Agent + grep
在做这套系统的过程中,我反复想过的一个问题是:
既然现在 Agent 框架(Claude Code、Cursor 之类的)这么成熟,让 Agent 直接去 grep Revit API 文档、扫 SDK 源码,不就够了吗?为什么还要做一套 RAG?
这是一个有诱惑力的方案。Agent + grep 的好处是显而易见的:
- 不需要预处理资料,文档放在哪就在哪
- 不需要维护向量库,没有重新 embedding 的成本
- 灵活性高,能处理偏门、需要追查的问题
- 改资料就是改文件,立即生效
我花了一段时间认真考虑这条路,最后还是选了 RAG 作为主链路。下面是几个关键的判断点。
一、Agent + grep 适合”探索”,不适合”高频结构化查询”
Agent 框架真正的优势在哪里?我自己的体会是:它擅长在不确定的地方做深度追查。 当问题是”这个 bug 到底从哪来的”或者”这份资料里有没有提到某个偏门用法”,Agent 一边搜一边推理的能力非常强。
但 Revit 自动编程的核心任务不是探索,是高频结构化的 API 定位。用户反复请求”创建墙”、”获取参数”、”修改类型”这类操作,每次需要的都是同一类信息:方法签名、参数列表、相关枚举。这种场景下,让 Agent 每次重新 grep 一遍是浪费。
RAG 的做法是把这些”重复劳动”前置到离线阶段:清洗一次、embed 一次、之后每次查询都是纯检索操作。在高频场景下,这是用”预处理成本”换”在线响应速度”。
二、误差会沿链路放大
Agent 在文档里找到”差不多”的方法很容易——同名但签名不对、类名对了但命名空间不对,或者把旧版本的 API 用在新版本上下文里。这些误差单独看都不大,但一旦发生在检索阶段,后面的推理和生成会一直建立在错误前提上。
RAG 流水线里的 rerank、hydrate 这两步都是为了过滤这些”差不多”。把它们砍掉让 LLM 自己判断,错误率会肉眼可见地上升。
三、Agent 模式没有沉淀
这是我后来才意识到的、最关键的一点。
Agent 跑完一次任务,下一次还是从零开始。这和我前面讲的 Tool Solidification 是反向的:Agent 强调”每次动态推理”,Tool 强调”把验证过的推理结果固化下来”。
在我这个场景里,”每次动态推理”意味着每次都重新承担一次生成的不确定性。而 Tool 固化让系统用得越多越快、越用越稳定——这个复利在 Agent 模式下是不存在的。
我的结论
这两种思路不是替代关系,是分工关系:
- RAG + Tool:适合高频、结构化、对速度和稳定性敏感的主链路
- Agent + grep:适合低频、开放式、需要深入探索的疑难场景
我的主链路用 RAG,但保留了一个 fallback——如果用户提了一个 RAG 完全召回不到、工具库也没覆盖的偏门问题,可以降级到 Agent 模式去搜原始资料。这种”主链路稳定、边缘场景灵活”的搭配,比单押任何一种都更合适。
十、执行端:把约束放在该放的地方
知识层和检索层做得再好,最后还是要把代码送到 Revit 里跑。这一段也有几个不显眼但很关键的工程决策。
1. 服务端通过插件桥接,不直接控制 Revit
我的服务端不在 Revit 进程内运行。它通过一个本地 TCP / JSON-RPC 协议和 Revit 插件通信:
1 | [服务端 Python] ←→ [Revit Plugin (.NET)] ←→ [Revit Document] |
这种分离的好处是清晰的执行边界。服务端负责一切”生成”工作,插件端负责一切”执行”工作。两边通过协议解耦,调试、版本升级、错误隔离都更可控。
2. 生成器只输出方法体
这是 anti-hallucination 上最有效的约束之一。
我没让模型输出一个完整的 IExternalCommand 实现,而是只让它输出方法体:
1 | // 模型只生成花括号里的内容 |
为什么这样设计?
- 方法签名固定:参数、返回值都是确定的,模型不需要决定这些
- 不允许声明 Transaction:插件端统一包事务,模型不需要管
- 不允许 import 未授权命名空间:插件端预先 using 了允许的命名空间
- 必须返回 object:保持返回值序列化的一致性
把模型的”自由度”压缩到这个小框里,输出空间就被收得很紧。这比”在 prompt 里反复叮嘱”有效得多。
3. Roslyn 动态编译 + ExternalEvent 执行
插件端拿到代码后,会:
- 把方法体包装进一个固定的执行类
- 用 Roslyn 动态编译为内存程序集
- 通过 Revit 的
ExternalEvent机制在事务里调用 - 把结果序列化返回
这条链路里每一步都是可观测的。出错时可以精确定位到是编译失败、事务失败、还是 API 调用失败。这个粒度对调试和工具固化都很重要——只有能精确定位失败原因,才能决定是不是要把这次执行加入工具库。
十一、把整条链路串起来
1 | 用户输入 |
这条链路真正的价值不在于”用了多少模型”,而在于每一层都在解决一个具体的、可解释的工程问题:
| 层 | 解决的问题 |
|---|---|
| 数据层 | 知识源不准、噪音大 |
| RAG 层 | API 对齐、生成边界 |
| Dynamic Choices | 项目级实体依赖 |
| Tool 层 | 执行步骤的复用与沉淀 |
| Skills 层 | 团队/项目原则的沉淀 |
| 生成约束层 | 执行环境边界 |
| 插件层 | 事务、编译、单位 |
每一层都不是”为了用某个技术”才存在的。它们存在是因为前面那个问题确实会发生。

十二、RAG 模式与 MCP 模式的融合
讲到这里,整套基于 RAG 的设计已经讲完了。但还有一个值得展开讨论的问题——这套方案和现在越来越流行的 MCP(Model Context Protocol)模式之间是什么关系?
我自己在做这个 RAG 项目的同时,也在维护另一个 MCP 项目(revit-mcp-net)——一个独立的 MCP 服务器,让 Claude Code、Cursor 之类的 AI Agent 能直接连进 Revit 操作。
我想说明白:这两种模式不是对立关系,而是各自擅长不同的场景。下面把它们的特点摊开讲,再讲怎么融合起来用。
两种模式各自擅长什么
RAG 模式(这个项目):把领域知识结构化、预先索引、运行时检索注入。整个流程是确定的、可调试的、低延迟的。擅长:
- 高频结构化任务(创建墙、放柱子、改类型)
- 对响应速度敏感的工作流
- 需要稳定一致输出的场景
- 设计师友好——结构化 UI 引导,零代码门槛
MCP 模式(Claude Code、Cursor 等接 MCP 服务器):让 AI Agent 通过标准协议直接访问外部工具/服务。Agent 自己决定调用什么工具、什么时候调、如何串联。擅长:
- 开放式探索(”看看这个项目的柱布置有没有规范问题”)
- 跨多个系统协作(Revit + Excel + Email 一起完成)
- 处理之前没遇到过的复杂场景
- 程序员友好——灵活、强、Agent 临场推理
融合的工作流程
举一个具体的工作流:
1 | [程序员] 用 Claude Code + revit-mcp-net 做开放式探索 |
这个流程把两种模式的优点都拿到了:
- MCP 模式做”探索”:程序员有充分的灵活度去搞定复杂的、之前没遇到过的问题
- RAG 模式做”分发”:探索的成果通过 Tool/Skills 沉淀,设计师可以直接复用
换句话说——
MCP 让一个程序员能解决一类复杂问题;RAG 让一个程序员的成果,能被一百个设计师复用。
融合让设计师和开发工程师都能参与进来
回到第一章的核心论点——“让有逻辑、没代码的人参与编程”。
仅靠 RAG 模式,能让设计师在已有工具范围内自动化。但当设计师遇到工具库还没覆盖的复杂场景,他们就卡住了。
仅靠 MCP 模式,程序员可以解决复杂场景。但解决方案往往是”一次性的”——下次别的设计师遇到类似问题,程序员还得再来一遍。
把两者打通之后:
- 程序员用 MCP 模式做前沿探索 → 解决最难的、最新的问题
- 沉淀机制(Tool + Skills)把成功路径转化为可复用资产
- 设计师用 RAG 模式调用这些资产 → 不需要重新发明轮子
每一类人都在做自己最擅长的事。程序员发挥探索能力,设计师发挥领域判断能力,沉淀机制做翻译。
我觉得这才是 AI 时代的”人机协作”应该长的样子——不是一个工具一统天下,而是一个生态:探索层、沉淀层、复用层各自有最适合的人。

工程上的连接点
技术上 RAG 和 MCP 怎么具体融合?最直接的方式是把 MCP 服务器作为”sandbox 之上的 fallback 通道”:
1 | 用户请求 |
这个链路里,RAG 是”日常路径”,MCP 是”探索路径”,两者通过 Tool/Skills 沉淀相互喂养。系统会随着使用越来越完善——程序员探索的边界越广,设计师能用的工具就越多。
revit-api-rag 现在还没有把这个 MCP fallback 完整实现——这是接下来一个值得做的方向。但架构上已经留好了入口:sandbox 之后的 fallback 钩子可以直接接到 revit-mcp-net 或任何其他 MCP 服务器上。
这种”双模式 + 沉淀桥梁”的架构,我觉得不只适用于 Revit。任何”专业软件 + AI 自动化”的场景,只要同时存在懂代码的人和不懂代码的领域专家,这套结构都可能适用。
十三、一个案例
如果你正在学习怎么搭一套类似的系统,下面这几块都可以单独抽出来看:
- 原始资料剪枝与 embedding:
pipeline/api_parser/里有完整的 .chm 解析、HTML 噪音清洗、结构化对象抽取流程。这部分的核心不是”用了什么 embedding 模型”,是如何把脏数据变干净——大多数 RAG 项目失败在这一步,不是失败在向量库。 - 多模型组合:embedding 用 OpenAI text-embedding-3-large,rerank 用 Cohere rerank-v3.5,主生成用 Gemini,意图判断用更小更快的模型。每个环节用最适合的模型,靠 OpenRouter 做统一网关。这种组合方式比单押一个模型更经济也更准。
- Agent 调用与 Workflow 编排:
mcp_bridge/展示了怎么把生成、校验、执行拆成可独立调试的步骤;tool_store展示了怎么把 Workflow 的成功路径沉淀成 Tool。 - Skill 封装:
skills/目录把”操作模式”、”工作流蓝图”、”规范约束”分成三类知识,每一类的封装格式和调用方式都不一样。这套划分可以直接借鉴到其他领域。 - 运行时桥接:
revit_plugin/演示了怎么用 TCP/JSON-RPC 把外部生成器和宿主软件解耦,怎么用 Roslyn 做动态编译,怎么用 ExternalEvent 在事务里安全执行。这套模式可以套用到任何有插件 API 的桌面软件。
每一块都尽量保持了独立性。你可以只抄数据清洗那一段,或者只参考 Tool 固化那个机制,不用整套照搬。
写在最后
我做这个项目,最初想解决的是代码生成的正确性:让 AI 在写 Revit 代码时少猜、少编造、少产生 silently wrong 的结果。
但越往下做越发现,这件事的本质不是”模型问题”,是”数据流问题”:
模型已经够强了。它缺的不是脑子,是项目里那些只存在于运行时的、模型从训练数据里看不到的事实——哪些族类型、哪些标高、哪些视图、用户当前选了什么。把这些事实有结构地、可信地、低延迟地送到模型面前,是这个框架要做的全部事情。
这套思路在 Revit 上跑通了。同样的逻辑,在 CAD 自动建模、EDA 设计辅助、专业仿真软件、医疗影像分析——任何”复杂 API + 真实运行环境 + 高失败成本”的领域里——都会撞上类似的问题,也都会需要类似的解法。
希望它对你有用。无论你是想直接用这个框架,还是想自己造一个类似的——又或者只是想从中拆出某一块独立的设计思路。


