1. 为什么我要折腾一个会自己跑代码的 Lisp 助手做工程绘图这行的朋友多半都有体会AutoLISP 这门语言属于典型的用起来真香、写起来真烦。语法老、调试弱、报错信息含糊写个批量改图层的小脚本光是在编辑器里来回切、加载、试运行就能耗掉大半天。更别提让 AI 帮忙写代码了——它给你吐出来一段看着挺像那么回事的 Lisp你复制粘贴进去一跑要么括号对不上要么变量没定义要么逻辑跑偏最后还得自己一行行排查。我最近在做的这个项目核心目标就一句话让 AI 写完 AutoLISP 之后自己把代码跑一遍确认结果正确了再交付给我。听起来像是给 AI 加了个自检闭环但真正落地的时候涉及的东西比想象中多——怎么让 AI 拿到运行环境、怎么把执行结果回传给 AI、怎么判断结果正确、跑挂了怎么让它自己修。这套东西我用 TmAgent 作为调度骨架配合 Lisp 解释器做执行层折腾出了一套还算顺手的流程。这篇文章适合两类人看一类是天天跟 AutoLISP 打交道、想用 AI 提效但被AI 写的代码不能直接用劝退的工程绘图从业者另一类是对AI 代码自验证闭环这个思路感兴趣、想在自己领域复刻一套的开发者。我会把整体设计思路、关键环节的实现、踩过的坑都摊开讲代码和配置尽量给到能直接抄的程度。先说清楚一个前提这套方案的核心不是让 AI 一次写对而是让 AI 有机会自己发现写错了并改对。这个思路的转变很关键后面所有的设计都是围绕它展开的。2. 整体设计思路把执行变成 AI 的一个工具2.1 传统 AI 写代码流程的断点在哪大部分人用 AI 写 Lisp 的流程是这样的描述需求 → AI 输出代码 → 人工复制到 CAD 环境 → 手动加载运行 → 看结果 → 出错了再回去描述问题 → AI 再改。这个链条里有两个致命的断点。第一个断点是执行环节完全脱离 AI。AI 根本不知道它写的代码跑起来是什么样它只能靠语言模型对语法的理解来猜。但 Lisp 这种括号嵌套极深的语言光靠猜准确率很有限尤其是涉及具体 API 调用比如entget、ssget这些的时候参数顺序、返回值类型稍微记错就崩。第二个断点是反馈回路太长。人工跑一遍、把报错信息复制回去、等 AI 改、再跑一遍一轮下来好几分钟。如果代码有五个问题就得来回五轮效率低到不如自己写。所以我的设计目标很明确把执行环境封装成 AI 可以主动调用的工具让它在交付之前自己跑、自己看结果、自己判断对错、自己修。这样断点就被接上了反馈回路从分钟级压缩到秒级。2.2 为什么选 TmAgent 做调度骨架市面上能编排AI 工具调用的框架不少我选 TmAgent 主要看中三点。第一是工具注册足够轻。我只需要把执行 Lisp 代码这个能力包装成一个函数注册进去AI 就能在对话中自主决定什么时候调用它。不需要写复杂的插件系统一个装饰器加一个函数体就搞定。第二是支持多轮工具调用循环。这点很关键——AI 调用执行工具拿到结果后如果发现报错它需要能基于这个结果继续推理、继续调用工具比如重新执行修改后的代码。TmAgent 的 agent loop 天然支持这种思考-调用-观察-再思考的循环不用我自己写状态机。第三是对执行结果的透传比较干净。执行工具返回的 stdout、stderr、返回值能原封不动地喂回给模型模型看到的就是真实的运行现场而不是被框架加工过的摘要。这一点对调试类任务太重要了报错信息少一个字都可能误导判断。提示如果你用的是别的 agent 框架只要它满足能注册自定义工具 支持多轮工具调用这两个条件思路完全可以平移不是非 TmAgent 不可。2.3 执行层为什么用独立进程而不是内嵌一开始我图省事想把 Lisp 解释器直接嵌到 agent 进程里跑。试了两天就放弃了原因有三个。一是崩溃隔离。AutoLISP 脚本里一个死循环或者一个非法内存访问很容易把宿主进程带崩。如果解释器和 agent 在同一个进程agent 一崩整个会话上下文全丢前面几轮对话白费。独立进程跑崩了只是子进程退出agent 收到一个非零返回码还能继续处理。二是环境干净。每次执行都起一个全新的解释器实例避免上一次执行残留的全局变量污染下一次。Lisp 的全局变量特别容易互相干扰独立进程天然解决了这个问题。三是超时可控。独立进程可以用操作系统的超时机制直接 kill不用担心线程杀不干净。写 Lisp 的人都知道一个写错的while循环能跑到天荒地老没有硬超时兜底是不行的。所以最终架构是TmAgent 主进程负责对话和调度通过子进程调用 Lisp 解释器执行代码捕获输出后回传给模型。中间用标准输入输出做数据交换简单可靠。3. 核心细节拆解执行工具到底怎么设计3.1 执行工具的接口设计执行工具是整个方案的心脏它的接口设计直接决定了 AI 用起来顺不顺手。我最终定下来的接口是这样的def run_lisp(code: str, timeout: int 30) - dict: 执行一段 AutoLISP 代码并返回结果。 参数: code: 完整的 Lisp 代码字符串 timeout: 超时秒数默认 30 秒 返回: { success: bool, # 是否正常执行完成 stdout: str, # 标准输出 stderr: str, # 错误输出 return_value: str, # 最后一个表达式的返回值 elapsed: float # 耗时秒数 } 这里有几个设计决策值得展开说。为什么返回结构化的 dict 而不是纯字符串。因为 AI 需要区分执行成功但结果不对和执行直接报错这两种情况处理策略完全不同。前者要检查逻辑后者要检查语法。结构化返回让模型能明确知道当前处于哪种状态。为什么要有 return_value 字段。Lisp 的特点是每个表达式都有返回值最后一个表达式的值往往就是整个脚本的结果。把这个值单独拎出来AI 判断对错的时候就有了明确的锚点不用去 stdout 里大海捞针。为什么 timeout 默认 30 秒。实测下来正常的批量处理脚本比如遍历几百个图元改属性大多在 10 秒内跑完30 秒足够覆盖绝大多数场景又不至于让一个死循环卡太久。这个值可以按需调整但我不建议设太大宁可让 AI 早点拿到超时信号去修代码。3.2 代码注入与沙箱隔离执行工具不能傻乎乎地把 AI 给的代码直接丢进解释器中间得做几层处理。第一层是语法预检。在真正执行之前先用一个轻量的括号匹配检查扫一遍。Lisp 最常见的错误就是括号不配对这个检查能在毫秒级发现大部分低级错误直接返回给 AI省得启动解释器的开销。实现很简单维护一个计数器遇到左括号加一、右括号减一最后不为零就报错同时给出出错的大致位置。第二层是危险函数拦截。AutoLISP 里有些函数会直接操作文件系统或者执行外部命令比如vl-file-delete、startapp这类。AI 生成的代码里如果出现这些我会先拦下来返回一个该函数被禁用的提示让 AI 换个思路。这不是不信任 AI而是防止它在调试过程中暴力地删掉测试文件或者弹出莫名其妙的窗口。第三层是执行环境变量注入。为了让 AI 写的代码能拿到测试数据我会在执行前把一些预置的变量注入进去比如测试用的图元列表、模拟的当前图层名等。这样 AI 写代码时可以直接引用这些变量不用自己造数据降低了它写测试代码的负担。;; 执行前自动注入的测试上下文示例 (setq *test-entities* ((0 . LINE) (8 . 0) (10 0.0 0.0 0.0) (11 100.0 100.0 0.0))) (setq *test-layer* TEST_LAYER) (setq *test-count* 5)注意注入的变量名统一用*xxx*这种带星号的命名这是 Lisp 社区的惯例表示特殊变量能避免和 AI 自己定义的局部变量撞名。3.3 结果正确性的判断逻辑这是整个方案里最微妙的部分。执行成功和结果正确是两码事。一段代码可能跑得顺顺当当但改错了图层、算错了坐标。怎么让 AI 判断结果对不对我的做法是把判断标准也交给 AI但给它足够的判断依据。具体来说执行工具返回的结果里除了 stdout 和返回值还会附带一份执行前后的环境快照对比。比如执行前图元数量是 10执行后是 8这个变化会明确列出来。AI 拿到这个对比就能结合需求判断我本来是要删掉两个图元现在确实少了两个对了或者我只想改颜色怎么数量变了有问题。为了让快照对比有意义我在执行前后各做一次环境采样采样的内容包括图元总数、各图层图元数、当前选择集内容、关键全局变量的值。采样本身用一段固定的 Lisp 代码完成不依赖 AI 生成。;; 环境快照采样代码固定不由 AI 生成 (defun snapshot-env () (list (cons entity-count (sslength (ssget X))) (cons layer-list (mapcar car (dictsearch (namedobjdict) ACAD_LAYER))) (cons current-layer (getvar CLAYER)) (cons test-var *test-count*) ) )这样 AI 每次执行后拿到的就是一份结构化的前后对比表判断起来有据可依而不是靠感觉。3.4 多轮修复循环的终止条件AI 自己跑、自己修这个循环不能无限转下去。我设了三重终止条件。第一重是成功终止AI 判断结果正确主动结束循环输出最终代码。这是理想情况。第二重是轮次上限最多允许 AI 修复 5 轮。超过 5 轮还没搞定说明要么需求描述有问题要么这个问题超出了 AI 的能力范围继续转下去只是浪费 token。这时候我会让 agent 输出当前最佳版本 未解决的问题说明交回人工处理。第三重是重复错误检测如果 AI 连续两轮犯同样的错误比如反复在同一个函数上栽跟头直接中断循环。这种情况通常是 AI 陷入了某种思维定式再给它机会也是白搭不如早点叫停。这三重条件里我觉得重复错误检测最有价值。实测中遇到过 AI 反复尝试用同一个错误的方式调用某个 API每次报错都一样它就是不换思路。加了重复检测之后这种情况能及时止损。4. 实操过程从零搭起这套闭环4.1 环境准备与依赖安装先把基础环境搭起来。我用的是一台普通的开发机系统是常见的 Linux 发行版Python 版本 3.10 以上。核心依赖就两个TmAgent 框架本身以及一个能跑 AutoLISP 的解释器。AutoLISP 的解释器选择上我用的是一个开源的 Lisp 解释器它兼容大部分 AutoLISP 语法能独立运行不依赖 CAD 环境。这一点很重要——我不可能为了跑测试去启动一个完整的 CAD 软件太重了。这个解释器虽然不能覆盖 100% 的 AutoLISP API但覆盖了常用的图元操作、列表处理、字符串处理足够验证大部分逻辑。# 创建虚拟环境 python -m venv venv source venv/bin/activate # 安装核心依赖 pip install tmagents pip install lisp-runner # 假设的解释器包名 # 验证解释器可用 lisp-runner --version安装完之后先跑个冒烟测试确认解释器能正常执行最简单的 Lisp 代码echo (princ ( 1 2)) | lisp-runner # 期望输出: 3这一步别跳过。我见过有人环境没配好就开始写 agent 逻辑结果调试半天发现是解释器根本没装上。4.2 执行工具的完整实现环境就绪后开始写执行工具。这是整个项目里代码量最大的一块我拆成几个函数来实现。首先是子进程调用的封装import subprocess import time import json def _invoke_interpreter(code: str, timeout: int) - dict: 底层调用解释器执行代码 start time.time() try: proc subprocess.run( [lisp-runner, --stdin, --json-output], inputcode, capture_outputTrue, textTrue, timeouttimeout ) elapsed time.time() - start return { success: proc.returncode 0, stdout: proc.stdout, stderr: proc.stderr, elapsed: round(elapsed, 3) } except subprocess.TimeoutExpired: return { success: False, stdout: , stderr: f执行超时超过 {timeout} 秒可能存在死循环, elapsed: timeout }然后是语法预检def _check_parens(code: str) - tuple[bool, str]: 检查括号是否配对返回 (是否通过, 错误信息) depth 0 for i, ch in enumerate(code): if ch (: depth 1 elif ch ): depth - 1 if depth 0: return False, f位置 {i} 处出现多余的右括号 if depth ! 0: return False, f括号不配对缺少 {depth} 个右括号 return True, 再是危险函数拦截DANGEROUS_FUNCS [ vl-file-delete, vl-file-rename, startapp, vl-registry-write, command-s ] def _check_dangerous(code: str) - tuple[bool, str]: 检查是否包含危险函数 for func in DANGEROUS_FUNCS: if func in code: return False, f检测到被禁用的函数{func}请换用其他方式实现 return True, 最后把它们串起来形成对外的run_lisp工具def run_lisp(code: str, timeout: int 30) - dict: # 第一层语法预检 ok, msg _check_parens(code) if not ok: return {success: False, stderr: f语法错误{msg}, stage: syntax} # 第二层危险函数拦截 ok, msg _check_dangerous(code) if not ok: return {success: False, stderr: msg, stage: security} # 第三层注入测试上下文 wrapped _inject_context(code) # 第四层真正执行 result _invoke_interpreter(wrapped, timeout) result[stage] execution return result这个分层结构的好处是每一层的失败都能给出明确的stage标记AI 看到stage: syntax就知道是语法问题看到stage: execution就知道是运行期问题排查方向完全不同。4.3 把工具注册进 TmAgent工具写好了接下来注册到 agent 里。TmAgent 的工具注册方式很直接from tmagents import Agent, tool tool def run_lisp(code: str, timeout: int 30) - dict: 执行 AutoLISP 代码并返回结果。 当你写完 Lisp 代码后必须调用此工具验证代码能否正常运行。 如果执行失败请根据错误信息修改代码后再次调用直到成功为止。 # ... 上面的实现 ... agent Agent( modelyour-model-name, tools[run_lisp], system_prompt你是一个 AutoLISP 编程助手。 你的工作流程是 1. 理解用户的绘图需求 2. 编写 AutoLISP 代码 3. 调用 run_lisp 工具执行代码 4. 检查执行结果如果失败则修复后重新执行 5. 确认结果正确后输出最终代码和说明 你必须在交付代码前完成至少一次成功执行。 )这里 system_prompt 的写法很关键。我特意强调了必须调用工具验证和确认结果正确后再交付这是给 AI 立规矩。实测下来如果不明确要求AI 有时候会偷懒写完代码直接输出根本不调用执行工具。把流程写死在 prompt 里它才会老老实实走闭环。4.4 一次完整的执行现场记录光看代码不够直观我记录一次真实的执行过程让大家看看这套东西跑起来是什么样。需求是写一个函数把当前图纸中所有位于 OLD_LAYER 图层的直线移动到 NEW_LAYER 图层。AI 第一版代码(defun move-lines ( / ss i ent) (setq ss (ssget X ((0 . LINE) (8 . OLD_LAYER)))) (setq i 0) (while ( i (sslength ss)) (setq ent (ssname ss i)) (entmod (subst (cons 8 NEW_LAYER) (assoc 8 (entget ent)) (entget ent))) (setq i (1 i)) ) (princ done) )调用run_lisp后返回{ success: false, stderr: 错误变量 ss 为 nilsslength 无法处理, stage: execution, elapsed: 0.42 }AI 看到这个错误分析出问题在于测试环境里没有 OLD_LAYER 图层的直线ssget返回 nil。它修改代码加了空值判断(defun move-lines ( / ss i ent) (setq ss (ssget X ((0 . LINE) (8 . OLD_LAYER)))) (if ss (progn (setq i 0) (while ( i (sslength ss)) (setq ent (ssname ss i)) (entmod (subst (cons 8 NEW_LAYER) (assoc 8 (entget ent)) (entget ent))) (setq i (1 i)) ) (princ done) ) (princ no matching entities) ) )再次执行返回{ success: true, stdout: no matching entities, stage: execution, elapsed: 0.38 }执行成功了但 AI 注意到输出是 no matching entities说明测试环境里确实没有匹配的图元。它主动在测试上下文里造了几条 OLD_LAYER 的直线再次执行这次输出 done并且环境快照显示图元图层确实从 OLD_LAYER 变成了 NEW_LAYER。AI 确认结果正确才把最终代码交付出来。整个过程 AI 自己跑了三轮全程没有人工干预。这就是闭环的价值——它自己发现了空值问题自己造了测试数据自己验证了结果。5. 常见问题与排查技巧实录5.1 执行工具相关的典型问题这套东西跑起来之后我踩的坑主要集中在执行工具这一层。整理成一张速查表方便对照排查。问题现象可能原因排查方向解决方式工具调用后无返回子进程卡死检查是否有死循环确认 timeout 生效加硬超时返回 success 但结果不对逻辑错误非语法错误看环境快照对比让 AI 对比前后状态反复报同一个错AI 陷入思维定式看连续两轮错误是否相同触发重复错误检测中断循环括号检查通过但执行报错语义错误看 stderr 具体信息定位到具体函数调用执行时间异常长数据量过大或死循环看 elapsed 字段缩小测试数据规模这张表里我觉得最值得说的是返回 success 但结果不对这一行。这是最容易被忽视的情况因为工具层面它确实成功了但业务层面它是错的。如果没有环境快照对比AI 很容易被 success 迷惑以为万事大吉。所以快照对比这个设计不是锦上添花是必需品。5.2 让 AI 自己修代码的几个技巧让 AI 自己修代码不是简单地把错误信息丢给它就行有几个技巧能显著提高修复成功率。技巧一错误信息要完整不要截断。Lisp 的报错信息有时候很长包含调用栈。我一开始为了省 token把错误信息截断到 200 字符结果 AI 经常看不到关键的那一行。后来改成完整透传修复成功率明显上升。token 该花的地方不能省。技巧二给 AI 明确的修复方向提示。在工具返回的错误信息里我会根据错误类型附加一句提示。比如语法错误附请检查括号配对和函数名拼写运行期错误附请检查变量是否已定义、参数类型是否正确。这种提示能帮 AI 快速定位方向减少无效尝试。技巧三保留历史执行记录。AI 修复的时候我会把前面几轮的执行结果一起给它让它能看到我上次改了什么、改完之后错误变成了什么。这样它能判断自己的修改是往好的方向走还是越改越糟。没有历史记录AI 容易在原地打转。提示历史记录不要无限保留我一般保留最近 3 轮。太多会干扰 AI 判断太少又看不出趋势。5.3 性能与成本的平衡这套闭环跑起来token 消耗比AI 直接输出代码要高不少因为多了执行结果的往返。怎么控制成本我总结了几个做法。一是语法预检前置。括号不配对这种低级错误在本地毫秒级就能查出来不用启动解释器更不用消耗 token 让 AI 分析。把这类检查放在最前面能挡掉相当一部分无效执行。二是测试数据精简。测试用的图元数据不要造太多几条到十几条足够验证逻辑。数据量大了执行慢返回的 stdout 也长都是成本。三是设置合理的轮次上限。前面说的 5 轮上限既是防止无限循环也是成本控制。实测下来90% 的问题能在 3 轮内解决设 5 轮留有余量。四是缓存成功的执行结果。如果同一段代码重复执行结果是一样的没必要每次都跑。我加了个简单的哈希缓存代码内容哈希相同就直接返回上次的结果。这在 AI 反复微调代码的时候能省不少。5.4 边界情况的处理经验有几个边界情况我踩过坑之后专门做了处理。空输入。AI 有时候会传一个空字符串或者只有注释的代码进来。这种情况直接返回代码为空不用启动解释器。超长代码。如果 AI 生成的代码超过一定长度我设的是 5000 字符先警告一下。超长代码往往是 AI 思路混乱的表现与其执行它不如让 AI 重新组织一下。编码问题。Lisp 代码里如果有中文注释编码处理不当会乱码。我统一用 UTF-8并且在解释器调用时显式指定编码避免默认编码不一致导致的乱码。并发执行。如果同时有多个执行请求要确保它们用的是独立的解释器实例不能共享。我用进程池管理每个请求分配一个独立进程用完回收。6. 这套方案还能怎么扩展跑通基础闭环之后我又做了几个扩展效果不错一并分享。第一个扩展是多文件支持。有些 Lisp 项目不是一个文件能搞定的涉及多个函数文件互相调用。我在执行工具里加了文件集的概念AI 可以一次性提交多个文件工具按依赖顺序加载后执行。这样就能验证跨文件的调用逻辑。第二个扩展是回归测试。每次 AI 修改代码后除了跑当前需求还会自动跑一遍之前通过的测试用例确保没有把别的功能改坏。这个思路借鉴了软件工程里的回归测试在 Lisp 这种缺乏类型检查的语言里尤其有用。第三个扩展是执行结果可视化。对于涉及坐标计算、图形生成的代码光看数字不直观。我加了一个简单的可视化输出把关键坐标点画成图AI 和人都能一眼看出对不对。这个还在完善中但对几何相关的 Lisp 代码帮助很大。第四个扩展是错误模式库。把历史上遇到过的典型错误和对应的修复方式积累下来形成一个知识库。AI 遇到类似错误时先从库里查有没有现成的修复方案有的话直接参考没有的话再自己推理。这个库越用越厚修复效率也越来越高。我个人在实际操作中的体会是这套方案的价值不在于AI 能写 Lisp 了而在于AI 能对自己写的东西负责了。以前是人给 AI 擦屁股现在是 AI 自己擦。这个转变带来的效率提升比单纯让 AI 写代码快几倍要实在得多。最后再分享一个小技巧如果你也想复刻这套东西别一上来就追求大而全先把执行 回传这个最小闭环跑通哪怕只支持最简单的代码跑通了再往上加功能。我见过太多人卡在设计完美架构的阶段结果一行能跑的代码都没写出来。
阅读完成 · 觉得有帮助?