ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity批处理模式+AI Agent:构建自动编译测试闭环

Unity批处理模式+AI Agent:构建自动编译测试闭环 最近在项目里做了一件事把 Unity 的编译和测试能力从编辑器界面里“拆”出来交给 AI Agent 直接调用。起因很简单——Agent 写 C# 脚本确实快但写完一堆代码怎么验证总不能每次都让人点编辑器里的编译按钮、切到 Test Runner 跑测试再把日志复制回去。这个人工回路一旦跑几轮就会让人抓狂尤其是 Agent 改一个文件、改一个参数就要重跑一次。所以我想办法把 Unity 工具链命令行化让 Agent 能自己敲编译、自己跑测试、自己读报错形成一条自动验证回路。如果你也在做 Unity 项目里的 AI 辅助开发或者想在自己项目里搭建类似的 Agent 驱动编译测试机制这篇实录应该能帮你避开不少坑。我不打算讲高大上的框架设计只讲我实际踩过的问题、最后落地的方案以及一些编辑器批处理模式的隐藏细节。核心就一句话别让 AI Agent 只会写代码要让它能闭环验证自己写的代码。1. 为什么绕不开“驱动编辑器”这一步1.1 AI Agent 的验证困境代码写出来不算完大模型生成 Unity 脚本的能力很强给它一个需求它能立刻给你列出一堆类、方法、生命周期回调。但 Unity 的脚本系统有个特点代码不是孤立存在的。脚本要经过编译、程序集引用检查、序列化规则校验、资源依赖绑定这一层一层的检查只要有一个不过整个工程都会卡在“编译失败”的状态里。这个状态有多麻烦最直观的例子是如果某个脚本里有语法错误或类型引用找不到Unity 编辑器启动后基本处于半瘫状态很多菜单功能不可用资源也导入不了。传统开发里你是能马上看到错误列表的但 AI Agent 看不到。它能读你给它的代码文件却不知道编辑器里那个红色的报错面板长什么样。它只能从你描述的反馈里猜自己写错了什么这个猜的过程效率极低。所以我一开始想的是能不能让 Agent 自己触发一次编译然后自己把编译错误读出来自己修复再自己验证这样整个回路就不需要人肉传话了。1.2 方案设计把 Unity 的编译与测试能力“接口化”想清楚之后目标就明确了。我需要给 Agent 提供至少三个“工具接口”触发编译、运行测试、读取结构化错误日志。这三个能力背后对应的是 Unity 的批处理模式Batch Mode命令行参数以及编辑器日志和测试结果 XML 的解析。为什么不用“编辑器常驻 插件 Socket 通信”这种方案我考虑过在编辑器里开一个 TCP 服务让 Agent 通过 WebSocket 来发指令看起来更“AI-native”但维护成本高。编辑器版本一换可能就崩而且常驻进程本身就和批处理模式完全相反。批处理模式是“命令进、命令出”的标准 CLI 风格一次运行一个指令跑完就退出干净利落天然适合程序化调用。CI 系统里跑自动化测试也是这么干的Agent 调用它没有任何额外的协议负担。所以最终架构就是封装一层命令行工具脚本内部调用 Unity 批处理模式外部暴露精简的 CLI 接口。Agent 只需要执行命令、读取 stdout 和结果文件。这条链路稳定、可控、可调试。2. 命令行底牌Unity 批处理模式与 -executeMethod2.1 批处理模式不是简单加个 -quit很多第一次接触 Unity 命令行的同学会以为批处理模式就是在启动命令后面加一个-batchmode -quit。这个认知方向对但离“能用”还差很远。批处理模式的核心约束是所有逻辑都必须在命令行参数里明确定义。编辑器不会弹窗提醒你、不会等你点按钮一切行为都是“非交互”的。一个最基础的批处理调用长这样# Windows 示例 C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exe \ -batchmode \ -nographics \ -quit \ -projectPath C:\MyUnityProject \ -logFile C:\build_logs\editor.logmacOS 上路径是/Applications/Unity/Hub/Editor/2022.3.20f1/Unity.app/Contents/MacOS/Unity \ -batchmode \ -nographics \ -quit \ -projectPath /Users/me/MyUnityProject \ -logFile /Users/me/build_logs/editor.log这里有几个细节值得展开。第一-quit表示命令执行完后自动退出但如果你没有给定-executeMethod或-runTestsUnity 可能只是启动一下然后退出不会做任何实质操作。第二-logFile极其重要不指定的话日志可能写到系统固定的 Editor.log 里Agent 要去翻系统路径很麻烦所以必须手动重定向。第三-nographics用在服务器或 CI 环境里跑编辑器逻辑时能省掉图形渲染资源但如果你要跑 PlayMode 测试且涉及具体渲染管线需要谨慎。2.2 用自定义方法拿到“编译完成”的信号批处理模式本身不会主动告诉你“编译完了”它只会默默地执行。所以你需要一个入口方法让 Unity 在启动完成后去调用。这个入口就是-executeMethod。先写一个静态编辑器方法// 放在 Editor 目录下例如 Assets/Editor/AgentBuild.cs using UnityEditor; using UnityEditor.Compilation; using UnityEngine; public static class AgentBuild { public static void CompileAndReport() { var assemblies CompilationPipeline.GetAssemblies(AssemblerType.Editor); int errorCount CompilationPipeline.GetErrorCount(); if (errorCount 0) { Debug.LogError($UnityAgent: compile failed with {errorCount} error(s).); } else { Debug.Log(UnityAgent: compile passed.); } } }执行时Unity -batchmode -nographics -quit -projectPath C:\MyUnityProject \ -executeMethod AgentBuild.CompileAndReport \ -logFile C:\build_logs\compile.log注意一个关键点Unity 在调用 -executeMethod 之前已经完成了当前工程的脚本编译。如果脚本编译失败这个静态方法很可能无法被调用因为在编译成功后程序集才加载。这个行为和直觉是反的——你以为你在“触发编译”实际上你是在说“等编译完了再跑我”。正确理解是编译是 Unity 启动流程的一部分而-executeMethod是在编译成功之后执行的一个回调。那编译失败时 Agent 怎么知道答案在日志里。批处理模式启动过程中如果脚本有问题Editor.log 里会写满Assets/xxx.cs(行号,列号): error CSxxxx: ...这样的错误。Agent 不需要靠-executeMethod判断编译结果它只需要解析日志。所以我的封装逻辑是优先用-runTests跑一次 EditMode 测试因为它天然覆盖“编译测试”两个环节。如果编译失败测试根本不会跑如果编译成功测试结果会完整呈现。只有在不想跑测试、只想知道“当前代码能不能编译”的时候才单独用-executeMethod加一个只读编译结果的入口。3. 跑通编译与测试的完整链路3.1 编译触发AssetDatabase.Refresh 与 -executeMethod 的配合如果 Agent 在项目里增删了脚本文件但 Unity 的资产数据库没有感知到批处理模式启动时可能不会触发重新导入。这个坑我实际遇到过。比如 Agent 通过 git 拉取代码或者用脚本复制了一堆.cs文件到工程目录然后马上跑批处理结果 Unity 还在用旧的程序集缓存。解决办法是在-executeMethod调用的方法里显式刷新资产数据库public static void ForceRefresh() { AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); Debug.Log(UnityAgent: asset database refreshed.); }ForceSynchronousImport会强制同步导入所有变更的资源之后再调用CompilationPipeline或直接抛异常让 Agent 去日志里看错误。注意这个刷新过程在大型工程里可能很慢几十秒甚至几分钟都有可能所以 Agent 调用时需要有超时重试的心理预期。3.2 测试EditMode 与 PlayMode 的取舍测试环节是整个自动验证回路里我最喜欢用的一块因为 Unity Test Framework 自带命令行支持而且测试结果会输出 NUnit 格式 XML机器可读性极好。EditMode 测试是纯逻辑测试跑在编辑器进程里适合验证算法、工具类、数据解析这类不依赖场景的代码。PlayMode 测试会进入真正的游戏运行时模式可以测 MonoBehaviour 生命周期、协程、物理系统等。在 AI Agent 驱动的验证场景里我强烈建议优先跑 EditMode跑通之后再补 PlayMode。原因很简单EditMode 启动快不依赖场景失败信息非常干净PlayMode 每次启动都要加载场景资源耗时可能翻几倍而且很多失败是因为场景状态干扰对 Agent 定位问题不友好。如果只是验证“这段工具代码写得对不对”EditMode 足够了。跑 EditMode 测试的命令Unity -batchmode -nographics -projectPath C:\MyUnityProject \ -runTests \ -testPlatform EditMode \ -testResults C:\build_logs\editmode_results.xml \ -logFile C:\build_logs\editmode_test.log跑 PlayMode 测试Unity -batchmode -projectPath C:\MyUnityProject \ -runTests \ -testPlatform PlayMode \ -testResults C:\build_logs\playmode_results.xml \ -logFile C:\build_logs\playmode_test.log注意PlayMode 测试我不建议加-nographics除非测试代码里明确不依赖渲染。否则某些渲染相关的回调会静默失败测试结果不可信。3.3 返回码陷阱不能只盯进程退出码这里有个大坑不少第一次做 Unity CI 的人都会踩Unity 批处理模式跑完测试后无论测试通过还是失败进程退出码经常都是 0。也就是说你不能在封装脚本里写“退出码非 0 就报错”这种逻辑否则测试挂了一堆你也不知道。正确姿势是解析-testResults指定的 XML 文件。文件根节点长这样test-run id2 testcasecount20 resultFailed total20 passed18 failed2 ...看result属性就知道整体成败再看test-case节点的result和label能定位具体失败用例。Agent 拿到这个 XML 后能精确知道哪几个测试用例挂了比看日志里乱糟糟的栈信息高效得多。我一律要求在封装脚本里用 Python 或 PowerShell 解析 XML提取出passed、failed、skipped数量以及失败用例的名字列表然后以纯文本形式返回给 Agent。只有进程卡住或崩溃时才靠退出码兜底。4. 修复实录让 Agent 能“看懂”编译与测试日志4.1 日志里藏着三种“错误”别混为一谈用自己的在项目里跑了一段时间后我发现 Agent 最大的困惑不是不会调用命令而是拿到日志后分不清什么是它该修的问题。Unity 相关的错误信息至少有三类必须分类处理。错误类型典型格式日志位置Agent 该怎么操作C# 编译错误Assets/Scripts/GameManager.cs(25,3): error CS1061:Editor.log / 重定向的 logFile直接改对应源码重新编译MSBuild 子进程错误error MSB6006: cmd.exe exited with code 3Editor.log / 构建日志多为自定义构建工具或资源管线问题需要检查环境变量和依赖测试断言失败test-case resultFailed label...result.xml根据堆栈定位到测试代码修正生产代码或测试代码如果 Agent 把MSB6006当成普通 C# 编译错误去改代码那是白费力气因为根源往往不在代码。这条区分在接入 Agent 之前必须写进它的系统提示词里或者让封装脚本在返回日志时就自动标注错误分类。4.2 一次 MSB6006 的完整排查链路这是我踩过最深的一个坑值得完整复盘。当时 Agent 报告“编译失败”我把日志拿过来一看没有error CS开头的行反而是error MSB6006: cmd.exe exited with code 3.这种错误在纯 Unity 项目里其实不常见它通常意味着构建管线里有额外的 MSBuild 任务——比如引用了一个自定义编辑器工具、一个用Process.Start调外部命令的后处理步骤或者某个 SDK 自带的生成器。批处理模式下这些工具经常会炸因为它依赖的环境变量和你在 Windows 桌面环境下不一样。我的排查路径是这样走的。第一步先确认不是 C# 代码问题。我去 logFile 里搜error CS一条都没有说明脚本编译本身是过的。问题出在更下游的“子进程”环节。第二步把报错上下文往前翻找MSB6006是哪个任务触发的。日志里通常会有Task Exec或Command:之类的内容它会把要执行的命令行完整打印出来。我在日志里找到了那条命令——是某个第三方资源后处理工具要调用node.exe去压缩纹理元数据。第三步我手动在终端里执行同样的命令结果直接报“找不到文件”或者 DLL 缺失。原因是批处理模式启动时Unity 没有加载用户系统里的完整PATH环境变量导致node.exe没被找到。第四步修复方式不是去全局改系统环境变量那太粗暴容易影响其他流程。我在封装脚本里显式构造了子进程环境变量把node.exe的目录加进PATH再传给 Unity 批处理进程# 在调用 Unity 的父进程里先设置好 export PATH/c/Program Files/nodejs:$PATH或者用 Python 的subprocessimport os import subprocess base_env os.environ.copy() base_env[PATH] /c/Program Files/nodejs; base_env[PATH] subprocess.run(unity_cmd, envbase_env, checkFalse)第五步重新跑编译同样的报错消失。我顺手把这个环境变量注入逻辑固化进封装工具里Agent 以后每次调用都会带上完整环境。这条链路如果当初让 Agent 自己看日志它只会一头雾水地告诉你“编译报错了”并尝试改 C# 代码。这就是为什么封装层必须做“环境准备 错误分类”两步——不是把原汁原味的日志丢给 Agent而是帮它先把噪音过滤掉。4.3 测试失败时如何定位到具体测试用例除了编译日志测试结果 XML 里的失败信息也有讲究。NUnit XML 的失败测试用例节点通常长这样test-case id17 namePlayerStats_ApplyDamage_ShouldClampToZero resultFailed labelFailed failure messageExpected: 0 But was: -10/message stack-trace at PlayerStatsTests.PlayerStats_ApplyDamage_ShouldClampToZero () ... /stack-trace /failure /test-caseAgent 从stack-trace里能直接看到测试方法和报错位置但注意它不会自动知道“这是业务代码的问题还是测试代码的问题”。我的做法是给 Agent 一条明确的判断规则如果message里的期望值和实际值差得很明显且stack-trace指向测试代码的行号多半是生产代码里逻辑有偏差如果stack-trace指向的是测试代码的辅助方法可能是测试环境准备不充分。比如上面的例子PlayerStats_ApplyDamage_ShouldClampToZero期望伤害后生命值钳位到 0实际得到了 -10显然生产代码里Mathf.Clamp没写对。Agent 看到这种格式就能立刻定位到PlayerStats.ApplyDamage方法。关键是把“测试报告格式”提前告诉 Agent它甚至不需要读完整日志只要读我解析出来的精简失败摘要就够了。5. 把能力封装进 Agent三层封装设计5.1 CLI 层三个命令足矣为了让 Agent 调用起来不迷路我把整条链路收敛成三个 CLI 命令。命令数越少Agent 的决策路径越短也越不容易因为调错参数而浪费时间。命令作用返回内容unity_compile触发编译并刷新资源库编译是否通过 错误列表按文件:行号:列号:错误码:消息格式unity_test运行 EditMode/PlayMode 测试测试结果摘要通过数、失败数、失败用例名列表unity_errors读取最近一次操作的重定向日志并解析错误结构化错误列表含错误分类标签这三个命令都包在同一份脚本里参数上只需区分平台和项目路径。# 使用方式示例 unity_compile --project C:\MyUnityProject unity_test --project C:\MyUnityProject --platform EditMode unity_errors --log-file C:\build_logs\compile.log5.2 输出约束给 Agent 的结构化错误清单Agent 的上下文窗口是有限的你不能把 20MB 的 Editor.log 一股脑塞给它。就算塞得下它也会被海量无关警告淹没最后不知道该处理哪个。所以封装脚本里最关键的一步是输出收敛。我用 Python 写了一个轻量解析器只提取特定格式的错误行转成结构化的紧凑文本import re # Unity 编译错误行格式 # Assets/Scripts/Test.cs(15,10): error CS1056: ... pattern re.compile(r^(.*\.cs)\((\d),(\d)\):\s(error|warning)\s(CS\d):\s*(.*)$) def extract_errors(log_text): errors [] for line in log_text.splitlines(): match pattern.match(line.strip()) if match: errors.append(match.groups()) return errors返回给 Agent 的格式我统一成ERROR: Assets/Scripts/GameManager.cs(25,3): CS1061: GameManager does not contain a definition for StartGame. WARNING: Assets/Scripts/AudioHelper.cs(10,5): CS0108: AudioHelper.Play hides inherited member ...这种格式对模型非常友好因为它和代码文件路径、行号直接对应Agent 能立即知道下一步去打开哪个文件、改哪一行。反观原始日志里的大段堆栈和 Git 内容只会让 Agent 浪费 token 去理解。5.3 接进 Agent 的两种方式本地工具调用与 MCP封装好 CLI 之后怎么让 Agent 真正“调用”它有两条路。第一条是直接用支持 Function Calling 的模型平台在 Agent 的工具定义里声明这三个命令路径模型会自动决定什么时候执行。这种方式简单直接几乎没有额外组件我现在的主力流程就是这个。Agent 生成代码后我给它一句“跑一下unity_test --platform EditMode”它会自己去执行命令、看返回摘要、决定是否继续修。第二条是接入 MCP Server把这三个 CLI 工具包成 MCP 的资源。好处是标准化以后接入其他 AI 客户端比如桌面端 Agent、IDE 插件里的 Agent很顺畅不用重复写工具协议。缺点是 MCP 本身多了一层运行进程要处理服务启动、鉴权和日志透传。如果你只是自己项目里用我建议先走 Function Calling等团队统一基建再切 MCP别一上来就上重设备。6. 边界、防呆与真实工作中的体会6.1 批处理模式并发锁避免和编辑器进程打架一个非常现实的坑是如果你已经打开了同一个 Unity 项目的编辑器窗口此时再用命令行跑批处理Unity 默认会直接退出或者报错因为项目目录被锁定了。日志里会提示无法打开项目或找不到有效的 license。Agent 可不管你有没有开着编辑器你给它一个任务它就闷头执行。所以我在封装脚本里加了一个前置检查扫描系统进程里是否有同名 Unity 进程且参数里包含同一个-projectPath。有就直接返回“请先关闭编辑器窗口再试”而不是让命令跑挂。如果没有这个检查Agent 会反复尝试浪费至少 10 分钟然后报告“编译工具不可用”。这种看似低级的错误在自动化链路里非常致命因为 Agent 没有人那样的常识判断必须由封装工具替它兜底。6.2 性能与增量编译优化Unity 批处理模式的启动时间在大型工程里相当可观冷启动可能 1 到 3 分钟如果 Agent 每次改一行代码就重跑一次整个反馈链路会慢到让人无法忍受。我采取的策略有三个。第一尽量跑 EditMode 而不是 PlayModeEditMode 的启动开销小一个量级。第二在测试命令里用-testFilter指定和当前改动相关的测试集而不是跑全量测试Unity -batchmode -nographics -projectPath C:\MyUnityProject \ -runTests \ -testPlatform EditMode \ -testFilter PlayerStatsTests \ -testResults C:\build_logs\test_results.xml \ -logFile C:\build_logs\test.log第三提醒 Agent 把多个改动合并成一次验证不要改一个文件就跑一次。我甚至会在系统提示词里直接写“只有在完成当前任务的全部代码修改后再执行编译和测试命令。”这能显著减少无谓的批处理启动。6.3 这套工具链还能往哪里延伸把 Unity 的编译测试能力封装成 CLI 后你会发现它能做的事不止是服务 AI Agent。同一套命令可以直接被 CI 系统复用——比如 GitLab CI 或 GitHub Actions 里拉下代码后跑一波 EditMode 测试跟 Agent 本地用的完全一致。也可以把编译结果自动回传到分析面板统计项目错误趋势。我个人现在的工作流是Agent 提交代码前必须自己跑一遍unity_compile得到零错误再跑unity_test --platform EditMode得到全绿然后才允许生成提交说明。人工介入率比之前手动搬运日志低了很多。对我来说这才是这套工具链最大的价值——不是“自动化”本身而是把人和 Agent 之间的协同降到了最低限度。你不需要告诉 Agent“你刚才那行报错了去看第 25 行”它自己就能做到。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进