如何用AI写出能运行的SKILL脚本?框架+细节实战指南
2026/9/9 8:59:07 网站建设 项目流程

从最早几个人在论坛里求SKILL脚本,到如今一开口就是“让AI帮我写个SKILL”,这个变化其实挺快的。我用Cadence Allegro做PCB设计有些年头了,SKILL脚本一直是我又爱又恨的东西:爱它是真能提升效率,恨它是真难写。语法冷门、API散乱、调试全靠试错,所以大模型一出来,我第一反应就是赶紧让它替我干这活儿。结果嘛,理想很丰满,现实非常骨感。

这篇文章是原计划里SKILL主题的第4篇,但这篇和之前不大一样,主题不在SKILL本身,而在“怎么让AI老老实实帮你把SKILL代码写出来并且能跑”。我前后折腾了一整天,看着AI反复给出格式漂亮但一load就崩的代码,听它用各种语气解释“这应该能运行”,最后终于通过“框架+细节”的方式,让脚本在Allegro里真正跑通了。也算是对SKILL开发方式的一次全新尝试。全程的调试过程、踩过的坑、最终沉淀的方法,都写在这篇里,希望能帮同样想用AI写SKILL、却老是被“看似正确”的代码坑到的人,省下一点宝贵时间。

1. 先搞清楚:AI写的SKILL脚本为什么跑不起来

1.1 SKILL是一门“存在感极低”的领域语言

先说一个很多人忽略的事实:SKILL这门语言的训练样本,在大模型语料里少得可怜。它只服务于Cadence这套EDA工具生态,离开Allegro、OrCAD,SKILL脚本没有任何用处。网上能找到的SKILL代码,翻来覆去就那么几个来源:Cadence官方手册、论坛求助帖、以及像我这样二次开发工程师零散分享的开源脚本。这点训练量,跟Python、JavaScript动辄几个T的语料完全没法比。

所以AI对SKILL的理解,很大程度上是靠“猜”的。它知道SKILL长得像Lisp,就照着Lisp的语法去靠;它模糊记得有一些axl开头的函数,就根据自己的理解去“编造”。结果就是,AI生成的SKILL代码,表面上看语法整齐、注释清晰,实际一检查全是坑。我在调试时甚至遇到过它编出一个叫做axlDBGetAllNets的函数,信誓旦旦说这是获取所有网络的标准API,实际上SKILL里压根没有这个函数,正确写法是通过axlDBGetDesign()->nets去拿网络列表。

这就是AI在小众领域语言上的通病:它不是不会写代码,是它对这门语言的理解本身就不准确。你让它写一个通用算法,它可能比人写得还利索;你让它调用一个冷门API,它就开始一本正经地胡编。

1.2 AI在SKILL上最容易翻车的三类问题

这次调试过程中我碰到了大量问题,归纳下来其实只有三类。先把这三类搞明白,后面的排查思路就清晰了。

第一类是语法层问题。SKILL支持两种函数定义风格,一种是传统Lisp风格的(defun 函数名 (参数) 函数体),另一种是SKILL自家的“函数名前置”风格defun( 函数名 (参数) 函数体 )。AI经常把两种混着写,最外层用SKILL风格,嵌套的函数又切成Lisp风格,一个文件里两种括号风格来回跳。SKILL解析器对括号极其敏感,稍微错一层就整个文件解析失败。这种问题最隐蔽,因为人眼扫过去很难发现,只有靠编辑器做括号匹配检查才能定位。

第二类是API层问题,也就是函数名和参数写错。这一类的典型表现是“错误信息说undefined function,但AI坚持说这个函数存在”。SKILL的函数命名有自己的一套体系,比如获取设计要用axlDBGetDesign,注册命令要用axlCmdRegister,创建对话框要用axlFormCreate。AI对这类API的掌握非常不稳定,经常张冠李戴,把别的语言的函数习惯带进来。每次遇到undefined function,我都得去翻一遍记忆里的官方手册,最后发现又是AI在“创作”。

第三类是逻辑层问题。语法和API都对,但算法思路不对。比如SKILL里foreach会返回遍历后的列表,而for返回的是最后一个表达式的结果,AI在这些语义细节上经常出错。还有nil和空表的区别、consappend的选择、strncmp返回0表示相同的这个反直觉设计,都是AI逻辑翻车的重灾区。

1.3 为什么“框架+细节”能解决这个问题

搞清楚了AI翻车的原因,思路就很简单了:既然AI对SKILL的“细节知识”不靠谱,那就不让它一次处理太多细节。原来的做法是让AI“一步到位生成整个脚本”,这里面涉及几十个函数调用、十几次语法选择,任何一处出错,整个脚本就跑不起来。而AI没有执行环境,它自己感知不到错误,于是只能顺着错误的思路继续往下编。

“框架+细节”的思路,是把整个任务拆成两个阶段。第一阶段只谈架构,不涉及具体API。比如脚本分几个函数、每个函数的输入输出是什么、调用关系怎么走。这部分是通用软件工程知识,AI在行得很。第二阶段逐块填充细节,每次只让AI写一个函数,范围小、变量少、API调用也就那几个,出错的概率大幅下降。更关键的是,每填完一块,我立刻拿进Allegro里实测,有问题当场发现、当场反馈,不让错误往深处蔓延。

这套方法本质上就是“分治法加小步验证”。AI擅长的是在明确约束下生成合理代码,人擅长的是判断代码在真实环境里是否真的能用。框架阶段把约束定死,细节阶段把验证做勤,两边的优势都能发挥出来。

2. 框架先行:把脚本的骨架钉死

2.1 框架里必须写清楚的五件事

第一轮失败之后,我意识到问题出在我自己的提问方式上。光说“写一个SKILL脚本统计网络pin数”,AI自由发挥的空间太大了,大到它一定会发挥错。第二次尝试,我只让它写框架,不写具体实现,但框架里必须包括五件事。

一是脚本要实现的功能清单。功能要列到“可验证”的程度,不能只说“统计网络”,要说“遍历当前设计所有网络,只保留指定前缀开头的网络,统计每个网络上的pin数量,把结果输出到CSV文件”。

二是模块划分。明确告诉AI这个脚本拆成哪几个函数,每个函数的职责范围、输入参数、返回值是什么。这一步相当于给AI画好了房间的隔断,它只能在房间里装修,不能自己再砌墙拆墙。

三是SKILL语法风格的硬性约束。我在框架提示词里直接写死:全部使用defun( 函数名 (参数) ... )的SKILL风格,禁止混用Lisp括号风格。这个约束能有效把第一类语法问题挡在门外。

四是关键API的指定。框架阶段我直接告诉AI,获取设计对象必须用axlDBGetDesign(),注册命令必须用axlCmdRegister("命令名" '函数名)。把最核心的API由人指定,AI就少了编造的空间。

五是禁止输出多余内容。要求在写某个函数时只输出该函数代码,不要写解释性文字,不要改动其他已经确认的部分。这一步很重要,因为AI特别喜欢“顺手”帮你把别的函数也改了,改完就引入新bug。

2.2 我实际使用的框架提示词

以下是我这次实际使用的框架阶段提示词,原样贴出来供参考。目标是让AI只输出脚本的骨架和模块说明,所有函数体允许写TODO。

请为Cadence Allegro PCB Editor设计一个SKILL脚本,功能如下: 1. 注册一个名为netstat的命令 2. 命令执行后,提示用户输入一个网络名前缀 3. 遍历当前设计中的所有网络,筛选出名称以该前缀开头的网络 4. 统计每个筛选后网络上的pin数量 5. 将结果按“网络名,pin数量”的格式写入文件net_report.txt 模块划分要求: - netstat_main:命令入口,负责获取用户输入前缀并调用其他模块 - netstat_get_prefix_nets:接收前缀参数,返回匹配的网络列表 - netstat_write_report:接收网络列表和文件名,写入报告文件 硬性约束: - 全部使用SKILL语言,函数定义一律采用 defun( 函数名 (参数) ... ) 风格 - 获取设计对象必须使用 axlDBGetDesign() - 注册命令必须使用 axlCmdRegister("netstat" 'netstat_main) - 不要使用form图形界面,保持纯命令行交互 - 现在只输出整体框架,每个函数给出函数签名和职责注释,函数体写成TODO

这段提示词发过去之后,AI这次没有跑偏。它老老实实输出了一份三个函数的骨架,函数签名、参数列表、返回值和我的要求完全一致。需要说明的是,这不代表AI一下子就变聪明了,而是我把它的发挥空间压缩到了几乎为零。框架阶段本来就该这样,宁可多花五分钟把约束说清楚,也不要等代码写完了再花一小时改错。

2.3 框架阶段的确认清单

拿到AI输出的框架后,我没有急着让它填充细节,而是做了几个确认动作。这个环节最容易被跳过,但恰恰是最值钱的。

第一,确认所有模块的调用关系是单向的。netstat_mainnetstat_get_prefix_netsnetstat_write_report,两个子模块互不依赖。调用关系越简单,后面逐模块调试的隔离性越好。

第二,确认每个函数的参数和返回值类型明确。前缀参数是字符串,返回的网络列表是列表类型,文件名是字符串。类型不明确会让AI在细节阶段胡乱发挥。

第三,确认输出格式。CSV文件的列顺序、分隔符、每行结构,都在框架阶段固定下来。如果等代码写完了再改输出格式,牵一发动全身。

第四,手动把框架代码跑一个“静态检查”。我习惯把AI输出的骨架复制到支持括号高亮的编辑器里,人工确认一下defun和各层括号是否对齐。框架阶段括号都错了,后面细节阶段不用看了,肯定全错。

第五,跟AI明确“接下来我一次只让你实现一个函数”。这个约定很重要,相当于提前给AI打了预防针,避免它在后续回答里越俎代庖。我后来发现,只要这个约定没说清楚,AI在填充第一个函数细节时,往往会把另外两个函数也重新写一遍。

3. 细节填充:逐函数实现和真机调试

3.1 模块一:注册命令与入口函数

框架阶段通过后,我开始让AI实现第一个函数:入口函数netstat_main。这一步的提示词,核心是“只输出这一个函数”和“不要改动其他函数”。我给的提示词大致是这样:

现在只实现netstat_main函数,其他函数保持不动。 要求: - 通过axlEnterString函数提示用户输入网络名前缀 - 如果用户输入为空则提前退出 - 调用netstat_get_prefix_nets获取网络列表 - 调用netstat_write_report写文件 - 最后用printf打印匹配到的网络数量 请只输出这个函数的完整代码。

AI第一次给出的代码是这样的:

defun( netstat_main () let( (prefix nets) prefix = axlEnterString( "Enter net name prefix: " ) when( prefix nets = netstat_get_prefix_nets( prefix ) printf( "Found %d nets\n" length(nets) ) netstat_write_report( nets "./net_report.txt" ) ) ) )

这个函数一眼看过去没什么大问题,但我注意到一个细节:printf( "Found %d nets\n" length(nets) )。在SKILL里,length是函数,printf的第一个参数是格式串,后面的参数用空格分隔。这行代码理论上能编译,但可读性太差,我让AI改成先把长度存进变量再输出。这不是必须的改动,但作为长期维护的脚本,清晰比简洁更重要。

3.2 模块二:遍历网络并筛选前缀

入口函数确认后,接着实现netstat_get_prefix_nets。这个模块是整个脚本的核心,也是AI最容易出错的地方。原因很简单:它涉及遍历设计对象、访问网络属性、字符串匹配、列表构造,一堆SKILL特有操作集中在同一个函数里。

我给AI的提示词强调了以下几点:必须用axlDBGetDesign()获取设计对象,并且要判断返回值为空的情况;遍历要用foreach;字符串前缀匹配用strncmpstrlen;结果列表用cons构造;最后返回结果列表。同时要求它在关键步骤后面加上printf调试输出,方便我在Allegro里看到实际执行情况。

AI给出的第二版是这个样子:

defun( netstat_get_prefix_nets (prefix) let( (design allnets result) design = axlDBGetDesign() when( design allnets = design->nets result = nil foreach( net allnets when( net->name && strncmp( net->name prefix strlen(prefix) ) == 0 result = cons( net result ) ) ) printf( "DEBUG: found %d matching nets\n" length(result) ) ) result ) )

这段代码让我比较满意,几个关键点都对了:design->nets获取所有网络、foreach遍历、strncmp做前缀匹配、cons构造结果、最后返回result。唯一让我警惕的是strncmp( net->name prefix strlen(prefix) ) == 0这个写法。SKILL的strncmp和C语言一样,相等时返回0,而不是像Python的startswith那样返回布尔值。AI这次写对了,但我知道它在这个点上出过错,所以特意检查了一遍。

3.3 模块三:统计pin数量并写文件

最后一个函数是netstat_write_report,负责把筛选后的网络和pin数量写入文件。这个模块的技术点在文件操作上。AI之前用过几种写法,要么忘了close,要么没有判断文件是否打开成功。

第三轮提示词我专门加了两条要求:一是用outfile打开文件时必须判断返回句柄是否为nil,二是写完必须close。AI给出的实现如下:

defun( netstat_write_report (nets filename) let( (fp pinCount) fp = outfile( filename "w" ) when( fp foreach( net nets pinCount = if( net->pins length(net->pins) 0 ) fprintf( fp "%s,%d\n" net->name pinCount ) ) close( fp ) printf( "Report written: %s\n" filename ) ) when( !fp printf( "ERROR: cannot open file %s\n" filename ) ) ) )

这段代码我认为可以接受。if( net->pins length(net->pins) 0 )这个写法利用了SKILL里“nil为假、非nil为真”的特性:如果net->pins是nil,pin数记为0;否则取length的值。这个写法很SKILL,AI居然没用错,算是个惊喜。文件打不开的情况也做了打印,不至于脚本静默失败。

3.4 全链路验证:从load到命令执行

三个模块都实现完并且各自检查过之后,我把它们拼装成完整脚本netstat.il,然后在Allegro命令行里执行load("D:/skill/netstat.il")。这里有一个操作细节值得说:在Allegro命令窗口输入skill可以进入SKILL命令行模式,但load命令在正常命令行模式下就可以直接执行,不需要先进入skill模式。

load顺利通过,没有任何报错。接着我在命令行输入netstat,脚本提示输入前缀,我输入USB_,回车。控制台输出Found 12 nets,然后显示Report written: D:/skill/net_report.txt。打开文件,12个USB网络及其pin数量整整齐齐列在CSV里。到这里,脚本算是真正跑通了。从load到执行再到结果文件,全链路无异常。

这次全链路验证给我的最大感受是:分段实现、分段验证,最后拼装的时候几乎没有多余的错误。对比之前“一步到位”的版本,那种方案一旦出错,你完全不知道错误出在哪个模块,只能在一堆代码里大海捞针。

4. 全程调试实录:AI反复不执行的那些典型现场

4.1 场景一:load成功但命令“不见了”

这是最诡异、也最让人上火的场景。脚本load没有报错,但你输入netstat,Allegro提示“Unknown command”。这个问题我前前后后遇到好几次,每次原因还不一样。

最常见的原因是axlCmdRegister没有写在顶层,而是被嵌在了某个函数内部。axlCmdRegister是注册命令的顶层调用,必须在文件加载的瞬间执行。AI有时候会把这一行顺手写进netstat_main的开头,看起来逻辑上没问题,实际上load时不会执行,命令自然就不存在。处理方法很简单:让AI把axlCmdRegister移到所有函数定义之外,放在文件最底部。

另一个原因是命令注册成功,但注册的命令名和你想调用的名字不一致。AI偶尔会把命令注册成netstat,却在提示里让你输入net_stat或者别的变体。这种问题只能靠人工核对,没有任何报错信息可以依赖。我在调试时养成一个习惯:load之后立刻在命令行输入netstat,如果提示Unknown command,先检查axlCmdRegister的位置和名字,再考虑其他可能。

4.2 场景二:undefined function连环报错

undefined function是SKILL调试里最常见的错误,也是AI幻觉的重灾区。前面提到AI编造axlDBGetAllNets就是典型例子。这种报错很容易识别:错误信息会明确告诉你哪个函数未定义,比如:

E- *Error* undefined function - axlDBGetAllNets

问题在于,AI看到这个报错后的反应不是承认自己写错了函数名,而是坚持说这个函数存在,然后开始给你讲一堆“这个函数应该能工作”的理由。我试过把错误信息原样贴给它,它回复“该函数在较新版本的Allegro中可用”,这纯粹是幻觉。Cadence的SKILL API文档里从来就没有这个函数。

应对这种“嘴硬”情况,我的办法是:直接把正确的参考语法发给AI,让它模仿。比如明确告诉它:

SKILL中获取所有网络列表的正确写法是: design = axlDBGetDesign() allnets = design->nets 请基于这个写法重写相关代码。

给AI一个它无法反驳的正确示例,远比跟它争论“这个函数不存在”更高效。这其实就是给模型提供“few-shot”样例,让它从你的示例里学会正确模式,而不是从自己混乱的记忆里猜测。

4.3 场景三:AI“嘴硬”,反复输出同一份错误代码

比编造函数更折磨人的,是AI反复输出几乎一模一样的错误代码。你把错误信息贴给它,它道歉,然后把原来的代码换个变量名再发回来,错误原封不动。

这个现象我研究了一下,大概率是因为AI在生成修复代码时,被自己上一轮的输出“锚定”了。它看到自己的代码,觉得大体没问题,于是只做表面修改,没有真正理解错误的根源。你让它“重新生成”,它还是会沿着原有思路走。

我的破解办法有两个。第一,要求AI“从零重写整个函数,不要参考上一版本”,并且给它更明确的实现步骤提示,把步骤拆到不能再细。第二,也是更有效的办法,把出错的函数整体删除,重新开一个对话,给全新的提示词,让AI不带上历史包袱重新实现。新对话里的AI没有被上一轮错误代码“污染”,反而更容易写对。

这个方法听起来有点玄学,但在AI辅助开发里真的管用。我后来养成了习惯:同一个函数修改超过两轮仍然失败,就果断开新对话,不要恋战。

4.4 调试问题速查表

把这次全程调试遇到的问题汇总成一张表,方便以后对照排查。

症状可能原因解决方法
load成功但命令不存在axlCmdRegister未在顶层或命令名不一致检查注册语句位置和命令拼写,确保在文件顶层
undefined functionAI编造不存在的SKILL函数向AI提供正确的参考语法,要求基于示例重写
括号不匹配导致解析失败混用Lisp风格和SKILL风格在框架阶段写死风格约束,用编辑器检查括号配对
命令执行后无输出设计对象获取失败或遍历逻辑为空在关键步骤加printf调试输出,确认数据流
输出文件为空outfile打开失败或未close检查路径权限,添加文件句柄判断
AI反复输出同样错误模型被上一轮输出锚定开新对话重写函数,或要求完全从零实现
中文字符注释乱码文件编码问题用纯英文注释或ANSI编码保存
strncmp判断结果反了返回值0表示相等,容易被当false牢记SKILL的strncmp与C语言语义一致

4.5 几个通用的排查动作

最后分享几个我在调试SKILL脚本时反复用到的通用排查动作。这些动作看着基础,但真的能省下大量时间。

第一个动作是“小步输出”。在每个关键节点前加printf,把中间结果打出来。比如在netstat_get_prefix_nets里,遍历之前先打印网络总数,筛选之后打印匹配数量。数据流清晰了,问题定位就是几分钟的事。脚本跑通之后再把这些调试输出删掉就好。

第二个动作是“隔离运行”。如果对某个函数不放心,直接在Allegro命令行用skill进入SKILL模式,单独调用这个函数并传入测试参数。比如单独执行netstat_get_prefix_nets("USB_"),看返回列表是否正常。这种隔离测试能快速判断问题到底在哪一层,不用每次都走完整的命令流程。

第三个动作是“注意文件编码”。在中文Windows环境下,SKILL文件如果用了UTF-8编码且带BOM头,load时可能报解析错误;如果代码里有中文注释,保存成ANSI通常更稳妥。我这次调试中队友就踩过一次这样的坑,半天找不出原因,最后发现是编码问题,非常冤。

第四个动作是“善用错误定位信息”。SKILL的报错信息里通常会带行号和文件名,比如- at line 12 in file netstat.il。拿到报错别急着整体修改,先定位到那一行附近,往往就是那一两个字符的问题。

5. 沉淀下来的协作方法论

5.1 给AI喂SKILL上下文的正确姿势

经过这一整天的调试,我越来越确定一件事:让AI写好SKILL代码的关键,不是提示词写得多么花哨,而是你愿不愿意花时间给它“喂养”正确的SKILL上下文。

所谓上下文,一是语法风格示例,二是核心API的正确写法,三是你期望的代码结构。这些内容不用一次全给,而是在每个细节阶段按需提供。比如实现文件读取之前,我先在提示词里附上outfilefprintf的正确调用示例,AI照着写,出错概率就低很多。

我建议的做法是,在自己电脑上维护一个“SKILL提示词素材库”。把你实践中确认过的SKILL代码片段、常用API用法、容易踩坑的点,整理成一个个小条目。下次让AI写SKILL时,直接把相关条目附在提示词后面。这比每次现场翻手册再手动转述要高效得多,而且随着积累,素材库越全,AI写对的概率越高。

5.2 关键约束词,一句顶十句

有几句话在让AI生成SKILL代码时特别管用,我称之为“约束词”。看似简单,但加上它们之后,AI的输出质量有明显的提升。

第一句是“只输出代码,不要解释”。AI一旦开始解释,就会把自己幻觉出来的理由写得头头是道,反而影响判断。我要求它输出纯代码块,不附带任何说明文字。

第二句是“只修改我指定的函数,不要改动其他代码”。这个约束能防止AI在修一个函数时“顺手”重写其他函数,引入新错误。经历过一次就知道了,AI特别爱干这种事。

第三句是“如果某个函数在当前SKILL版本中不存在,请直接告诉我,不要自行推断”。这句能在一定程度上抑制AI编造API的冲动。虽然不能完全杜绝,但至少让AI在虚构函数名之前多犹豫一下。

第四句是“请在关键步骤加上printf调试输出”。这句话能让AI生成的代码自带调试埋点,省得我后续手动加。

5.3 人和AI的分工边界

这次实践下来,我对“AI辅助开发”这件事有了更清晰的认识。至少在SKILL这种小众领域语言上,AI和人的分工应该非常明确:AI负责实现,人负责决策。

架构设计、模块划分、API选型这些涉及判断和经验的事情,必须由人来做。AI在这些事情上给出的是“看起来合理的猜测”,而不是“经过验证的结论”。尤其是API层面,SKILL的函数那么多,每个版本的Allegro还可能有细微差异,AI完全无法感知,只有人通过文档和实测才能确认。我在框架阶段直接钉死axlDBGetDesignaxlCmdRegister的用法,就是不想让AI在核心API上乱猜。

代码实现、列表遍历、格式输出这些执行层面的工作,可以放手交给AI。只要范围和约束明确,AI在这些事上的产出质量还是相当不错的。这次三个模块的代码,我花了大量精力在检查API和逻辑细节上,但语句本身、遍历结构、变量处理这些部分,AI基本没有让我操心。

还有一件很重要的事是,不要让AI对“代码是否能运行”下结论。AI没有SKILL执行环境,它说“这段代码应该没问题”和说“这段代码肯定能运行”一样,都是没有实际依据的。能让代码跑起来的唯一方式,是把脚本load进Allegro里实测。所以整个调试过程中,不管AI怎么保证,我始终保持“每补一块就load一次”的节奏。事实证明,这个节奏是我最后能跑通脚本的最重要保证。


现在回过头看这次实践,最大的收获反而不是那个跑通的netstat脚本,而是一套“怎么跟AI协作写领域代码”的通用方法。框架先行、细节分段、小步验证、及时开新对话,这套打法实际上不限于SKILL,任何AI训练语料覆盖不足的垂直领域语言,应该都适用。我在这次过程中反复体会到一个朴素的道理:AI是一个执行力很强但判断力有待商榷的搭档,你给它的边界越清晰,它给你的回报就越稳定。以后我再让AI写SKILL,大概率还是会按照“先把框架钉死,再逐块填充验证”的节奏来,我也建议你试试看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询