最近在梳理体系结构方向的论文精读清单,第111篇正好落到VerilogCoder这个题目上。第一眼看上去像又一个大模型写Verilog的demo,但顺着标题里的“Graph-based Planning”和“Abstract Syntax”往下读,才发现它在“如何让智能体真正懂硬件结构”这件事上做了不少实打实的设计。这篇笔记想把它的核心思路为什么成立、落地时有哪些坑、以及我们能从里面带走什么经验一次说清楚。适合芯片前端设计、RTL生成方向的人,也适合所有在电子设计自动化工具链里折腾过AI辅助开发的同学。1. 为什么LLM写Verilog时总是“看起来没问题,一综合就翻车”1.1 RTL代码和软件代码最大的区别:结构即语义很多从软件工程背景转来做硬件设计的同学,第一次让大模型帮忙写Verilog时,都会下意识觉得这不就是“生成几行代码”吗。结果通常是被现实教育得很惨。软件代码只要语法正确、算法逻辑没跑偏,大概率就能在CPU上执行;而RTL代码面对的是综合工具、时序约束和物理电路,一段看似正确的Verilog可能在仿真器里跑得好好的,综合之后却冒出难以收敛的组合逻辑,甚至因为一个敏感列表漏了信号,被工具悄悄推断出锁存器。问题的根源在于,RTL里的“结构”本身就是语义。模块实例化层级决定了硬件拓扑,always块边界决定了时序和组合逻辑的划分,信号有没有被正确声明成reg或wire直接关系到能不能被综合成预期的电路。而大语言模型本质上是在做文本序列的概率预测,它没有见过真正的电路,也不理解“可综合性”这个概念,只是在模仿它训练数据里见过的Verilog片段。所以它写出来的代码可以很“像”Verilog,但很难保证每一个结构上的细节都符合硬件语义。还有一个常被忽略的差异是上下文长度和信号可见性。一个完整的设计往往有几个甚至十几个模块,模块之间靠端口和信号交互。如果给模型一个超长的顶层描述,让它一口气生成整个RTL文件,模型在长序列的后续部分很容易忘记前面定义过的信号名和位宽。软件代码里这种问题还会被编译器或类型检查器挡住,但Verilog的很多错误在编译阶段并不报错,而是以warning的形式混过去,最后在综合或者上板时才暴露出来。这也是为什么像VerilogCoder这样的方案要把生成过程拆开,并且依赖外部工具做结构约束,而不是让模型自由发挥到底。1.2 Verilog生成中最常见的四类结构性问题在没有约束的裸生成场景下,LLM写Verilog时的问题其实高度集中,我自己把遇到的报错和返工情况归了归类,大致有四类:问题类别典型表现为什么容易被忽略时序块内赋值混用在always块里同时使用阻塞赋值和非阻塞赋值仿真器不一定立刻报错,综合后才出现行为偏差敏感列表不完整组合逻辑写成了always@(a or b),漏掉了输入信号c仿真时可能偶然正常,换一种输入组合就翻车复位极性不一致生成器默认高有效复位,工程里实际是低有效复位接口适配阶段大量返工位宽与拼接错误参数化模块里写死位宽,或拼接操作符使用不当综合时出现一连串不明所以的宽度警告这些错误并不是模型不会写Verilog,而是它在生成时缺少“结构上下文”。比如一个模块的复位信号到底叫什么名字、是上升沿还是下降沿复位、输入输出位宽由谁决定,这些信息如果不在提示词里明确给出,模型就只能靠训练数据里的统计偏好去猜。猜对了算运气,猜错了就得靠后端的调试和修复来填坑。VerilogCoder这类方案给出的解法,干脆不把希望寄托在模型“凭感觉猜对结构”上,而是从外面给它搭一套脚手架:图规划负责定义模块之间的关系和顺序,抽象语法负责约束每一个模块内部的结构骨架,模型只负责在骨架里做内容填充。这个思路看起来简单,但真正实现过一遍的人都知道,里面每一个环节都有细节要处理。2. 图规划:不是让Agent随便写,而是先把模块依赖画出来2.1 不按顺序生成会引发的“接口漫游”问题如果让智能体一次性把整个项目从头到尾写成一个几万字符的Verilog文件,最常出现的问题不是语法错误,而是“接口漫游”。具体来说,模型在描写模块A时,会定义一个信号名比如data_valid,并假设模块B会输出这个信号;等写到模块B时,模型很可能忘了自己之前的设定,把输出端口改成了data_vld或valid_o。结果是,语法检查一切正常,但模块连起来之后信号对不上。就算把设计拆成多个文件逐个生成,生成顺序依然会直接影响结果。如果先自动生成被依赖的下层模块,再生成上层模块,上层可以严格按照下层模块的真实接口去例化,但前提是我们在最开始就要知道整个设计有哪些模块、每个模块的接口长什么样;如果先写上层再补下层,那么生成下层模块时就必须严格遵守已经在顶层写死的信号命名和位宽。无论哪种方向,都要求模块之间的接口先被固定下来,而不是边写边变。图规划解决的就是这个问题。它要求Agent在真正写代码之前,先花一轮时间把整个设计拆成一张模块依赖图,把每个模块的输入、输出信号名单和位宽确定下来,再根据数据流关系排出一个生成顺序。这张图就像是硬件设计的“施工蓝图”,蓝图不改,后面每个模块的生成就都有了可参照的统一标准。2.2 一张依赖图如何变成Agent的执行计划在常见的实现流程里,图规划一般分为四步:首先是需求解析。智能体把自然语言或者高层次规格描述转成一组结构化的功能需求,比如“有一个fifo控制器,可以处理读请求和写请求,支持full和empty两个状态信号输出”。这一步的输出是一份机器可以参考的功能清单。然后是模块识别。根据功能清单,把设计拆成候选模块。拆分的核心原则是高内聚低耦合:每个模块最好只做一件事,模块之间的信号交互越简单越好。比如上面那个fifo控制器,可以拆成写地址生成模块、读地址生成模块、状态统计模块和顶层控制器,而不是把它们全部塞进一个大模块里。接着是依赖建图。根据信号流向,在模块之间连边。例如状态统计模块需要依赖写地址和读地址的更新信号,那么在依赖图里就有一条从写地址模块指向状态统计模块的边。得到有向图之后,还需要检查有没有环。如果两个模块需要互相依赖对方的输出,说明模块划分本身就有问题,需要回头重新切分。最后是顺序调度。基于依赖图做拓扑排序,确定先写哪个模块、后写哪个模块。对于没有依赖关系的并行模块,还可以考虑让多个编码Agent同时开工,提高整体生成速度。图规划最大的价值不在于“排个序”,而在于它把整体设计问题分解成了一个一个足够小、足够明确的子问题。每个子问题交给LLM时,提示词里只需要描述清楚一个模块的接口和功能,模型的注意力可以集中在几十行代码范围内,端口名幻觉和跨模块遗忘问题会大幅度缓解。2.3 图规划粒度怎么把握:并不是拆得越碎越好这里必须泼一盆冷水:模块拆分的粒度如果把握不好,图规划反而会变成新的负担。拆得太细,模块数量爆炸,模块之间的接口数量和信号握手协议会膨胀,光是对齐这些协议就要消耗大量提示词空间,还会引入许多原本出现在一个模块内部就可以解决、现在却要跨模块处理的握手状态机。我的经验是,在模块级生成场景里,单个模块的代码规模控制在200行以内、端口数量在几十个以内是比较合适的。这个量级既不会因为上下文太长导致模型丢失信号定义,也不会因为模块太碎导致Agent每一步都在处理“模块间的连接问题”。更具体一点说,如果一个模块的内部逻辑已经复杂到需要三层状态机嵌套,就应该继续往下拆;如果模块本身就是一块很薄的接口转换层,再拆下去只会增加无意义的依赖边。另外,依赖图的建立不能完全靠模型自动完成。最好由工程端预先给定一份模块候选列表,或者至少给定一个约束性的框架,让Agent在框架之内做微调。因为模型自己定义模块时往往会跟着自己的思路走,初版接口设计很可能和工程师长期维护的代码风格不一致。图规划是给智能体用的执行计划,但规划的依据必须来源于真实设计经验和工程规则,这一点越早明确,后续的返工越少。3. 抽象语法约束:把生成过程关进“合法结构”的笼子里3.1 AST到底约束了什么图规划解决了模块之间的组织问题,但模块内部的语法结构依然可能出问题。VerilogCoder的方案里,抽象语法(Abstract Syntax)在这里承担的就是第二层约束:在让模型直接生成原始Verilog文本之前,先为它生成一个AST蓝图,也就是这段代码的结构骨架。举个具体例子。一个典型的计数器模块,AST里会预先声明模块名、端口列表、端口方向、内部reg和wire声明、always的事件控制、always内部的语句块结构。模型生成时不是一口气写出一整段代码,而是沿着AST节点逐层展开:先确定模块声明,再细化到端口定义,再填充内部逻辑。从根到叶子,每一层都有明确的语法边界,模型不太可能在结构上“跑飞”。这件事解决的核心痛点是语法错误的压缩。用通用大模型直接写Verilog,最烦人的就是它会漏掉endmodule、把端口列表后面的逗号写成句号、或者在一个括号表达式里少加一个右括号。这些错误在软件语言里通常会被编译器非常清楚地标出来,但在Verilog里有些工具只会给warning,而且一行warning往往对应着不远处另一个真正的问题。AST约束在生成阶段就把这些错误的空间压缩到极小,因为它不允许模型在结构层面上做自由选择。但要注意,AST约束只约束“骨架”,不约束“内容”。模型依然可能在allowed的case分支中把fsm_state的二进制编码写错,也可能把一个低有效复位信号在逻辑里写成高有效判断。图规划和AST约束解决的是结构性问题,语义性问题还是要靠验证和修复闭环来处理。所以我在看这套方案的时候,从不把它理解成“一次生成就全部正确”,而是理解成“用结构约束把错误的搜索空间缩小,然后把精力集中在语义检查上”。3.2 结构与内容分离,为什么能减少LLM的幻觉抽象语法约束本质上是把硬件生成的问题拆成了两层:结构层和内容层。结构层由AST保证,内容层由LLM生成。这个分离非常符合当前大语言模型的能力边界——它最不擅长的是在超长序列上保持一个完整且一致的全局结构,最擅长的反而是根据局部上下文做内容补全,比如给一个信号起合适的名字、写一段组合逻辑的判断条件、填充一个case分支里的表达式。在实际工程里,这个分离还有一个额外的好处:可回溯性更强。传统方式下,如果整个模块生成失败,调试者面对的可能是一整段充满模型“自由发挥”痕迹的代码,只能逐行去找问题。而AST约束生成时,失败可以被定位到具体的结构节点,比如“状态机模块的case语句段生成了冲突的转移条件”,修复范围立即缩小到那一个节点。对Agent来说,这种更加精准的定位比“把整个模块重新生成一遍”要省钱得多。我自己在搭建类似流程时,会把AST里的节点分成两类:强约束节点和松弛节点。强约束节点包括模块声明、端口列表、always块边界、复位逻辑结构,这些地方必须严格符合模板;松弛节点包括case分支里的条件表达式、组合逻辑里的判断语句、参数的默认值,这些地方允许模型在合理范围内自由发挥。如果对每个节点都做一模一样的强约束,AST就退化成了模板套壳,模型既没灵活空间,也不容易适应不同模块的设计差异。4. 验证与修复闭环:自主智能体真正的分水岭4.1 一个模块一个关卡:静态检查和局部仿真图规划和AST约束能显著提高一次生成的成功率,但离“自主编码”还差最后且最关键的一环:验证与修复。一个只会生成代码的Agent只能叫高级代码补全器,真正有资格叫“自主智能体”的方案,必须包含生成之后的自动检查、错误反馈和目标重规划。比较合理的闭环是每个模块生成后立刻做两次关卡。第一关是静态检查:调用开源仿真器或HDL分析工具对模块做纯语法编译,同时挂上常见的编码规范检查,比如有没有组合逻辑里混入非阻塞赋值、有没有漏掉敏感列表、有没有未定义信号、有没有位宽不匹配的拼接警告。这一步速度快、反馈明确,能拦住至少一半的明显错误。第二关是局部仿真:把生成的模块放进一个最小测试环境里,喂几组关键输入波形,检查模块输出是否符合预期。我见过不少团队在这一步偷懒,觉得“等整个设计都生成完再统一仿真”就行了。实际这样做的后果是,顶层仿真报错时,错误信息往往涉及多个模块的交互,Agent需要同时理解好几个文件和它们的信号关系,修复难度陡增。而一个模块一个关卡的好处是,每个模块在作为依赖项被后续模块引用之前,已经通过了基本验收,错误不会向下游扩散。4.2 修复策略:不是把报错信息原样丢回给模型验证关过了之后,Agent要么宣布成功,要么带着错误信息回到生成循环。这里我要特别强调一个误区:很多人在搭建修复回路时,只是把仿真器输出的一坨红色错误文本原样拼回提示词,再让模型“重新生成一遍”。这种做法的成功率其实很低,因为错误信息里的术语对模型来说是碎片化的,它缺乏和错误对应的设计上下文。更有效的做法是给修复环节提供四样东西:原始需求、模块接口定义、AST骨架定位、报错位置对应的代码片段。也就是说,Agent在修复时不应该只看到“line 32: inferring latch”,还应该看到“这个模块接收ready信号和data总线,输出valid信号,AST骨架里组合逻辑段的结构是这个样子”。有了这些上下文,模型才知道那个锁存器推断是因为某个else分支没写,还是因为某个输出信号在组合块里被赋值了多个地方。另外,修复轮次一定要做限制。我自己的经验是,同一段代码连续修三轮还过不了,就不该继续在错误版本上打补丁了。因为在迭代过程中,模型看到的提示词里错误代码的占比越来越高,它反而会开始模仿错误写法。正确的切换策略是回到AST骨架层,换一个更细粒度的模板重新生成,或者把这个模块拆成两个更简单的模块,让模型在更小的搜索空间里重新作答。4.3 它在真实FPGA/ASIC流程里的三个落点拿这套方案去对比完整的芯片前端流程时,一定要有清醒的预期:短期内它大概率替代不了整个设计流程,但在几个特定场景里已经可以当半个主力军用。第一个场景是协议胶水代码和跨时钟域接口的快速实现。这类代码结构高度模板化,细节容易错,但接口相对固定,非常适合“图规划确定接口、AST约束固定结构、LLM填充细节”的组合。第二个场景是寄存器模型和寄存器访问逻辑生成。寄存器模块通常是对称重复的,几百个寄存器的读写逻辑本质上是同一种结构的多次展开,Agent生成初版后由人工做抽查复核,效率提升会非常可观。第三个场景是测试bench草稿和波形分析脚本。这些内容允许一定程度的不完美,Agent只要能生成一个能跑的骨架,剩下的调试交给工程师就好。在FPGA流程里实际使用这类Agent时,最重要的一个前置条件是要把顶层模块接口固定下来。如果顶层接口没有固定,Agent每生成一个模块都可能改一次端口名,依赖图最后会变成一团乱麻。正确姿势是先靠图规划把顶层和主要子模块的端口协议固定成一份“接口契约”,再把这份契约一并放进每个模块的提示词里,让所有Agent基于同一份事实工作。5. 自己复刻这套思路时的工程建议与避坑记录5.1 最小闭环工具链是什么样想复现VerilogCoder思路的人,不需要一上来就搭一个完整的论文级基础设施。我建议从最小闭环开始:一个大模型服务、一个开源仿真器、一个支持Verilog静态检查的命令行工具,再加最简单的一层Python胶水把它们串起来。选模型的时候不用贪大,模块级生成场景下,专注于代码生成的小模型往往比超大通用模型要稳,因为任务焦点集中,通用模型反而容易在自由发挥里跑题。仿真器和静态检查工具的选择主要看两件事:编译速度和错误信息的可读性。第一次生成后的静态检查要追求“快”,最好在几秒内给出结论,这样Agent的迭代循环才跑得起来。模块级验证阶段可以启用波形记录,用于分析跨模块的时序交互。这里有个小技巧:把编译检查、仿真、波形导出写成三条独立的命令,让Agent分步调用,别一次性把所有检查跑完混在一个输出里,否则错误信息一多,模型分不清优先级,修复时会东一榔头西一棒子。5.2 修复轮数不是越多越好前面提过修复轮数超过三轮后成功率会断崖式下跌,这里想再展开一下背后的原因。模型在每一轮修复时都会看到之前生成的不完整或错误代码,这些错误代码会污染它的概率分布。换句话说,模型会把“看到过的错误写法”当成一种风格特征去模仿,而不是单纯把它当成“需要被替换掉的东西”。所以不太建议设计成“直到成功为止一直重试”的循环,而应该给Agent一个明确的策略切换机制。到达修复上限之后,可选的策略包括:重新生成当前AST节点对应的小片段、拆细模块粒度后重新规划依赖图、或者换一个AST模板。这些操作本质上都是“改变搜索空间”,而不是“在同一个空间里继续找解”。把这三条路写进Agent的决策逻辑里,效果远远好于无限重试同一个方向的修复。5.3 把人的经验变成“接口契约”提示词最后一个实操体会是,图规划阶段不应该完全依赖模型自己发挥。对于长期维护的工程来说,最好的做法是维护一份“接口契约”文件,里面写明系统的命名规范、信号前缀、有效极性、时钟域归属和跨模块握手协议。比如统一规定低有效复位信号必须带_rst_n后缀,所有模块顶层端口禁止出现同名不同含义的信号。在Agent生成时把这份契约放进系统提示词,模型就会明显减少自己编造信号名的冲动。我自己的格式爱好是先把所有模块的端口名、方向、位宽、时钟域列成一张表,然后用markdown表格形式注入到Agent的system prompt里。这样每个模块生成时都能看到同一份事实约束,跨模块连线的返工率能肉眼可见地下降。这套操作其实就是标题里Graph-based Planning在工程上的朴素落地:规划不是代码写完以后的补丁校验,而是生成之前的第一优先级约束。最后说一点个人体会。VerilogCoder这套思路放在两年前可能还显得过于理想,但如今硬件设计自动化的边界确实在往前移动。我看一个智能体方案时,已经不太关心它单次生成代码的正确率,而在意它有没有把结构规划、语法约束、验证反馈三件事串成一个可闭环的框架。图规划解决模块组织,抽象语法解决结构正确性,验证修复解决语义正确性,三层打通之后,即使模型的推理能力原地踏步,工程效率也会明显上一个大台阶。如果你也在做类似方向,不妨从一个小模块生成和静态检查做起,把接口契约和AST骨架沉淀成自己的工程资产,不用等论文里的完美方案,这条路自己先走一遍,收获会大得多。
最近在梳理体系结构方向的论文精读清单,第111篇正好落到VerilogCoder这个题目上。第一眼看上去像又一个大模型写Verilog的demo,但顺着标题里的“Graph-based Planning”和“Abstract Syntax”往下读,才发现它在“如何让智能体真正懂硬件结构”这件事上做了不少实打实的设计。这篇笔记想把它的核心思路为什么成立、落地时有哪些坑、以及我们能从里面带走什么经验一次说清楚。适合芯片前端设计、RTL生成方向的人,也适合所有在电子设计自动化工具链里折腾过AI辅助开发的同学。1. 为什么LLM写Verilog时总是“看起来没问题,一综合就翻车”1.1 RTL代码和软件代码最大的区别:结构即语义很多从软件工程背景转来做硬件设计的同学,第一次让大模型帮忙写Verilog时,都会下意识觉得这不就是“生成几行代码”吗。结果通常是被现实教育得很惨。软件代码只要语法正确、算法逻辑没跑偏,大概率就能在CPU上执行;而RTL代码面对的是综合工具、时序约束和物理电路,一段看似正确的Verilog可能在仿真器里跑得好好的,综合之后却冒出难以收敛的组合逻辑,甚至因为一个敏感列表漏了信号,被工具悄悄推断出锁存器。问题的根源在于,RTL里的“结构”本身就是语义。模块实例化层级决定了硬件拓扑,always块边界决定了时序和组合逻辑的划分,信号有没有被正确声明成reg或wire直接关系到能不能被综合成预期的电路。而大语言模型本质上是在做文本序列的概率预测,它没有见过真正的电路,也不理解“可综合性”这个概念,只是在模仿它训练数据里见过的Verilog片段。所以它写出来的代码可以很“像”Verilog,但很难保证每一个结构上的细节都符合硬件语义。还有一个常被忽略的差异是上下文长度和信号可见性。一个完整的设计往往有几个甚至十几个模块,模块之间靠端口和信号交互。如果给模型一个超长的顶层描述,让它一口气生成整个RTL文件,模型在长序列的后续部分很容易忘记前面定义过的信号名和位宽。软件代码里这种问题还会被编译器或类型检查器挡住,但Verilog的很多错误在编译阶段并不报错,而是以warning的形式混过去,最后在综合或者上板时才暴露出来。这也是为什么像VerilogCoder这样的方案要把生成过程拆开,并且依赖外部工具做结构约束,而不是让模型自由发挥到底。1.2 Verilog生成中最常见的四类结构性问题在没有约束的裸生成场景下,LLM写Verilog时的问题其实高度集中,我自己把遇到的报错和返工情况归了归类,大致有四类:问题类别典型表现为什么容易被忽略时序块内赋值混用在always块里同时使用阻塞赋值和非阻塞赋值仿真器不一定立刻报错,综合后才出现行为偏差敏感列表不完整组合逻辑写成了always@(a or b),漏掉了输入信号c仿真时可能偶然正常,换一种输入组合就翻车复位极性不一致生成器默认高有效复位,工程里实际是低有效复位接口适配阶段大量返工位宽与拼接错误参数化模块里写死位宽,或拼接操作符使用不当综合时出现一连串不明所以的宽度警告这些错误并不是模型不会写Verilog,而是它在生成时缺少“结构上下文”。比如一个模块的复位信号到底叫什么名字、是上升沿还是下降沿复位、输入输出位宽由谁决定,这些信息如果不在提示词里明确给出,模型就只能靠训练数据里的统计偏好去猜。猜对了算运气,猜错了就得靠后端的调试和修复来填坑。VerilogCoder这类方案给出的解法,干脆不把希望寄托在模型“凭感觉猜对结构”上,而是从外面给它搭一套脚手架:图规划负责定义模块之间的关系和顺序,抽象语法负责约束每一个模块内部的结构骨架,模型只负责在骨架里做内容填充。这个思路看起来简单,但真正实现过一遍的人都知道,里面每一个环节都有细节要处理。2. 图规划:不是让Agent随便写,而是先把模块依赖画出来2.1 不按顺序生成会引发的“接口漫游”问题如果让智能体一次性把整个项目从头到尾写成一个几万字符的Verilog文件,最常出现的问题不是语法错误,而是“接口漫游”。具体来说,模型在描写模块A时,会定义一个信号名比如data_valid,并假设模块B会输出这个信号;等写到模块B时,模型很可能忘了自己之前的设定,把输出端口改成了data_vld或valid_o。结果是,语法检查一切正常,但模块连起来之后信号对不上。就算把设计拆成多个文件逐个生成,生成顺序依然会直接影响结果。如果先自动生成被依赖的下层模块,再生成上层模块,上层可以严格按照下层模块的真实接口去例化,但前提是我们在最开始就要知道整个设计有哪些模块、每个模块的接口长什么样;如果先写上层再补下层,那么生成下层模块时就必须严格遵守已经在顶层写死的信号命名和位宽。无论哪种方向,都要求模块之间的接口先被固定下来,而不是边写边变。图规划解决的就是这个问题。它要求Agent在真正写代码之前,先花一轮时间把整个设计拆成一张模块依赖图,把每个模块的输入、输出信号名单和位宽确定下来,再根据数据流关系排出一个生成顺序。这张图就像是硬件设计的“施工蓝图”,蓝图不改,后面每个模块的生成就都有了可参照的统一标准。2.2 一张依赖图如何变成Agent的执行计划在常见的实现流程里,图规划一般分为四步:首先是需求解析。智能体把自然语言或者高层次规格描述转成一组结构化的功能需求,比如“有一个fifo控制器,可以处理读请求和写请求,支持full和empty两个状态信号输出”。这一步的输出是一份机器可以参考的功能清单。然后是模块识别。根据功能清单,把设计拆成候选模块。拆分的核心原则是高内聚低耦合:每个模块最好只做一件事,模块之间的信号交互越简单越好。比如上面那个fifo控制器,可以拆成写地址生成模块、读地址生成模块、状态统计模块和顶层控制器,而不是把它们全部塞进一个大模块里。接着是依赖建图。根据信号流向,在模块之间连边。例如状态统计模块需要依赖写地址和读地址的更新信号,那么在依赖图里就有一条从写地址模块指向状态统计模块的边。得到有向图之后,还需要检查有没有环。如果两个模块需要互相依赖对方的输出,说明模块划分本身就有问题,需要回头重新切分。最后是顺序调度。基于依赖图做拓扑排序,确定先写哪个模块、后写哪个模块。对于没有依赖关系的并行模块,还可以考虑让多个编码Agent同时开工,提高整体生成速度。图规划最大的价值不在于“排个序”,而在于它把整体设计问题分解成了一个一个足够小、足够明确的子问题。每个子问题交给LLM时,提示词里只需要描述清楚一个模块的接口和功能,模型的注意力可以集中在几十行代码范围内,端口名幻觉和跨模块遗忘问题会大幅度缓解。2.3 图规划粒度怎么把握:并不是拆得越碎越好这里必须泼一盆冷水:模块拆分的粒度如果把握不好,图规划反而会变成新的负担。拆得太细,模块数量爆炸,模块之间的接口数量和信号握手协议会膨胀,光是对齐这些协议就要消耗大量提示词空间,还会引入许多原本出现在一个模块内部就可以解决、现在却要跨模块处理的握手状态机。我的经验是,在模块级生成场景里,单个模块的代码规模控制在200行以内、端口数量在几十个以内是比较合适的。这个量级既不会因为上下文太长导致模型丢失信号定义,也不会因为模块太碎导致Agent每一步都在处理“模块间的连接问题”。更具体一点说,如果一个模块的内部逻辑已经复杂到需要三层状态机嵌套,就应该继续往下拆;如果模块本身就是一块很薄的接口转换层,再拆下去只会增加无意义的依赖边。另外,依赖图的建立不能完全靠模型自动完成。最好由工程端预先给定一份模块候选列表,或者至少给定一个约束性的框架,让Agent在框架之内做微调。因为模型自己定义模块时往往会跟着自己的思路走,初版接口设计很可能和工程师长期维护的代码风格不一致。图规划是给智能体用的执行计划,但规划的依据必须来源于真实设计经验和工程规则,这一点越早明确,后续的返工越少。3. 抽象语法约束:把生成过程关进“合法结构”的笼子里3.1 AST到底约束了什么图规划解决了模块之间的组织问题,但模块内部的语法结构依然可能出问题。VerilogCoder的方案里,抽象语法(Abstract Syntax)在这里承担的就是第二层约束:在让模型直接生成原始Verilog文本之前,先为它生成一个AST蓝图,也就是这段代码的结构骨架。举个具体例子。一个典型的计数器模块,AST里会预先声明模块名、端口列表、端口方向、内部reg和wire声明、always的事件控制、always内部的语句块结构。模型生成时不是一口气写出一整段代码,而是沿着AST节点逐层展开:先确定模块声明,再细化到端口定义,再填充内部逻辑。从根到叶子,每一层都有明确的语法边界,模型不太可能在结构上“跑飞”。这件事解决的核心痛点是语法错误的压缩。用通用大模型直接写Verilog,最烦人的就是它会漏掉endmodule、把端口列表后面的逗号写成句号、或者在一个括号表达式里少加一个右括号。这些错误在软件语言里通常会被编译器非常清楚地标出来,但在Verilog里有些工具只会给warning,而且一行warning往往对应着不远处另一个真正的问题。AST约束在生成阶段就把这些错误的空间压缩到极小,因为它不允许模型在结构层面上做自由选择。但要注意,AST约束只约束“骨架”,不约束“内容”。模型依然可能在allowed的case分支中把fsm_state的二进制编码写错,也可能把一个低有效复位信号在逻辑里写成高有效判断。图规划和AST约束解决的是结构性问题,语义性问题还是要靠验证和修复闭环来处理。所以我在看这套方案的时候,从不把它理解成“一次生成就全部正确”,而是理解成“用结构约束把错误的搜索空间缩小,然后把精力集中在语义检查上”。3.2 结构与内容分离,为什么能减少LLM的幻觉抽象语法约束本质上是把硬件生成的问题拆成了两层:结构层和内容层。结构层由AST保证,内容层由LLM生成。这个分离非常符合当前大语言模型的能力边界——它最不擅长的是在超长序列上保持一个完整且一致的全局结构,最擅长的反而是根据局部上下文做内容补全,比如给一个信号起合适的名字、写一段组合逻辑的判断条件、填充一个case分支里的表达式。在实际工程里,这个分离还有一个额外的好处:可回溯性更强。传统方式下,如果整个模块生成失败,调试者面对的可能是一整段充满模型“自由发挥”痕迹的代码,只能逐行去找问题。而AST约束生成时,失败可以被定位到具体的结构节点,比如“状态机模块的case语句段生成了冲突的转移条件”,修复范围立即缩小到那一个节点。对Agent来说,这种更加精准的定位比“把整个模块重新生成一遍”要省钱得多。我自己在搭建类似流程时,会把AST里的节点分成两类:强约束节点和松弛节点。强约束节点包括模块声明、端口列表、always块边界、复位逻辑结构,这些地方必须严格符合模板;松弛节点包括case分支里的条件表达式、组合逻辑里的判断语句、参数的默认值,这些地方允许模型在合理范围内自由发挥。如果对每个节点都做一模一样的强约束,AST就退化成了模板套壳,模型既没灵活空间,也不容易适应不同模块的设计差异。4. 验证与修复闭环:自主智能体真正的分水岭4.1 一个模块一个关卡:静态检查和局部仿真图规划和AST约束能显著提高一次生成的成功率,但离“自主编码”还差最后且最关键的一环:验证与修复。一个只会生成代码的Agent只能叫高级代码补全器,真正有资格叫“自主智能体”的方案,必须包含生成之后的自动检查、错误反馈和目标重规划。比较合理的闭环是每个模块生成后立刻做两次关卡。第一关是静态检查:调用开源仿真器或HDL分析工具对模块做纯语法编译,同时挂上常见的编码规范检查,比如有没有组合逻辑里混入非阻塞赋值、有没有漏掉敏感列表、有没有未定义信号、有没有位宽不匹配的拼接警告。这一步速度快、反馈明确,能拦住至少一半的明显错误。第二关是局部仿真:把生成的模块放进一个最小测试环境里,喂几组关键输入波形,检查模块输出是否符合预期。我见过不少团队在这一步偷懒,觉得“等整个设计都生成完再统一仿真”就行了。实际这样做的后果是,顶层仿真报错时,错误信息往往涉及多个模块的交互,Agent需要同时理解好几个文件和它们的信号关系,修复难度陡增。而一个模块一个关卡的好处是,每个模块在作为依赖项被后续模块引用之前,已经通过了基本验收,错误不会向下游扩散。4.2 修复策略:不是把报错信息原样丢回给模型验证关过了之后,Agent要么宣布成功,要么带着错误信息回到生成循环。这里我要特别强调一个误区:很多人在搭建修复回路时,只是把仿真器输出的一坨红色错误文本原样拼回提示词,再让模型“重新生成一遍”。这种做法的成功率其实很低,因为错误信息里的术语对模型来说是碎片化的,它缺乏和错误对应的设计上下文。更有效的做法是给修复环节提供四样东西:原始需求、模块接口定义、AST骨架定位、报错位置对应的代码片段。也就是说,Agent在修复时不应该只看到“line 32: inferring latch”,还应该看到“这个模块接收ready信号和data总线,输出valid信号,AST骨架里组合逻辑段的结构是这个样子”。有了这些上下文,模型才知道那个锁存器推断是因为某个else分支没写,还是因为某个输出信号在组合块里被赋值了多个地方。另外,修复轮次一定要做限制。我自己的经验是,同一段代码连续修三轮还过不了,就不该继续在错误版本上打补丁了。因为在迭代过程中,模型看到的提示词里错误代码的占比越来越高,它反而会开始模仿错误写法。正确的切换策略是回到AST骨架层,换一个更细粒度的模板重新生成,或者把这个模块拆成两个更简单的模块,让模型在更小的搜索空间里重新作答。4.3 它在真实FPGA/ASIC流程里的三个落点拿这套方案去对比完整的芯片前端流程时,一定要有清醒的预期:短期内它大概率替代不了整个设计流程,但在几个特定场景里已经可以当半个主力军用。第一个场景是协议胶水代码和跨时钟域接口的快速实现。这类代码结构高度模板化,细节容易错,但接口相对固定,非常适合“图规划确定接口、AST约束固定结构、LLM填充细节”的组合。第二个场景是寄存器模型和寄存器访问逻辑生成。寄存器模块通常是对称重复的,几百个寄存器的读写逻辑本质上是同一种结构的多次展开,Agent生成初版后由人工做抽查复核,效率提升会非常可观。第三个场景是测试bench草稿和波形分析脚本。这些内容允许一定程度的不完美,Agent只要能生成一个能跑的骨架,剩下的调试交给工程师就好。在FPGA流程里实际使用这类Agent时,最重要的一个前置条件是要把顶层模块接口固定下来。如果顶层接口没有固定,Agent每生成一个模块都可能改一次端口名,依赖图最后会变成一团乱麻。正确姿势是先靠图规划把顶层和主要子模块的端口协议固定成一份“接口契约”,再把这份契约一并放进每个模块的提示词里,让所有Agent基于同一份事实工作。5. 自己复刻这套思路时的工程建议与避坑记录5.1 最小闭环工具链是什么样想复现VerilogCoder思路的人,不需要一上来就搭一个完整的论文级基础设施。我建议从最小闭环开始:一个大模型服务、一个开源仿真器、一个支持Verilog静态检查的命令行工具,再加最简单的一层Python胶水把它们串起来。选模型的时候不用贪大,模块级生成场景下,专注于代码生成的小模型往往比超大通用模型要稳,因为任务焦点集中,通用模型反而容易在自由发挥里跑题。仿真器和静态检查工具的选择主要看两件事:编译速度和错误信息的可读性。第一次生成后的静态检查要追求“快”,最好在几秒内给出结论,这样Agent的迭代循环才跑得起来。模块级验证阶段可以启用波形记录,用于分析跨模块的时序交互。这里有个小技巧:把编译检查、仿真、波形导出写成三条独立的命令,让Agent分步调用,别一次性把所有检查跑完混在一个输出里,否则错误信息一多,模型分不清优先级,修复时会东一榔头西一棒子。5.2 修复轮数不是越多越好前面提过修复轮数超过三轮后成功率会断崖式下跌,这里想再展开一下背后的原因。模型在每一轮修复时都会看到之前生成的不完整或错误代码,这些错误代码会污染它的概率分布。换句话说,模型会把“看到过的错误写法”当成一种风格特征去模仿,而不是单纯把它当成“需要被替换掉的东西”。所以不太建议设计成“直到成功为止一直重试”的循环,而应该给Agent一个明确的策略切换机制。到达修复上限之后,可选的策略包括:重新生成当前AST节点对应的小片段、拆细模块粒度后重新规划依赖图、或者换一个AST模板。这些操作本质上都是“改变搜索空间”,而不是“在同一个空间里继续找解”。把这三条路写进Agent的决策逻辑里,效果远远好于无限重试同一个方向的修复。5.3 把人的经验变成“接口契约”提示词最后一个实操体会是,图规划阶段不应该完全依赖模型自己发挥。对于长期维护的工程来说,最好的做法是维护一份“接口契约”文件,里面写明系统的命名规范、信号前缀、有效极性、时钟域归属和跨模块握手协议。比如统一规定低有效复位信号必须带_rst_n后缀,所有模块顶层端口禁止出现同名不同含义的信号。在Agent生成时把这份契约放进系统提示词,模型就会明显减少自己编造信号名的冲动。我自己的格式爱好是先把所有模块的端口名、方向、位宽、时钟域列成一张表,然后用markdown表格形式注入到Agent的system prompt里。这样每个模块生成时都能看到同一份事实约束,跨模块连线的返工率能肉眼可见地下降。这套操作其实就是标题里Graph-based Planning在工程上的朴素落地:规划不是代码写完以后的补丁校验,而是生成之前的第一优先级约束。最后说一点个人体会。VerilogCoder这套思路放在两年前可能还显得过于理想,但如今硬件设计自动化的边界确实在往前移动。我看一个智能体方案时,已经不太关心它单次生成代码的正确率,而在意它有没有把结构规划、语法约束、验证反馈三件事串成一个可闭环的框架。图规划解决模块组织,抽象语法解决结构正确性,验证修复解决语义正确性,三层打通之后,即使模型的推理能力原地踏步,工程效率也会明显上一个大台阶。如果你也在做类似方向,不妨从一个小模块生成和静态检查做起,把接口契约和AST骨架沉淀成自己的工程资产,不用等论文里的完美方案,这条路自己先走一遍,收获会大得多。