资讯详情

资讯详情

AI Agent驱动Unity自动化编译测试闭环实践

做 Unity 项目的人都有一个共同体验写业务代码时思路再顺只要一碰到“改完代码 → 开编辑器 → 等导入 → 点构建 → 等进度条 → 跑测试 → 翻日志”这套流程灵感就全断了。我最近把 Unity 工具链中的编译、测试环节全部交给 AI Agent 直接驱动让 Agent 自己拉起编辑器批处理模式干活跑完测试读完日志该改代码就改代码改完继续验证形成一条自动修复的闭环。这篇实录会把这套工具链的架构思路、关键代码、踩过的坑和调优方法完整写下来。适合两类人看一类是被项目反复编译、回归测试折磨到想辞职的 Unity 开发者另一类是刚开始研究 AI Agent 开发、想知道 Agent 到底怎么落地的朋友。文章不预设你懂 Agent也不预设你懂 Unity 底层只要你愿意动手照着搭就能跑起来。1. 让 AI Agent 驱动 Unity 的底层思路拆解1.1 为什么 Unity 这么难被“自动化”Unity 归根结底是一个图形界面程序它没有像 gcc 那样干净的纯命令行接口。平时你按一下 Build 按钮背后要做的事情非常多全量导入、资源打包、代码编译、IL/IL2CPP 后处理、平台工具链调用。这个过程被封装在编辑器 GUI 里你用 shell 一两行命令是替代不了的。但 Unity 有一项很关键的官方能力批处理模式 batchmode。加上-executeMethod可以指定一个静态方法编辑器就能在无人值守的情况下执行任意 C# 编译脚本加上-runTests能跑 Test Framework 的测试加上-logFile能输出日志。这意味着我们完全可以把 Unity 编辑器当作一个有状态的“编译与测试后端”来调用这也是整套工具链的第一块地基。另外要说清楚一个现实AI Agent 本身并不能“看见”编辑器窗口它只能通过文本输入输出和工作所以 Unity 必须暴露成一种“文本进、文本出”的接口形态批处理模式恰好满足这个条件。你给 Agent 一把能安全调用的命令行它就能干活。1.2 架构Agent、编排层、Unity 后端三层我最终采用的架构分三层从简到繁说最底层是 Unity 本身以批处理模式对外暴露编译和测试能力输入是命令行参数输出是日志文件和测试结果 XML。中间层是很薄的一个编排脚本我用 Python 写。它只负责拼参数、启动 Unity 进程、采集退出码和超时兜底。这一层越薄越好不要塞业务逻辑。最上层是 AI Agent。Agent 不直接操作 shell而是通过“工具调用”的方式调用我预先定义好的unity_build、unity_test、read_file、edit_file这组工具。工作流就是一条很简单的循环Agent 先看项目结构然后调unity_build编译如果失败Agent 读取构建日志定位报错文件修改代码再调一次unity_build直到编译通过通过之后调unity_test跑测试测试挂了就继续改、继续跑。这套设计和传统 CI 的最大区别在于CI 只能告诉你“挂了”而 Agent 能拿到挂掉的日志之后自己判断为什么挂、然后动手修。真正解决的是上下文切换的成本问题。过去改一个编译错误要人肉看日志、想修复方案、改完再触发下一次构建现在这一整轮都交给了 Agent。1.3 为什么选“工具调用”而不是给 Agent 一个终端可能有人会问直接给 Agent 一个终端不就行了吗让它自己敲命令看输出不是更省事我试过确实能跑通但很危险。Agent 在自由终端里的操作不可控比如误删文件、跑了不该跑的构建变体、改了配置文件。更现实的问题是自由终端里 Unity 的大量输出会把 Agent 的上下文窗口瞬间撑爆几千行日志堆进去Agent 根本看不过来。所以我选择把工具切成一个个粒度合适的接口每个工具的输入输出都经过精简。比如unity_test返回的不是几千行原始日志而是TEST_RUN total42 passed40 failed2加失败用例的精炼信息。这样既保证 Agent 有足够的决策依据又不会让它淹没在日志噪音里。工具调用的另一个好处是可控——你可以规定 Agent 能做什么、不能做什么这对后续安全性来说太重要了。2. 工具链准备把 Unity 编辑器改造成可调用的“编译后端”2.1 Unity 批处理模式的正确启动姿势先确认你本机 Unity 编辑器的完整路径。通过 Unity Hub 安装的版本通常在固定位置Windows 下常见路径C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exemacOS 下常见路径/Applications/Unity/Hub/Editor/2022.3.20f1/Unity.app/Contents/MacOS/Unity第一个可以直接调用的命令是这个样子Unity.exe -batchmode -quit -projectPath D:\Projects\MyGame -logFile -逐个解释参数的含义。-batchmode告诉 Unity 别开图形界面-quit在命令完成后自动退出-projectPath指定工程路径-logFile -把日志输出到标准输出方便编排层捕获。这里有个从实际使用中总结出来很重要的经验-quit和-executeMethod一起用时务必在自定义方法内部显式调用EditorApplication.Exit(0)或Exit(1)。因为单独依赖-quit时进程退出码有时不按预期返回而编排层判断成败主要靠退出码这个细节后面会细说。2.2 先跑一个空操作验证批处理模式是否可用不推荐一上来就直接上完整构建先做一个最小验证。我在工程里放一个非常简单的静态方法using UnityEditor; using UnityEngine; public static class BatchTool { public static void Ping() { Debug.Log([BatchTool] Unity batch mode is alive.); EditorApplication.Exit(0); } }然后用命令验证Unity.exe -batchmode -quit -projectPath D:\Projects\MyGame \ -executeMethod BatchTool.Ping -logFile -看到日志里出现[BatchTool] Unity batch mode is alive.就说明环境通了。这一步至关重要很多机器第一次跑会发现许可证激活、路径错误、工程锁等问题先用最低成本探明环境后面能省掉大量排查时间。还建议把工程的版本控制系统状态也确认一下批处理运行时不排除对工程下的文件做写入操作如果工程里有未提交的改动先确认要不要保留。我有一个习惯就是工具链跑构建之前自动执行一次git status --short并把结果发给 Agent让它在改动前心里有数。2.3 用 Python 编排层包一层进程管理批处理命令虽然简单但裸调 shell 有几个问题没有超时控制、捕获标准输出不方便、Unity 在某些平台上的输出编码不统一。我写了一个很轻的 Python 封装import subprocess import shlex UNITY_PATH rC:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exe PROJECT_PATH rD:\Projects\MyGame def run_unity(extra_args: list[str], timeout: int 1800) - dict: cmd [ UNITY_PATH, -batchmode, -quit, -projectPath, PROJECT_PATH, -logFile, -, ] extra_args print([run_unity], .join(shlex.quote(c) for c in cmd)) try: proc subprocess.run( cmd, capture_outputTrue, textTrue, timeouttimeout, encodingutf-8, errorsreplace ) raw_log (proc.stdout or ) (proc.stderr or ) return {exit_code: proc.returncode, log: raw_log} except subprocess.TimeoutExpired: return {exit_code: -1, log: TIMEOUT: Unity process exceeded %d seconds % timeout}这个函数是整套工具链的“管钳”所有 Unity 调用都走这一条路。timeout参数尤其重要批处理模式再稳也有卡住的时候某些编辑器版本在导入资源时偶尔会长时间无响应必须先兜住一层超时否则 Agent 会被永远卡住。errorsreplace是处理 Windows 上 GBK 编译输出时的保底策略后面我会专门讲编码问题。3. 编译构建让 Agent 能调用的 Build 管线实现3.1 自定义构建入口不要改手点的构建配置画重点让 Agent 驱动构建不是说让 Agent 去点 Editor 菜单里的 Build 按钮那样没法传参数。需要在工程里写一个独立的构建脚本用-executeMethod来调用。这里最关键的设计是“把构建参数外部化”——所有可变参数从命令行传入脚本本身保持无状态。下面是我工程里简化后的构建入口using System; using System.Linq; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; public static class BuildScript { public static void PerformBuild() { string outputPath GetArg(-outputPath); string targetArg GetArg(-buildTarget); if (string.IsNullOrEmpty(outputPath)) { Debug.LogError([BuildScript] Missing required arg: -outputPath); EditorApplication.Exit(2); return; } BuildTarget target targetArg switch { android BuildTarget.Android, webgl BuildTarget.WebGL, windows BuildTarget.StandaloneWindows64, macos BuildTarget.StandaloneOSX, _ BuildTarget.StandaloneWindows64 }; var options new BuildPlayerOptions { scenes EditorBuildSettings.scenes .Where(s s.enabled) .Select(s s.path) .ToArray(), locationPathName outputPath, target target, options BuildOptions.None }; BuildReport report BuildPipeline.BuildPlayer(options); if (report.summary.result BuildResult.Succeeded) { Debug.Log($[BuildScript] Build succeeded. size{report.summary.totalSize}); EditorApplication.Exit(0); } else { Debug.LogError($[BuildScript] Build failed: {report.summary.result} ${report.summary.totalErrors} errors.); EditorApplication.Exit(1); } } private static string GetArg(string name) { string[] args Environment.GetCommandLineArgs(); for (int i 0; i args.Length - 1; i) { if (args[i] name) return args[i 1]; } return null; } }有几个地方值得展开说明。GetArg解析的是 Unity 命令行里-outputPath这样的自定义参数Unity 本身不认这些参数但Environment.GetCommandLineArgs能拿到完整参数列表所以在静态方法里照样能读取。EditorBuildSettings.scenes只取 enabled 的场景这样构建出来的版本和手点 Build 按钮时行为一致不会出现 Agent 构建出的包少了场景这种诡异问题。还有一点容易被忽视BuildPipeline.BuildPlayer返回的BuildReport里summary.result有三种典型状态——Succeeded、Failed、Cancelled。不要在非成功状态下还返回退出码 0否则 Agent 会误判构建成功接着去跑测试白白浪费一整轮。我最早就是在这个细节上吃过亏后面会写进排查表。3.2 构建目标平台与模块检查第一次构建某个目标平台时经常遇到“模块未安装”的问题。Unity 命令行里有一个内置参数-buildTarget它本身可以切换目标平台。我先用一行命令确认平台模块Unity.exe -batchmode -quit -projectPath D:\Projects\MyGame \ -buildTarget Android -executeMethod BatchTool.Ping -logFile -如果日志里出现Building library...之后直接报 “Cannot find module for Android”说明 Android 模块没装。iOS、Android 等目标平台需要额外的构建模块很多新手在 Unity Hub 安装时没勾选命令行第一次跑就会卡在这里。不同平台还有各自的工具链依赖这一点最容易让人措手不及。拿 Android 举例批处理构建需要 Android SDK、NDK 和 JDK 都配好WebGL 需要 Emscripten 工具链Unity 首次启动时会自动下载下载过程很慢iOS 则必须在 macOS 上配合 Xcode 工作。千万别以为装了个 Unity 编辑器就万事大吉节点机实际跑构建的机器要提前把对应平台依赖补齐这是工具链搭建里最容易被忽略的部分。另外提醒一句不是所有平台都能在同一个操作系统上构建。Windows 上没法构建 iOS 包Linux 上跑不了 UWP。做自动化之前先明确你的目标平台和可用机器否则 Agent 再怎么聪明也变不出 iOS 包。3.3 日志采集策略Agent 真的要读哪些内容-logFile -会把所有日志打到标准输出但这里有一个很现实的坑一次完整构建日志可能几千行Agent 上下文有限更重要的是 Agent 解析超长文本时容易“注意力漂移”把无关告警当成错误。所以我不让 Agent 直接读原始日志而是在编排层做一次日志摘要。我用正则把日志里的关键编译错误行抽出来import re ERROR_RE re.compile( r(?PfileAssets/.\.cs)\((?Pline\d)(?:,(?Pcol\d))?\):\s* rerror\s(?Pcode[A-Z]{2}\d):\s*(?Pmsg.) ) def summarize_build_log(raw_log: str) - str: if Build succeeded in raw_log: return BUILD_OK errors [] for line in raw_log.splitlines(): m ERROR_RE.search(line) if m: errors.append( f{m.group(file)}:{m.group(line)} [{m.group(code)}] {m.group(msg).strip()} ) if not errors: errors [line.strip() for line in raw_log.splitlines() if error in line.lower()][:10] return BUILD_FAILED\n \n.join(errors[:15])这个摘要函数返回的内容很短但信息密度很高文件名、行号、错误码、错误信息全都在。Agent 拿到这段文本后能精准去打开对应文件做修改。这就是我前面说的“把工具的输出做成 Agent 友好格式”的实际做法也是整套工具链里性价比最高的一个小设计。4. 自动化测试把 Test Runner 接进 Agent 闭环4.1 Unity Test Framework 的命令行跑法Unity Test Framework 是官方的测试框架支持 EditMode 和 PlayMode 两种测试。命令行调用方式Unity.exe -batchmode -quit -projectPath D:\Projects\MyGame \ -runTests -testPlatform EditMode \ -testResults D:\Projects\MyGame\TestResults\editmode.xml \ -logFile --runTests启动测试流程-testPlatform EditMode指定测试平台-testResults指定结果 XML 的导出路径。这里有一个从实际项目里总结出的经验结果 XML 的目录必须是已存在的目录否则部分 Unity 版本不会自动创建直接报错退出。我在编排层调用前先执行一下os.makedirs(..., exist_okTrue)避免这种低级问题。PlayMode 测试和 EditMode 差别不小。EditMode 测试跑在编辑器进程内启动快适合测纯逻辑、工具类、算法PlayMode 测试要进入播放模式相当于启动一个游戏运行时自然更慢有时还会因为场景初始化等原因卡住。我的建议是构建验证用 EditMode 快速铺量涉及运行时行为和场景交互的关键用例再放到 PlayMode。等 EditMode 闭环稳定之后再逐步把 PlayMode 接进来否则一开始就上 PlayModeAgent 一轮轮重跑会慢到让人崩溃。4.2 测试结果 XML 解析Agent 只需要失败清单Unity Test Framework 导出的 XML 是 NUnit 风格结构长这样test-run id1 testcasecount42 resultFailed total42 passed40 failed2 test-suite nameMyAssembly resultFailed test-case nameTests.Example resultFailed failure messageExpected: 5 But was: 6/message stack-traceat Tests.Example () [0x00012] in Assets/Tests/Example.cs:23/stack-trace /failure /test-case /test-suite /test-runNUnit 的 XML 结构层级很深直接把整份 XML 丢给 Agent 会浪费大量上下文。我在编排层解析成紧凑格式import xml.etree.ElementTree as ET def parse_test_results(xml_path: str) - str: root ET.parse(xml_path).getroot() total root.get(total); passed root.get(passed); failed root.get(failed) lines [fTEST_RUN total{total} passed{passed} failed{failed}] for case in root.iter(test-case): if case.get(result) in (Failed, Error): name case.get(name) msg stack failure case.find(failure) if failure is not None: msg (failure.findtext(message) or ).strip() stack (failure.findtext(stack-trace) or ).strip() lines.append(fFAILED: {name}\n msg: {msg}\n stack: {stack}) return \n.join(lines[:30]) if lines else fNO_TESTS_FOUND in {xml_path}这样 Agent 拿到的就是一条非常干净的失败清单用例名、断言消息、堆栈位置。它可以直接去对应源码文件里找问题不需要费力去解 XML。4.3 让 Agent 修复测试失败循环的完整闭环测试闭环和构建闭环几乎是同一个模式只是修复对象不同。完整流程是Agent 调用unity_test拿到TEST_RUN total... failedN的结果。如果 failed 是 0本轮通过。如果有失败Agent 打开失败用例对应的测试文件和被测代码分析是测试写错了还是实现有 bug。修改代码后重新调用unity_test。注意需要重跑整个测试集而不是只跑那一个用例。-testFilter参数虽然存在但在批处理模式下并不总是可靠重跑全量 EditMode 测试一般也就几十秒更省心。连续修复到全绿或者 Agent 判断改不动了主动上报转入人工。这套闭环里有一个容易被忽略的点Agent 修改代码产生的影响可能不止一个用例重跑全量是为了确认没有回归。实际上在真实项目里我遇到过 Agent 修好一个用例却弄坏另外两个用例的情况所以循环里的“全量重跑”绝对不能省。我还习惯在指令里要求 Agent 修完代码后用git diff自查改动确认没有误动无关文件。Unity 工程里有很多 meta 文件、配置文件Agent 要是不小心改了后面就是连环坑。5. 常见问题与排查技巧实录5.1 许可证激活问题批处理模式下Unity 依然要求合法激活的许可证。很多人在命令行里第一次跑-batchmode时会遇到 “No valid Unity Editor license found”。解决方案不是去找什么歪门邪道而是先手动打开一次 Unity 编辑器完成正常激活许可证文件会存在本机。如果是在 CI 机器上官方其实提供了完整流程先用批处理参数生成手动激活文件上传到官网换取许可文件再安装回来。步骤稍微繁琐但很稳妥。我自己在本机就直接用最简单的方案手动开一次编辑器登录后续所有批处理调用都正常了。自动化跑批处理之前一定先确认目标机器上 Unity 处于已激活状态否则 Agent 会在第一步就卡死。5.2 工程锁与并发问题Unity 对“同一个工程同时被两个进程打开”是严格禁止的。如果上次批处理异常退出、进程没死透再启动时就会报 “Another Unity instance is running with the same project” 之类的错误。我的排查办法是先查进程列表杀掉残留的 Unity 进程。检查工程目录下有没有异常的.lock文件或临时目录如果有残留就删掉。在编排层给每次构建加锁比如用filelock库防止 Agent 在多轮循环里并发触发两次 Unity。还有一个实际问题同一台机器上跑多个互相独立的工程是允许的但为了稳定我建议一个节点一次只跑一个 Unity 进程。并发带来的资源抢占和日志错乱成本远高于省下来的那点时间。尤其 IL2CPP 构建非常吃内存两个构建同时跑轻则互相拖慢重则直接 OOM。5.3 日志为空或编码乱码的问题-logFile -在 macOS 上很稳但在 Windows 上有时会遇到标准输出捕获不到、日志迟迟不刷新的情况。我后来的做法是显式指定一个日志文件路径Unity.exe -batchmode -quit -projectPath D:\Projects\MyGame \ -logFile D:\Projects\MyGame\Logs\build.log ...然后编排层再读那个文件。这样就算进程卡死文件里也会留下最后写出的内容对排查有极大价值。编码问题也很常见。Windows 中文环境里Unity 部分编译错误信息是 GBK 编码Python 用 utf-8 读会乱码。我在run_unity里统一用了errorsreplace乱码也不会导致崩溃如果发现错误信息不完整再用gbk编码回退重新读一次日志。总之不要因为编码问题让整条链路断掉这是工具链在 Windows 上最容易踩的隐形坑。5.4 Agent 上下文管理与提示词设计工具链跑通之后真正决定上限的是 Agent 的提示词设计。我总结了几个关键纪律告诉 Agent 它有哪些工具、每个工具的输入输出是什么不要让它猜。与其靠模型悟性不如在 system prompt 里把工具语义写清楚。要求 Agent 引用日志原文再下结论。Agent 在解析错误时容易脑补宁愿让它多贴一行原始错误也不能让它凭印象修代码。限制单次修动的范围。我一般让 Agent 一次只处理一个错误构建失败时先修第一个错误重新编译后再看下一个。很多编译错误是滚雪球式的第一个修好了后面一串自动好。明确“不能做什么”不允许删除生成目录、不允许修改无关的ProjectSettings配置、不允许做大范围重构。这里还要多说一句安全话题。如果你的 Agent 会读取外部文本尤其是测试输出、用户生成内容之类的非受信文本就要小心“指令注入”问题——异常输出里的内容理论上可能反过来操纵 Agent 的行为。稳妥做法是把测试输出严格视为“数据”只提取结构化信息不让 Agent 把输出内容当作“指令”去执行。这一点在 AI Agent 开发里越来越重要。下面把批处理模式最常见的几类问题整理成一个速查表错误特征常见原因处理办法启动即报许可证错误编辑器未激活先手动开一次编辑器激活或在 CI 上走官方许可文件流程提示工程被占用残留 Unity 进程或锁文件杀进程、清锁文件、编排层加互斥锁提示找不到平台模块对应构建模块未安装用 Unity Hub 安装模块或检查构建机平台依赖日志全是乱码Windows 中文编码显式 logFile 路径读取时用 utf-8/errorsreplace必要时回退 gbk构建成功但退出码为负超时或进程被杀检查超时设置排查卡死点看日志文件尾段跑测试但结果为 0测试程序集未配置检查 asmdef 是否正确引用 nunit测试框架包是否完整IL2CPP 构建极慢平台工具链未配好用 Mono 快速迭代正式发布再切 IL2CPP6. 实操心得与后续扩展6.1 我最想强调的三个工程化习惯这套工具链从搭建到稳定我前后折腾了将近一周。真正花时间的不是写代码而是踩那些看起来玄学的坑退出码不准确、日志截断、平台模块缺失、Agent 改错文件。把这些坑逐一定位之后链路就非常顺了。第一个习惯工具层一定要薄。编排脚本永远是那两百行不要往里面加业务判断否则一旦 Agent 行为异常你会分不清到底是 Agent 的问题还是你的脚本的问题。第二个习惯构建输出一定要做摘要别直接灌给 Agent。这一步看起来只是多写了几个正则实际上救了我太多次——Agent 读短文本的准确率远高于读几千行日志。第三个习惯一定要加超时和重试机制。批处理模式再稳也有抽风的时候兜底逻辑不能省。还有一个很小的建议把所有 Unity 路径、工程路径、目标平台统一放进一个配置文件不要散落在各个脚本里。这个项目我用的是config.jsonAgent 每次启动时先读配置再决定调用哪些工具。配置集中管理之后换机器、换 Unity 版本都是几分钟的事。6.2 后续可以做深的方向这套工具链搭好以后我计划继续往三个方向扩展。一是把 WebGL 构建产物接到无头浏览器里做冒烟测试这样能抓出像运行时 IDBFS 写入失败这种纯构建阶段测不出的问题。WebGL 的 IndexedDB 文件系统受浏览器权限和用户手势影响很大光靠 Unity 层构建通过远远不够必须跑一次真实浏览器环境。二是接入 Unity 的 Asset Bundle 构建让 Agent 能做完整出包验证。目前我只做了场景构建和测试AssetBundle 资源管线还没纳入闭环后续把资源依赖检查和打包产物校验一起交给 Agent。三是给 Agent 加一个“构建耗时统计”工具让它能自动发现编译性能回归。比如某个模块突然从一分钟变成五分钟这种问题靠人眼巡检很难发现Agent 反而非常合适。它可以对比历史记录主动报告异常趋势相当于给工具链加了一层可观测性。最后分享一个让整个使用体验质的提升的小技巧在 system prompt 里给 Agent 加一条规则——“每当一次任务全部结束时输出一张简洁工单构建结果、测试通过率、改动过的文件列表、下一步建议”。这样你回来 review 时只需要看一张表根本不用自己翻日志。这套工具链跑了一段时间我最深的感受是不是要让 Agent 替代人做决策而是让人从机械的“编译、等、报错、改”循环里抽身出来把注意力留给真正需要判断力的事情上。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →